Skip to main content

write_a_policy/
write_a_policy.rs

1//! Three ways to say what to redact: build a policy in Rust, read one
2//! from a file, or start from a built-in and edit it.
3//!
4//! Run with: `cargo run --example write_a_policy`
5
6use er7_redact::{Action, Policy, Posture, Redactor, Unrecognised};
7
8fn main() -> Result<(), er7_redact::Error> {
9    // 1. Built in Rust, rule by rule. Order matters: rules apply in the
10    //    order they are listed. `accept_all` is the starting point that
11    //    redacts nothing until a rule says so — the other one is
12    //    `reject_all`, which redacts everything until a `keep` rule says
13    //    otherwise.
14    let built = Policy::accept_all()
15        .with("PID-3.1", Action::Pseudonym)?
16        .with("PID-5", Action::redacted())?
17        .with("PID-7", Action::First(4))?
18        .with("PID-19", Action::Clear)?;
19
20    // 2. Read from a policy file — the same thing, in the form a team
21    //    reviews in a pull request.
22    let read = Policy::parse(
23        "
24        PID-3.1  pseudonym    # keep linkage, lose the record number
25        PID-5    replace REDACTED
26        PID-7    first 4      # the birth year is enough for most tests
27        PID-19   clear
28        ",
29    )?;
30    assert_eq!(built.rules, read.rules);
31
32    // Both accept by default: a position no rule names is left alone.
33    assert_eq!(built.posture, Posture::Accept);
34    assert_eq!(read.posture, Posture::Accept);
35
36    // They differ on one thing, and it is worth knowing about. A payload
37    // that is not ER7 at all has no positions in it, so no rule can speak
38    // to it. `accept_all` passes one through, because it is a policy that
39    // redacts nothing and says so. A policy *file* that mentions no
40    // disposition refuses one instead: it was written by somebody who may
41    // simply not have considered the case, and refusing loses no value
42    // quietly.
43    assert_eq!(built.unrecognised, Unrecognised::Pass);
44    assert_eq!(read.unrecognised, Unrecognised::Refuse);
45
46    // Either way, say it outright and the two agree.
47    let built = built.on_unrecognised(Unrecognised::Refuse);
48    assert_eq!(built, read);
49
50    // 3. Start from a built-in and add to it. `--show-policy` on the
51    //    command line writes the built-in out as a file to edit.
52    let extended = Policy::patient_identifiers()
53        .with("NTE-3", Action::Clear)? // free text: nothing positional finds what is in here
54        .with("OBX-5", Action::Clear)?;
55    assert_eq!(
56        extended.rules.len(),
57        Policy::patient_identifiers().rules.len() + 2
58    );
59
60    // A policy writes itself back out in the file format, so the one that
61    // ran can be recorded beside the message it redacted.
62    println!("{built}");
63    assert_eq!(Policy::parse(&built.to_string())?, built);
64
65    let text = "MSH|^~\\&|LAB\rPID|1||PATID1234||EVERYWOMAN^EVE||19610615|F";
66    let mut message = er7::parse(text)?;
67    Redactor::new(built).redact(&mut message);
68    assert_eq!(message.query("PID-5.1")?.as_deref(), Some("REDACTED"));
69
70    // A malformed policy is rejected at load time, with the line number:
71    // a typo here means a value that silently was not redacted.
72    let error = Policy::parse("PID-5 obfuscate").unwrap_err();
73    println!("{error}");
74    assert!(error.to_string().contains("policy line 1"));
75
76    Ok(())
77}