use std::fmt::Write as _;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Separators {
pub field: char,
pub component: char,
pub repetition: char,
pub escape: char,
pub subcomponent: char,
}
impl Default for Separators {
fn default() -> Self {
Self {
field: '|',
component: '^',
repetition: '~',
escape: '\\',
subcomponent: '&',
}
}
}
impl Separators {
fn from_msh(line: &str) -> Result<Self, ParseError> {
let mut chars = line.chars();
let (Some(_), Some(_), Some(_), Some(field)) =
(chars.next(), chars.next(), chars.next(), chars.next())
else {
return Err(ParseError::new(
0,
"MSH segment is truncated before the field separator",
));
};
if field.is_alphanumeric() || field.is_whitespace() {
return Err(ParseError::new(
0,
format!("MSH-1 field separator {field:?} is not a usable delimiter"),
));
}
let mut sep = Self {
field,
..Default::default()
};
let mut encoding = chars.take_while(|c| *c != field);
if let Some(c) = encoding.next() {
sep.component = c;
}
if let Some(c) = encoding.next() {
sep.repetition = c;
}
if let Some(c) = encoding.next() {
sep.escape = c;
}
if let Some(c) = encoding.next() {
sep.subcomponent = c;
}
let extra = encoding.count();
if extra > 0 {
return Err(ParseError::new(
0,
format!(
"MSH-2 declares {} encoding characters, expected at most 4",
4 + extra
),
));
}
let all = [
sep.field,
sep.component,
sep.repetition,
sep.escape,
sep.subcomponent,
];
for i in 0..all.len() {
for j in (i + 1)..all.len() {
if all[i] == all[j] {
return Err(ParseError::new(
0,
format!("delimiter {:?} is declared twice in MSH-1/MSH-2", all[i]),
));
}
}
}
Ok(sep)
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
pub line: usize,
pub message: String,
}
impl ParseError {
fn new(line: usize, message: impl Into<String>) -> Self {
Self {
line,
message: message.into(),
}
}
pub(crate) fn no_message() -> Self {
Self::new(0, "no MSH segment found - is this an HL7 v2 message?")
}
}
impl std::error::Error for ParseError {}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.line > 0 {
write!(f, "line {}: {}", self.line, self.message)
} else {
write!(f, "{}", self.message)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Component<'a> {
raw: &'a str,
sep: Separators,
literal: bool,
}
impl<'a> Component<'a> {
#[must_use]
pub fn sub(&self, seq: usize) -> &'a str {
self.subs().nth(seq.wrapping_sub(1)).unwrap_or("")
}
pub fn subs(&self) -> impl Iterator<Item = &'a str> {
split(self.raw, self.sep.subcomponent, self.literal)
}
#[must_use]
pub fn is_empty(&self) -> bool {
if self.literal {
return self.raw.is_empty();
}
self.raw.chars().all(|c| c == self.sep.subcomponent)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Repetition<'a> {
raw: &'a str,
sep: Separators,
literal: bool,
}
impl<'a> Repetition<'a> {
#[must_use]
pub fn comp(&self, seq: usize) -> Component<'a> {
Component {
raw: self.comp_text(seq),
sep: self.sep,
literal: self.literal,
}
}
pub fn comps(&self) -> impl Iterator<Item = Component<'a>> {
let (sep, literal) = (self.sep, self.literal);
split(self.raw, self.sep.component, self.literal).map(move |raw| Component {
raw,
sep,
literal,
})
}
#[must_use]
pub fn comp_text(&self, seq: usize) -> &'a str {
split(self.raw, self.sep.component, self.literal)
.nth(seq.wrapping_sub(1))
.unwrap_or("")
}
#[must_use]
pub const fn text(&self) -> &'a str {
self.raw
}
#[must_use]
pub fn is_empty(&self) -> bool {
if self.literal {
return self.raw.is_empty();
}
self.raw
.chars()
.all(|c| c == self.sep.component || c == self.sep.subcomponent)
}
#[must_use]
pub fn filled_comps(&self) -> usize {
self.comps()
.enumerate()
.filter(|(_, c)| !c.is_empty())
.map(|(i, _)| i + 1)
.last()
.unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Field<'a> {
raw: &'a str,
sep: Separators,
literal: bool,
}
impl<'a> Field<'a> {
#[must_use]
pub fn is_empty(&self) -> bool {
if self.literal {
return self.raw.is_empty();
}
self.raw.chars().all(|c| {
c == self.sep.repetition || c == self.sep.component || c == self.sep.subcomponent
})
}
#[must_use]
pub fn is_null(&self) -> bool {
self.rep_count() == 1
&& split(self.raw, self.sep.component, self.literal).count() == 1
&& self.rep(1).comp(1).sub(1) == "\"\""
}
#[must_use]
pub fn rep(&self, seq: usize) -> Repetition<'a> {
Repetition {
raw: split(self.raw, self.sep.repetition, self.literal)
.nth(seq.wrapping_sub(1))
.unwrap_or(""),
sep: self.sep,
literal: self.literal,
}
}
pub fn reps(&self) -> impl Iterator<Item = Repetition<'a>> {
let (sep, literal) = (self.sep, self.literal);
split(self.raw, self.sep.repetition, self.literal).map(move |raw| Repetition {
raw,
sep,
literal,
})
}
#[must_use]
pub fn rep_count(&self) -> usize {
split(self.raw, self.sep.repetition, self.literal).count()
}
#[must_use]
pub fn comp(&self, seq: usize) -> &'a str {
self.rep(1).comp_text(seq)
}
#[must_use]
pub const fn text(&self) -> &'a str {
self.raw
}
}
fn split(raw: &str, sep: char, literal: bool) -> impl Iterator<Item = &str> {
let mut whole = literal.then_some(raw);
let mut parts = (!literal).then(|| raw.split(sep));
std::iter::from_fn(move || match &mut parts {
Some(parts) => parts.next(),
None => whole.take(),
})
}
#[derive(Debug, Clone)]
pub struct Segment<'a> {
pub name: &'a str,
pub line: usize,
pub occurrence: usize,
fields: Vec<&'a str>,
pub raw: &'a str,
sep: Separators,
}
impl<'a> Segment<'a> {
#[must_use]
pub fn field(&self, seq: usize) -> Option<Field<'a>> {
let raw = *self.fields.get(seq.wrapping_sub(1))?;
Some(Field {
raw,
sep: self.sep,
literal: self.name == "MSH" && seq <= 2,
})
}
#[must_use]
pub fn has(&self, seq: usize) -> bool {
self.field(seq).is_some_and(|f| !f.is_empty())
}
#[must_use]
pub fn text(&self, seq: usize) -> &'a str {
self.field(seq).map_or("", |f| f.text())
}
#[must_use]
pub fn comp(&self, seq: usize, c: usize) -> &'a str {
self.field(seq).map_or("", |f| f.comp(c))
}
#[must_use]
pub fn last_populated(&self) -> usize {
(1..=self.fields.len())
.rfind(|seq| self.has(*seq))
.unwrap_or(0)
}
#[must_use]
pub fn is_custom(&self) -> bool {
self.name.starts_with('Z')
}
fn parse(name: &'a str, raw: &'a str, line: usize, sep: &Separators) -> Self {
let parts: Vec<&str> = raw.split(sep.field).collect();
let mut fields: Vec<&'a str> = Vec::new();
let rest = if name == "MSH" {
fields.push(&raw[name.len()..name.len() + sep.field.len_utf8()]);
fields.push(parts.get(1).copied().unwrap_or(""));
&parts[2.min(parts.len())..]
} else {
&parts[1.min(parts.len())..]
};
fields.extend_from_slice(rest);
Self {
name,
line,
occurrence: 1,
fields,
raw,
sep: *sep,
}
}
}
#[derive(Debug, Clone)]
pub struct Message<'a> {
pub sep: Separators,
pub segments: Vec<Segment<'a>>,
pub start_line: usize,
pub notes: Vec<String>,
}
impl<'a> Message<'a> {
#[must_use]
pub fn msh(&self) -> &Segment<'a> {
&self.segments[0]
}
#[must_use]
pub fn version(&self) -> &'a str {
self.msh().comp(12, 1)
}
#[must_use]
pub fn message_type(&self) -> (&'a str, &'a str, &'a str) {
let f = self.msh();
(f.comp(9, 1), f.comp(9, 2), f.comp(9, 3))
}
#[must_use]
pub fn type_label(&self) -> String {
let (code, trigger, _) = self.message_type();
match (code.is_empty(), trigger.is_empty()) {
(true, _) => "(no MSH-9)".to_string(),
(false, true) => code.to_string(),
(false, false) => format!("{code}^{trigger}"),
}
}
#[must_use]
pub fn control_id(&self) -> &'a str {
self.msh().comp(10, 1)
}
#[must_use]
pub fn find(&self, name: &str) -> Vec<&Segment<'a>> {
self.segments.iter().filter(|s| s.name == name).collect()
}
#[must_use]
pub fn first(&self, name: &str) -> Option<&Segment<'a>> {
self.segments.iter().find(|s| s.name == name)
}
}
#[derive(Debug)]
pub struct RawMessage<'a> {
pub start_line: usize,
text: &'a str,
pub notes: Vec<String>,
}
impl<'a> RawMessage<'a> {
pub fn lines(&self) -> impl Iterator<Item = (usize, &'a str)> + '_ {
lines(self.text)
.enumerate()
.filter_map(move |(offset, (_, line))| {
let cleaned = clean(line);
if cleaned.is_empty() || is_batch_wrapper(head(cleaned)) {
return None;
}
Some((self.start_line + offset, cleaned))
})
}
fn first_line(&self) -> (usize, &'a str) {
self.lines().next().unwrap_or((self.start_line, self.text))
}
}
fn lines(text: &str) -> impl Iterator<Item = (usize, &str)> {
let mut offset = 0usize;
let mut rest = Some(text);
std::iter::from_fn(move || {
let current = rest?;
let start = offset;
match current.find(['\r', '\n']) {
None => {
rest = None;
Some((start, current))
}
Some(at) => {
let (line, tail) = current.split_at(at);
let skip = usize::from(tail.starts_with("\r\n")) + 1;
offset += at + skip;
rest = Some(&tail[skip..]);
Some((start, line))
}
}
})
}
fn clean(line: &str) -> &str {
line.trim_matches(|c: char| {
c == '\u{0b}' || c == '\u{1c}' || c == '\u{1d}' || c == '\0' || c.is_whitespace()
})
}
fn is_batch_wrapper(head: &str) -> bool {
matches!(head, "FHS" | "BHS" | "BTS" | "FTS")
}
fn head(text: &str) -> &str {
let end = text.char_indices().nth(3).map_or(text.len(), |(i, _)| i);
&text[..end]
}
#[must_use]
pub fn split_messages(raw: &str) -> (Vec<RawMessage<'_>>, Vec<String>) {
let mut messages: Vec<RawMessage<'_>> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut pending_notes: Vec<String> = Vec::new();
let mut stray_reported = false;
let mut open: Option<(usize, usize)> = None;
for (idx, (offset, line)) in lines(raw).enumerate() {
let lineno = idx + 1;
let cleaned = clean(line);
if cleaned.is_empty() {
continue;
}
let head = head(cleaned);
if is_batch_wrapper(head) {
pending_notes.push(format!("line {lineno}: batch wrapper {head} skipped"));
continue;
}
let line_end = offset + line.len();
if head == "MSH" {
if let (Some((start, end)), Some(previous)) =
(open.replace((offset, line_end)), messages.last_mut())
{
previous.text = &raw[start..end];
}
messages.push(RawMessage {
start_line: lineno,
text: &raw[offset..line_end],
notes: std::mem::take(&mut pending_notes),
});
} else if let Some((_, end)) = open.as_mut() {
*end = line_end;
} else if !stray_reported {
stray_reported = true;
warnings.push(format!(
"line {lineno}: content before the first MSH segment was ignored"
));
}
}
if let (Some((start, end)), Some(last)) = (open, messages.last_mut()) {
last.text = &raw[start..end];
}
(messages, warnings)
}
fn segment_name(text: &str) -> Option<&str> {
let name = head(text);
let usable = name.chars().count() == 3
&& name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
&& name.starts_with(|c: char| c.is_ascii_uppercase());
usable.then_some(name)
}
impl RawMessage<'_> {
pub fn separators(&self) -> Result<Separators, ParseError> {
let (lineno, text) = self.first_line();
let sep = Separators::from_msh(text).map_err(|e| ParseError::new(lineno, e.message))?;
if segment_name(text) != Some("MSH") {
return Err(ParseError::new(
lineno,
"message does not begin with a parsable MSH segment",
));
}
Ok(sep)
}
}
pub fn parse_message<'a>(raw: &RawMessage<'a>) -> Result<Message<'a>, ParseError> {
let sep = raw.separators()?;
let mut segments: Vec<Segment<'a>> = Vec::new();
let mut notes = raw.notes.clone();
let mut counts: Vec<(&str, usize)> = Vec::new();
for (lineno, text) in raw.lines() {
let Some(name) = segment_name(text) else {
notes.push(format!(
"line {}: skipped unrecognisable segment starting {:?}",
lineno,
text.chars().take(8).collect::<String>()
));
continue;
};
if text.chars().nth(3) != Some(sep.field) {
notes.push(format!(
"line {lineno}: segment {name} has no field separator after the name"
));
}
let mut seg = Segment::parse(name, text, lineno, &sep);
let entry = counts.iter_mut().find(|(n, _)| *n == name);
seg.occurrence = if let Some((_, c)) = entry {
*c += 1;
*c
} else {
counts.push((name, 1));
1
};
segments.push(seg);
}
Ok(Message {
sep,
segments,
start_line: raw.start_line,
notes,
})
}
#[must_use]
pub fn unescape(s: &str, sep: &Separators) -> String {
if !s.contains(sep.escape) {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] != sep.escape {
out.push(chars[i]);
i += 1;
continue;
}
let end = chars[i + 1..]
.iter()
.position(|c| *c == sep.escape)
.map(|p| i + 1 + p);
let Some(end) = end else {
out.push(chars[i]);
i += 1;
continue;
};
let code: String = chars[i + 1..end].iter().collect();
match code.as_str() {
"F" => out.push(sep.field),
"S" => out.push(sep.component),
"T" => out.push(sep.subcomponent),
"R" => out.push(sep.repetition),
".br" | ".sp" => out.push('\n'),
"E" | "" => out.push(sep.escape),
other if other.starts_with('X') => {
let hex = &other[1..];
let decoded = (!hex.is_empty() && hex.len() % 2 == 0)
.then(|| {
hex.as_bytes()
.chunks(2)
.map(|pair| {
u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()
})
.collect::<Option<Vec<u8>>>()
})
.flatten();
match decoded {
Some(bytes) => out.push_str(&String::from_utf8_lossy(&bytes)),
None => {
let _ = write!(out, "{}{}{}", sep.escape, other, sep.escape);
}
}
}
other if other.starts_with('H') || other.starts_with('N') || other.starts_with('Z') => {
}
other => {
let _ = write!(out, "{}{}{}", sep.escape, other, sep.escape);
}
}
i = end + 1;
}
out
}
#[cfg(test)]
#[must_use]
pub fn parse_str(text: &str) -> Message<'_> {
let (raws, _) = split_messages(text);
parse_message(&raws[0]).expect("fixture should parse")
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
reason = "panicking is the failure mode a test wants"
)]
use super::*;
const ADT: &str = "MSH|^~\\&|HIS|MERCY|LIS|LAB|20240115143200||ADT^A01^ADT_A01|MSG1|P|2.5.1\r\
PID|1||123456^^^MERCY^MR~999^^^SSA^SS||Smith^John^A||19850312|M\r\
PV1|1|I|ER^101^A&Bay 2^MERCY\r";
#[test]
fn reads_default_delimiters() {
let m = parse_str(ADT);
assert_eq!(m.sep, Separators::default());
assert_eq!(m.segments.len(), 3);
}
#[test]
fn honours_custom_delimiters() {
let m = parse_str("MSH#@~\\&#A#B#C#D#20240101120000##ADT@A01#1#P#2.5.1\r");
assert_eq!(m.sep.field, '#');
assert_eq!(m.sep.component, '@');
assert_eq!(m.type_label(), "ADT^A01");
}
#[test]
#[cfg(target_pointer_width = "64")]
fn the_message_tree_stays_borrowed() {
use std::mem::size_of;
assert_eq!(size_of::<Segment<'_>>(), 96, "Segment grew");
assert_eq!(size_of::<Message<'_>>(), 80, "Message grew");
assert_eq!(size_of::<RawMessage<'_>>(), 48, "RawMessage grew");
assert_eq!(size_of::<Field<'_>>(), 40, "Field grew");
assert_eq!(size_of::<Repetition<'_>>(), 40, "Repetition grew");
assert_eq!(size_of::<Component<'_>>(), 40, "Component grew");
}
#[test]
fn msh_field_numbering_is_offset_by_the_separator() {
let m = parse_str(ADT);
let msh = m.msh();
assert_eq!(msh.text(1), "|");
assert_eq!(msh.text(2), "^~\\&");
assert_eq!(msh.text(3), "HIS");
assert_eq!(msh.comp(9, 2), "A01");
assert_eq!(m.version(), "2.5.1");
assert_eq!(m.control_id(), "MSG1");
}
#[test]
fn splits_repetitions_components_and_subcomponents() {
let m = parse_str(ADT);
let pid = m.first("PID").unwrap();
let ids = pid.field(3).unwrap();
assert_eq!(ids.rep_count(), 2);
assert_eq!(ids.rep(2).comp_text(1), "999");
assert_eq!(ids.rep(1).comp_text(5), "MR");
let pv1 = m.first("PV1").unwrap();
let location = pv1.field(3).unwrap().rep(1);
assert_eq!(location.comp(3).sub(1), "A");
assert_eq!(location.comp(3).sub(2), "Bay 2");
}
#[test]
fn tracks_segment_occurrence_and_line() {
let m = parse_str("MSH|^~\\&|A|B|C|D|20240101120000||ORU^R01|1|P|2.5.1\rOBX|1\rOBX|2\r");
let obx = m.find("OBX");
assert_eq!(obx.len(), 2);
assert_eq!(obx[1].occurrence, 2);
assert_eq!(obx[1].line, 3);
}
#[test]
fn accepts_lf_crlf_and_mllp_framing() {
for text in [
"MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\nMSA|AA|1\n",
"MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\r\nMSA|AA|1\r\n",
"\u{b}MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\rMSA|AA|1\r\u{1c}\r",
] {
let m = parse_str(text);
assert_eq!(m.segments.len(), 2, "{text:?}");
assert_eq!(m.segments[1].name, "MSA");
}
}
#[test]
fn skips_batch_wrappers_and_splits_messages() {
let text = "FHS|^~\\&\rBHS|^~\\&\r\
MSH|^~\\&|A|B|C|D|20240101120000||ADT^A01|1|P|2.5.1\rPID|1\r\
MSH|^~\\&|A|B|C|D|20240101130000||ADT^A03|2|P|2.5.1\rPID|1\rBTS|2\rFTS|1\r";
let (raws, warnings) = split_messages(text);
assert_eq!(raws.len(), 2);
assert!(warnings.is_empty());
let first = parse_message(&raws[0]).unwrap();
assert_eq!(first.segments.len(), 2);
assert_eq!(first.notes.len(), 2, "batch wrappers should be noted");
assert_eq!(parse_message(&raws[1]).unwrap().control_id(), "2");
}
#[test]
fn rejects_input_without_msh() {
let (raws, warnings) = split_messages("PID|1||123\r");
assert!(raws.is_empty());
assert_eq!(warnings.len(), 1);
}
#[test]
fn rejects_duplicate_delimiters() {
let (raws, _) = split_messages("MSH|^~\\^|A|B|C|D|20240101120000||ACK|1|P|2.5.1\r");
assert!(parse_message(&raws[0]).is_err());
}
#[test]
fn detects_explicit_null() {
let m = parse_str("MSH|^~\\&|A|B|C|D|20240101120000||ADT^A08|1|P|2.5.1\rPID|1||\"\"\r");
assert!(m.first("PID").unwrap().field(3).unwrap().is_null());
}
#[test]
fn resolves_escape_sequences() {
let sep = Separators::default();
assert_eq!(unescape("Smith \\T\\ Sons", &sep), "Smith & Sons");
assert_eq!(unescape("100\\S\\200", &sep), "100^200");
assert_eq!(unescape("a\\F\\b", &sep), "a|b");
assert_eq!(unescape("line1\\.br\\line2", &sep), "line1\nline2");
assert_eq!(unescape("\\X0A\\", &sep), "\n");
assert_eq!(unescape("50\\E\\50", &sep), "50\\50");
assert_eq!(unescape("a\\Q9\\b", &sep), "a\\Q9\\b");
}
#[test]
fn last_populated_ignores_trailing_empties() {
let m = parse_str(
"MSH|^~\\&|A|B|C|D|20240101120000||ADT^A01|1|P|2.5.1\rEVN|A01|20240101120000||||\r",
);
assert_eq!(m.first("EVN").unwrap().last_populated(), 2);
}
#[test]
fn a_truncated_msh_line_cannot_declare_delimiters() {
let (raws, _) = split_messages("MSH\r");
let e = parse_message(&raws[0]).expect_err("nothing to read");
assert!(e.message.contains("truncated"), "{e}");
}
#[test]
fn more_than_four_encoding_characters_is_reported() {
for (line, count) in [("MSH|^~\\&%|A\r", 5), ("MSH|^~\\&%$|A\r", 6)] {
let (raws, _) = split_messages(line);
let e = parse_message(&raws[0]).expect_err("too many");
assert!(e.message.contains(&count.to_string()), "{e}");
assert!(e.message.contains("at most 4"), "{e}");
}
}
#[test]
fn a_message_with_no_message_type_says_so() {
let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200|||MSG1|P|2.5.1\r");
assert_eq!(m.type_label(), "(no MSH-9)");
}
#[test]
fn a_segment_without_a_field_separator_after_its_name_is_noted() {
let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200||ADT^A01|1|P|2.5.1\rPID\r");
assert!(
m.notes.iter().any(|n| n.contains("no field separator")),
"{:?}",
m.notes
);
}
#[test]
fn an_unreadable_segment_name_is_skipped_with_a_note() {
let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200||ADT^A01|1|P|2.5.1\r??|1|x\r");
assert!(
m.notes.iter().any(|n| n.contains("unrecognisable segment")),
"{:?}",
m.notes
);
assert_eq!(m.segments.len(), 1, "only MSH survives");
}
#[test]
fn the_delimiter_fields_are_never_split_apart() {
let m = parse_str(ADT);
let msh2 = m.msh().field(2).expect("MSH-2 exists");
assert_eq!(msh2.text(), "^~\\&");
assert_eq!(msh2.rep_count(), 1);
assert_eq!(msh2.rep(1).comp(1).sub(1), "^~\\&");
assert!(!msh2.is_empty());
}
#[test]
fn a_field_beyond_the_end_of_a_segment_is_absent() {
let m = parse_str(ADT);
assert!(m.msh().field(999).is_none());
assert_eq!(m.msh().text(999), "");
assert_eq!(m.msh().comp(999, 1), "");
assert!(!m.msh().has(999));
}
}