Skip to main content

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}