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