Skip to main content

Action

Enum Action 

Source
pub enum Action {
    Keep,
    Clear,
    Null,
    Replace(String),
    Mask(char),
    First(usize),
    Last(usize),
    Pseudonym,
}
Expand description

What to do to a value the policy selected.

Every variant except Action::Null rewrites leaf text and leaves the shape of the message alone (D1); Null collapses the position it names to the explicit HL7 null, because that is what a null means (D6, spec §3.4).

Example:

use er7_redact::Action;

// Applied to a decoded value, with the pseudonym key.
assert_eq!(Action::redacted().apply("EVERYWOMAN", 0).as_deref(), Some("REDACTED"));
assert_eq!(Action::Mask('*').apply("EVERYWOMAN", 0).as_deref(), Some("**********"));
assert_eq!(Action::First(4).apply("19610615", 0).as_deref(), Some("1961"));
assert_eq!(Action::Last(4).apply("444333222", 0).as_deref(), Some("3222"));
assert_eq!(Action::Clear.apply("EVERYWOMAN", 0).as_deref(), Some(""));

// `Keep` changes nothing, and so returns nothing to write.
assert_eq!(Action::Keep.apply("EVERYWOMAN", 0), None);

Variants§

§

Keep

Leave the value exactly as it is.

This is the action that accepts a position: its use is to exempt one from a policy that rejects by default (spec §2.6).

It does not undo an earlier rule: rules apply in order to the message as it stands, so once a value has been replaced there is nothing left to restore it from (D7, spec §2.4). That is also why a rejecting rule beats a Keep for the same leaf whichever order the two are in (D19).

§

Clear

Empty the value, leaving the position in place.

This is what redaction usually means. A receiver reads an empty field as “the sender said nothing about this”, which leaves its stored value alone — see Action::Null for the other reading.

§

Null

Replace the named position with the explicit HL7 null "".

A receiver reads this as “clear your stored value”, which is a different instruction from Action::Clear and a much stronger one. It is also the only action that changes the shape of a message: everything beneath the named position is replaced by one subcomponent holding "" (D6, spec §3.4).

§

Replace(String)

Replace the value with fixed text, e.g. REDACTED.

Delimiters in the text are encoded on the way in, so a replacement can never break the message (D11).

§

Mask(char)

Replace each character of the value with this one, preserving the length.

The length is exactly what this leaks; use Action::Clear or Action::Replace where that matters (spec §5.5).

§

First(usize)

Keep the first n characters and drop the rest — a birth date reduced to its year, say. First(0) is legal and equivalent to Action::Clear (spec §3.7).

§

Last(usize)

Keep the last n characters and drop the rest — an account number reduced to the digits a human matches on.

§

Pseudonym

Replace the value with a stable pseudonym, so that equal values stay equal across every message redacted with the same key.

Read crate::pseudonym() before using this: a pseudonym preserves linkage on purpose, and it is not a cryptographic guarantee (D12, spec §7.3).

Implementations§

Source§

impl Action

Source

pub fn redacted() -> Action

Action::Replace with the placeholder the built-in policies use.

Example:

use er7_redact::Action;

assert_eq!(Action::redacted(), Action::Replace("REDACTED".to_string()));
assert_eq!(Action::redacted().to_string(), "replace REDACTED");
Examples found in repository?
examples/redact_absent_empty_null.rs (line 17)
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}
More examples
Hide additional examples
examples/write_a_policy.rs (line 16)
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}
Source

pub fn parse(text: &str) -> Result<Action, Error>

Read an action as a policy file spells it (spec §6.2).

The action name is matched case-insensitively; an argument, where one is allowed, is the rest of the text as written.

Example:

use er7_redact::Action;

assert_eq!(Action::parse("clear")?, Action::Clear);
assert_eq!(Action::parse("first 4")?, Action::First(4));
assert_eq!(Action::parse("replace NOT ON FILE")?,
           Action::Replace("NOT ON FILE".to_string()));

// The two arguments that may be left out have a default.
assert_eq!(Action::parse("replace")?, Action::redacted());
assert_eq!(Action::parse("mask")?, Action::Mask('*'));

assert!(Action::parse("obfuscate").is_err());
assert!(Action::parse("first three").is_err());
§Errors

Error::BadPolicy naming the problem: an unknown action name, an argument where none belongs, a mask argument that is not one character, or a first/last count that is not a number (spec §6.4).

Source

pub fn apply(&self, value: &str, key: u64) -> Option<String>

The text this action writes in place of value, or None to leave the value alone.

value is the leaf’s decoded text, and key is the redactor’s pseudonym key, used only by Action::Pseudonym.

Action::Keep returns None because it writes nothing, and so does Action::Null, which the redactor applies structurally rather than as text (spec §3.4).

Example:

use er7_redact::Action;

// Counting is by character, so an escape that stands for one
// character counts as one.
assert_eq!(Action::First(3).apply("O'BRIEN", 0).as_deref(), Some("O'B"));

// Asking for more characters than there are is not an error.
assert_eq!(Action::First(99).apply("MR", 0).as_deref(), Some("MR"));
assert_eq!(Action::Last(99).apply("MR", 0).as_deref(), Some("MR"));

Trait Implementations§

Source§

impl Clone for Action

Source§

fn clone(&self) -> Action

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 Action

Source§

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

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

impl Display for Action

Source§

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

The spelling a policy file uses, so that a policy written out re-reads as the same policy (D18, spec §6.5).

The one value that does not survive the trip is Replace with empty text, written as clear, because nothing downstream can tell the two apart.

Example:

use er7_redact::Action;

assert_eq!(Action::Pseudonym.to_string(), "pseudonym");
assert_eq!(Action::First(4).to_string(), "first 4");
assert_eq!(Action::Mask('#').to_string(), "mask #");
assert_eq!(Action::Replace(String::new()).to_string(), "clear");
Source§

impl Eq for Action

Source§

impl PartialEq for Action

Source§

fn eq(&self, other: &Action) -> 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 Action

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.