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}