hl7_2/structure.rs
1//! Matching a message's segments against an abstract message structure.
2//!
3//! HL7 sends a flat list of segments and expects the receiver to know that
4//! in an `ORU_R01` the third `OBX` belongs to the second `OBR`. The
5//! grammars that say so live in the dictionary ([`crate::dictionary::Item`]);
6//! this module is the greedy recursive-descent matcher that applies one to
7//! a segment list and reports the nesting it found.
8//!
9//! Matching is all-or-nothing and never rewrites the message: either the
10//! whole segment list fits the grammar, or the caller keeps the flat list
11//! it already has. That is deliberate — a partial match would have to guess
12//! where an unexpected Z-segment belongs, and a wrong guess is worse than
13//! no grouping at all. [`crate::Message::tree`] takes exactly this fallback,
14//! and [`crate::Message::validate`] reports the failure as a diagnostic
15//! instead of hiding it.
16
17use crate::dictionary::Item;
18
19/// Where one segment, or one group of segments, sits in the matched
20/// structure. Segments are named by their index into the list that was
21/// matched, so the caller keeps ownership of the segments themselves.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Layout {
24 /// The segment at this index in the input list.
25 Segment(usize),
26 /// A group occurrence and what it contains.
27 Group {
28 /// The group's name from the grammar, e.g. `ORDER_OBSERVATION`.
29 name: String,
30 /// The group's contents, in order.
31 items: Vec<Layout>,
32 },
33}
34
35impl Layout {
36 /// The indices of every segment under this layout, in order.
37 pub fn segment_indices(&self, out: &mut Vec<usize>) {
38 match self {
39 Layout::Segment(index) => out.push(*index),
40 Layout::Group { items, .. } => {
41 for item in items {
42 item.segment_indices(out);
43 }
44 }
45 }
46 }
47}
48
49/// Arrange `segments` (their names, in message order) into `items`.
50///
51/// Returns `None` unless every segment is consumed by the grammar, which
52/// is what makes an unknown or misplaced segment fall back to a flat
53/// reading rather than being silently dropped.
54#[must_use]
55pub fn group(items: &[Item], segments: &[&str]) -> Option<Vec<Layout>> {
56 let mut position = 0;
57 let mut out = Vec::new();
58 if match_items(items, segments, &mut position, &mut out) && position == segments.len() {
59 Some(out)
60 } else {
61 None
62 }
63}
64
65/// Match `items` against `segments` starting at `position`, appending what
66/// matched to `out`. Greedy: a repeating item consumes as many occurrences
67/// as it can before the next item is tried.
68fn match_items(
69 items: &[Item],
70 segments: &[&str],
71 position: &mut usize,
72 out: &mut Vec<Layout>,
73) -> bool {
74 for item in items {
75 let mut occurrences = 0;
76 loop {
77 let before = *position;
78 if *position < segments.len() && item.can_start(segments[*position]) {
79 match item {
80 Item::Segment { .. } => {
81 out.push(Layout::Segment(*position));
82 *position += 1;
83 }
84 Item::Group { name, items, .. } => {
85 let mut contents = Vec::new();
86 if !match_items(items, segments, position, &mut contents) {
87 return false;
88 }
89 out.push(Layout::Group {
90 name: name.clone(),
91 items: contents,
92 });
93 }
94 }
95 occurrences += 1;
96 }
97 // A group whose leading items are all optional can match while
98 // consuming nothing; stop rather than loop forever on it.
99 if *position == before || !item.repeats() {
100 break;
101 }
102 }
103 if occurrences == 0 && item.required() {
104 return false;
105 }
106 }
107 true
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::Version;
114
115 fn layout(structure: &str, segments: &[&str]) -> Option<Vec<Layout>> {
116 let dictionary = Version::V2_5.dictionary();
117 group(dictionary.structure(structure).unwrap(), segments)
118 }
119
120 #[test]
121 fn groups_a_message_that_fits() {
122 let found = layout("ORU_R01", &["MSH", "PID", "OBR", "OBX", "OBX"]).unwrap();
123 assert_eq!(found[0], Layout::Segment(0));
124 let Layout::Group { name, items } = &found[1] else {
125 panic!("expected a group, got {:?}", found[1]);
126 };
127 assert_eq!(name, "PATIENT_RESULT");
128 // PATIENT wraps the PID; ORDER_OBSERVATION wraps OBR and both OBXs.
129 assert_eq!(items.len(), 2);
130 let mut indices = Vec::new();
131 found[1].segment_indices(&mut indices);
132 assert_eq!(indices, [1, 2, 3, 4]);
133 }
134
135 #[test]
136 fn repeats_a_group_once_per_occurrence() {
137 let found = layout("ORU_R01", &["MSH", "PID", "OBR", "OBX", "OBR", "OBX"]).unwrap();
138 let Layout::Group { items, .. } = &found[1] else {
139 panic!("expected PATIENT_RESULT");
140 };
141 // One PATIENT group, then two ORDER_OBSERVATION groups.
142 assert_eq!(items.len(), 3);
143 }
144
145 #[test]
146 fn refuses_a_message_that_does_not_fit() {
147 // A Z-segment the grammar has no place for.
148 assert_eq!(layout("ORU_R01", &["MSH", "PID", "OBR", "ZZZ"]), None);
149 // A required segment missing.
150 assert_eq!(layout("ACK", &["MSH"]), None);
151 // Segments in an order the grammar does not allow.
152 assert_eq!(layout("ACK", &["MSA", "MSH"]), None);
153 }
154
155 #[test]
156 fn matches_the_flat_structures_too() {
157 let found = layout("ACK", &["MSH", "MSA", "ERR", "ERR"]).unwrap();
158 assert_eq!(
159 found,
160 [
161 Layout::Segment(0),
162 Layout::Segment(1),
163 Layout::Segment(2),
164 Layout::Segment(3)
165 ]
166 );
167 }
168}