pub struct Report {
pub changes: Vec<Change>,
}Expand description
What a redaction did: one entry per position that changed.
Entries are in the order the changes were made — rule by rule, and in
message order within each rule (spec §8.4). A rule that matched nothing
contributes none, and neither does an Action::Keep, an empty leaf,
or a null one.
Example:
use er7_redact::{Action, Policy, Redactor};
let mut message = er7::parse("MSH|^~\\&|LAB\rPID|1||9||SMITH^JOHN")?;
let policy = Policy::accept_all().with("PID-5", Action::redacted())?;
let report = Redactor::new(policy).redact(&mut message);
// One row per leaf that actually changed.
let rows: Vec<String> = report.changes.iter().map(|c| c.to_string()).collect();
assert_eq!(rows, [
"PID[1]-5[1].1.1 replace REDACTED",
"PID[1]-5[1].2.1 replace REDACTED",
]);Fields§
§changes: Vec<Change>The changes, in the order they were made.
Implementations§
Source§impl Report
impl Report
Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
True when nothing changed — which means either that the message carried none of the positions the policy names, or that the policy is wrong. The crate does not presume to say which (spec §2.5).
Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
How many positions changed.
Examples found in repository?
examples/redact_a_message.rs (line 17)
7fn main() -> Result<(), er7_redact::Error> {
8 let text = "MSH|^~\\&|ADT1|MCM|LABADT|MCM|20260815140000||ADT^A08|MSG00001|P|2.5\r\
9 PID|1||PATID1234^^^ADT1^MR||EVERYWOMAN^EVE^E||19610615|F|||\
10 1200 N ELM STREET^^GREENSBORO^NC^27401-1020||(919)379-1212\r\
11 OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|100-199|H|||F";
12
13 let mut message = er7::parse(text)?;
14 let report = Redactor::new(Policy::patient_identifiers()).redact(&mut message);
15
16 println!("{}\n", message.to_er7().replace('\r', "\n"));
17 println!("{} positions changed:\n{report}", report.len());
18
19 // The identifiers are gone.
20 assert_eq!(
21 message.query("PID-5")?.as_deref(),
22 Some("REDACTED^REDACTED^REDACTED")
23 );
24 assert_eq!(message.query("PID-7")?.as_deref(), Some("1961"));
25 assert_eq!(message.query("PID-11")?.as_deref(), Some("^^^^"));
26 assert_eq!(message.query("PID-13")?.as_deref(), Some(""));
27 assert_ne!(message.query("PID-3.1")?.as_deref(), Some("PATID1234"));
28
29 // The message is not.
30 assert_eq!(message.control_id().as_deref(), Some("MSG00001"));
31 assert_eq!(message.query("PID-3.4")?.as_deref(), Some("ADT1")); // assigning authority
32 assert_eq!(message.query("PID-8")?.as_deref(), Some("F")); // not an identifier
33 assert_eq!(message.query("OBX-5")?.as_deref(), Some("187")); // the clinical content
34 assert!(er7::parse(&message.to_er7()).is_ok());
35
36 // And the shape did not move: every position that held a value still
37 // exists, so a test that asserted on PID-11.3 still finds it.
38 assert_eq!(message.query("PID-11.3")?.as_deref(), Some(""));
39
40 Ok(())
41}More examples
examples/read_the_report.rs (line 47)
8fn main() -> Result<(), er7_redact::Error> {
9 let text = "MSH|^~\\&|ADT1|MCM||||||ADT^A08|MSG1|P|2.5\r\
10 PID|1||PATID1234^^^ADT1^MR||EVERYWOMAN^EVE^E||19610615|F|||\
11 12 ELM ST^^BOSTON^MA^02101||555-555-1111~555-555-2222\r\
12 NK1|1|EVERYMAN^ADAM|SPO";
13
14 let mut message = er7::parse(text)?;
15 let report = Redactor::new(Policy::patient_identifiers()).redact(&mut message);
16
17 for change in &report.changes {
18 println!("{:<18} {}", change.path.to_string(), change.action);
19 }
20
21 // One row per leaf that actually changed — so a name in three
22 // components is three rows, and a repeated field is one row per
23 // repetition.
24 let paths: Vec<String> = report.changes.iter().map(|c| c.path.to_string()).collect();
25 assert!(paths.contains(&"PID[1]-5[1].1.1".to_string()));
26 assert!(paths.contains(&"PID[1]-5[1].3.1".to_string()));
27 assert!(paths.contains(&"PID[1]-13[2].1.1".to_string()));
28
29 // Every path is fully qualified, which means it is also a valid
30 // `er7 --query` argument: paste one in to see what is there now.
31 for change in &report.changes {
32 assert!(message.query(&change.path.to_string()).is_ok());
33 }
34
35 // A report carries no values — not the old text, and not the new. A
36 // log line quoting the old value puts the patient's name into the log.
37 let printed = report.to_string();
38 for value in ["EVERYWOMAN", "PATID1234", "19610615", "BOSTON"] {
39 assert!(!printed.contains(value));
40 }
41
42 // And nothing that was not there contributes a row: this message has
43 // no GT1 segment, so the guarantor rules matched nothing, which is not
44 // an error.
45 assert!(!paths.iter().any(|p| p.starts_with("GT1")));
46
47 println!("\n{} positions changed", report.len());
48 Ok(())
49}examples/redact_absent_empty_null.rs (line 27)
11fn main() -> Result<(), er7_redact::Error> {
12 // PID-1 has a value, PID-2 was sent blank, PID-3 is the explicit null,
13 // and PID-4 onwards was never sent at all.
14 let text = "MSH|^~\\&|LAB\rPID|1||\"\"";
15
16 let policy = Policy::accept_all()
17 .with("PID-1", Action::redacted())?
18 .with("PID-2", Action::redacted())?
19 .with("PID-3", Action::redacted())?
20 .with("PID-9", Action::redacted())?;
21
22 let mut message = er7::parse(text)?;
23 let report = Redactor::new(policy).redact(&mut message);
24
25 // Only the field that carried a value changed.
26 assert_eq!(message.to_er7(), "MSH|^~\\&|LAB\rPID|REDACTED||\"\"");
27 assert_eq!(report.len(), 1);
28
29 // Why each of the other three was left alone:
30 //
31 // PID-2 was empty. Writing REDACTED into it would invent a value, and
32 // would announce that one used to be there — which is a disclosure.
33 assert!(message.segment("PID").unwrap().field(2).unwrap().is_empty());
34 //
35 // PID-3 is the explicit null: an instruction to the receiver to clear
36 // its stored value, not patient data. Overwriting it would turn
37 // "clear this" into a value, and leave a withdrawn record standing.
38 assert!(message.segment("PID").unwrap().field(3).unwrap().is_null());
39 //
40 // PID-9 was never sent. Redaction does not lengthen a segment to reach
41 // a position that is not there: padding would change what the message
42 // says, and eleven new trailing pipes would announce the redaction.
43 assert!(message.segment("PID").unwrap().field(9).is_none());
44
45 // To *make* a position null — to tell the receiver to clear it — ask
46 // for that, which is the one action that changes the shape of a
47 // message, because an HL7 null is a single `""`.
48 let mut message = er7::parse("MSH|^~\\&|LAB\rPID|1||9||SMITH^JOHN")?;
49 let policy = Policy::accept_all().with("PID-5", Action::Null)?;
50 Redactor::new(policy).redact(&mut message);
51 assert_eq!(message.to_er7(), "MSH|^~\\&|LAB\rPID|1||9||\"\"");
52
53 // Compare with `clear`, which says nothing rather than saying "delete".
54 let mut message = er7::parse("MSH|^~\\&|LAB\rPID|1||9||SMITH^JOHN")?;
55 let policy = Policy::accept_all().with("PID-5", Action::Clear)?;
56 Redactor::new(policy).redact(&mut message);
57 assert_eq!(message.to_er7(), "MSH|^~\\&|LAB\rPID|1||9||^");
58
59 println!("absent, empty, and null all survived redaction unchanged");
60 Ok(())
61}examples/reject_by_default.rs (line 49)
12fn main() -> Result<(), er7_redact::Error> {
13 let text = "MSH|^~\\&|LAB|ACME|EHR|CLINIC|20260815120000||ORU^R01|MSG9|P|2.5\r\
14 PID|1||PATID1234||EVERYWOMAN^EVE||19610615|F\r\
15 OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|100-199|H|||F\r\
16 ZPD|1|LOCAL^EXTENSION^SEGMENT";
17
18 // Rejecting by default covers every leaf no rule named; a `keep` rule
19 // accepts a position, exempting it from that. A rejecting rule would
20 // beat the `keep` whichever order the two were written in (D19).
21 let policy = Policy::all_but_the_header()
22 .with("OBX-2", Action::Keep)? // value type
23 .with("OBX-3", Action::Keep)? // observation identifier
24 .with("OBX-5", Action::Keep)? // the number the test asserts on
25 .with("OBX-6", Action::Keep)?; // units
26
27 let mut message = er7::parse(text)?;
28 let report = Redactor::new(policy).redact(&mut message);
29 println!("{}\n", message.to_er7().replace('\r', "\n"));
30
31 // The header is untouched, so the message still routes and still says
32 // which version it is.
33 assert_eq!(message.query("MSH-9")?.as_deref(), Some("ORU^R01"));
34 assert_eq!(message.version().as_deref(), Some("2.5"));
35
36 // The result is intact, because four rules said so.
37 assert_eq!(message.query("OBX-3.2")?.as_deref(), Some("Cholesterol"));
38 assert_eq!(message.query("OBX-5")?.as_deref(), Some("187"));
39
40 // Everything else is gone — including the local segment, which no
41 // curated policy could have known about.
42 assert_eq!(message.query("PID-5.1")?.as_deref(), Some("REDACTED"));
43 assert_eq!(message.query("ZPD-2.1")?.as_deref(), Some("REDACTED"));
44
45 // The cost: values a positional policy would have kept are gone too.
46 assert_eq!(message.query("PID-8")?.as_deref(), Some("REDACTED"));
47 assert_eq!(message.query("OBX-8")?.as_deref(), Some("REDACTED"));
48
49 println!("{} positions changed", report.len());
50
51 // And what this posture is really for: a payload that is not ER7 at
52 // all has no positions to name, so the policy says outright what
53 // becomes of it. The curated policy above refuses one, which is what
54 // makes the CLI exit non-zero rather than write it out.
55 let redactor = Redactor::new(Policy::all_but_the_header());
56 assert_eq!(redactor.unrecognised("{\"name\": \"EVERYWOMAN\"}"), None);
57
58 // `Policy::reject_all` masks it whole instead — nothing routable
59 // survives, and neither does anything else.
60 let redactor = Redactor::new(Policy::reject_all());
61 assert_eq!(
62 redactor.unrecognised("EVERYWOMAN").as_deref(),
63 Some("**********")
64 );
65 Ok(())
66}Trait Implementations§
impl Eq for Report
impl StructuralPartialEq for Report
Auto Trait Implementations§
impl Freeze for Report
impl RefUnwindSafe for Report
impl Send for Report
impl Sync for Report
impl Unpin for Report
impl UnsafeUnpin for Report
impl UnwindSafe for Report
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more