use crate::message::is_header_name;
use crate::{Component, Error, Field, Message, Repetition, Segment, Separators, Subcomponent};
const BATCH_ENVELOPE: [&str; 4] = ["FHS", "BHS", "BTS", "FTS"];
pub fn parse(text: &str) -> Result<Message, Error> {
let text = strip_bom(text);
let lines = segment_lines(text);
let (_, first) = *lines.first().ok_or(Error::Empty)?;
let name = segment_name(first);
if !is_header_name(name) {
return Err(Error::MissingHeader(name.to_string()));
}
let separators = Separators::from_header(first)?;
Ok(Message {
separators,
segments: lines
.iter()
.map(|(_, line)| parse_segment(line, &separators))
.collect(),
})
}
#[must_use]
pub fn parse_with(text: &str, separators: Separators) -> Message {
Message {
separators,
segments: segment_lines(strip_bom(text))
.iter()
.map(|(_, line)| parse_segment(line, &separators))
.collect(),
}
}
#[must_use]
pub fn split_messages(text: &str) -> Vec<&str> {
let text = strip_bom(text);
let mut spans: Vec<(usize, usize)> = Vec::new();
let mut current: Option<(usize, usize)> = None;
for (start, line) in segment_lines(text) {
let end = start + line.len();
if is_batch_envelope(line) {
spans.extend(current.take());
} else if segment_name(line) == "MSH" || current.is_none() {
spans.extend(current.replace((start, end)));
} else if let Some(span) = current.as_mut() {
span.1 = end;
}
}
spans.extend(current);
spans
.into_iter()
.map(|(start, end)| &text[start..end])
.collect()
}
fn is_batch_envelope(line: &str) -> bool {
BATCH_ENVELOPE.contains(&segment_name(line))
}
fn strip_bom(text: &str) -> &str {
text.strip_prefix('\u{feff}').unwrap_or(text)
}
fn segment_name(line: &str) -> &str {
let end = line
.find(|c: char| !c.is_ascii_alphanumeric())
.unwrap_or(line.len());
&line[..end]
}
fn segment_lines(text: &str) -> Vec<(usize, &str)> {
let mut lines = Vec::new();
let bytes = text.as_bytes();
let mut start = 0;
for end in 0..=bytes.len() {
if end < bytes.len() && bytes[end] != b'\r' && bytes[end] != b'\n' {
continue;
}
let line = &text[start..end];
if !line.trim().is_empty() {
lines.push((start, line));
}
start = end + 1;
}
lines
}
fn parse_segment(line: &str, separators: &Separators) -> Segment {
let mut tokens = line.split(separators.field);
let name = tokens.next().unwrap_or("").to_string();
let mut fields = Vec::new();
if is_header_name(&name) {
fields.push(literal_field(separators.field.to_string()));
if let Some(encoding) = tokens.next() {
fields.push(literal_field(encoding.to_string()));
}
}
fields.extend(tokens.map(|raw| parse_field(raw, separators)));
Segment { name, fields }
}
fn literal_field(raw: String) -> Field {
Field {
repetitions: vec![Repetition {
components: vec![Component {
subcomponents: vec![Subcomponent::new(raw)],
}],
}],
}
}
fn parse_field(raw: &str, separators: &Separators) -> Field {
if raw.is_empty() {
return Field::default();
}
Field {
repetitions: raw
.split(separators.repetition)
.map(|repetition| parse_repetition(repetition, separators))
.collect(),
}
}
fn parse_repetition(raw: &str, separators: &Separators) -> Repetition {
Repetition {
components: raw
.split(separators.component)
.map(|component| parse_component(component, separators))
.collect(),
}
}
fn parse_component(raw: &str, separators: &Separators) -> Component {
Component {
subcomponents: raw
.split(separators.subcomponent)
.map(Subcomponent::new)
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_hierarchy() {
let message = parse("MSH|^~\\&|LAB\rPID|1||A^B&C~D").unwrap();
assert_eq!(message.separators, Separators::default());
let pid = message.segment("PID").unwrap();
assert_eq!(pid.fields.len(), 3);
let field = pid.field(3).unwrap();
assert_eq!(field.repetitions.len(), 2);
assert_eq!(field.repetition(1).unwrap().components.len(), 2);
assert_eq!(
field
.repetition(1)
.unwrap()
.component(2)
.unwrap()
.subcomponents
.len(),
2
);
assert_eq!(
field
.repetition(2)
.unwrap()
.component(1)
.unwrap()
.subcomponent(1)
.unwrap()
.raw,
"D"
);
}
#[test]
fn numbers_header_fields_the_way_hl7_does() {
let message = parse("MSH|^~\\&|LAB|ACME").unwrap();
let msh = message.header().unwrap();
assert_eq!(msh.field(1).unwrap().to_er7(&message.separators), "|");
assert_eq!(msh.field(2).unwrap().to_er7(&message.separators), "^~\\&");
assert_eq!(msh.field(3).unwrap().to_er7(&message.separators), "LAB");
assert_eq!(msh.field(4).unwrap().to_er7(&message.separators), "ACME");
}
#[test]
fn accepts_every_terminator_and_drops_blank_lines() {
for text in [
"MSH|^~\\&|LAB\rPID|1",
"MSH|^~\\&|LAB\nPID|1",
"MSH|^~\\&|LAB\r\nPID|1",
"MSH|^~\\&|LAB\r\n\r\nPID|1\r\n",
"\u{feff}MSH|^~\\&|LAB\rPID|1",
] {
let message = parse(text).unwrap();
assert_eq!(message.segments.len(), 2, "for {text:?}");
assert_eq!(message.segments[1].name, "PID");
}
}
#[test]
fn rejects_input_without_a_header() {
assert!(matches!(parse(""), Err(Error::Empty)));
assert!(matches!(parse(" \r\n "), Err(Error::Empty)));
match parse("PID|1") {
Err(Error::MissingHeader(name)) => assert_eq!(name, "PID"),
other => panic!("expected a missing-header error, got {other:?}"),
}
assert!(matches!(parse("MSH"), Err(Error::BadHeader(_))));
}
#[test]
fn parses_a_fragment_with_given_delimiters() {
let fragment = parse_with("PID|1||A^B\rNTE|1||note", Separators::default());
assert_eq!(fragment.segments.len(), 2);
assert_eq!(fragment.query("PID-3.2").unwrap().as_deref(), Some("B"));
}
#[test]
fn splits_a_batch_file() {
let batch = "FHS|^~\\&|F\rBHS|^~\\&|B\r\
MSH|^~\\&|A\rMSA|AA|1\r\
MSH|^~\\&|B\rMSA|AA|2\r\
BTS|2\rFTS|1";
assert_eq!(
split_messages(batch),
vec!["MSH|^~\\&|A\rMSA|AA|1", "MSH|^~\\&|B\rMSA|AA|2"]
);
}
#[test]
fn an_empty_field_has_no_repetitions() {
let message = parse("MSH|^~\\&|LAB\rPID||A|A~~B|~|^^C|A&&B").unwrap();
let pid = message.segment("PID").unwrap();
assert_eq!(pid.field(1).unwrap().repetitions.len(), 0); assert_eq!(pid.field(2).unwrap().repetitions.len(), 1); assert_eq!(pid.field(3).unwrap().repetitions.len(), 3); assert_eq!(pid.field(4).unwrap().repetitions.len(), 2); assert_eq!(
pid.field(5)
.unwrap()
.repetition(1)
.unwrap()
.components
.len(),
3 );
assert_eq!(
pid.field(6)
.unwrap()
.component(1)
.unwrap()
.subcomponents
.len(),
3 );
assert!(pid.field(1).unwrap().is_empty());
assert!(!pid.field(1).unwrap().is_null());
assert_eq!(message.to_er7(), "MSH|^~\\&|LAB\rPID||A|A~~B|~|^^C|A&&B");
}
#[test]
fn splits_plain_concatenated_messages() {
let text = "MSH|^~\\&|A\rMSH|^~\\&|B\n";
assert_eq!(split_messages(text), vec!["MSH|^~\\&|A", "MSH|^~\\&|B"]);
assert_eq!(split_messages(""), Vec::<&str>::new());
}
#[test]
fn keeps_a_headerless_first_message_for_parse_to_reject() {
let text = "PID|1\rMSH|^~\\&|A";
assert_eq!(split_messages(text), vec!["PID|1", "MSH|^~\\&|A"]);
assert!(parse(split_messages(text)[0]).is_err());
}
#[test]
fn does_not_mistake_a_local_segment_for_an_envelope() {
let text = "MSH|^~\\&|A\rBTSX|1";
assert_eq!(split_messages(text), vec![text]);
}
}