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 {
Separators {
field: '|',
component: '^',
repetition: '~',
escape: '\\',
subcomponent: '&',
}
}
}
impl Separators {
fn from_msh(line: &str) -> Result<Separators, ParseError> {
let chars: Vec<char> = line.chars().collect();
if chars.len() < 4 {
return Err(ParseError::new(
0,
"MSH segment is truncated before the field separator",
));
}
let field = chars[3];
if field.is_alphanumeric() || field.is_whitespace() {
return Err(ParseError::new(
0,
format!(
"MSH-1 field separator {:?} is not a usable delimiter",
field
),
));
}
let enc: String = chars[4..].iter().take_while(|c| **c != field).collect();
let e: Vec<char> = enc.chars().collect();
let mut sep = Separators {
field,
..Default::default()
};
if !e.is_empty() {
sep.component = e[0];
}
if e.len() > 1 {
sep.repetition = e[1];
}
if e.len() > 2 {
sep.escape = e[2];
}
if e.len() > 3 {
sep.subcomponent = e[3];
}
if e.len() > 4 {
return Err(ParseError::new(
0,
format!(
"MSH-2 declares {} encoding characters, expected at most 4",
e.len()
),
));
}
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 {
ParseError {
line,
message: message.into(),
}
}
}
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)]
pub struct Component {
pub subs: Vec<String>,
}
impl Component {
pub fn sub(&self, seq: usize) -> &str {
self.subs
.get(seq.wrapping_sub(1))
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn is_empty(&self) -> bool {
self.subs.iter().all(|s| s.is_empty())
}
fn raw(&self, sep: &Separators) -> String {
self.subs.join(&sep.subcomponent.to_string())
}
}
#[derive(Debug, Clone)]
pub struct Repetition {
pub comps: Vec<Component>,
}
impl Repetition {
pub fn comp(&self, seq: usize) -> &Component {
const EMPTY: &Component = &Component { subs: Vec::new() };
self.comps.get(seq.wrapping_sub(1)).unwrap_or(EMPTY)
}
pub fn comp_text(&self, seq: usize, sep: &Separators) -> String {
self.comp(seq).raw(sep)
}
pub fn is_empty(&self) -> bool {
self.comps.iter().all(|c| c.is_empty())
}
pub fn raw(&self, sep: &Separators) -> String {
self.comps
.iter()
.map(|c| c.raw(sep))
.collect::<Vec<_>>()
.join(&sep.component.to_string())
}
pub fn filled_comps(&self) -> usize {
self.comps
.iter()
.rposition(|c| !c.is_empty())
.map(|i| i + 1)
.unwrap_or(0)
}
}
#[derive(Debug, Clone)]
pub struct Field {
pub reps: Vec<Repetition>,
literal: Option<String>,
}
impl Field {
fn literal(value: impl Into<String>) -> Field {
let value = value.into();
Field {
reps: vec![Repetition {
comps: vec![Component {
subs: vec![value.clone()],
}],
}],
literal: Some(value),
}
}
pub fn is_empty(&self) -> bool {
self.reps.iter().all(|r| r.is_empty())
}
pub fn is_null(&self) -> bool {
self.reps.len() == 1
&& self.reps[0].comps.len() == 1
&& self.reps[0].comp(1).sub(1) == "\"\""
}
pub fn rep(&self, seq: usize) -> &Repetition {
const EMPTY: &Repetition = &Repetition { comps: Vec::new() };
self.reps.get(seq.wrapping_sub(1)).unwrap_or(EMPTY)
}
pub fn comp(&self, seq: usize, sep: &Separators) -> String {
self.rep(1).comp_text(seq, sep)
}
pub fn raw(&self, sep: &Separators) -> String {
if let Some(lit) = &self.literal {
return lit.clone();
}
self.reps
.iter()
.map(|r| r.raw(sep))
.collect::<Vec<_>>()
.join(&sep.repetition.to_string())
}
}
#[derive(Debug, Clone)]
pub struct Segment {
pub name: String,
pub line: usize,
pub occurrence: usize,
pub fields: Vec<Field>,
pub raw: String,
}
impl Segment {
pub fn field(&self, seq: usize) -> Option<&Field> {
self.fields.get(seq.wrapping_sub(1))
}
pub fn has(&self, seq: usize) -> bool {
self.field(seq).map(|f| !f.is_empty()).unwrap_or(false)
}
pub fn text(&self, seq: usize, sep: &Separators) -> String {
self.field(seq).map(|f| f.raw(sep)).unwrap_or_default()
}
pub fn comp(&self, seq: usize, c: usize, sep: &Separators) -> String {
self.field(seq).map(|f| f.comp(c, sep)).unwrap_or_default()
}
pub fn last_populated(&self) -> usize {
self.fields
.iter()
.rposition(|f| !f.is_empty())
.map(|i| i + 1)
.unwrap_or(0)
}
pub fn is_custom(&self) -> bool {
self.name.starts_with('Z')
}
fn parse(name: &str, raw: &str, line: usize, sep: &Separators) -> Segment {
let parts: Vec<&str> = raw.split(sep.field).collect();
let mut fields: Vec<Field> = Vec::new();
let rest = if name == "MSH" {
fields.push(Field::literal(sep.field.to_string()));
fields.push(Field::literal(parts.get(1).copied().unwrap_or("")));
&parts[2.min(parts.len())..]
} else {
&parts[1.min(parts.len())..]
};
for part in rest {
fields.push(parse_field(part, sep));
}
Segment {
name: name.to_string(),
line,
occurrence: 1,
fields,
raw: raw.to_string(),
}
}
}
fn parse_field(s: &str, sep: &Separators) -> Field {
let reps = s
.split(sep.repetition)
.map(|rep| Repetition {
comps: rep
.split(sep.component)
.map(|c| Component {
subs: c.split(sep.subcomponent).map(|s| s.to_string()).collect(),
})
.collect(),
})
.collect();
Field {
reps,
literal: None,
}
}
#[derive(Debug, Clone)]
pub struct Message {
pub sep: Separators,
pub segments: Vec<Segment>,
pub start_line: usize,
pub notes: Vec<String>,
}
impl Message {
pub fn msh(&self) -> &Segment {
&self.segments[0]
}
pub fn version(&self) -> String {
self.msh().comp(12, 1, &self.sep)
}
pub fn message_type(&self) -> (String, String, String) {
let f = self.msh();
(
f.comp(9, 1, &self.sep),
f.comp(9, 2, &self.sep),
f.comp(9, 3, &self.sep),
)
}
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,
(false, false) => format!("{}^{}", code, trigger),
}
}
pub fn control_id(&self) -> String {
self.msh().comp(10, 1, &self.sep)
}
pub fn find(&self, name: &str) -> Vec<&Segment> {
self.segments.iter().filter(|s| s.name == name).collect()
}
pub fn first(&self, name: &str) -> Option<&Segment> {
self.segments.iter().find(|s| s.name == name)
}
}
pub struct RawMessage {
pub start_line: usize,
pub lines: Vec<(usize, String)>,
pub notes: Vec<String>,
}
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 normalized = raw.replace("\r\n", "\n").replace('\r', "\n");
let mut pending_notes: Vec<String> = Vec::new();
let mut stray_reported = false;
for (idx, line) in normalized.split('\n').enumerate() {
let lineno = idx + 1;
let cleaned = line.trim_matches(|c: char| {
c == '\u{0b}' || c == '\u{1c}' || c == '\u{1d}' || c == '\0' || c.is_whitespace()
});
if cleaned.is_empty() {
continue;
}
let head: String = cleaned.chars().take(3).collect();
match head.as_str() {
"FHS" | "BHS" | "BTS" | "FTS" => {
pending_notes.push(format!("line {}: batch wrapper {} skipped", lineno, head));
continue;
}
_ => {}
}
if head == "MSH" {
messages.push(RawMessage {
start_line: lineno,
lines: vec![(lineno, cleaned.to_string())],
notes: std::mem::take(&mut pending_notes),
});
} else if let Some(current) = messages.last_mut() {
current.lines.push((lineno, cleaned.to_string()));
} else if !stray_reported {
stray_reported = true;
warnings.push(format!(
"line {}: content before the first MSH segment was ignored",
lineno
));
}
}
(messages, warnings)
}
pub fn parse_message(raw: &RawMessage) -> Result<Message, ParseError> {
let (first_line, first_text) = &raw.lines[0];
let sep =
Separators::from_msh(first_text).map_err(|e| ParseError::new(*first_line, e.message))?;
let mut segments: Vec<Segment> = Vec::new();
let mut notes = raw.notes.clone();
let mut counts: Vec<(String, usize)> = Vec::new();
for (lineno, text) in &raw.lines {
let name: String = text.chars().take(3).collect();
let valid_name = name.chars().count() == 3
&& name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
&& name
.chars()
.next()
.map(|c| c.is_ascii_uppercase())
.unwrap_or(false);
if !valid_name {
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 {}: segment {} has no field separator after the name",
lineno, name
));
}
let mut seg = Segment::parse(&name, text, *lineno, &sep);
let entry = counts.iter_mut().find(|(n, _)| n == &name);
seg.occurrence = match entry {
Some((_, c)) => {
*c += 1;
*c
}
None => {
counts.push((name.clone(), 1));
1
}
};
segments.push(seg);
}
if segments.is_empty() || segments[0].name != "MSH" {
return Err(ParseError::new(
*first_line,
"message does not begin with a parsable MSH segment",
));
}
Ok(Message {
sep,
segments,
start_line: raw.start_line,
notes,
})
}
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),
"E" => out.push(sep.escape),
".br" => out.push('\n'),
".sp" => out.push('\n'),
"" => out.push(sep.escape),
other if other.starts_with('X') => {
let hex = &other[1..];
let mut bytes = Vec::new();
let mut ok = hex.len() % 2 == 0 && !hex.is_empty();
for pair in hex.as_bytes().chunks(2) {
match u8::from_str_radix(std::str::from_utf8(pair).unwrap_or("zz"), 16) {
Ok(b) => bytes.push(b),
Err(_) => {
ok = false;
break;
}
}
}
if ok {
out.push_str(&String::from_utf8_lossy(&bytes));
} else {
out.push_str(&format!("{}{}{}", sep.escape, other, sep.escape));
}
}
other if other.starts_with('H') || other.starts_with('N') || other.starts_with('Z') => {
}
other => out.push_str(&format!("{}{}{}", sep.escape, other, sep.escape)),
}
i = end + 1;
}
out
}
#[cfg(test)]
pub fn parse_str(text: &str) -> Message {
let (raws, _) = split_messages(text);
parse_message(&raws[0]).expect("fixture should parse")
}
#[cfg(test)]
mod tests {
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]
fn msh_field_numbering_is_offset_by_the_separator() {
let m = parse_str(ADT);
let msh = m.msh();
assert_eq!(msh.text(1, &m.sep), "|");
assert_eq!(msh.text(2, &m.sep), "^~\\&");
assert_eq!(msh.text(3, &m.sep), "HIS");
assert_eq!(msh.comp(9, 2, &m.sep), "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.reps.len(), 2);
assert_eq!(ids.rep(2).comp_text(1, &m.sep), "999");
assert_eq!(ids.rep(1).comp_text(5, &m.sep), "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);
}
}