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

toml
[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

rust
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.

AttributeOn readOn 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.
noneDefault::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

rust
// 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.

rust
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()?;
rust
// 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

rust
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.

rust
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");
AttributeReads
#[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.
noneDefault::default()

Two differences from the v2 macro are deliberate and worth stating plainly:

  • No Result anywhere. A missing attribute or child is not an error — it reads as that field's Default, matching the degrade-don't-reject choice hl7-3's own rim types make.
  • No Vec<T> support yet. A repeating child needs element.children_named(...) by hand for now.
  • No #[derive(ToElement)]. hl7-3 has 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.