Guides
Struct mode and derive
Map a struct's fields to HL7 paths once, in the type definition, instead of writing the same accessor calls at every call site — and keep a door open to the parsed message for the day the feed changes.
When struct mode is the right answer
Struct mode is the third of three parsing modes, and it is the
one to reach for once an interface has held still long enough to trust. The payoff is that the
compiler carries the mapping: a typo in a path is a build failure rather than a None at three in the morning.
The risk is the mirror image. A struct is a claim that you know what arrives, and real feeds
are stable until they are not. That is what #[hl7(raw)] is for,
and it is why you should keep it even when you are sure you do not need it.
Enabling it
[dependencies]
hl7-2 = { version = "0.2", features = ["derive"] }
# Or through the umbrella crate, which forwards the feature:
hl7 = { version = "0.1", features = ["derive"] }The macros live in hl7-2-derive, which you do
not depend on directly. Keeping them in a crate of their own is what lets the default build of hl7-2 hold exactly one dependency: syn and quote are
compiled only for callers who ask for the macros.
Reading: FromHl7
use hl7_2::{FromHl7, Raw};
#[derive(FromHl7)]
struct Admission {
#[hl7("PID-1")] sequence: u32,
#[hl7("PID-3")] identifiers: Vec<String>,
#[hl7("PID-5.1.1")] family_name: String,
#[hl7("PID-8")] sex: Option<String>,
#[hl7(nested)] visit: Visit, // its own FromHl7
#[hl7(raw)] raw: Raw, // the whole message, kept alongside
}
let admission: Admission = hl7_2::parse(text)?.decode()?;
assert_eq!(admission.family_name, "EVERYWOMAN");decode is a method on the parsed message, so nothing is re-read from text — the
struct is filled in from the tree you already have.
The attributes
One attribute per field.
| Attribute | On read | On write |
|---|---|---|
#[hl7("PID-5.1")] | Read the path. | Write the path. |
#[hl7(nested)] | The field's own FromHl7. | The field's own ToHl7. |
#[hl7(raw)] | The whole message, as a Raw. | Skipped. |
| none | Default::default() | Skipped. |
#[hl7(nested)] is how a large message becomes several small types rather than one
struct with forty fields — a Visit, a Patient, an Order, each deriving FromHl7 in its own right.
Field types
Field types convert through hl7_2::FromHl7Value and ToHl7Value: String, bool, the integer and floating-point
types, and Option<T> and Vec<T> of those.
Option<T>for a value that may be absent.Vec<T>for a value that repeats.- A plain type is required — a path that names nothing gives
Error::MissingField.
For a domain type of your own, implement hl7_2::FromHl7Text; then Option and Vec of it follow automatically.
The raw escape hatch
// The one vendor field no struct models — same object, no second parse.
assert_eq!(admission.raw.get("ZPD-1")?.as_deref(), Some("local"));A #[hl7(raw)] field keeps the whole parsed message beside the typed data. When a
message arrives with something the struct does not model, the fallback is a method call on the
object you already have — no second parse, no rewrite, no redeployment to add a field you did
not know about.
This is the same object you would have had in generic mode, so everything in Navigating applies to it.
Writing: ToHl7
#[derive(ToHl7)] is the reverse direction, and it has one requirement worth
knowing up front: writing needs the segments to exist already.
use hl7_2::{FromHl7, ToHl7};
#[derive(FromHl7, ToHl7)]
struct Admission {
#[hl7("PID-1")] sequence: u32,
#[hl7("PID-8")] sex: Option<String>,
}
// Writing needs the segments to exist already.
let message = hl7_2::Builder::new(hl7_2::Version::V2_5)
.message_type("ADT", "A01")
.control_id("MSG00042")
.segment("PID")
.encode(&admission)
.build_valid()?;// Or add the segment to a message you already hold:
let mut message = hl7_2::parse(text)?;
message.append_segment("ZPD");
message.set("ZPD-1", "local")?;Builder::encode takes any ToHl7, so a struct is written into a message
under construction. build_valid then runs validation and refuses to hand back something the dictionary
says is malformed.
Errors
match hl7_2::parse(text)?.decode::<Admission>() {
Ok(admission) => { /* every required path was present and fit */ }
Err(hl7_2::Error::MissingField { path }) => { /* a plain field's path named nothing */ }
Err(hl7_2::Error::BadValue { path, expected, found }) => { /* present, wrong shape */ }
Err(other) => { /* a bad path, or a message-level problem */ }
}The two struct-specific variants say different things. MissingField means a non-optional field's path named nothing — usually a sign the
field should have been Option. BadValue means the value was present
but did not fit the Rust type, and carries the path, what was expected, and what was found,
which is normally enough to identify the sender's bug without opening the message.
The v3 equivalent, and how it differs
hl7-3 has its own derive, behind its own derive feature, mapping struct fields to XML attributes and children instead of
HL7 paths.
use hl7_3::FromElement;
use hl7_3::rim::Act;
#[derive(FromElement, Default)]
struct Observation {
#[element("classCode")] class_code: String,
#[element("moodCode")] mood_code: String,
#[element(child = "note")] note: Option<String>,
#[element(nested = "component")] component: Act,
#[element(raw)] raw: hl7_3::xml::Element,
}
let element = hl7_3::xml::parse(xml)?;
let observation = Observation::from_element(&element); // no Result
assert_eq!(observation.class_code, "OBS");| Attribute | Reads |
|---|---|
#[element("classCode")] | The classCode attribute. |
#[element(child = "note")] | The note child's text. |
#[element(nested = "component")] | The component child, via the field type's own FromElement. |
#[element(raw)] | The whole element, as hl7_3::xml::Element. |
| none | Default::default() |
Two differences from the v2 macro are deliberate and worth stating plainly:
- No
Resultanywhere. A missing attribute or child is not an error — it reads as that field'sDefault, matching the degrade-don't-reject choicehl7-3's ownrimtypes make. - No
Vec<T>support yet. A repeating child needselement.children_named(...)by hand for now. - No
#[derive(ToElement)].hl7-3has no XML-writing capability yet, so a write-direction macro would have nothing real to generate.
Note that the umbrella crate's derive feature forwards to hl7-2's
only. For the v3 macro, enable the feature on hl7-3.