Skip to main content

pseudonyms_and_linkage/
pseudonyms_and_linkage.rs

1//! Why an identifier becomes a pseudonym rather than a blank, what that
2//! buys, and what it costs.
3//!
4//! Run with: `cargo run --example pseudonyms_and_linkage`
5
6use er7_redact::{Action, Policy, Redactor, pseudonym};
7
8// Any u64 will do. This one is a date, which is why its digits are not
9// grouped in threes — see spec §7.2 for what the key does and does not
10// protect.
11#[allow(clippy::unreadable_literal, reason = "the digits are a date")]
12const KEY: u64 = 20260815;
13
14fn main() -> Result<(), er7_redact::Error> {
15    let admit = "MSH|^~\\&|ADT1|MCM||||||ADT^A01|MSG1|P|2.5\r\
16                 PID|1||PATID1234^^^ADT1^MR||EVERYWOMAN^EVE";
17    let result = "MSH|^~\\&|LAB|ACME||||||ORU^R01|MSG2|P|2.5\r\
18                  PID|1||PATID1234^^^ADT1^MR||EVERYWOMAN^EVE\r\
19                  OBX|1|NM|2093-3^Cholesterol^LN||187";
20
21    let redactor = Redactor::new(Policy::patient_identifiers()).with_key(KEY);
22
23    let mut admit = er7::parse(admit)?;
24    let mut result = er7::parse(result)?;
25    redactor.redact(&mut admit);
26    redactor.redact(&mut result);
27
28    // The record number is gone from both...
29    let one = admit.query("PID-3.1")?.expect("a value");
30    let two = result.query("PID-3.1")?.expect("a value");
31    assert_ne!(one, "PATID1234");
32
33    // ...and the two messages still agree that this is the same patient,
34    // which is what makes them useful as a test case.
35    assert_eq!(one, two);
36    println!("both messages now say PID-3.1 = {one}");
37
38    // A different key produces an unrelated mapping, so two data sets
39    // redacted under different keys cannot be joined.
40    let mut other = er7::parse("MSH|^~\\&|ADT1|MCM\rPID|1||PATID1234^^^ADT1^MR")?;
41    Redactor::new(Policy::patient_identifiers())
42        .with_key(1)
43        .redact(&mut other);
44    assert_ne!(other.query("PID-3.1")?.expect("a value"), one);
45
46    // The function is available directly, for building an expected value
47    // in a test.
48    assert_eq!(pseudonym(KEY, "PATID1234"), one);
49
50    // What it costs. A pseudonym preserves equality on purpose, so anyone
51    // holding the redacted data can count how many messages each patient
52    // generated — and anyone holding the key can invert the mapping by
53    // trying every candidate identifier, because record numbers come from
54    // small spaces. Inside your own trust boundary, that is a fair trade;
55    // for data leaving it, clear the value instead.
56    let mut leaving = er7::parse("MSH|^~\\&|ADT1|MCM\rPID|1||PATID1234^^^ADT1^MR")?;
57    let policy = Policy::patient_identifiers().with("PID-3.1", Action::Clear)?;
58    Redactor::new(policy).redact(&mut leaving);
59    assert_eq!(leaving.query("PID-3.1")?.as_deref(), Some(""));
60
61    Ok(())
62}