er7
The ER7 encoding: parse, query, edit, and write HL7 v2 messages, with a byte-for-byte round trip and no dependencies.
The crate family
One small crate for the encoding, and crates on top of it that each do one job. The split is the point.
ER7 is one small encoding, stable across every HL7 v2 release from v2.1 in 1990 to v2.9. The data-type tables and message structures that give a field its meaning are specific to a version, and there are nine of them.
Fusing the two would mean either shipping one version’s tables to everyone, or shipping all of them to everyone. Keeping them apart means the encoding is maintained and tested once, a dictionary crate chooses its own version, and a user who only needs to route or audit messages pays for nothing.
What sits on top comes in two kinds. A dictionary knows what a position means in one HL7 version. A tool does one job to any message and
needs no dictionary at all: er7-redact removes a patient name knowing only which
position holds it, and serde-er7 moves the tree through a Serde format knowing
only its shape. Neither has to choose an HL7 version, which is why neither is a dictionary.
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ er7-redact serde-er7 │ │ hl7-2-5-to-xml │
│ policies, actions, pseudonyms; │ │ hl7-2-5-to-json │
│ Serde impls for the value tree │ │ v2.5 types, structures, render │
└──────────────────────────────────┘ └──────────────────────────────────┘
Tools — any HL7 version HL7 v2.5 dictionary
│ │
└─────────────────┬───────────────────┘
depends on │
▼
┌──────────────────────────────────────────────────────────────────────┐
│ er7 │
│ delimiters, value tree, escapes, batch input │
└──────────────────────────────────────────────────────────────────────┘ Encoding
│
▼
(no dependencies)er7The ER7 encoding: parse, query, edit, and write HL7 v2 messages, with a byte-for-byte round trip and no dependencies.
er7-redactRemoves patient detail from a message without moving its shape: a policy of HL7 paths and actions, stable pseudonyms, and a report of every position it changed.
serde-er7Gives every er7 type a Serialize and Deserialize implementation, so a message tree can flow through JSON, YAML, or any other Serde data format.
hl7-2-5-to-xmlConverts HL7 v2.5 messages to the official v2.xml representation, using the v2.5 data-type tables and message-structure grammars.
hl7-2-5-to-jsonThe JSON sibling: the same v2.5 tables and grammars, rendered as idiomatic JSON instead of XML.
| Concern | Lives in |
|---|---|
| Delimiters, read from MSH-1 and MSH-2 | er7 |
| The six-level value tree | er7 |
| Escape sequences | er7 |
| Batch splitting | er7 |
| Absent / empty / null | er7 |
| Byte-for-byte round trip | er7 |
| Which positions carry patient detail | er7-redact |
| Masking a value without moving the shape | er7-redact |
| Stable pseudonyms across messages | er7-redact |
| Serialize and deserialize the tree | serde-er7 |
| Which data type each field carries | dictionary crate |
| Composite component names (XPN.1, CX.4, …) | dictionary crate |
| Message-structure grammars and grouping | dictionary crate |
| XML or JSON rendering | dictionary crate |
| MLLP framing, acknowledgements | neither — out of scope |
| Validation of any kind | neither — out of scope |
| Whether a redacted message is safe to share | none — a judgement |
Given PID|1||241900||TEST^FOUAZ, each crate answers a different question about
it.
er7 — what is at this position?assert_eq!(message.query("PID-5.1")?.as_deref(), Some("TEST"));
assert_eq!(message.to_er7(), text); // unchanged er7-redact — what of it can be shared?PID|1||33f9458d5c426675||REDACTED^REDACTED Same fields, same components, same delimiters — the record number is a stable pseudonym, so two messages about this patient still agree they are about the same one. The crate also reports what it changed, as paths and actions with no values in them:
PID[1]-3[1].1.1 pseudonym
PID[1]-5[1].1.1 replace REDACTED
PID[1]-5[1].2.1 replace REDACTED serde-er7 — how does the tree travel?{
"name": "PID",
"fields": [
[[["1"]]],
[],
[[["241900"]]],
[],
[[["TEST"], ["FOUAZ"]]]
]
} Positions, not names: field, repetition, component, subcomponent, nested in that order, with PID-2 an empty array because it was sent empty. Compare it with the hl7-2-5-to-json output below — same message, same format, and the difference
between them is exactly the dictionary.
hl7-2-5-to-xml — what does it mean, as v2.xml?<PID>
<PID.1>1</PID.1>
<PID.3>
<CX.1>241900</CX.1>
</PID.3>
<PID.5>
<XPN.1>
<FN.1>TEST</FN.1>
</XPN.1>
<XPN.2>FOUAZ</XPN.2>
</PID.5>
</PID> The XPN.1 and FN.1 names come from the v2.5 data-type tables — that
is exactly the knowledge er7 does not carry.
hl7-2-5-to-json — the same, as JSON"PID": {
"PID.1": "1",
"PID.3": { "CX.1": "241900" },
"PID.5": {
"XPN.1": { "FN.1": "TEST" },
"XPN.2": "FOUAZ"
}
}If you need a dictionary for a version other than v2.5, a different output format, or a tool
that does one job to any message, er7 is the foundation to build on. The crates
above are worked examples of both shapes: a dictionary holds its version’s tables,
structure grammars, and renderer, while a tool such as er7-redact holds only the
positions it acts on — and both delegate everything about the encoding.
Three things they do that yours probably should:
er7::Error with a From impl, so your public API does not leak the parser.er7 trims nothing because
it guarantees a round trip; a converter that renders XML or JSON can afford to trim, and
should do it before parsing.&Separators through your renderer. Leaf text is
decoded on demand, at the point it becomes output.