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            segment_number: None,
215        }
216    }
217
218    fn tree_with_sg4_sg8() -> AssembledTree {
219        // SG4[0] -> segments: [IDE, STS]
220        //        -> SG8[0]: [SEQ+Z98, CCI+Z30++Z07]
221        //             -> SG10[0]: [CCI+Z23, CAV+Z91:value1]
222        //             -> SG10[1]: [CCI, CAV+:value2]
223        //        -> SG8[1]: [SEQ+Z01, CCI+++ZC0]
224        //             (no SG10)
225        AssembledTree {
226            segments: vec![make_seg("UNH", vec![vec!["001"]])],
227            groups: vec![AssembledGroup {
228                group_id: "SG4".to_string(),
229                repetitions: vec![AssembledGroupInstance {
230                    segments: vec![
231                        make_seg("IDE", vec![vec!["24", "TX001"]]),
232                        make_seg("STS", vec![vec!["E01"], vec![], vec!["A05"]]),
233                    ],
234                    child_groups: vec![AssembledGroup {
235                        group_id: "SG8".to_string(),
236                        repetitions: vec![
237                            AssembledGroupInstance {
238                                segments: vec![
239                                    make_seg("SEQ", vec![vec!["Z98"]]),
240                                    make_seg("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
241                                ],
242                                child_groups: vec![AssembledGroup {
243                                    group_id: "SG10".to_string(),
244                                    repetitions: vec![
245                                        AssembledGroupInstance {
246                                            segments: vec![
247                                                make_seg("CCI", vec![vec!["Z23"]]),
248                                                make_seg("CAV", vec![vec!["Z91", "value1"]]),
249                                            ],
250                                            child_groups: vec![],
251                                            entry_mig_number: None,
252                                            variant_mig_numbers: vec![],
253                                            skipped_segments: vec![],
254                                            skipped_positions: Vec::new(),
255                                        },
256                                        AssembledGroupInstance {
257                                            segments: vec![
258                                                make_seg("CCI", vec![vec![""]]),
259                                                make_seg("CAV", vec![vec!["", "value2"]]),
260                                            ],
261                                            child_groups: vec![],
262                                            entry_mig_number: None,
263                                            variant_mig_numbers: vec![],
264                                            skipped_segments: vec![],
265                                            skipped_positions: Vec::new(),
266                                        },
267                                    ],
268                                }],
269                                entry_mig_number: None,
270                                variant_mig_numbers: vec![],
271                                skipped_segments: vec![],
272                                skipped_positions: Vec::new(),
273                            },
274                            AssembledGroupInstance {
275                                segments: vec![
276                                    make_seg("SEQ", vec![vec!["Z01"]]),
277                                    make_seg("CCI", vec![vec![""], vec![], vec!["ZC0"]]),
278                                ],
279                                child_groups: vec![],
280                                entry_mig_number: None,
281                                variant_mig_numbers: vec![],
282                                skipped_segments: vec![],
283                                skipped_positions: Vec::new(),
284                            },
285                        ],
286                    }],
287                    entry_mig_number: None,
288                    variant_mig_numbers: vec![],
289                    skipped_segments: vec![],
290                    skipped_positions: Vec::new(),
291                }],
292            }],
293            post_group_start: 1,
294            inter_group_segments: std::collections::BTreeMap::new(),
295        }
296    }
297
298    #[test]
299    fn test_find_in_sg8_instance_0() {
300        let tree = tree_with_sg4_sg8();
301        let nav = AssembledTreeNavigator::new(&tree);
302        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
303        assert_eq!(segs.len(), 1);
304        assert_eq!(segs[0].get_element(0), "Z98");
305    }
306
307    #[test]
308    fn test_find_in_sg8_instance_1() {
309        let tree = tree_with_sg4_sg8();
310        let nav = AssembledTreeNavigator::new(&tree);
311        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 1);
312        assert_eq!(segs.len(), 1);
313        assert_eq!(segs[0].get_element(0), "Z01");
314    }
315
316    #[test]
317    fn test_qualifier_in_group_scoped() {
318        let tree = tree_with_sg4_sg8();
319        let nav = AssembledTreeNavigator::new(&tree);
320        let segs = nav.find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 1);
321        assert_eq!(segs.len(), 1);
322        // NOT in instance 0
323        assert!(nav
324            .find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 0)
325            .is_empty());
326    }
327
328    #[test]
329    fn test_group_instance_count() {
330        let tree = tree_with_sg4_sg8();
331        let nav = AssembledTreeNavigator::new(&tree);
332        assert_eq!(nav.group_instance_count(&["SG4"]), 1);
333        assert_eq!(nav.group_instance_count(&["SG4", "SG8"]), 2);
334        assert_eq!(nav.group_instance_count(&["SG4", "SG5"]), 0);
335    }
336
337    #[test]
338    fn test_find_in_sg4_directly() {
339        let tree = tree_with_sg4_sg8();
340        let nav = AssembledTreeNavigator::new(&tree);
341        let segs = nav.find_segments_in_group("STS", &["SG4"], 0);
342        assert_eq!(segs.len(), 1);
343    }
344
345    #[test]
346    fn test_invalid_path_returns_empty() {
347        let tree = tree_with_sg4_sg8();
348        let nav = AssembledTreeNavigator::new(&tree);
349        assert!(nav.find_segments_in_group("SEQ", &["SG99"], 0).is_empty());
350        assert!(nav
351            .find_segments_in_group("SEQ", &["SG4", "SG8"], 99)
352            .is_empty());
353        assert!(nav.find_segments_in_group("SEQ", &[], 0).is_empty());
354    }
355
356    #[test]
357    fn test_child_group_instance_count() {
358        let tree = tree_with_sg4_sg8();
359        let nav = AssembledTreeNavigator::new(&tree);
360        // SG8[0] has 2 SG10 children
361        assert_eq!(
362            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
363            2
364        );
365        // SG8[1] has no SG10 children
366        assert_eq!(
367            nav.child_group_instance_count(&["SG4", "SG8"], 1, "SG10"),
368            0
369        );
370        // Non-existent child group
371        assert_eq!(
372            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG12"),
373            0
374        );
375    }
376
377    #[test]
378    fn test_find_segments_in_child_group() {
379        let tree = tree_with_sg4_sg8();
380        let nav = AssembledTreeNavigator::new(&tree);
381        // SG8[0] -> SG10[0] has CCI+Z23
382        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0);
383        assert_eq!(segs.len(), 1);
384        assert_eq!(segs[0].get_element(0), "Z23");
385        // SG8[0] -> SG10[1] has CCI with empty qualifier
386        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 1);
387        assert_eq!(segs.len(), 1);
388        assert_eq!(segs[0].get_element(0), "");
389        // SG8[0] -> SG10[0] has CAV
390        let segs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], 0, "SG10", 0);
391        assert_eq!(segs.len(), 1);
392    }
393
394    #[test]
395    fn test_child_group_invalid_path() {
396        let tree = tree_with_sg4_sg8();
397        let nav = AssembledTreeNavigator::new(&tree);
398        // Invalid parent path
399        assert_eq!(nav.child_group_instance_count(&["SG99"], 0, "SG10"), 0);
400        // Invalid parent instance
401        assert!(nav
402            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 99, "SG10", 0)
403            .is_empty());
404        // Invalid child instance
405        assert!(nav
406            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 99)
407            .is_empty());
408    }
409
410    #[test]
411    fn test_extract_value_in_group() {
412        let tree = tree_with_sg4_sg8();
413        let nav = AssembledTreeNavigator::new(&tree);
414        // SEQ qualifier in SG8[0]
415        assert_eq!(
416            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
417            Some("Z98".to_string()),
418        );
419        // SEQ qualifier in SG8[1]
420        assert_eq!(
421            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 1),
422            Some("Z01".to_string()),
423        );
424    }
425
426    #[test]
427    fn test_extract_value_missing() {
428        let tree = tree_with_sg4_sg8();
429        let nav = AssembledTreeNavigator::new(&tree);
430        // Non-existent segment
431        assert_eq!(
432            nav.extract_value_in_group("LOC", 0, 0, &["SG4", "SG8"], 0),
433            None
434        );
435        // Element index out of bounds
436        assert_eq!(
437            nav.extract_value_in_group("SEQ", 5, 0, &["SG4", "SG8"], 0),
438            None
439        );
440        // Component index out of bounds
441        assert_eq!(
442            nav.extract_value_in_group("SEQ", 0, 5, &["SG4", "SG8"], 0),
443            None
444        );
445        // Invalid group path
446        assert_eq!(nav.extract_value_in_group("SEQ", 0, 0, &["SG99"], 0), None);
447    }
448}