1use std::fmt::Write as _;
5
6use std::fmt;
7
8#[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 fn from_msh(line: &str) -> Result<Self, ParseError> {
34 let mut chars = line.chars();
35 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 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 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 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#[derive(Debug, Clone, Copy)]
142pub struct Component<'a> {
143 raw: &'a str,
144 sep: Separators,
145 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#[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 #[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 #[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 #[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#[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 #[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 #[must_use]
286 pub fn comp(&self, seq: usize) -> &'a str {
287 self.rep(1).comp_text(seq)
288 }
289
290 #[must_use]
292 pub const fn text(&self) -> &'a str {
293 self.raw
294 }
295}
296
297fn 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#[derive(Debug, Clone)]
310pub struct Segment<'a> {
311 pub name: &'a str,
312 pub line: usize,
314 pub occurrence: usize,
316 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 literal: self.name == "MSH" && seq <= 2,
332 })
333 }
334
335 #[must_use]
337 pub fn has(&self, seq: usize) -> bool {
338 self.field(seq).is_some_and(|f| !f.is_empty())
339 }
340
341 #[must_use]
343 pub fn text(&self, seq: usize) -> &'a str {
344 self.field(seq).map_or("", |f| f.text())
345 }
346
347 #[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 #[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 #[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 let rest = if name == "MSH" {
373 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#[derive(Debug, Clone)]
394pub struct Message<'a> {
395 pub sep: Separators,
396 pub segments: Vec<Segment<'a>>,
397 pub start_line: usize,
399 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 #[must_use]
411 pub fn version(&self) -> &'a str {
412 self.msh().comp(12, 1)
413 }
414
415 #[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 #[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#[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 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 fn first_line(&self) -> (usize, &'a str) {
480 self.lines().next().unwrap_or((self.start_line, self.text))
481 }
482}
483
484fn 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
510fn 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
521fn head(text: &str) -> &str {
523 let end = text.char_indices().nth(3).map_or(text.len(), |(i, _)| i);
524 &text[..end]
525}
526
527#[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 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 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
579fn 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 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
614pub 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#[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" | "" => out.push(sep.escape),
693 other if other.starts_with('X') => {
694 let hex = &other[1..];
695 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 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]
728pub 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 #[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 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 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 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}