# er7
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-1 |
MSH-2 ^~\&
MSH-3 LAB
MSH-9.1 ORU
MSH-9.2 R01
MSH-9.3 ORU_R01
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 path in that outline can be pasted straight back in:
```sh
# Pull out specific values
er7 --query PID-5.1 --query OBX-5 samples/oru_r01.er7
# Rewrite as canonical ER7, with line feeds so a terminal can show it
er7 --normalize --terminator lf samples/oru_r01.er7
# Read a batch file, take its second message
`er7 --help` lists the rest.
## Library
```rust
assert_eq!(message.control_id().as_deref(), Some("MSG00042"));
assert_eq!(message.query("PID-5.1")?.as_deref(), Some("EVERYWOMAN"));
assert_eq!(message.query("OBX-3.2")?.as_deref(), Some("Cholesterol"));
// What went in comes back out, byte for byte.
assert_eq!(message.to_er7(), text);
```
`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 the
structure cannot be broken by a value:
```rust
let separators = message.separators;
message
.segment_at_mut("PID", 1).unwrap()
.field_mut(5).unwrap()
.repetition_mut(1).unwrap()
.component_mut(1).unwrap()
.subcomponent_mut(1).unwrap()
.set("O'BRIEN & SONS", &separators);
assert!(message.to_er7().contains(r"O'BRIEN \T\ SONS"));
assert_eq!(message.query("PID-5.1")?.as_deref(), Some("O'BRIEN & SONS"));
```
The whole escape-sequence vocabulary is available directly when you need
it: `er7::escape::escapes` tokenizes and classifies every sequence, and
`unescape` / `escape` are the two passes built on it.
## 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.
- **HL7 paths**: `PID-5.1`, `OBX[2]-5`, `PID-13[2].1`, in either the
`PID-5.1` or `PID.5.1` spelling.
- **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`/`FTS` envelopes are recognized and
messages come out one at a time.
- **Nothing fails except a missing header**: unknown segments, local `Z`
segments, 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.
For the v2.5 dictionary layer, see the sibling crate
[hl7-2-5-to-xml-using-rust](https://github.com/joelparkerhenderson/hl7-2-5-to-xml-using-rust),
which converts ER7 to the official v2.xml representation.
## Documentation
- [`spec/index.md`](spec/index.md) — the normative specification (source of
truth for behavior)
- [`spec/er7-format.md`](spec/er7-format.md) — the ER7 format itself:
delimiters, escape sequences, batch files, and why it persists
- `cargo doc --no-deps --open` — the library API
- [`AGENTS.md`](AGENTS.md) — conventions and required checks for anyone,
human or agent, changing this code
- [`samples/`](samples/) — example messages: a lab result, an admission
update with a `Z` segment, and a batch file
## Development
```sh
cargo test # unit, integration, and doc tests
cargo clippy --all-targets -- -D warnings # lint-clean
cargo fmt --check # formatting
cargo rustdoc --lib -- -W missing-docs # every public item documented
cargo run -- samples/oru_r01.er7
```
## License
MIT OR Apache-2.0, at your option.