er7
website • documentation • source • crate • email
Parse, query, edit, and write HL7 v2 messages in the ER7 pipe-hat encoding — as a Rust library and a command-line tool, with zero dependencies.
MSH|^~\&|LAB|ACME|EHR|CLINIC|20260815081500||ORU^R01^ORU_R01|MSG00042|P|2.5
PID|1||444333222^^^ACME&1.2.840.114398.1.100&ISO^MR||EVERYWOMAN^EVE^E||19620320|F
OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|<200|N|||F
ER7 is compact, ubiquitous, and unforgiving: a value's meaning comes
entirely from its position, so one misplaced | silently shifts everything
after it. This crate makes that structure explicit and keeps it intact.
Contents
- Install
- Command line
- Library
- What it does
- What it deliberately does not do
- Documentation
- Development
- License
Install
Or for the command-line tool:
Command line
# Show every value with the HL7 path that names it
MSH-1 |
MSH-2 ^~\&
MSH-3 LAB
MSH-9.1 ORU
MSH-9.2 R01
MSH-9.3 ORU_R01
MSH-10 MSG00042
MSH-11 P
MSH-12 2.5
PID-3.1 444333222
PID-3.4.1 ACME
PID-3.4.2 1.2.840.114398.1.100
PID-5.1 EVERYWOMAN
PID-5.2 EVE
PID-13[1] 555-555-1111
PID-13[2] 555-555-2222
OBX[1]-3.2 Cholesterol
OBX[1]-5 187
OBX[2]-3.2 Triglycerides
Every label in that outline is a valid query, so you can read a path off the output and paste it straight back in:
# Pull out specific values
# Show text exactly as sent, without decoding escape sequences
# Rewrite as canonical ER7, with line feeds so a terminal can show it
# Read a batch file, take its second message
|
er7 --help lists the rest. The full contract is
spec §12.
Library
let text = "MSH|^~\\&|LAB|ACME|EHR|CLINIC|20260815081500||ORU^R01|MSG00042|P|2.5\r\
PID|1||444333222^^^ACME^MR||EVERYWOMAN^EVE^E||19620320|F\r\
OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|||||F";
let message = parse?;
assert_eq!;
assert_eq!;
assert_eq!;
// What went in comes back out, byte for byte.
assert_eq!;
query returns the first match and query_all returns every one, so
OBX-5 across a result with three observations gives three values.
Editing goes through set, which encodes delimiters on the way in, so a
value can never break the structure that holds it:
let separators = message.separators;
message
.segment_at_mut.unwrap
.field_mut.unwrap
.repetition_mut.unwrap
.component_mut.unwrap
.subcomponent_mut.unwrap
.set;
assert!;
assert_eq!;
There is a runnable program for each of these in
examples/, and a step-by-step walk-through in
docs/usage/.
What it does
- Full hierarchy: message, segment, field, repetition (
~), component (^), subcomponent (&). - Delimiters from the message: MSH-1 and MSH-2 are read, never assumed,
including the truncation character HL7 v2.7 added. A message that uses
#*!?@parses as happily as one that uses|^~\&. - Round trip, byte for byte: text is stored exactly as it arrived and decoded only when you ask for a value, so a message survives a trip through this crate unchanged — unusual delimiters, unknown segments, empty positions, escape sequences and all.
- Escape sequences: the whole vocabulary —
\F\ \S\ \T\ \R\ \E\,\Xdd..\,\H\,\N\,\Zdd..\,\Cxxyy\,\Mxxyyzz\,\.br\— is tokenized and classified. The ones that stand for characters decode; the ones that describe presentation are preserved as written. Seedocs/escapes/. - HL7 paths:
PID-5.1,OBX[2]-5,PID-13[2].1, in either thePID-5.1orPID.5.1spelling. Seedocs/paths/. - Absent, empty, and null are three different answers, not one — the
explicit
""means clear this value, and losing that distinction corrupts patient records. - Batch files:
FHS/BHS/BTS/FTSenvelopes are recognized and messages come out one at a time, as borrowed slices of the input. - Nothing fails except a missing header: unknown segments, local
Zsegments, ragged field counts, and stray positions are data, not errors.
What it deliberately does not do
This crate is an encoding, not a dictionary. It does not know which fields a segment has, what data type each carries, which message structures exist, or what any code table means — all of that is version-specific and belongs in a layer above. It performs no validation, and no transport (MLLP framing is a separate concern).
The one exception is a handful of MSH accessors — message_code,
trigger_event, message_structure, control_id, version — because
routing a message requires reading them and those positions have never moved
in any HL7 v2 release. The reasoning, and what was declined, is in
spec §10.
The crate family
er7 is the bottom of a stack; each layer above it is a separate crate, so
a caller pays only for what they use.
| Crate | Adds |
|---|---|
er7-redact |
redaction: remove patient detail without changing the shape of the message |
serde-er7 |
Serde support, so a message tree can travel as JSON, YAML, or any other format |
hl7-2-5-to-xml / hl7-2-5-to-json |
the HL7 v2.5 dictionary: data types, message structures, and a renderer |
All four are presented together at https://er7-rust.github.io/ecosystem/, and the boundary between them is spec §1.3.1.
Documentation
| Where | What |
|---|---|
docs/usage/ |
tutorial: from a string to values, edits, and back |
docs/paths/ |
HL7 path notation, in full |
docs/escapes/ |
escape sequences, with worked examples |
docs/api/ |
the complete public API surface |
docs/faq/ |
frequently asked questions |
examples/ |
runnable programs, one concept each |
spec/ |
the normative specification — source of truth for behaviour |
spec/02-er7-encoding.md |
the ER7 format itself, independent of this crate |
AGENTS.md |
conventions and required checks for anyone, human or agent, changing this code |
samples/ |
example messages: a lab result, an admission update with a Z segment, a batch file |
Rendered API docs are at https://docs.rs/er7/, or locally with
cargo doc --no-deps --open. The same material, presented for the web, is
at https://er7-rust.github.io/.
Development
Behavioural changes start in spec/, not in the code —
see AGENTS/spec-driven-development.md.
License
Multi-licensed, so a downstream project can pick whichever fits: MIT, Apache-2.0, BSD-3-Clause, GPL-2.0-only, or GPL-3.0-only. See LICENSE.md.