Skip to main content

hl7probe/
parser.rs

1//! HL7 v2 lexical parser: MLLP/batch stripping, segment/field/component/subcomponent
2//! decomposition and escape-sequence handling.
3
4use std::fmt::Write as _;
5
6use std::fmt;
7
8/// The five delimiters an HL7 v2 message declares in MSH-1 and MSH-2.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Separators {
11    pub field: char,
12    pub component: char,
13    pub repetition: char,
14    pub escape: char,
15    pub subcomponent: char,
16}
17
18impl Default for Separators {
19    fn default() -> Self {
20        Self {
21            field: '|',
22            component: '^',
23            repetition: '~',
24            escape: '\\',
25            subcomponent: '&',
26        }
27    }
28}
29
30impl Separators {
31    /// Reads MSH-1 (the character right after `MSH`) and MSH-2 (the encoding
32    /// characters up to the next field separator).
33    fn from_msh(line: &str) -> Result<Self, ParseError> {
34        let mut chars = line.chars();
35        // MSH-1 is the character straight after the segment name.
36        let (Some(_), Some(_), Some(_), Some(field)) =
37            (chars.next(), chars.next(), chars.next(), chars.next())
38        else {
39            return Err(ParseError::new(
40                0,
41                "MSH segment is truncated before the field separator",
42            ));
43        };
44        if field.is_alphanumeric() || field.is_whitespace() {
45            return Err(ParseError::new(
46                0,
47                format!("MSH-1 field separator {field:?} is not a usable delimiter"),
48            ));
49        }
50        let mut sep = Self {
51            field,
52            ..Default::default()
53        };
54        // MSH-2 declares up to four encoding characters, ending at the next
55        // field separator. Read straight off the iterator: this runs for every
56        // message in a batch, and collecting five characters into a Vec first
57        // cost three allocations a message.
58        let mut encoding = chars.take_while(|c| *c != field);
59        if let Some(c) = encoding.next() {
60            sep.component = c;
61        }
62        if let Some(c) = encoding.next() {
63            sep.repetition = c;
64        }
65        if let Some(c) = encoding.next() {
66            sep.escape = c;
67        }
68        if let Some(c) = encoding.next() {
69            sep.subcomponent = c;
70        }
71        // Anything left means all four slots were filled and more followed.
72        let extra = encoding.count();
73        if extra > 0 {
74            return Err(ParseError::new(
75                0,
76                format!(
77                    "MSH-2 declares {} encoding characters, expected at most 4",
78                    4 + extra
79                ),
80            ));
81        }
82        let all = [
83            sep.field,
84            sep.component,
85            sep.repetition,
86            sep.escape,
87            sep.subcomponent,
88        ];
89        for i in 0..all.len() {
90            for j in (i + 1)..all.len() {
91                if all[i] == all[j] {
92                    return Err(ParseError::new(
93                        0,
94                        format!("delimiter {:?} is declared twice in MSH-1/MSH-2", all[i]),
95                    ));
96                }
97            }
98        }
99        Ok(sep)
100    }
101}
102
103#[derive(Debug, Clone)]
104pub struct ParseError {
105    pub line: usize,
106    pub message: String,
107}
108
109impl ParseError {
110    fn new(line: usize, message: impl Into<String>) -> Self {
111        Self {
112            line,
113            message: message.into(),
114        }
115    }
116
117    /// The whole input held nothing that looks like an HL7 message.
118    pub(crate) fn no_message() -> Self {
119        Self::new(0, "no MSH segment found - is this an HL7 v2 message?")
120    }
121}
122
123impl std::error::Error for ParseError {}
124
125impl fmt::Display for ParseError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        if self.line > 0 {
128            write!(f, "line {}: {}", self.line, self.message)
129        } else {
130            write!(f, "{}", self.message)
131        }
132    }
133}
134
135/// A single component, itself made of `&`-delimited subcomponents.
136///
137/// This and its siblings are views over the segment line, not owned trees: a
138/// field carries the text it occupied and splits it when asked. HL7 leaves are
139/// two or three characters on average, so building a `Vec<String>` for each one
140/// cost far more than the text it held.
141#[derive(Debug, Clone, Copy)]
142pub struct Component<'a> {
143    raw: &'a str,
144    sep: Separators,
145    /// MSH-1 and MSH-2 are the delimiters themselves and must never be split.
146    literal: bool,
147}
148
149impl<'a> Component<'a> {
150    #[must_use]
151    pub fn sub(&self, seq: usize) -> &'a str {
152        self.subs().nth(seq.wrapping_sub(1)).unwrap_or("")
153    }
154
155    pub fn subs(&self) -> impl Iterator<Item = &'a str> {
156        split(self.raw, self.sep.subcomponent, self.literal)
157    }
158
159    #[must_use]
160    pub fn is_empty(&self) -> bool {
161        if self.literal {
162            return self.raw.is_empty();
163        }
164        self.raw.chars().all(|c| c == self.sep.subcomponent)
165    }
166}
167
168/// One repetition of a field (`~`-delimited at the field level).
169#[derive(Debug, Clone, Copy)]
170pub struct Repetition<'a> {
171    raw: &'a str,
172    sep: Separators,
173    literal: bool,
174}
175
176impl<'a> Repetition<'a> {
177    #[must_use]
178    pub fn comp(&self, seq: usize) -> Component<'a> {
179        Component {
180            raw: self.comp_text(seq),
181            sep: self.sep,
182            literal: self.literal,
183        }
184    }
185
186    pub fn comps(&self) -> impl Iterator<Item = Component<'a>> {
187        let (sep, literal) = (self.sep, self.literal);
188        split(self.raw, self.sep.component, self.literal).map(move |raw| Component {
189            raw,
190            sep,
191            literal,
192        })
193    }
194
195    /// Component `seq` as text, or `""` when the repetition has no such
196    /// component.
197    #[must_use]
198    pub fn comp_text(&self, seq: usize) -> &'a str {
199        split(self.raw, self.sep.component, self.literal)
200            .nth(seq.wrapping_sub(1))
201            .unwrap_or("")
202    }
203
204    /// The repetition as it appeared, component separators included.
205    #[must_use]
206    pub const fn text(&self) -> &'a str {
207        self.raw
208    }
209
210    #[must_use]
211    pub fn is_empty(&self) -> bool {
212        if self.literal {
213            return self.raw.is_empty();
214        }
215        self.raw
216            .chars()
217            .all(|c| c == self.sep.component || c == self.sep.subcomponent)
218    }
219
220    /// Number of components actually carrying data.
221    #[must_use]
222    pub fn filled_comps(&self) -> usize {
223        self.comps()
224            .enumerate()
225            .filter(|(_, c)| !c.is_empty())
226            .map(|(i, _)| i + 1)
227            .last()
228            .unwrap_or(0)
229    }
230}
231
232/// A field: one or more repetitions.
233#[derive(Debug, Clone, Copy)]
234pub struct Field<'a> {
235    raw: &'a str,
236    sep: Separators,
237    literal: bool,
238}
239
240impl<'a> Field<'a> {
241    #[must_use]
242    pub fn is_empty(&self) -> bool {
243        if self.literal {
244            return self.raw.is_empty();
245        }
246        self.raw.chars().all(|c| {
247            c == self.sep.repetition || c == self.sep.component || c == self.sep.subcomponent
248        })
249    }
250
251    /// HL7 explicit null: the two-character value `""` means "delete this value".
252    #[must_use]
253    pub fn is_null(&self) -> bool {
254        self.rep_count() == 1
255            && split(self.raw, self.sep.component, self.literal).count() == 1
256            && self.rep(1).comp(1).sub(1) == "\"\""
257    }
258
259    #[must_use]
260    pub fn rep(&self, seq: usize) -> Repetition<'a> {
261        Repetition {
262            raw: split(self.raw, self.sep.repetition, self.literal)
263                .nth(seq.wrapping_sub(1))
264                .unwrap_or(""),
265            sep: self.sep,
266            literal: self.literal,
267        }
268    }
269
270    pub fn reps(&self) -> impl Iterator<Item = Repetition<'a>> {
271        let (sep, literal) = (self.sep, self.literal);
272        split(self.raw, self.sep.repetition, self.literal).map(move |raw| Repetition {
273            raw,
274            sep,
275            literal,
276        })
277    }
278
279    #[must_use]
280    pub fn rep_count(&self) -> usize {
281        split(self.raw, self.sep.repetition, self.literal).count()
282    }
283
284    /// First repetition, component `seq`, as text.
285    #[must_use]
286    pub fn comp(&self, seq: usize) -> &'a str {
287        self.rep(1).comp_text(seq)
288    }
289
290    /// Whole field as it appeared on the wire (repetitions included).
291    #[must_use]
292    pub const fn text(&self) -> &'a str {
293        self.raw
294    }
295}
296
297/// Splits on `sep`, or yields the whole text when it must not be split. An
298/// empty input still yields one empty piece, matching `str::split`.
299fn split(raw: &str, sep: char, literal: bool) -> impl Iterator<Item = &str> {
300    let mut whole = literal.then_some(raw);
301    let mut parts = (!literal).then(|| raw.split(sep));
302    std::iter::from_fn(move || match &mut parts {
303        Some(parts) => parts.next(),
304        None => whole.take(),
305    })
306}
307
308/// One segment line.
309#[derive(Debug, Clone)]
310pub struct Segment<'a> {
311    pub name: &'a str,
312    /// 1-based line number in the source file, for error reporting.
313    pub line: usize,
314    /// 1-based occurrence among segments with the same name.
315    pub occurrence: usize,
316    /// The text of each field, in order. Index 0 holds field 1.
317    fields: Vec<&'a str>,
318    pub raw: &'a str,
319    sep: Separators,
320}
321
322impl<'a> Segment<'a> {
323    #[must_use]
324    pub fn field(&self, seq: usize) -> Option<Field<'a>> {
325        let raw = *self.fields.get(seq.wrapping_sub(1))?;
326        Some(Field {
327            raw,
328            sep: self.sep,
329            // MSH-1 is the field separator and MSH-2 the encoding characters:
330            // both are delimiters rather than values.
331            literal: self.name == "MSH" && seq <= 2,
332        })
333    }
334
335    /// True when field `seq` exists and carries data.
336    #[must_use]
337    pub fn has(&self, seq: usize) -> bool {
338        self.field(seq).is_some_and(|f| !f.is_empty())
339    }
340
341    /// Field `seq` as raw text, or `""` when absent.
342    #[must_use]
343    pub fn text(&self, seq: usize) -> &'a str {
344        self.field(seq).map_or("", |f| f.text())
345    }
346
347    /// First repetition, component `c`, of field `seq`.
348    #[must_use]
349    pub fn comp(&self, seq: usize, c: usize) -> &'a str {
350        self.field(seq).map_or("", |f| f.comp(c))
351    }
352
353    /// Highest field number carrying data.
354    #[must_use]
355    pub fn last_populated(&self) -> usize {
356        (1..=self.fields.len())
357            .rfind(|seq| self.has(*seq))
358            .unwrap_or(0)
359    }
360
361    /// Z-segments are site-defined and exempt from dictionary checks.
362    #[must_use]
363    pub fn is_custom(&self) -> bool {
364        self.name.starts_with('Z')
365    }
366
367    fn parse(name: &'a str, raw: &'a str, line: usize, sep: &Separators) -> Self {
368        let parts: Vec<&str> = raw.split(sep.field).collect();
369        let mut fields: Vec<&'a str> = Vec::new();
370        // MSH is positionally special: MSH-1 *is* the field separator, so the
371        // first split part after the name is MSH-2, not MSH-1.
372        let rest = if name == "MSH" {
373            // The separator itself, sliced from the line rather than rebuilt.
374            fields.push(&raw[name.len()..name.len() + sep.field.len_utf8()]);
375            fields.push(parts.get(1).copied().unwrap_or(""));
376            &parts[2.min(parts.len())..]
377        } else {
378            &parts[1.min(parts.len())..]
379        };
380        fields.extend_from_slice(rest);
381        Self {
382            name,
383            line,
384            occurrence: 1,
385            fields,
386            raw,
387            sep: *sep,
388        }
389    }
390}
391
392/// A fully decomposed HL7 message.
393#[derive(Debug, Clone)]
394pub struct Message<'a> {
395    pub sep: Separators,
396    pub segments: Vec<Segment<'a>>,
397    /// 1-based line where this message's MSH was found.
398    pub start_line: usize,
399    /// Non-fatal observations made while tokenising (stray bytes, batch wrappers).
400    pub notes: Vec<String>,
401}
402
403impl<'a> Message<'a> {
404    #[must_use]
405    pub fn msh(&self) -> &Segment<'a> {
406        &self.segments[0]
407    }
408
409    /// MSH-12.1, e.g. `2.5.1`.
410    #[must_use]
411    pub fn version(&self) -> &'a str {
412        self.msh().comp(12, 1)
413    }
414
415    /// (message code, trigger event, structure) from MSH-9.
416    #[must_use]
417    pub fn message_type(&self) -> (&'a str, &'a str, &'a str) {
418        let f = self.msh();
419        (f.comp(9, 1), f.comp(9, 2), f.comp(9, 3))
420    }
421
422    /// `ADT^A01`, or just `ADT` when no trigger event is present.
423    #[must_use]
424    pub fn type_label(&self) -> String {
425        let (code, trigger, _) = self.message_type();
426        match (code.is_empty(), trigger.is_empty()) {
427            (true, _) => "(no MSH-9)".to_string(),
428            (false, true) => code.to_string(),
429            (false, false) => format!("{code}^{trigger}"),
430        }
431    }
432
433    #[must_use]
434    pub fn control_id(&self) -> &'a str {
435        self.msh().comp(10, 1)
436    }
437
438    #[must_use]
439    pub fn find(&self, name: &str) -> Vec<&Segment<'a>> {
440        self.segments.iter().filter(|s| s.name == name).collect()
441    }
442
443    #[must_use]
444    pub fn first(&self, name: &str) -> Option<&Segment<'a>> {
445        self.segments.iter().find(|s| s.name == name)
446    }
447}
448
449/// One message's worth of source lines, still unparsed.
450/// Where a message sits in the file, rather than a copy of it or an index of
451/// its lines. Splitting the lines again when the message is parsed costs the
452/// same walk either way, and it keeps a whole batch down to a few bytes per
453/// message instead of twenty-four per line.
454#[derive(Debug)]
455pub struct RawMessage<'a> {
456    pub start_line: usize,
457    text: &'a str,
458    pub notes: Vec<String>,
459}
460
461impl<'a> RawMessage<'a> {
462    /// The segment lines this message is made of, cleaned of framing bytes and
463    /// numbered as they are in the file. Blank lines and batch wrappers are
464    /// skipped here exactly as `split_messages` skipped them.
465    pub fn lines(&self) -> impl Iterator<Item = (usize, &'a str)> + '_ {
466        lines(self.text)
467            .enumerate()
468            .filter_map(move |(offset, (_, line))| {
469                let cleaned = clean(line);
470                if cleaned.is_empty() || is_batch_wrapper(head(cleaned)) {
471                    return None;
472                }
473                Some((self.start_line + offset, cleaned))
474            })
475    }
476
477    /// The MSH line, which every message starts with and which decides on its
478    /// own whether the message can be read at all.
479    fn first_line(&self) -> (usize, &'a str) {
480        self.lines().next().unwrap_or((self.start_line, self.text))
481    }
482}
483
484/// Splits on CR, LF or CRLF, counting CRLF as one break so line numbers match
485/// what an editor shows, and reports where each line starts. Iterating beats
486/// normalising the whole file into a new `String` first, which cost two full
487/// copies of the input.
488fn lines(text: &str) -> impl Iterator<Item = (usize, &str)> {
489    let mut offset = 0usize;
490    let mut rest = Some(text);
491    std::iter::from_fn(move || {
492        let current = rest?;
493        let start = offset;
494        match current.find(['\r', '\n']) {
495            None => {
496                rest = None;
497                Some((start, current))
498            }
499            Some(at) => {
500                let (line, tail) = current.split_at(at);
501                let skip = usize::from(tail.starts_with("\r\n")) + 1;
502                offset += at + skip;
503                rest = Some(&tail[skip..]);
504                Some((start, line))
505            }
506        }
507    })
508}
509
510/// Strips MLLP framing bytes and surrounding whitespace from a line.
511fn clean(line: &str) -> &str {
512    line.trim_matches(|c: char| {
513        c == '\u{0b}' || c == '\u{1c}' || c == '\u{1d}' || c == '\0' || c.is_whitespace()
514    })
515}
516
517fn is_batch_wrapper(head: &str) -> bool {
518    matches!(head, "FHS" | "BHS" | "BTS" | "FTS")
519}
520
521/// The first three characters of a line, which is where a segment name lives.
522fn head(text: &str) -> &str {
523    let end = text.char_indices().nth(3).map_or(text.len(), |(i, _)| i);
524    &text[..end]
525}
526
527/// Splits a file into messages, tolerating CR/LF/CRLF endings, MLLP framing
528/// bytes and HL7 batch (FHS/BHS/BTS/FTS) wrappers.
529#[must_use]
530pub fn split_messages(raw: &str) -> (Vec<RawMessage<'_>>, Vec<String>) {
531    let mut messages: Vec<RawMessage<'_>> = Vec::new();
532    let mut warnings: Vec<String> = Vec::new();
533    let mut pending_notes: Vec<String> = Vec::new();
534    let mut stray_reported = false;
535    // Byte range of the message being accumulated, so it can be sliced out of
536    // `raw` once the next MSH (or the end of the file) closes it.
537    let mut open: Option<(usize, usize)> = None;
538
539    for (idx, (offset, line)) in lines(raw).enumerate() {
540        let lineno = idx + 1;
541        let cleaned = clean(line);
542        if cleaned.is_empty() {
543            continue;
544        }
545        let head = head(cleaned);
546        if is_batch_wrapper(head) {
547            pending_notes.push(format!("line {lineno}: batch wrapper {head} skipped"));
548            continue;
549        }
550        let line_end = offset + line.len();
551        if head == "MSH" {
552            // The open range and the message it belongs to are pushed
553            // together, so these are both Some or both None.
554            if let (Some((start, end)), Some(previous)) =
555                (open.replace((offset, line_end)), messages.last_mut())
556            {
557                previous.text = &raw[start..end];
558            }
559            messages.push(RawMessage {
560                start_line: lineno,
561                text: &raw[offset..line_end],
562                notes: std::mem::take(&mut pending_notes),
563            });
564        } else if let Some((_, end)) = open.as_mut() {
565            *end = line_end;
566        } else if !stray_reported {
567            stray_reported = true;
568            warnings.push(format!(
569                "line {lineno}: content before the first MSH segment was ignored"
570            ));
571        }
572    }
573    if let (Some((start, end)), Some(last)) = (open, messages.last_mut()) {
574        last.text = &raw[start..end];
575    }
576    (messages, warnings)
577}
578
579/// The three-character name a line starts with, when it is a usable segment
580/// name. Shared so the whole-message check and the segment loop cannot drift.
581fn segment_name(text: &str) -> Option<&str> {
582    let name = head(text);
583    let usable = name.chars().count() == 3
584        && name
585            .chars()
586            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
587        && name.starts_with(|c: char| c.is_ascii_uppercase());
588    usable.then_some(name)
589}
590
591impl RawMessage<'_> {
592    /// The delimiters this message declares, or the reason it cannot be read.
593    /// Both of `parse_message`'s failure modes are settled by the first line,
594    /// so a batch can be checked for unreadable messages without building a
595    /// single tree.
596    /// # Errors
597    ///
598    /// Returns [`ParseError`] when the MSH line is truncated, declares a
599    /// separator that cannot serve as one, or declares more than four
600    /// encoding characters.
601    pub fn separators(&self) -> Result<Separators, ParseError> {
602        let (lineno, text) = self.first_line();
603        let sep = Separators::from_msh(text).map_err(|e| ParseError::new(lineno, e.message))?;
604        if segment_name(text) != Some("MSH") {
605            return Err(ParseError::new(
606                lineno,
607                "message does not begin with a parsable MSH segment",
608            ));
609        }
610        Ok(sep)
611    }
612}
613
614/// # Errors
615///
616/// Returns [`ParseError`] when the message does not begin with a readable MSH
617/// segment. Unrecognisable segments *within* a message are recorded as notes
618/// rather than failing the parse.
619pub fn parse_message<'a>(raw: &RawMessage<'a>) -> Result<Message<'a>, ParseError> {
620    let sep = raw.separators()?;
621
622    let mut segments: Vec<Segment<'a>> = Vec::new();
623    let mut notes = raw.notes.clone();
624    let mut counts: Vec<(&str, usize)> = Vec::new();
625
626    for (lineno, text) in raw.lines() {
627        let Some(name) = segment_name(text) else {
628            notes.push(format!(
629                "line {}: skipped unrecognisable segment starting {:?}",
630                lineno,
631                text.chars().take(8).collect::<String>()
632            ));
633            continue;
634        };
635        if text.chars().nth(3) != Some(sep.field) {
636            notes.push(format!(
637                "line {lineno}: segment {name} has no field separator after the name"
638            ));
639        }
640        let mut seg = Segment::parse(name, text, lineno, &sep);
641        let entry = counts.iter_mut().find(|(n, _)| *n == name);
642        seg.occurrence = if let Some((_, c)) = entry {
643            *c += 1;
644            *c
645        } else {
646            counts.push((name, 1));
647            1
648        };
649        segments.push(seg);
650    }
651
652    Ok(Message {
653        sep,
654        segments,
655        start_line: raw.start_line,
656        notes,
657    })
658}
659
660/// Resolves HL7 escape sequences for human-readable display.
661#[must_use]
662pub fn unescape(s: &str, sep: &Separators) -> String {
663    if !s.contains(sep.escape) {
664        return s.to_string();
665    }
666    let mut out = String::with_capacity(s.len());
667    let chars: Vec<char> = s.chars().collect();
668    let mut i = 0;
669    while i < chars.len() {
670        if chars[i] != sep.escape {
671            out.push(chars[i]);
672            i += 1;
673            continue;
674        }
675        let end = chars[i + 1..]
676            .iter()
677            .position(|c| *c == sep.escape)
678            .map(|p| i + 1 + p);
679        let Some(end) = end else {
680            out.push(chars[i]);
681            i += 1;
682            continue;
683        };
684        let code: String = chars[i + 1..end].iter().collect();
685        match code.as_str() {
686            "F" => out.push(sep.field),
687            "S" => out.push(sep.component),
688            "T" => out.push(sep.subcomponent),
689            "R" => out.push(sep.repetition),
690            ".br" | ".sp" => out.push('\n'),
691            // \E\ is the escape character itself; \\ is the same thing written bare.
692            "E" | "" => out.push(sep.escape),
693            other if other.starts_with('X') => {
694                let hex = &other[1..];
695                // Collecting into `Option<Vec<_>>` stops at the first bad pair,
696                // so a malformed \Xnn\ falls through to the literal branch.
697                let decoded = (!hex.is_empty() && hex.len() % 2 == 0)
698                    .then(|| {
699                        hex.as_bytes()
700                            .chunks(2)
701                            .map(|pair| {
702                                u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()
703                            })
704                            .collect::<Option<Vec<u8>>>()
705                    })
706                    .flatten();
707                match decoded {
708                    Some(bytes) => out.push_str(&String::from_utf8_lossy(&bytes)),
709                    None => {
710                        let _ = write!(out, "{}{}{}", sep.escape, other, sep.escape);
711                    }
712                }
713            }
714            // Highlighting and site-defined escapes carry no display text.
715            other if other.starts_with('H') || other.starts_with('N') || other.starts_with('Z') => {
716            }
717            other => {
718                let _ = write!(out, "{}{}{}", sep.escape, other, sep.escape);
719            }
720        }
721        i = end + 1;
722    }
723    out
724}
725
726#[cfg(test)]
727#[must_use]
728/// # Panics
729///
730/// Panics when `text` does not hold a parsable message. Test fixtures are
731/// written by hand, so a fixture that does not parse is a broken test.
732pub fn parse_str(text: &str) -> Message<'_> {
733    let (raws, _) = split_messages(text);
734    parse_message(&raws[0]).expect("fixture should parse")
735}
736
737#[cfg(test)]
738mod tests {
739    #![allow(
740        clippy::unwrap_used,
741        reason = "panicking is the failure mode a test wants"
742    )]
743    use super::*;
744
745    const ADT: &str = "MSH|^~\\&|HIS|MERCY|LIS|LAB|20240115143200||ADT^A01^ADT_A01|MSG1|P|2.5.1\r\
746PID|1||123456^^^MERCY^MR~999^^^SSA^SS||Smith^John^A||19850312|M\r\
747PV1|1|I|ER^101^A&Bay 2^MERCY\r";
748
749    #[test]
750    fn reads_default_delimiters() {
751        let m = parse_str(ADT);
752        assert_eq!(m.sep, Separators::default());
753        assert_eq!(m.segments.len(), 3);
754    }
755
756    #[test]
757    fn honours_custom_delimiters() {
758        let m = parse_str("MSH#@~\\&#A#B#C#D#20240101120000##ADT@A01#1#P#2.5.1\r");
759        assert_eq!(m.sep.field, '#');
760        assert_eq!(m.sep.component, '@');
761        assert_eq!(m.type_label(), "ADT^A01");
762    }
763
764    /// A message borrows its text rather than owning a tree of `String`s, and
765    /// the interactive viewer holds every message in a batch at once, so these
766    /// sizes are a documented cost rather than an incidental one. Turning a
767    /// borrowed field back into an owned one would show up here first.
768    #[test]
769    #[cfg(target_pointer_width = "64")]
770    fn the_message_tree_stays_borrowed() {
771        use std::mem::size_of;
772        assert_eq!(size_of::<Segment<'_>>(), 96, "Segment grew");
773        assert_eq!(size_of::<Message<'_>>(), 80, "Message grew");
774        assert_eq!(size_of::<RawMessage<'_>>(), 48, "RawMessage grew");
775        // Views over the segment text: pointer, delimiters, literal flag.
776        assert_eq!(size_of::<Field<'_>>(), 40, "Field grew");
777        assert_eq!(size_of::<Repetition<'_>>(), 40, "Repetition grew");
778        assert_eq!(size_of::<Component<'_>>(), 40, "Component grew");
779    }
780
781    #[test]
782    fn msh_field_numbering_is_offset_by_the_separator() {
783        let m = parse_str(ADT);
784        let msh = m.msh();
785        assert_eq!(msh.text(1), "|");
786        assert_eq!(msh.text(2), "^~\\&");
787        assert_eq!(msh.text(3), "HIS");
788        assert_eq!(msh.comp(9, 2), "A01");
789        assert_eq!(m.version(), "2.5.1");
790        assert_eq!(m.control_id(), "MSG1");
791    }
792
793    #[test]
794    fn splits_repetitions_components_and_subcomponents() {
795        let m = parse_str(ADT);
796        let pid = m.first("PID").unwrap();
797        let ids = pid.field(3).unwrap();
798        assert_eq!(ids.rep_count(), 2);
799        assert_eq!(ids.rep(2).comp_text(1), "999");
800        assert_eq!(ids.rep(1).comp_text(5), "MR");
801
802        let pv1 = m.first("PV1").unwrap();
803        let location = pv1.field(3).unwrap().rep(1);
804        assert_eq!(location.comp(3).sub(1), "A");
805        assert_eq!(location.comp(3).sub(2), "Bay 2");
806    }
807
808    #[test]
809    fn tracks_segment_occurrence_and_line() {
810        let m = parse_str("MSH|^~\\&|A|B|C|D|20240101120000||ORU^R01|1|P|2.5.1\rOBX|1\rOBX|2\r");
811        let obx = m.find("OBX");
812        assert_eq!(obx.len(), 2);
813        assert_eq!(obx[1].occurrence, 2);
814        assert_eq!(obx[1].line, 3);
815    }
816
817    #[test]
818    fn accepts_lf_crlf_and_mllp_framing() {
819        for text in [
820            "MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\nMSA|AA|1\n",
821            "MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\r\nMSA|AA|1\r\n",
822            "\u{b}MSH|^~\\&|A|B|C|D|20240101120000||ACK|1|P|2.5.1\rMSA|AA|1\r\u{1c}\r",
823        ] {
824            let m = parse_str(text);
825            assert_eq!(m.segments.len(), 2, "{text:?}");
826            assert_eq!(m.segments[1].name, "MSA");
827        }
828    }
829
830    #[test]
831    fn skips_batch_wrappers_and_splits_messages() {
832        let text = "FHS|^~\\&\rBHS|^~\\&\r\
833MSH|^~\\&|A|B|C|D|20240101120000||ADT^A01|1|P|2.5.1\rPID|1\r\
834MSH|^~\\&|A|B|C|D|20240101130000||ADT^A03|2|P|2.5.1\rPID|1\rBTS|2\rFTS|1\r";
835        let (raws, warnings) = split_messages(text);
836        assert_eq!(raws.len(), 2);
837        assert!(warnings.is_empty());
838        let first = parse_message(&raws[0]).unwrap();
839        assert_eq!(first.segments.len(), 2);
840        assert_eq!(first.notes.len(), 2, "batch wrappers should be noted");
841        assert_eq!(parse_message(&raws[1]).unwrap().control_id(), "2");
842    }
843
844    #[test]
845    fn rejects_input_without_msh() {
846        let (raws, warnings) = split_messages("PID|1||123\r");
847        assert!(raws.is_empty());
848        assert_eq!(warnings.len(), 1);
849    }
850
851    #[test]
852    fn rejects_duplicate_delimiters() {
853        let (raws, _) = split_messages("MSH|^~\\^|A|B|C|D|20240101120000||ACK|1|P|2.5.1\r");
854        assert!(parse_message(&raws[0]).is_err());
855    }
856
857    #[test]
858    fn detects_explicit_null() {
859        let m = parse_str("MSH|^~\\&|A|B|C|D|20240101120000||ADT^A08|1|P|2.5.1\rPID|1||\"\"\r");
860        assert!(m.first("PID").unwrap().field(3).unwrap().is_null());
861    }
862
863    #[test]
864    fn resolves_escape_sequences() {
865        let sep = Separators::default();
866        assert_eq!(unescape("Smith \\T\\ Sons", &sep), "Smith & Sons");
867        assert_eq!(unescape("100\\S\\200", &sep), "100^200");
868        assert_eq!(unescape("a\\F\\b", &sep), "a|b");
869        assert_eq!(unescape("line1\\.br\\line2", &sep), "line1\nline2");
870        assert_eq!(unescape("\\X0A\\", &sep), "\n");
871        assert_eq!(unescape("50\\E\\50", &sep), "50\\50");
872        // Unknown escapes survive untouched rather than eating the text.
873        assert_eq!(unescape("a\\Q9\\b", &sep), "a\\Q9\\b");
874    }
875
876    #[test]
877    fn last_populated_ignores_trailing_empties() {
878        let m = parse_str(
879            "MSH|^~\\&|A|B|C|D|20240101120000||ADT^A01|1|P|2.5.1\rEVN|A01|20240101120000||||\r",
880        );
881        assert_eq!(m.first("EVN").unwrap().last_populated(), 2);
882    }
883
884    #[test]
885    fn a_truncated_msh_line_cannot_declare_delimiters() {
886        let (raws, _) = split_messages("MSH\r");
887        let e = parse_message(&raws[0]).expect_err("nothing to read");
888        assert!(e.message.contains("truncated"), "{e}");
889    }
890
891    #[test]
892    fn more_than_four_encoding_characters_is_reported() {
893        for (line, count) in [("MSH|^~\\&%|A\r", 5), ("MSH|^~\\&%$|A\r", 6)] {
894            let (raws, _) = split_messages(line);
895            let e = parse_message(&raws[0]).expect_err("too many");
896            assert!(e.message.contains(&count.to_string()), "{e}");
897            assert!(e.message.contains("at most 4"), "{e}");
898        }
899    }
900
901    #[test]
902    fn a_message_with_no_message_type_says_so() {
903        let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200|||MSG1|P|2.5.1\r");
904        assert_eq!(m.type_label(), "(no MSH-9)");
905    }
906
907    #[test]
908    fn a_segment_without_a_field_separator_after_its_name_is_noted() {
909        let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200||ADT^A01|1|P|2.5.1\rPID\r");
910        assert!(
911            m.notes.iter().any(|n| n.contains("no field separator")),
912            "{:?}",
913            m.notes
914        );
915    }
916
917    #[test]
918    fn an_unreadable_segment_name_is_skipped_with_a_note() {
919        let m = parse_str("MSH|^~\\&|A|B|C|D|20240115143200||ADT^A01|1|P|2.5.1\r??|1|x\r");
920        assert!(
921            m.notes.iter().any(|n| n.contains("unrecognisable segment")),
922            "{:?}",
923            m.notes
924        );
925        assert_eq!(m.segments.len(), 1, "only MSH survives");
926    }
927
928    #[test]
929    fn the_delimiter_fields_are_never_split_apart() {
930        let m = parse_str(ADT);
931        let msh2 = m.msh().field(2).expect("MSH-2 exists");
932        // The encoding characters contain the component and repetition
933        // separators; splitting on them would destroy the value.
934        assert_eq!(msh2.text(), "^~\\&");
935        assert_eq!(msh2.rep_count(), 1);
936        assert_eq!(msh2.rep(1).comp(1).sub(1), "^~\\&");
937        assert!(!msh2.is_empty());
938    }
939
940    #[test]
941    fn a_field_beyond_the_end_of_a_segment_is_absent() {
942        let m = parse_str(ADT);
943        assert!(m.msh().field(999).is_none());
944        assert_eq!(m.msh().text(999), "");
945        assert_eq!(m.msh().comp(999, 1), "");
946        assert!(!m.msh().has(999));
947    }
948}