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 instance_has_mig_number(
68        &self,
69        group_path: &[&str],
70        instance_index: usize,
71        mig_number: &str,
72    ) -> bool {
73        let Some(inst) = resolve_instance(&self.tree.groups, group_path, instance_index) else {
74            return true;
75        };
76        if !inst.variant_mig_numbers.is_empty() {
77            return inst.variant_mig_numbers.iter().any(|n| n == mig_number);
78        }
79        has_mig_in_instance(inst, mig_number)
80    }
81
82    fn child_group_instance_count(
83        &self,
84        parent_path: &[&str],
85        parent_instance: usize,
86        child_group_id: &str,
87    ) -> usize {
88        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
89            return 0;
90        };
91        parent
92            .child_groups
93            .iter()
94            .find(|g| g.group_id == child_group_id)
95            .map(|g| g.repetitions.len())
96            .unwrap_or(0)
97    }
98
99    fn find_segments_in_child_group(
100        &self,
101        segment_id: &str,
102        parent_path: &[&str],
103        parent_instance: usize,
104        child_group_id: &str,
105        child_instance: usize,
106    ) -> Vec<OwnedSegment> {
107        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
108            return Vec::new();
109        };
110        let Some(child_group) = parent
111            .child_groups
112            .iter()
113            .find(|g| g.group_id == child_group_id)
114        else {
115            return Vec::new();
116        };
117        let Some(instance) = child_group.repetitions.get(child_instance) else {
118            return Vec::new();
119        };
120        instance
121            .segments
122            .iter()
123            .enumerate()
124            .filter(|(_, s)| s.tag.eq_ignore_ascii_case(segment_id))
125            .map(|(i, s)| to_owned(s, i as u32))
126            .collect()
127    }
128
129    fn extract_value_in_group(
130        &self,
131        segment_id: &str,
132        element_index: usize,
133        component_index: usize,
134        group_path: &[&str],
135        instance_index: usize,
136    ) -> Option<String> {
137        let instance = resolve_instance(&self.tree.groups, group_path, instance_index)?;
138        let seg = instance
139            .segments
140            .iter()
141            .find(|s| s.tag.eq_ignore_ascii_case(segment_id))?;
142        seg.elements
143            .get(element_index)?
144            .get(component_index)
145            .cloned()
146    }
147}
148
149/// Navigate group hierarchy to find an AssembledGroup at the given path.
150fn resolve_group<'a>(groups: &'a [AssembledGroup], path: &[&str]) -> Option<&'a AssembledGroup> {
151    if path.is_empty() {
152        return None;
153    }
154    let group = groups.iter().find(|g| g.group_id == path[0])?;
155    if path.len() == 1 {
156        return Some(group);
157    }
158    // Navigate deeper: use first repetition of intermediate groups
159    let instance = group.repetitions.first()?;
160    resolve_group(&instance.child_groups, &path[1..])
161}
162
163/// Navigate to a specific group instance at the given path.
164fn resolve_instance<'a>(
165    groups: &'a [AssembledGroup],
166    path: &[&str],
167    instance_index: usize,
168) -> Option<&'a AssembledGroupInstance> {
169    let group = resolve_group(groups, path)?;
170    group.repetitions.get(instance_index)
171}
172
173/// Fallback lookup when `variant_mig_numbers` is not populated: scan the
174/// instance's direct and nested segments for the given mig number.
175fn has_mig_in_instance(inst: &AssembledGroupInstance, mig_number: &str) -> bool {
176    if inst
177        .segments
178        .iter()
179        .any(|s| s.mig_number.as_deref() == Some(mig_number))
180    {
181        return true;
182    }
183    inst.child_groups.iter().any(|g| {
184        g.repetitions
185            .iter()
186            .any(|r| has_mig_in_instance(r, mig_number))
187    })
188}
189
190fn to_owned(seg: &AssembledSegment, segment_number: u32) -> OwnedSegment {
191    OwnedSegment {
192        id: seg.tag.clone(),
193        elements: seg.elements.clone(),
194        segment_number,
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::assembler::{
202        AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
203    };
204    use mig_types::navigator::GroupNavigator;
205
206    fn make_seg(tag: &str, elements: Vec<Vec<&str>>) -> AssembledSegment {
207        AssembledSegment {
208            tag: tag.to_string(),
209            elements: elements
210                .into_iter()
211                .map(|e| e.into_iter().map(|c| c.to_string()).collect())
212                .collect(),
213            mig_number: None,
214        }
215    }
216
217    fn tree_with_sg4_sg8() -> AssembledTree {
218        // SG4[0] -> segments: [IDE, STS]
219        //        -> SG8[0]: [SEQ+Z98, CCI+Z30++Z07]
220        //             -> SG10[0]: [CCI+Z23, CAV+Z91:value1]
221        //             -> SG10[1]: [CCI, CAV+:value2]
222        //        -> SG8[1]: [SEQ+Z01, CCI+++ZC0]
223        //             (no SG10)
224        AssembledTree {
225            segments: vec![make_seg("UNH", vec![vec!["001"]])],
226            groups: vec![AssembledGroup {
227                group_id: "SG4".to_string(),
228                repetitions: vec![AssembledGroupInstance {
229                    segments: vec![
230                        make_seg("IDE", vec![vec!["24", "TX001"]]),
231                        make_seg("STS", vec![vec!["E01"], vec![], vec!["A05"]]),
232                    ],
233                    child_groups: vec![AssembledGroup {
234                        group_id: "SG8".to_string(),
235                        repetitions: vec![
236                            AssembledGroupInstance {
237                                segments: vec![
238                                    make_seg("SEQ", vec![vec!["Z98"]]),
239                                    make_seg("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
240                                ],
241                                child_groups: vec![AssembledGroup {
242                                    group_id: "SG10".to_string(),
243                                    repetitions: vec![
244                                        AssembledGroupInstance {
245                                            segments: vec![
246                                                make_seg("CCI", vec![vec!["Z23"]]),
247                                                make_seg("CAV", vec![vec!["Z91", "value1"]]),
248                                            ],
249                                            child_groups: vec![],
250                                            entry_mig_number: None,
251                                            variant_mig_numbers: vec![],
252                                            skipped_segments: vec![],
253                                            skipped_positions: Vec::new(),
254                                        },
255                                        AssembledGroupInstance {
256                                            segments: vec![
257                                                make_seg("CCI", vec![vec![""]]),
258                                                make_seg("CAV", vec![vec!["", "value2"]]),
259                                            ],
260                                            child_groups: vec![],
261                                            entry_mig_number: None,
262                                            variant_mig_numbers: vec![],
263                                            skipped_segments: vec![],
264                                            skipped_positions: Vec::new(),
265                                        },
266                                    ],
267                                }],
268                                entry_mig_number: None,
269                                variant_mig_numbers: vec![],
270                                skipped_segments: vec![],
271                                skipped_positions: Vec::new(),
272                            },
273                            AssembledGroupInstance {
274                                segments: vec![
275                                    make_seg("SEQ", vec![vec!["Z01"]]),
276                                    make_seg("CCI", vec![vec![""], vec![], vec!["ZC0"]]),
277                                ],
278                                child_groups: vec![],
279                                entry_mig_number: None,
280                                variant_mig_numbers: vec![],
281                                skipped_segments: vec![],
282                                skipped_positions: Vec::new(),
283                            },
284                        ],
285                    }],
286                    entry_mig_number: None,
287                    variant_mig_numbers: vec![],
288                    skipped_segments: vec![],
289                    skipped_positions: Vec::new(),
290                }],
291            }],
292            post_group_start: 1,
293            inter_group_segments: std::collections::BTreeMap::new(),
294        }
295    }
296
297    #[test]
298    fn test_find_in_sg8_instance_0() {
299        let tree = tree_with_sg4_sg8();
300        let nav = AssembledTreeNavigator::new(&tree);
301        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
302        assert_eq!(segs.len(), 1);
303        assert_eq!(segs[0].get_element(0), "Z98");
304    }
305
306    #[test]
307    fn test_find_in_sg8_instance_1() {
308        let tree = tree_with_sg4_sg8();
309        let nav = AssembledTreeNavigator::new(&tree);
310        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 1);
311        assert_eq!(segs.len(), 1);
312        assert_eq!(segs[0].get_element(0), "Z01");
313    }
314
315    #[test]
316    fn test_qualifier_in_group_scoped() {
317        let tree = tree_with_sg4_sg8();
318        let nav = AssembledTreeNavigator::new(&tree);
319        let segs = nav.find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 1);
320        assert_eq!(segs.len(), 1);
321        // NOT in instance 0
322        assert!(nav
323            .find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 0)
324            .is_empty());
325    }
326
327    #[test]
328    fn test_group_instance_count() {
329        let tree = tree_with_sg4_sg8();
330        let nav = AssembledTreeNavigator::new(&tree);
331        assert_eq!(nav.group_instance_count(&["SG4"]), 1);
332        assert_eq!(nav.group_instance_count(&["SG4", "SG8"]), 2);
333        assert_eq!(nav.group_instance_count(&["SG4", "SG5"]), 0);
334    }
335
336    #[test]
337    fn test_find_in_sg4_directly() {
338        let tree = tree_with_sg4_sg8();
339        let nav = AssembledTreeNavigator::new(&tree);
340        let segs = nav.find_segments_in_group("STS", &["SG4"], 0);
341        assert_eq!(segs.len(), 1);
342    }
343
344    #[test]
345    fn test_invalid_path_returns_empty() {
346        let tree = tree_with_sg4_sg8();
347        let nav = AssembledTreeNavigator::new(&tree);
348        assert!(nav.find_segments_in_group("SEQ", &["SG99"], 0).is_empty());
349        assert!(nav
350            .find_segments_in_group("SEQ", &["SG4", "SG8"], 99)
351            .is_empty());
352        assert!(nav.find_segments_in_group("SEQ", &[], 0).is_empty());
353    }
354
355    #[test]
356    fn test_child_group_instance_count() {
357        let tree = tree_with_sg4_sg8();
358        let nav = AssembledTreeNavigator::new(&tree);
359        // SG8[0] has 2 SG10 children
360        assert_eq!(
361            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
362            2
363        );
364        // SG8[1] has no SG10 children
365        assert_eq!(
366            nav.child_group_instance_count(&["SG4", "SG8"], 1, "SG10"),
367            0
368        );
369        // Non-existent child group
370        assert_eq!(
371            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG12"),
372            0
373        );
374    }
375
376    #[test]
377    fn test_find_segments_in_child_group() {
378        let tree = tree_with_sg4_sg8();
379        let nav = AssembledTreeNavigator::new(&tree);
380        // SG8[0] -> SG10[0] has CCI+Z23
381        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0);
382        assert_eq!(segs.len(), 1);
383        assert_eq!(segs[0].get_element(0), "Z23");
384        // SG8[0] -> SG10[1] has CCI with empty qualifier
385        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 1);
386        assert_eq!(segs.len(), 1);
387        assert_eq!(segs[0].get_element(0), "");
388        // SG8[0] -> SG10[0] has CAV
389        let segs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], 0, "SG10", 0);
390        assert_eq!(segs.len(), 1);
391    }
392
393    #[test]
394    fn test_child_group_invalid_path() {
395        let tree = tree_with_sg4_sg8();
396        let nav = AssembledTreeNavigator::new(&tree);
397        // Invalid parent path
398        assert_eq!(nav.child_group_instance_count(&["SG99"], 0, "SG10"), 0);
399        // Invalid parent instance
400        assert!(nav
401            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 99, "SG10", 0)
402            .is_empty());
403        // Invalid child instance
404        assert!(nav
405            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 99)
406            .is_empty());
407    }
408
409    #[test]
410    fn test_extract_value_in_group() {
411        let tree = tree_with_sg4_sg8();
412        let nav = AssembledTreeNavigator::new(&tree);
413        // SEQ qualifier in SG8[0]
414        assert_eq!(
415            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
416            Some("Z98".to_string()),
417        );
418        // SEQ qualifier in SG8[1]
419        assert_eq!(
420            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 1),
421            Some("Z01".to_string()),
422        );
423    }
424
425    #[test]
426    fn test_extract_value_missing() {
427        let tree = tree_with_sg4_sg8();
428        let nav = AssembledTreeNavigator::new(&tree);
429        // Non-existent segment
430        assert_eq!(
431            nav.extract_value_in_group("LOC", 0, 0, &["SG4", "SG8"], 0),
432            None
433        );
434        // Element index out of bounds
435        assert_eq!(
436            nav.extract_value_in_group("SEQ", 5, 0, &["SG4", "SG8"], 0),
437            None
438        );
439        // Component index out of bounds
440        assert_eq!(
441            nav.extract_value_in_group("SEQ", 0, 5, &["SG4", "SG8"], 0),
442            None
443        );
444        // Invalid group path
445        assert_eq!(nav.extract_value_in_group("SEQ", 0, 0, &["SG99"], 0), None);
446    }
447}