# er7-redact
**[website](https://er7-rust.github.io/er7-redact/)**
•
**[documentation](https://docs.rs/er7-redact/)**
•
**[source](https://github.com/er7-rust/er7-rust/tree/main/er7-redact)**
•
**[crate](https://crates.io/crates/er7-redact)**
•
**[email](mailto:joel@joelparkerhenderson.com)**
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**.
```
becomes
```
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](#install)
- [Command line](#command-line)
- [Library](#library)
- [What it does](#what-it-does)
- [What it deliberately does not do](#what-it-deliberately-does-not-do)
- [Documentation](#documentation)
- [Development](#development)
- [License](#license)
## Install
```sh
cargo add er7-redact
```
Or for the command-line tool:
```sh
cargo install er7-redact
```
## Command line
```sh
# Redact with the built-in policy
er7-redact 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.
```sh
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
```rust
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:
```rust
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:
```rust
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`](https://crates.io/crates/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
| [`spec/`](spec/index.md) | the normative specification — one file per section, rules `D1`–`D18` |
| [`docs/usage/`](docs/usage/index.md) | the walk-through |
| [`docs/policies/`](docs/policies/index.md) | the policy format, the actions, the built-in tables |
| [`docs/api/`](docs/api/index.md) | every public item |
| [`docs/faq/`](docs/faq/index.md) | the questions the rest raise |
| [`examples/`](examples/README.md) | runnable programs that assert their own results |
| [`AGENTS.md`](AGENTS.md) | how to change this code |
## Development
```sh
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/`](spec/index.md) — see
[`AGENTS/spec-driven-development.md`](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`](AGENTS/safety.md).
## See also
- [`er7`](https://github.com/er7-rust/er7-rust/tree/main/er7) — parse,
query, edit, and write ER7, with zero dependencies. The layer underneath
this one.
- [`serde-er7`](https://github.com/er7-rust/er7-rust/tree/main/serde-er7) —
Serde support for the same value tree.
- [`hl7-2-5-to-xml`](https://crates.io/crates/hl7-2-5-to-xml) and
[`hl7-2-5-to-json`](https://crates.io/crates/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](LICENSE.md).