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 find_segments_in_instance(
63        &self,
64        segment_id: &str,
65        path: &[(&str, usize)],
66    ) -> Vec<OwnedSegment> {
67        let Some(instance) = resolve_instance_path(&self.tree.groups, path) else {
68            return Vec::new();
69        };
70        instance
71            .segments
72            .iter()
73            .enumerate()
74            .filter(|(_, s)| s.tag.eq_ignore_ascii_case(segment_id))
75            .map(|(i, s)| to_owned(s, i as u32))
76            .collect()
77    }
78
79    fn child_instance_count(&self, parent: &[(&str, usize)], child_group_id: &str) -> usize {
80        let groups = if parent.is_empty() {
81            &self.tree.groups
82        } else {
83            match resolve_instance_path(&self.tree.groups, parent) {
84                Some(instance) => &instance.child_groups,
85                None => return 0,
86            }
87        };
88        groups
89            .iter()
90            .find(|g| g.group_id == child_group_id)
91            .map_or(0, |g| g.repetitions.len())
92    }
93
94    fn find_segments_in_subtree(
95        &self,
96        segment_id: &str,
97        path: &[(&str, usize)],
98    ) -> Vec<OwnedSegment> {
99        let Some(instance) = resolve_instance_path(&self.tree.groups, path) else {
100            return Vec::new();
101        };
102        let mut out = Vec::new();
103        collect_subtree(instance, segment_id, &mut out);
104        out
105    }
106
107    fn has_any_segment_in_group(&self, group_path: &[&str], instance_index: usize) -> bool {
108        resolve_instance(&self.tree.groups, group_path, instance_index)
109            .is_some_and(|inst| !inst.segments.is_empty())
110    }
111
112    fn instance_has_mig_number(
113        &self,
114        group_path: &[&str],
115        instance_index: usize,
116        mig_number: &str,
117    ) -> bool {
118        let Some(inst) = resolve_instance(&self.tree.groups, group_path, instance_index) else {
119            return true;
120        };
121        if !inst.variant_mig_numbers.is_empty() {
122            return inst.variant_mig_numbers.iter().any(|n| n == mig_number);
123        }
124        has_mig_in_instance(inst, mig_number)
125    }
126
127    fn instance_at_has_mig_number(&self, path: &[(&str, usize)], mig_number: &str) -> bool {
128        let Some(inst) = resolve_instance_path(&self.tree.groups, path) else {
129            return true;
130        };
131        if !inst.variant_mig_numbers.is_empty() {
132            return inst.variant_mig_numbers.iter().any(|n| n == mig_number);
133        }
134        has_mig_in_instance(inst, mig_number)
135    }
136
137    fn child_group_instance_count(
138        &self,
139        parent_path: &[&str],
140        parent_instance: usize,
141        child_group_id: &str,
142    ) -> usize {
143        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
144            return 0;
145        };
146        parent
147            .child_groups
148            .iter()
149            .find(|g| g.group_id == child_group_id)
150            .map(|g| g.repetitions.len())
151            .unwrap_or(0)
152    }
153
154    fn find_segments_in_child_group(
155        &self,
156        segment_id: &str,
157        parent_path: &[&str],
158        parent_instance: usize,
159        child_group_id: &str,
160        child_instance: usize,
161    ) -> Vec<OwnedSegment> {
162        let Some(parent) = resolve_instance(&self.tree.groups, parent_path, parent_instance) else {
163            return Vec::new();
164        };
165        let Some(child_group) = parent
166            .child_groups
167            .iter()
168            .find(|g| g.group_id == child_group_id)
169        else {
170            return Vec::new();
171        };
172        let Some(instance) = child_group.repetitions.get(child_instance) else {
173            return Vec::new();
174        };
175        instance
176            .segments
177            .iter()
178            .enumerate()
179            .filter(|(_, s)| s.tag.eq_ignore_ascii_case(segment_id))
180            .map(|(i, s)| to_owned(s, i as u32))
181            .collect()
182    }
183
184    fn extract_value_in_group(
185        &self,
186        segment_id: &str,
187        element_index: usize,
188        component_index: usize,
189        group_path: &[&str],
190        instance_index: usize,
191    ) -> Option<String> {
192        let instance = resolve_instance(&self.tree.groups, group_path, instance_index)?;
193        let seg = instance
194            .segments
195            .iter()
196            .find(|s| s.tag.eq_ignore_ascii_case(segment_id))?;
197        seg.elements
198            .get(element_index)?
199            .get(component_index)
200            .cloned()
201    }
202}
203
204/// Navigate group hierarchy to find an AssembledGroup at the given path.
205fn resolve_group<'a>(groups: &'a [AssembledGroup], path: &[&str]) -> Option<&'a AssembledGroup> {
206    if path.is_empty() {
207        return None;
208    }
209    let group = groups.iter().find(|g| g.group_id == path[0])?;
210    if path.len() == 1 {
211        return Some(group);
212    }
213    // Navigate deeper: use first repetition of intermediate groups
214    let instance = group.repetitions.first()?;
215    resolve_group(&instance.child_groups, &path[1..])
216}
217
218/// The group instance at `path`, taking the given repetition at every level.
219fn resolve_instance_path<'a>(
220    groups: &'a [AssembledGroup],
221    path: &[(&str, usize)],
222) -> Option<&'a AssembledGroupInstance> {
223    let (&(group_id, index), rest) = path.split_first()?;
224    let instance = groups
225        .iter()
226        .find(|g| g.group_id == group_id)?
227        .repetitions
228        .get(index)?;
229    if rest.is_empty() {
230        Some(instance)
231    } else {
232        resolve_instance_path(&instance.child_groups, rest)
233    }
234}
235
236/// The segments tagged `segment_id` in `instance` and every group nested in it,
237/// in message order.
238fn collect_subtree(
239    instance: &AssembledGroupInstance,
240    segment_id: &str,
241    out: &mut Vec<OwnedSegment>,
242) {
243    for (i, s) in instance.segments.iter().enumerate() {
244        if s.tag.eq_ignore_ascii_case(segment_id) {
245            out.push(to_owned(s, i as u32));
246        }
247    }
248    for group in &instance.child_groups {
249        for repetition in &group.repetitions {
250            collect_subtree(repetition, segment_id, out);
251        }
252    }
253}
254
255/// Navigate to a specific group instance at the given path.
256fn resolve_instance<'a>(
257    groups: &'a [AssembledGroup],
258    path: &[&str],
259    instance_index: usize,
260) -> Option<&'a AssembledGroupInstance> {
261    let group = resolve_group(groups, path)?;
262    group.repetitions.get(instance_index)
263}
264
265/// Fallback lookup when `variant_mig_numbers` is not populated: scan the
266/// instance's direct and nested segments for the given mig number.
267fn has_mig_in_instance(inst: &AssembledGroupInstance, mig_number: &str) -> bool {
268    if inst
269        .segments
270        .iter()
271        .any(|s| s.mig_number.as_deref() == Some(mig_number))
272    {
273        return true;
274    }
275    inst.child_groups.iter().any(|g| {
276        g.repetitions
277            .iter()
278            .any(|r| has_mig_in_instance(r, mig_number))
279    })
280}
281
282fn to_owned(seg: &AssembledSegment, segment_number: u32) -> OwnedSegment {
283    OwnedSegment {
284        id: seg.tag.clone(),
285        elements: seg.elements.clone(),
286        segment_number,
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::assembler::{
294        AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
295    };
296    use mig_types::navigator::GroupNavigator;
297
298    fn make_seg(tag: &str, elements: Vec<Vec<&str>>) -> AssembledSegment {
299        AssembledSegment {
300            tag: tag.to_string(),
301            elements: elements
302                .into_iter()
303                .map(|e| e.into_iter().map(|c| c.to_string()).collect())
304                .collect(),
305            mig_number: None,
306            segment_number: None,
307        }
308    }
309
310    fn tree_with_sg4_sg8() -> AssembledTree {
311        // SG4[0] -> segments: [IDE, STS]
312        //        -> SG8[0]: [SEQ+Z98, CCI+Z30++Z07]
313        //             -> SG10[0]: [CCI+Z23, CAV+Z91:value1]
314        //             -> SG10[1]: [CCI, CAV+:value2]
315        //        -> SG8[1]: [SEQ+Z01, CCI+++ZC0]
316        //             (no SG10)
317        AssembledTree {
318            segments: vec![make_seg("UNH", vec![vec!["001"]])],
319            groups: vec![AssembledGroup {
320                group_id: "SG4".to_string(),
321                repetitions: vec![AssembledGroupInstance {
322                    segments: vec![
323                        make_seg("IDE", vec![vec!["24", "TX001"]]),
324                        make_seg("STS", vec![vec!["E01"], vec![], vec!["A05"]]),
325                    ],
326                    child_groups: vec![AssembledGroup {
327                        group_id: "SG8".to_string(),
328                        repetitions: vec![
329                            AssembledGroupInstance {
330                                segments: vec![
331                                    make_seg("SEQ", vec![vec!["Z98"]]),
332                                    make_seg("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
333                                ],
334                                child_groups: vec![AssembledGroup {
335                                    group_id: "SG10".to_string(),
336                                    repetitions: vec![
337                                        AssembledGroupInstance {
338                                            segments: vec![
339                                                make_seg("CCI", vec![vec!["Z23"]]),
340                                                make_seg("CAV", vec![vec!["Z91", "value1"]]),
341                                            ],
342                                            child_groups: vec![],
343                                            entry_mig_number: None,
344                                            variant_mig_numbers: vec![],
345                                            skipped_segments: vec![],
346                                            skipped_positions: Vec::new(),
347                                        },
348                                        AssembledGroupInstance {
349                                            segments: vec![
350                                                make_seg("CCI", vec![vec![""]]),
351                                                make_seg("CAV", vec![vec!["", "value2"]]),
352                                            ],
353                                            child_groups: vec![],
354                                            entry_mig_number: None,
355                                            variant_mig_numbers: vec![],
356                                            skipped_segments: vec![],
357                                            skipped_positions: Vec::new(),
358                                        },
359                                    ],
360                                }],
361                                entry_mig_number: None,
362                                variant_mig_numbers: vec![],
363                                skipped_segments: vec![],
364                                skipped_positions: Vec::new(),
365                            },
366                            AssembledGroupInstance {
367                                segments: vec![
368                                    make_seg("SEQ", vec![vec!["Z01"]]),
369                                    make_seg("CCI", vec![vec![""], vec![], vec!["ZC0"]]),
370                                ],
371                                child_groups: vec![],
372                                entry_mig_number: None,
373                                variant_mig_numbers: vec![],
374                                skipped_segments: vec![],
375                                skipped_positions: Vec::new(),
376                            },
377                        ],
378                    }],
379                    entry_mig_number: None,
380                    variant_mig_numbers: vec![],
381                    skipped_segments: vec![],
382                    skipped_positions: Vec::new(),
383                }],
384            }],
385            post_group_start: 1,
386            inter_group_segments: std::collections::BTreeMap::new(),
387        }
388    }
389
390    /// Two SG4s: SG4[0] has one SG8 (SEQ+Z98, with an SG10 CCI+Z23);
391    /// SG4[1] has two SG8s (SEQ+Z01, SEQ+Z03 with an SG10 CCI+E13).
392    fn tree_with_two_sg4() -> AssembledTree {
393        fn instance(
394            segments: Vec<AssembledSegment>,
395            child_groups: Vec<AssembledGroup>,
396        ) -> AssembledGroupInstance {
397            AssembledGroupInstance {
398                segments,
399                child_groups,
400                entry_mig_number: None,
401                variant_mig_numbers: vec![],
402                skipped_segments: vec![],
403                skipped_positions: Vec::new(),
404            }
405        }
406        fn group(id: &str, repetitions: Vec<AssembledGroupInstance>) -> AssembledGroup {
407            AssembledGroup {
408                group_id: id.to_string(),
409                repetitions,
410            }
411        }
412        let sg10 = |code: &str| {
413            group(
414                "SG10",
415                vec![instance(
416                    vec![make_seg("CCI", vec![vec![""], vec![], vec![code]])],
417                    vec![],
418                )],
419            )
420        };
421        AssembledTree {
422            segments: vec![],
423            groups: vec![group(
424                "SG4",
425                vec![
426                    instance(
427                        vec![make_seg("IDE", vec![vec!["24", "TX0"]])],
428                        vec![group(
429                            "SG8",
430                            vec![instance(
431                                vec![make_seg("SEQ", vec![vec!["Z98"]])],
432                                vec![sg10("Z23")],
433                            )],
434                        )],
435                    ),
436                    instance(
437                        vec![make_seg("IDE", vec![vec!["24", "TX1"]])],
438                        vec![group(
439                            "SG8",
440                            vec![
441                                instance(vec![make_seg("SEQ", vec![vec!["Z01"]])], vec![]),
442                                instance(
443                                    vec![make_seg("SEQ", vec![vec!["Z03"]])],
444                                    vec![sg10("E13")],
445                                ),
446                            ],
447                        )],
448                    ),
449                ],
450            )],
451            post_group_start: 0,
452            inter_group_segments: std::collections::BTreeMap::new(),
453        }
454    }
455
456    #[test]
457    fn an_instance_is_addressed_at_every_level() {
458        let tree = tree_with_two_sg4();
459        let nav = AssembledTreeNavigator::new(&tree);
460        let seq = |path: &[(&str, usize)]| -> Vec<String> {
461            nav.find_segments_in_instance("SEQ", path)
462                .iter()
463                .map(|s| s.get_element(0).to_string())
464                .collect()
465        };
466        assert_eq!(seq(&[("SG4", 0), ("SG8", 0)]), ["Z98"]);
467        assert_eq!(seq(&[("SG4", 1), ("SG8", 0)]), ["Z01"]);
468        assert_eq!(seq(&[("SG4", 1), ("SG8", 1)]), ["Z03"]);
469        assert!(seq(&[("SG4", 0), ("SG8", 1)]).is_empty());
470    }
471
472    #[test]
473    fn a_subtree_includes_the_nested_groups() {
474        let tree = tree_with_two_sg4();
475        let nav = AssembledTreeNavigator::new(&tree);
476        let cci = |path: &[(&str, usize)]| -> Vec<String> {
477            nav.find_segments_in_subtree("CCI", path)
478                .iter()
479                .map(|s| s.get_component(2, 0).to_string())
480                .collect()
481        };
482        assert_eq!(cci(&[("SG4", 1), ("SG8", 1)]), ["E13"]);
483        assert!(cci(&[("SG4", 1), ("SG8", 0)]).is_empty());
484        assert_eq!(cci(&[("SG4", 1)]), ["E13"], "the whole SG4");
485        assert!(
486            nav.find_segments_in_instance("CCI", &[("SG4", 1), ("SG8", 1)])
487                .is_empty(),
488            "the instance alone holds no CCI"
489        );
490    }
491
492    #[test]
493    fn test_find_in_sg8_instance_0() {
494        let tree = tree_with_sg4_sg8();
495        let nav = AssembledTreeNavigator::new(&tree);
496        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
497        assert_eq!(segs.len(), 1);
498        assert_eq!(segs[0].get_element(0), "Z98");
499    }
500
501    #[test]
502    fn test_find_in_sg8_instance_1() {
503        let tree = tree_with_sg4_sg8();
504        let nav = AssembledTreeNavigator::new(&tree);
505        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 1);
506        assert_eq!(segs.len(), 1);
507        assert_eq!(segs[0].get_element(0), "Z01");
508    }
509
510    #[test]
511    fn test_qualifier_in_group_scoped() {
512        let tree = tree_with_sg4_sg8();
513        let nav = AssembledTreeNavigator::new(&tree);
514        let segs = nav.find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 1);
515        assert_eq!(segs.len(), 1);
516        // NOT in instance 0
517        assert!(nav
518            .find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 0)
519            .is_empty());
520    }
521
522    #[test]
523    fn test_group_instance_count() {
524        let tree = tree_with_sg4_sg8();
525        let nav = AssembledTreeNavigator::new(&tree);
526        assert_eq!(nav.group_instance_count(&["SG4"]), 1);
527        assert_eq!(nav.group_instance_count(&["SG4", "SG8"]), 2);
528        assert_eq!(nav.group_instance_count(&["SG4", "SG5"]), 0);
529    }
530
531    #[test]
532    fn test_find_in_sg4_directly() {
533        let tree = tree_with_sg4_sg8();
534        let nav = AssembledTreeNavigator::new(&tree);
535        let segs = nav.find_segments_in_group("STS", &["SG4"], 0);
536        assert_eq!(segs.len(), 1);
537    }
538
539    #[test]
540    fn test_invalid_path_returns_empty() {
541        let tree = tree_with_sg4_sg8();
542        let nav = AssembledTreeNavigator::new(&tree);
543        assert!(nav.find_segments_in_group("SEQ", &["SG99"], 0).is_empty());
544        assert!(nav
545            .find_segments_in_group("SEQ", &["SG4", "SG8"], 99)
546            .is_empty());
547        assert!(nav.find_segments_in_group("SEQ", &[], 0).is_empty());
548    }
549
550    #[test]
551    fn test_child_group_instance_count() {
552        let tree = tree_with_sg4_sg8();
553        let nav = AssembledTreeNavigator::new(&tree);
554        // SG8[0] has 2 SG10 children
555        assert_eq!(
556            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
557            2
558        );
559        // SG8[1] has no SG10 children
560        assert_eq!(
561            nav.child_group_instance_count(&["SG4", "SG8"], 1, "SG10"),
562            0
563        );
564        // Non-existent child group
565        assert_eq!(
566            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG12"),
567            0
568        );
569    }
570
571    #[test]
572    fn test_find_segments_in_child_group() {
573        let tree = tree_with_sg4_sg8();
574        let nav = AssembledTreeNavigator::new(&tree);
575        // SG8[0] -> SG10[0] has CCI+Z23
576        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0);
577        assert_eq!(segs.len(), 1);
578        assert_eq!(segs[0].get_element(0), "Z23");
579        // SG8[0] -> SG10[1] has CCI with empty qualifier
580        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 1);
581        assert_eq!(segs.len(), 1);
582        assert_eq!(segs[0].get_element(0), "");
583        // SG8[0] -> SG10[0] has CAV
584        let segs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], 0, "SG10", 0);
585        assert_eq!(segs.len(), 1);
586    }
587
588    #[test]
589    fn test_child_group_invalid_path() {
590        let tree = tree_with_sg4_sg8();
591        let nav = AssembledTreeNavigator::new(&tree);
592        // Invalid parent path
593        assert_eq!(nav.child_group_instance_count(&["SG99"], 0, "SG10"), 0);
594        // Invalid parent instance
595        assert!(nav
596            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 99, "SG10", 0)
597            .is_empty());
598        // Invalid child instance
599        assert!(nav
600            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 99)
601            .is_empty());
602    }
603
604    #[test]
605    fn test_extract_value_in_group() {
606        let tree = tree_with_sg4_sg8();
607        let nav = AssembledTreeNavigator::new(&tree);
608        // SEQ qualifier in SG8[0]
609        assert_eq!(
610            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
611            Some("Z98".to_string()),
612        );
613        // SEQ qualifier in SG8[1]
614        assert_eq!(
615            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 1),
616            Some("Z01".to_string()),
617        );
618    }
619
620    #[test]
621    fn test_extract_value_missing() {
622        let tree = tree_with_sg4_sg8();
623        let nav = AssembledTreeNavigator::new(&tree);
624        // Non-existent segment
625        assert_eq!(
626            nav.extract_value_in_group("LOC", 0, 0, &["SG4", "SG8"], 0),
627            None
628        );
629        // Element index out of bounds
630        assert_eq!(
631            nav.extract_value_in_group("SEQ", 5, 0, &["SG4", "SG8"], 0),
632            None
633        );
634        // Component index out of bounds
635        assert_eq!(
636            nav.extract_value_in_group("SEQ", 0, 5, &["SG4", "SG8"], 0),
637            None
638        );
639        // Invalid group path
640        assert_eq!(nav.extract_value_in_group("SEQ", 0, 0, &["SG99"], 0), None);
641    }
642}