The crate family

Ecosystem

One small crate for the encoding, and crates on top of it that each do one job. The split is the point.

Why the encoding is its own layer

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)

The crates

er7

The ER7 encoding: parse, query, edit, and write HL7 v2 messages, with a byte-for-byte round trip and no dependencies.

Encoding

er7-redact

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

Tool

serde-er7

Gives every er7 type a Serialize and Deserialize implementation, so a message tree can flow through JSON, YAML, or any other Serde data format.

Tool

hl7-2-5-to-xml

Converts HL7 v2.5 messages to the official v2.xml representation, using the v2.5 data-type tables and message-structure grammars.

HL7 v2.5 dictionary

hl7-2-5-to-json

The JSON sibling: the same v2.5 tables and grammars, rendered as idiomatic JSON instead of XML.

HL7 v2.5 dictionary

What each layer owns

ConcernLives in
Delimiters, read from MSH-1 and MSH-2er7
The six-level value treeer7
Escape sequenceser7
Batch splittinger7
Absent / empty / nuller7
Byte-for-byte round triper7
Which positions carry patient detailer7-redact
Masking a value without moving the shapeer7-redact
Stable pseudonyms across messageser7-redact
Serialize and deserialize the treeserde-er7
Which data type each field carriesdictionary crate
Composite component names (XPN.1, CX.4, …)dictionary crate
Message-structure grammars and groupingdictionary crate
XML or JSON renderingdictionary crate
MLLP framing, acknowledgementsneither — out of scope
Validation of any kindneither — out of scope
Whether a redacted message is safe to sharenone — a judgement

Worked example The same message, five ways

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"
  }
}

Building your own layer

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:

  • Keep your own error type and convert from er7::Error with a From impl, so your public API does not leak the parser.
  • Decide your own whitespace policy. 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.
  • Take &Separators through your renderer. Leaf text is decoded on demand, at the point it becomes output.