///|
/// HL7 v2 field-level action used by the message sanitizer.
pub(all) enum Hl7Action {
Retain
Redact
Replace(String)
Remove
} derive(Debug, Eq)
///|
pub(all) struct Hl7Field {
segment_name : String
segment_index : Int
field_number : Int
value : String
} derive(Debug, Eq)
///|
pub(all) struct Hl7Segment {
name : String
fields : Array[String]
index : Int
} derive(Debug, Eq)
///|
pub(all) struct Hl7Message {
field_separator : Char
repetition_separator : Char
component_separator : Char
escape_character : Char
subcomponent_separator : Char
version : String
segments : Array[Hl7Segment]
} derive(Debug)
///|
pub(all) struct Hl7FieldRule {
segment_name : String
field_number : Int
component_number : Int
label : String
action : Hl7Action
required : Bool
} derive(Debug, Eq)
///|
pub(all) struct Hl7ValidationIssue {
code : String
message : String
segment_index : Int
field_number : Int
severity : DiagnosticSeverity
} derive(Debug, Eq)
///|
pub fn hl7_action_name(action : Hl7Action) -> String {
match action {
Retain => "retain"
Redact => "redact"
Replace(_) => "replace"
Remove => "remove"
}
}
///|
pub fn hl7_field_rule(
segment_name : String,
field_number : Int,
label : String,
action : Hl7Action,
) -> Hl7FieldRule {
{
segment_name: segment_name.to_upper(),
field_number,
component_number: 0,
label,
action,
required: false,
}
}
///|
pub fn hl7_field_rule_component(
segment_name : String,
field_number : Int,
component_number : Int,
label : String,
action : Hl7Action,
) -> Hl7FieldRule {
{
segment_name: segment_name.to_upper(),
field_number,
component_number,
label,
action,
required: false,
}
}
///|
pub fn hl7_default_rules() -> Array[Hl7FieldRule] {
[
{ ..hl7_field_rule("PID", 3, "patient identifier", Redact), required: true },
{ ..hl7_field_rule("PID", 5, "patient name", Redact), required: true },
{ ..hl7_field_rule("PID", 7, "date of birth", Redact), required: false },
{ ..hl7_field_rule("PID", 11, "patient address", Redact), required: false },
{ ..hl7_field_rule("PID", 13, "patient phone", Redact), required: false },
{ ..hl7_field_rule("PID", 14, "alternate phone", Redact), required: false },
{ ..hl7_field_rule("PID", 18, "account number", Redact), required: false },
{
..hl7_field_rule("PV1", 7, "attending provider", Redact),
required: false,
},
{
..hl7_field_rule("PV1", 8, "referring provider", Redact),
required: false,
},
{ ..hl7_field_rule("PV1", 19, "visit number", Redact), required: false },
{
..hl7_field_rule("ORC", 2, "placer order number", Redact),
required: false,
},
{
..hl7_field_rule("ORC", 3, "filler order number", Redact),
required: false,
},
{
..hl7_field_rule("OBR", 2, "placer order number", Redact),
required: false,
},
{
..hl7_field_rule("OBR", 3, "filler order number", Redact),
required: false,
},
{ ..hl7_field_rule("OBX", 5, "observation value", Redact), required: false },
{ ..hl7_field_rule("NTE", 3, "clinical note", Redact), required: false },
]
}
///|
fn hl7_trim_line(line : String) -> String {
line.trim(chars="\r\n").to_owned()
}
///|
pub fn hl7_segment_lines(text : String) -> Array[String] {
let normalized = text.replace(old="\r\n", new="\r")
let first = normalized.split("\r").to_array()
let lines = []
for item in first {
let second = item.to_owned().split("\n").to_array()
for line in second {
let clean = hl7_trim_line(line.to_owned())
if !clean.is_empty() {
lines.push(clean)
}
}
}
lines
}
///|
pub fn hl7_segment_name(line : String) -> String {
if line.length() < 3 {
""
} else {
line[:3].to_upper().to_owned()
}
}
///|
pub fn hl7_parse_segment(line : String, index : Int) -> Hl7Segment? {
let clean = hl7_trim_line(line)
let name = hl7_segment_name(clean)
if name.length() != 3 {
None
} else {
let parts = clean.split("|").to_array()
let fields = []
for i in 1.. Array[Hl7Segment] {
let segments = []
for i in 0.. segments.push(segment)
None => ()
}
}
segments
}
///|
pub fn hl7_message_type(segments : Array[Hl7Segment]) -> String {
match segments.get(0) {
Some(segment) if segment.name == "MSH" =>
if segment.fields.length() >= 8 {
segment.fields[7]
} else {
""
}
_ => ""
}
}
///|
pub fn hl7_version(segments : Array[Hl7Segment]) -> String {
match segments.get(0) {
Some(segment) if segment.name == "MSH" =>
if segment.fields.length() >= 11 {
segment.fields[10]
} else {
""
}
_ => ""
}
}
///|
pub fn hl7_parse(text : String) -> Hl7Message {
let lines = hl7_segment_lines(text)
let segments = hl7_parse_segments(lines)
let field_separator = '|'
let repetition_separator = '~'
let component_separator = '^'
let escape_character = '\\'
let subcomponent_separator = '&'
let version = hl7_version(segments)
{
field_separator,
repetition_separator,
component_separator,
escape_character,
subcomponent_separator,
version,
segments,
}
}
///|
pub fn Hl7Message::segment_count(self : Hl7Message) -> Int {
self.segments.length()
}
///|
pub fn Hl7Message::segment_names(self : Hl7Message) -> Array[String] {
self.segments.map(fn(segment) { segment.name })
}
///|
pub fn Hl7Message::has_segment(self : Hl7Message, name : String) -> Bool {
self.segments.any(fn(segment) { segment.name == name.to_upper() })
}
///|
pub fn Hl7Message::segments_named(
self : Hl7Message,
name : String,
) -> Array[Hl7Segment] {
self.segments.filter(fn(segment) { segment.name == name.to_upper() })
}
///|
pub fn Hl7Message::field(
self : Hl7Message,
segment_name : String,
segment_number : Int,
field_number : Int,
) -> Hl7Field? {
let selected = self.segments.filter(fn(segment) {
segment.name == segment_name.to_upper()
})
match selected.get(segment_number) {
Some(segment) =>
if field_number > 0 && field_number <= segment.fields.length() {
Some({
segment_name: segment.name,
segment_index: segment.index,
field_number,
value: segment.fields[field_number - 1],
})
} else {
None
}
None => None
}
}
///|
pub fn Hl7Message::fields_named(
self : Hl7Message,
segment_name : String,
field_number : Int,
) -> Array[Hl7Field] {
let result = []
let mut ordinal = 0
for segment in self.segments {
if segment.name == segment_name.to_upper() {
if field_number > 0 && field_number <= segment.fields.length() {
result.push({
segment_name: segment.name,
segment_index: segment.index,
field_number,
value: segment.fields[field_number - 1],
})
}
ordinal += 1
}
}
ignore(ordinal)
result
}
///|
pub fn Hl7Message::all_fields(self : Hl7Message) -> Array[Hl7Field] {
let result = []
for segment in self.segments {
for i in 0.. String {
segment_name.to_upper() + "-" + "\{field_number}"
}
///|
pub fn hl7_field_path(field : Hl7Field) -> String {
hl7_path(field.segment_name, field.field_number)
}
///|
pub fn hl7_component_values(value : String, separator : Char) -> Array[String] {
value
.split(separator.to_string())
.to_array()
.map(fn(item) { item.to_owned() })
}
///|
pub fn hl7_repeat_values(value : String, separator : Char) -> Array[String] {
hl7_component_values(value, separator)
}
///|
pub fn hl7_subcomponent_values(
value : String,
separator : Char,
) -> Array[String] {
hl7_component_values(value, separator)
}
///|
pub fn hl7_field_components(
message : Hl7Message,
field : Hl7Field,
) -> Array[String] {
hl7_component_values(field.value, message.component_separator)
}
///|
pub fn hl7_render_segment(segment : Hl7Segment, separator : Char) -> String {
segment.name +
separator.to_string() +
segment.fields.join(separator.to_string())
}
///|
pub fn Hl7Message::render(self : Hl7Message) -> String {
self.segments
.map(fn(segment) { hl7_render_segment(segment, self.field_separator) })
.join("\r")
}
///|
pub fn Hl7Message::render_with_newline(
self : Hl7Message,
newline : String,
) -> String {
self.segments
.map(fn(segment) { hl7_render_segment(segment, self.field_separator) })
.join(newline)
}
///|
pub fn hl7_escape(value : String, message : Hl7Message) -> String {
let builder = StringBuilder()
for c in value {
match c {
'|' => builder.write_string("\\F")
'^' => builder.write_string("\\S")
'~' => builder.write_string("\\R")
'&' => builder.write_string("\\T")
'\\' => builder.write_string("\\E")
'\r' => builder.write_string("\\.br\\")
'\n' => builder.write_string("\\.br\\")
_ => builder.write_char(c)
}
}
ignore(message)
builder.to_string()
}
///|
pub fn hl7_unescape(value : String) -> String {
value
.replace(old="\\F", new="|")
.replace(old="\\S", new="^")
.replace(old="\\R", new="~")
.replace(old="\\T", new="&")
.replace(old="\\E", new="\\")
.replace(old="\\.br\\", new="\n")
}
///|
fn hl7_rule_matches(rule : Hl7FieldRule, field : Hl7Field) -> Bool {
rule.segment_name == field.segment_name &&
rule.field_number == field.field_number
}
///|
pub fn hl7_matching_rule(
rules : Array[Hl7FieldRule],
field : Hl7Field,
) -> Hl7FieldRule? {
let mut result : Hl7FieldRule? = None
for rule in rules {
if hl7_rule_matches(rule, field) {
result = Some(rule)
}
}
result
}
///|
pub fn hl7_rule_paths(rules : Array[Hl7FieldRule]) -> Array[String] {
rules.map(fn(rule) { hl7_path(rule.segment_name, rule.field_number) })
}
///|
pub fn hl7_required_paths(rules : Array[Hl7FieldRule]) -> Array[String] {
rules
.filter(fn(rule) { rule.required })
.map(fn(rule) { hl7_path(rule.segment_name, rule.field_number) })
}
///|
pub fn hl7_set_field(
message : Hl7Message,
segment_index : Int,
field_number : Int,
value : String,
) -> Hl7Message {
let segments = message.segments.copy()
match segments.get(segment_index) {
Some(segment) if field_number > 0 => {
let fields = segment.fields.copy()
while fields.length() < field_number {
fields.push("")
}
fields[field_number - 1] = value
segments[segment_index] = { ..segment, fields, }
}
_ => ()
}
{ ..message, segments, }
}
///|
pub fn hl7_remove_field(
message : Hl7Message,
segment_index : Int,
field_number : Int,
) -> Hl7Message {
hl7_set_field(message, segment_index, field_number, "")
}
///|
pub fn hl7_apply_action(
value : String,
action : Hl7Action,
config : RedactionConfig,
) -> String raise DeidError {
match action {
Retain => value
Redact => redact_with_config(value, config).text
Replace(replacement) => replacement
Remove => ""
}
}
///|
pub fn hl7_redact(
text : String,
config : RedactionConfig,
rules : Array[Hl7FieldRule],
) -> String raise DeidError {
let message = hl7_parse(text)
let mut result = message
for field in message.all_fields() {
match hl7_matching_rule(rules, field) {
Some(rule) => {
let value = hl7_apply_action(field.value, rule.action, config)
result = hl7_set_field(
result,
field.segment_index,
field.field_number,
value,
)
}
None => ()
}
}
result.render()
}
///|
pub fn hl7_redact_default(
text : String,
config : RedactionConfig,
) -> String raise DeidError {
hl7_redact(text, config, hl7_default_rules())
}
///|
pub fn hl7_sensitive_fields(
message : Hl7Message,
rules : Array[Hl7FieldRule],
) -> Array[Hl7Field] {
message
.all_fields()
.filter(fn(field) { hl7_matching_rule(rules, field) is Some(_) })
}
///|
pub fn hl7_redaction_paths(
message : Hl7Message,
rules : Array[Hl7FieldRule],
) -> Array[String] {
hl7_sensitive_fields(message, rules).map(hl7_field_path)
}
///|
pub fn hl7_message_checksum(message : Hl7Message) -> String {
stable_hash(message.render())
}
///|
pub fn hl7_validate(message : Hl7Message) -> Array[Hl7ValidationIssue] {
let issues = []
if message.segments.is_empty() {
issues.push({
code: "HL7_EMPTY",
message: "message has no segments",
segment_index: -1,
field_number: 0,
severity: Error,
})
} else if message.segments[0].name != "MSH" {
issues.push({
code: "HL7_NO_MSH",
message: "first segment must be MSH",
segment_index: 0,
field_number: 0,
severity: Error,
})
}
for segment in message.segments {
if segment.name.length() != 3 {
issues.push({
code: "HL7_SEGMENT_NAME",
message: "segment name must contain three characters",
segment_index: segment.index,
field_number: 0,
severity: Error,
})
}
for value in segment.fields {
if value.contains("\r") || value.contains("\n") {
issues.push({
code: "HL7_FIELD_BREAK",
message: "field contains a line break",
segment_index: segment.index,
field_number: 0,
severity: Warning,
})
}
}
}
issues
}
///|
pub fn hl7_validate_required(
message : Hl7Message,
rules : Array[Hl7FieldRule],
) -> Array[Hl7ValidationIssue] {
let issues = hl7_validate(message)
for rule in rules.filter(fn(item) { item.required }) {
let values = message.fields_named(rule.segment_name, rule.field_number)
if values.is_empty() ||
values.all(fn(field) { field.value.trim().is_empty() }) {
issues.push({
code: "HL7_REQUIRED",
message: "required field is missing: " +
hl7_path(rule.segment_name, rule.field_number),
segment_index: -1,
field_number: rule.field_number,
severity: Error,
})
}
}
issues
}
///|
pub fn hl7_is_valid(message : Hl7Message) -> Bool {
!hl7_validate(message).any(fn(issue) { issue.severity == Error })
}
///|
pub fn hl7_issue_text(issue : Hl7ValidationIssue) -> String {
[
"\{issue.code}",
"\{severity_name(issue.severity)}",
"segment=\{issue.segment_index}",
"field=\{issue.field_number}",
issue.message,
].join(" ")
}
///|
pub fn hl7_issues_text(issues : Array[Hl7ValidationIssue]) -> String {
issues.map(hl7_issue_text).join("\n")
}
///|
pub fn hl7_message_summary(message : Hl7Message) -> String {
[
"segments=\{message.segment_count()}",
"message_type=\{hl7_message_type(message.segments)}",
"version=\{message.version}",
"checksum=\{hl7_message_checksum(message)}",
"valid=\{hl7_is_valid(message)}",
].join("\n")
}
///|
pub fn hl7_field_summary(field : Hl7Field) -> String {
[
"path=\{hl7_field_path(field)}",
"segment_index=\{field.segment_index}",
"value_length=\{field.value.length()}",
].join("\n")
}
///|
pub fn hl7_fields_json(fields : Array[Hl7Field]) -> String {
"[" +
fields
.map(fn(field) {
"{" +
"\"path\":\{json_escape(hl7_field_path(field))}," +
"\"segment_index\":\{field.segment_index}," +
"\"field_number\":\{field.field_number}," +
"\"value_length\":\{field.value.length()}" +
"}"
})
.join(",") +
"]"
}
///|
pub fn hl7_redaction_report(
input : String,
output : String,
rules : Array[Hl7FieldRule],
) -> String {
let before = hl7_parse(input)
let after = hl7_parse(output)
let paths = hl7_rule_paths(rules).join(",")
[
"input_checksum=\{hl7_message_checksum(before)}",
"output_checksum=\{hl7_message_checksum(after)}",
"sensitive_paths=\{paths}",
"input_segments=\{before.segment_count()}",
"output_segments=\{after.segment_count()}",
"output_valid=\{hl7_is_valid(after)}",
].join("\n")
}
///|
pub fn hl7_preserves_segment_order(
left : Hl7Message,
right : Hl7Message,
) -> Bool {
left.segment_names() == right.segment_names()
}
///|
pub fn hl7_preserves_non_sensitive_fields(
before : Hl7Message,
after : Hl7Message,
rules : Array[Hl7FieldRule],
) -> Bool {
let mut result = true
for field in before.all_fields() {
if hl7_matching_rule(rules, field) is None {
match after.field(field.segment_name, 0, field.field_number) {
Some(other) => if other.value != field.value { result = false }
None => result = false
}
}
}
result
}
///|
pub fn hl7_message_without_notes(message : Hl7Message) -> Hl7Message {
let segments = message.segments.filter(fn(segment) { segment.name != "NTE" })
{ ..message, segments, }
}
///|
pub fn hl7_segment_count_by_name(message : Hl7Message) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for segment in message.segments {
counts[segment.name] = counts.get_or_default(segment.name, 0) + 1
}
counts
}
///|
pub fn hl7_field_lengths(message : Hl7Message) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for field in message.all_fields() {
result[hl7_field_path(field)] = field.value.length()
}
result
}
///|
pub fn hl7_empty_field_paths(message : Hl7Message) -> Array[String] {
message
.all_fields()
.filter(fn(field) { field.value.is_empty() })
.map(hl7_field_path)
}
///|
pub fn hl7_nonempty_field_count(message : Hl7Message) -> Int {
message.all_fields().filter(fn(field) { !field.value.is_empty() }).length()
}
///|
pub fn hl7_repeated_segment_names(message : Hl7Message) -> Array[String] {
let counts = hl7_segment_count_by_name(message)
let names = []
for name, count in counts {
if count > 1 {
names.push(name)
}
}
names.sort()
names
}
///|
pub fn hl7_normalize_newlines(text : String) -> String {
hl7_parse(text).render()
}
///|
pub fn hl7_safe_for_storage(
text : String,
config : RedactionConfig,
) -> Bool raise DeidError {
let output = hl7_redact_default(text, config)
let message = hl7_parse(output)
let mut leakage = false
for field in hl7_sensitive_fields(message, hl7_default_rules()) {
let findings = scan(field.value) catch { _ => [] }
if !findings.is_empty() {
leakage = true
}
}
hl7_is_valid(message) && !leakage
}