Skip to main content

mig_assembly/
navigator.rs

1//! GroupNavigator implementation backed by AssembledTree.
2
3use crate::assembler::{AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree};
4use mig_types::navigator::GroupNavigator;
5use mig_types::segment::OwnedSegment;
6
7/// Wraps an `AssembledTree` reference to provide group-scoped segment queries.
8pub struct AssembledTreeNavigator<'a> {
9    tree: &'a AssembledTree,
10}
11
12impl<'a> AssembledTreeNavigator<'a> {
13    pub fn new(tree: &'a AssembledTree) -> Self {
14        Self { tree }
15    }
16}
17
18impl GroupNavigator for AssembledTreeNavigator<'_> {
19    fn find_segments_in_group(
20        &self,
21        segment_id: &str,
22        group_path: &[&str],
23        instance_index: usize,
24    ) -> Vec<OwnedSegment> {
25        let Some(instance) = resolve_instance(&self.tree.groups, group_path, instance_index) else {
26            return Vec::new();
27        };
28        instance
29            .segments
30            .iter()
31            .enumerate()
32            .filter(|(_, s)| s.tag.eq_ignore_ascii_case(segment_id))
33            .map(|(i, s)| to_owned(s, i as u32))
34            .collect()
35    }
36
37    fn find_segments_with_qualifier_in_group(
38        &self,
39        segment_id: &str,
40        element_index: usize,
41        qualifier: &str,
42        group_path: &[&str],
43        instance_index: usize,
44    ) -> Vec<OwnedSegment> {
45        self.find_segments_in_group(segment_id, group_path, instance_index)
46            .into_iter()
47            .filter(|s| {
48                s.elements
49                    .get(element_index)
50                    .and_then(|e| e.first())
51                    .is_some_and(|v| v == qualifier)
52            })
53            .collect()
54    }
55
56    fn group_instance_count(&self, group_path: &[&str]) -> usize {
57        resolve_group(&self.tree.groups, group_path)
58            .map(|g| g.repetitions.len())
59            .unwrap_or(0)
60    }
61
62    fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
63        resolve_instance(&self.tree.groups, group_path, instance_index)
64            .is_some_and(|inst| !inst.segments.is_empty())
65    }
66
67    fn child_group_instance_count(
68        &self,
69        parent_path: &[&str],
70        parent_instance: usize,
71        child_group_id: &str,
72    ) -> usize {
73        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
74            return 0;
75        };
76        parent
77            .child_groups
78            .iter()
79            .find(|g| g.group_id == child_group_id)
80            .map(|g| g.repetitions.len())
81            .unwrap_or(0)
82    }
83
84    fn find_segments_in_child_group(
85        &self,
86        segment_id: &str,
87        parent_path: &[&str],
88        parent_instance: usize,
89        child_group_id: &str,
90        child_instance: usize,
91    ) -> Vec<OwnedSegment> {
92        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
93            return Vec::new();
94        };
95        let Some(child_group) = parent
96            .child_groups
97            .iter()
98            .find(|g| g.group_id == child_group_id)
99        else {
100            return Vec::new();
101        };
102        let Some(instance) = child_group.repetitions.get(child_instance) else {
103            return Vec::new();
104        };
105        instance
106            .segments
107            .iter()
108            .enumerate()
109            .filter(|(_, s)| s.tag.eq_ignore_ascii_case(segment_id))
110            .map(|(i, s)| to_owned(s, i as u32))
111            .collect()
112    }
113
114    fn extract_value_in_group(
115        &self,
116        segment_id: &str,
117        element_index: usize,
118        component_index: usize,
119        group_path: &[&str],
120        instance_index: usize,
121    ) -> Option<String> {
122        let instance = resolve_instance(&self.tree.groups, group_path, instance_index)?;
123        let seg = instance
124            .segments
125            .iter()
126            .find(|s| s.tag.eq_ignore_ascii_case(segment_id))?;
127        seg.elements
128            .get(element_index)?
129            .get(component_index)
130            .cloned()
131    }
132}
133
134/// Navigate group hierarchy to find an AssembledGroup at the given path.
135fn resolve_group<'a>(groups: &'a [AssembledGroup], path: &[&str]) -> Option<&'a AssembledGroup> {
136    if path.is_empty() {
137        return None;
138    }
139    let group = groups.iter().find(|g| g.group_id == path[0])?;
140    if path.len() == 1 {
141        return Some(group);
142    }
143    // Navigate deeper: use first repetition of intermediate groups
144    let instance = group.repetitions.first()?;
145    resolve_group(&instance.child_groups, &path[1..])
146}
147
148/// Navigate to a specific group instance at the given path.
149fn resolve_instance<'a>(
150    groups: &'a [AssembledGroup],
151    path: &[&str],
152    instance_index: usize,
153) -> Option<&'a AssembledGroupInstance> {
154    let group = resolve_group(groups, path)?;
155    group.repetitions.get(instance_index)
156}
157
158fn to_owned(seg: &AssembledSegment, segment_number: u32) -> OwnedSegment {
159    OwnedSegment {
160        id: seg.tag.clone(),
161        elements: seg.elements.clone(),
162        segment_number,
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::assembler::{
170        AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
171    };
172    use mig_types::navigator::GroupNavigator;
173
174    fn make_seg(tag: &str, elements: Vec<Vec<&str>>) -> AssembledSegment {
175        AssembledSegment {
176            tag: tag.to_string(),
177            elements: elements
178                .into_iter()
179                .map(|e| e.into_iter().map(|c| c.to_string()).collect())
180                .collect(),
181            mig_number: None,
182        }
183    }
184
185    fn tree_with_sg4_sg8() -> AssembledTree {
186        // SG4[0] -> segments: [IDE, STS]
187        //        -> SG8[0]: [SEQ+Z98, CCI+Z30++Z07]
188        //             -> SG10[0]: [CCI+Z23, CAV+Z91:value1]
189        //             -> SG10[1]: [CCI, CAV+:value2]
190        //        -> SG8[1]: [SEQ+Z01, CCI+++ZC0]
191        //             (no SG10)
192        AssembledTree {
193            segments: vec![make_seg("UNH", vec![vec!["001"]])],
194            groups: vec![AssembledGroup {
195                group_id: "SG4".to_string(),
196                repetitions: vec![AssembledGroupInstance {
197                    segments: vec![
198                        make_seg("IDE", vec![vec!["24", "TX001"]]),
199                        make_seg("STS", vec![vec!["E01"], vec![], vec!["A05"]]),
200                    ],
201                    child_groups: vec![AssembledGroup {
202                        group_id: "SG8".to_string(),
203                        repetitions: vec![
204                            AssembledGroupInstance {
205                                segments: vec![
206                                    make_seg("SEQ", vec![vec!["Z98"]]),
207                                    make_seg("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
208                                ],
209                                child_groups: vec![AssembledGroup {
210                                    group_id: "SG10".to_string(),
211                                    repetitions: vec![
212                                        AssembledGroupInstance {
213                                            segments: vec![
214                                                make_seg("CCI", vec![vec!["Z23"]]),
215                                                make_seg("CAV", vec![vec!["Z91", "value1"]]),
216                                            ],
217                                            child_groups: vec![],
218                                            entry_mig_number: None,
219                                            variant_mig_numbers: vec![],
220                                            skipped_segments: vec![],
221                                        },
222                                        AssembledGroupInstance {
223                                            segments: vec![
224                                                make_seg("CCI", vec![vec![""]]),
225                                                make_seg("CAV", vec![vec!["", "value2"]]),
226                                            ],
227                                            child_groups: vec![],
228                                            entry_mig_number: None,
229                                            variant_mig_numbers: vec![],
230                                            skipped_segments: vec![],
231                                        },
232                                    ],
233                                }],
234                                entry_mig_number: None,
235                                variant_mig_numbers: vec![],
236                                skipped_segments: vec![],
237                            },
238                            AssembledGroupInstance {
239                                segments: vec![
240                                    make_seg("SEQ", vec![vec!["Z01"]]),
241                                    make_seg("CCI", vec![vec![""], vec![], vec!["ZC0"]]),
242                                ],
243                                child_groups: vec![],
244                                entry_mig_number: None,
245                                variant_mig_numbers: vec![],
246                                skipped_segments: vec![],
247                            },
248                        ],
249                    }],
250                    entry_mig_number: None,
251                    variant_mig_numbers: vec![],
252                    skipped_segments: vec![],
253                }],
254            }],
255            post_group_start: 1,
256            inter_group_segments: std::collections::BTreeMap::new(),
257        }
258    }
259
260    #[test]
261    fn test_find_in_sg8_instance_0() {
262        let tree = tree_with_sg4_sg8();
263        let nav = AssembledTreeNavigator::new(&tree);
264        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
265        assert_eq!(segs.len(), 1);
266        assert_eq!(segs[0].get_element(0), "Z98");
267    }
268
269    #[test]
270    fn test_find_in_sg8_instance_1() {
271        let tree = tree_with_sg4_sg8();
272        let nav = AssembledTreeNavigator::new(&tree);
273        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 1);
274        assert_eq!(segs.len(), 1);
275        assert_eq!(segs[0].get_element(0), "Z01");
276    }
277
278    #[test]
279    fn test_qualifier_in_group_scoped() {
280        let tree = tree_with_sg4_sg8();
281        let nav = AssembledTreeNavigator::new(&tree);
282        let segs = nav.find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 1);
283        assert_eq!(segs.len(), 1);
284        // NOT in instance 0
285        assert!(nav
286            .find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 0)
287            .is_empty());
288    }
289
290    #[test]
291    fn test_group_instance_count() {
292        let tree = tree_with_sg4_sg8();
293        let nav = AssembledTreeNavigator::new(&tree);
294        assert_eq!(nav.group_instance_count(&["SG4"]), 1);
295        assert_eq!(nav.group_instance_count(&["SG4", "SG8"]), 2);
296        assert_eq!(nav.group_instance_count(&["SG4", "SG5"]), 0);
297    }
298
299    #[test]
300    fn test_find_in_sg4_directly() {
301        let tree = tree_with_sg4_sg8();
302        let nav = AssembledTreeNavigator::new(&tree);
303        let segs = nav.find_segments_in_group("STS", &["SG4"], 0);
304        assert_eq!(segs.len(), 1);
305    }
306
307    #[test]
308    fn test_invalid_path_returns_empty() {
309        let tree = tree_with_sg4_sg8();
310        let nav = AssembledTreeNavigator::new(&tree);
311        assert!(nav.find_segments_in_group("SEQ", &["SG99"], 0).is_empty());
312        assert!(nav
313            .find_segments_in_group("SEQ", &["SG4", "SG8"], 99)
314            .is_empty());
315        assert!(nav.find_segments_in_group("SEQ", &[], 0).is_empty());
316    }
317
318    #[test]
319    fn test_child_group_instance_count() {
320        let tree = tree_with_sg4_sg8();
321        let nav = AssembledTreeNavigator::new(&tree);
322        // SG8[0] has 2 SG10 children
323        assert_eq!(
324            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
325            2
326        );
327        // SG8[1] has no SG10 children
328        assert_eq!(
329            nav.child_group_instance_count(&["SG4", "SG8"], 1, "SG10"),
330            0
331        );
332        // Non-existent child group
333        assert_eq!(
334            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG12"),
335            0
336        );
337    }
338
339    #[test]
340    fn test_find_segments_in_child_group() {
341        let tree = tree_with_sg4_sg8();
342        let nav = AssembledTreeNavigator::new(&tree);
343        // SG8[0] -> SG10[0] has CCI+Z23
344        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0);
345        assert_eq!(segs.len(), 1);
346        assert_eq!(segs[0].get_element(0), "Z23");
347        // SG8[0] -> SG10[1] has CCI with empty qualifier
348        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 1);
349        assert_eq!(segs.len(), 1);
350        assert_eq!(segs[0].get_element(0), "");
351        // SG8[0] -> SG10[0] has CAV
352        let segs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], 0, "SG10", 0);
353        assert_eq!(segs.len(), 1);
354    }
355
356    #[test]
357    fn test_child_group_invalid_path() {
358        let tree = tree_with_sg4_sg8();
359        let nav = AssembledTreeNavigator::new(&tree);
360        // Invalid parent path
361        assert_eq!(nav.child_group_instance_count(&["SG99"], 0, "SG10"), 0);
362        // Invalid parent instance
363        assert!(nav
364            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 99, "SG10", 0)
365            .is_empty());
366        // Invalid child instance
367        assert!(nav
368            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 99)
369            .is_empty());
370    }
371
372    #[test]
373    fn test_extract_value_in_group() {
374        let tree = tree_with_sg4_sg8();
375        let nav = AssembledTreeNavigator::new(&tree);
376        // SEQ qualifier in SG8[0]
377        assert_eq!(
378            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
379            Some("Z98".to_string()),
380        );
381        // SEQ qualifier in SG8[1]
382        assert_eq!(
383            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 1),
384            Some("Z01".to_string()),
385        );
386    }
387
388    #[test]
389    fn test_extract_value_missing() {
390        let tree = tree_with_sg4_sg8();
391        let nav = AssembledTreeNavigator::new(&tree);
392        // Non-existent segment
393        assert_eq!(
394            nav.extract_value_in_group("LOC", 0, 0, &["SG4", "SG8"], 0),
395            None
396        );
397        // Element index out of bounds
398        assert_eq!(
399            nav.extract_value_in_group("SEQ", 5, 0, &["SG4", "SG8"], 0),
400            None
401        );
402        // Component index out of bounds
403        assert_eq!(
404            nav.extract_value_in_group("SEQ", 0, 5, &["SG4", "SG8"], 0),
405            None
406        );
407        // Invalid group path
408        assert_eq!(nav.extract_value_in_group("SEQ", 0, 0, &["SG99"], 0), None);
409    }
410}