Skip to main content

Report

Struct Report 

Source
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

Source

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).

Source

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
Hide additional 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§

Source§

impl Clone for Report

Source§

fn clone(&self) -> Report

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Report

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Report

Source§

fn default() -> Report

Returns the “default value” for a type. Read more
Source§

impl Display for Report

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

One change per line. See Change for the row format.

Source§

impl Eq for Report

Source§

impl PartialEq for Report

Source§

fn eq(&self, other: &Report) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Report

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.