er7-redact 0.1.1

Redact patient detail from HL7 v2 messages in the ER7 pipe-hat encoding, without changing the shape of the message
Documentation
  • Coverage
  • 100%
    50 out of 50 items documented15 out of 29 items with examples
  • Size
  • Source code size: 228.95 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.12 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • joelparkerhenderson

er7-redact

websitedocumentationsourcecrateemail

Remove patient detail from HL7 v2 messages in the ER7 pipe-hat encoding — as a Rust library and a command-line tool — without breaking the message.

PID|1||PATID1234^5^M11^ADT1^MR^MCM||JONES^WILLIAM^A^III||19610615|M||C|1200 N ELM STREET^^GREENSBORO^NC^27401-1020

becomes

PID|1||11a9d74f8a6a54a7^5^M11^ADT1^MR^MCM||REDACTED^REDACTED^REDACTED^REDACTED||1961|M||C|^^^^

Same segments, same fields, same components, same delimiters. Every path that resolved to a value still resolves to one, so the interface engine, the test harness, and the message viewer downstream all behave the way they did on the original.

Contents

Install

cargo add er7-redact

Or for the command-line tool:

cargo install er7-redact

Command line

# Redact with the built-in policy
er7-redact samples/adt_a08.er7
MSH|^~\&|ADT1|MCM|LABADT|MCM|20260815140000||ADT^A08^ADT_A01|MSG00001|P|2.5
EVN|A08|20260815140000
PID|1||11a9d74f8a6a54a7^5^M11^ADT1^MR^MCM||REDACTED^REDACTED^REDACTED^REDACTED||1961|M||C|^^^^
NK1|1|REDACTED^REDACTED^REDACTED|SPO^Spouse^HL70063|^^^^|
PV1|1|I|2000^2012^01||||REDACTED^REDACTED^REDACTED^REDACTED|||SUR||||ADM|A0||||63d6f85fb1af0958
AL1|1|DA|1605^ACETAMINOPHEN^L|MO|HEADACHE
# Say what would change, and change nothing
er7-redact --report samples/adt_a08.er7
PID[1]-3[1].1.1   pseudonym
PID[1]-5[1].1.1   replace REDACTED
PID[1]-5[1].2.1   replace REDACTED
PID[1]-7[1].1.1   first 4
PID[1]-11[1].1.1  clear

The report carries paths and actions and no values, so it can go straight into a ticket.

er7-redact --show-policy > de-identify.policy   # the built-in list, as a file to edit
er7-redact -p de-identify.policy message.er7    # apply it
er7-redact -r "NTE-3 clear" message.er7         # or one rule, inline
er7-redact --all message.er7                    # redact everything except MSH

Library

use er7_redact::{Policy, Redactor};

let mut message = er7::parse(text)?;
let report = Redactor::new(Policy::patient_identifiers()).redact(&mut message);

println!("{}", message.to_er7());
println!("{report}");

A policy is an ordered list of rules — an HL7 path and an action:

use er7_redact::{Action, Policy};

let policy = Policy::new()
    .with("PID-3.1", Action::Pseudonym)?   // stable stand-in, so messages still join
    .with("PID-5", Action::redacted())?    // REDACTED^REDACTED^REDACTED
    .with("PID-7", Action::First(4))?      // 19610615 → 1961
    .with("PID-11", Action::Clear)?        // ^^^^
    .with("PID-19", Action::Null)?;        // "" — tell the receiver to clear its copy

…or the same thing as a file, which is what a team reviews in a pull request:

PID-3.1  pseudonym
PID-5    replace REDACTED
PID-7    first 4       # the birth year is enough for most tests
PID-11   clear
PID-19   null

Invert it — redact everything, name what to keep — when the message is unfamiliar:

let policy = Policy::everything()
    .with("OBX-3", Action::Keep)?
    .with("OBX-5", Action::Keep)?;

What it does

Preserves the shape Leaf text is rewritten; no segment, field, repetition, component, or subcomponent is added or removed. Null is the one documented exception.
Keeps absent, empty, and null apart An empty field stays empty — writing REDACTED into it would invent a value. An explicit "" stays null — overwriting it would turn "clear this" into a value.
Never creates a position A rule for a field the message does not carry does nothing, rather than padding the segment out to reach it.
Cannot corrupt the message Replacement text goes in escaped, so a | in a placeholder can never split a field.
Eight actions keep, clear, null, replace, mask, first, last, pseudonym.
Stable pseudonyms The same identifier maps the same way in every message redacted with the same key, so a redacted export is still joinable.
Reports what it did One row per position changed, fully qualified, with no values in it.
One dependency er7, which has none of its own.

What it deliberately does not do

This is a positional editor, not a compliance tool.

  • It cannot tell you whether the result is de-identified. That is a judgement about a whole data set, its recipients, and what else they hold — made by a person who is accountable for it.
  • It does not know which positions your senders use. Run er7 message.er7 and read what is actually in there.
  • It does not find an identifier written into free text. A name in an NTE-3 comment survives every positional policy; name that position, or use --all.
  • pseudonym is not cryptographic. It is a keyed hash that preserves equality on purpose, and anyone with the key can invert it. Use it inside your own trust boundary; for data leaving it, clear or replace.
  • There is no way back: no mapping table, no key escrow, no undo.

A message this crate has redacted is a message with less in it, which is progress, and is not the same thing as a safe one.

Documentation

Where What
spec/ the normative specification — one file per section, rules D1D18
docs/usage/ the walk-through
docs/policies/ the policy format, the actions, the built-in tables
docs/api/ every public item
docs/faq/ the questions the rest raise
examples/ runnable programs that assert their own results
AGENTS.md how to change this code

Development

cargo test                                  # unit, integration, and doc tests
cargo clippy --all-targets -- -D warnings   # lint
cargo fmt --check                           # format
cargo rustdoc --lib -- -W missing-docs      # every public item documented

All four are clean on main and must stay that way. Behavioural changes start in spec/ — see AGENTS/spec-driven-development.md.

Every message in this repository is synthetic, and must stay that way: a repository about redaction is exactly where somebody would be tempted to commit a real one. See AGENTS/safety.md.

See also

  • er7 — parse, query, edit, and write ER7, with zero dependencies. The layer underneath this one.
  • serde-er7 — Serde support for the same value tree.
  • hl7-2-5-to-xml and hl7-2-5-to-json — the HL7 v2.5 dictionary layer.

The whole family, and the boundary between the layers, is at https://er7-rust.github.io/ecosystem/; this crate's own tutorial is at https://er7-rust.github.io/er7-redact/.

License

MIT OR Apache-2.0 OR BSD-3-Clause OR GPL-2.0-only OR GPL-3.0-only — see LICENSE.md.