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
282/// `seg` as an owned segment, with its input counter; `index` (its place in
283/// its group) for a segment the reverse mapping built, which has none.
284fn to_owned(seg: &AssembledSegment, index: u32) -> OwnedSegment {
285    OwnedSegment {
286        id: seg.tag.clone(),
287        elements: seg.elements.clone(),
288        segment_number: seg.segment_number.unwrap_or(index),
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::assembler::{
296        AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
297    };
298    use mig_types::navigator::GroupNavigator;
299
300    fn make_seg(tag: &str, elements: Vec<Vec<&str>>) -> AssembledSegment {
301        AssembledSegment {
302            tag: tag.to_string(),
303            elements: elements
304                .into_iter()
305                .map(|e| e.into_iter().map(|c| c.to_string()).collect())
306                .collect(),
307            mig_number: None,
308            segment_number: None,
309        }
310    }
311
312    fn tree_with_sg4_sg8() -> AssembledTree {
313        // SG4[0] -> segments: [IDE, STS]
314        //        -> SG8[0]: [SEQ+Z98, CCI+Z30++Z07]
315        //             -> SG10[0]: [CCI+Z23, CAV+Z91:value1]
316        //             -> SG10[1]: [CCI, CAV+:value2]
317        //        -> SG8[1]: [SEQ+Z01, CCI+++ZC0]
318        //             (no SG10)
319        AssembledTree {
320            segments: vec![make_seg("UNH", vec![vec!["001"]])],
321            groups: vec![AssembledGroup {
322                group_id: "SG4".to_string(),
323                repetitions: vec![AssembledGroupInstance {
324                    segments: vec![
325                        make_seg("IDE", vec![vec!["24", "TX001"]]),
326                        make_seg("STS", vec![vec!["E01"], vec![], vec!["A05"]]),
327                    ],
328                    child_groups: vec![AssembledGroup {
329                        group_id: "SG8".to_string(),
330                        repetitions: vec![
331                            AssembledGroupInstance {
332                                segments: vec![
333                                    make_seg("SEQ", vec![vec!["Z98"]]),
334                                    make_seg("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
335                                ],
336                                child_groups: vec![AssembledGroup {
337                                    group_id: "SG10".to_string(),
338                                    repetitions: vec![
339                                        AssembledGroupInstance {
340                                            segments: vec![
341                                                make_seg("CCI", vec![vec!["Z23"]]),
342                                                make_seg("CAV", vec![vec!["Z91", "value1"]]),
343                                            ],
344                                            child_groups: vec![],
345                                            entry_mig_number: None,
346                                            variant_mig_numbers: vec![],
347                                            skipped_segments: vec![],
348                                            skipped_positions: Vec::new(),
349                                        },
350                                        AssembledGroupInstance {
351                                            segments: vec![
352                                                make_seg("CCI", vec![vec![""]]),
353                                                make_seg("CAV", vec![vec!["", "value2"]]),
354                                            ],
355                                            child_groups: vec![],
356                                            entry_mig_number: None,
357                                            variant_mig_numbers: vec![],
358                                            skipped_segments: vec![],
359                                            skipped_positions: Vec::new(),
360                                        },
361                                    ],
362                                }],
363                                entry_mig_number: None,
364                                variant_mig_numbers: vec![],
365                                skipped_segments: vec![],
366                                skipped_positions: Vec::new(),
367                            },
368                            AssembledGroupInstance {
369                                segments: vec![
370                                    make_seg("SEQ", vec![vec!["Z01"]]),
371                                    make_seg("CCI", vec![vec![""], vec![], vec!["ZC0"]]),
372                                ],
373                                child_groups: vec![],
374                                entry_mig_number: None,
375                                variant_mig_numbers: vec![],
376                                skipped_segments: vec![],
377                                skipped_positions: Vec::new(),
378                            },
379                        ],
380                    }],
381                    entry_mig_number: None,
382                    variant_mig_numbers: vec![],
383                    skipped_segments: vec![],
384                    skipped_positions: Vec::new(),
385                }],
386            }],
387            post_group_start: 1,
388            inter_group_segments: std::collections::BTreeMap::new(),
389        }
390    }
391
392    /// Two SG4s: SG4[0] has one SG8 (SEQ+Z98, with an SG10 CCI+Z23);
393    /// SG4[1] has two SG8s (SEQ+Z01, SEQ+Z03 with an SG10 CCI+E13).
394    fn tree_with_two_sg4() -> AssembledTree {
395        fn instance(
396            segments: Vec<AssembledSegment>,
397            child_groups: Vec<AssembledGroup>,
398        ) -> AssembledGroupInstance {
399            AssembledGroupInstance {
400                segments,
401                child_groups,
402                entry_mig_number: None,
403                variant_mig_numbers: vec![],
404                skipped_segments: vec![],
405                skipped_positions: Vec::new(),
406            }
407        }
408        fn group(id: &str, repetitions: Vec<AssembledGroupInstance>) -> AssembledGroup {
409            AssembledGroup {
410                group_id: id.to_string(),
411                repetitions,
412            }
413        }
414        let sg10 = |code: &str| {
415            group(
416                "SG10",
417                vec![instance(
418                    vec![make_seg("CCI", vec![vec![""], vec![], vec![code]])],
419                    vec![],
420                )],
421            )
422        };
423        AssembledTree {
424            segments: vec![],
425            groups: vec![group(
426                "SG4",
427                vec![
428                    instance(
429                        vec![make_seg("IDE", vec![vec!["24", "TX0"]])],
430                        vec![group(
431                            "SG8",
432                            vec![instance(
433                                vec![make_seg("SEQ", vec![vec!["Z98"]])],
434                                vec![sg10("Z23")],
435                            )],
436                        )],
437                    ),
438                    instance(
439                        vec![make_seg("IDE", vec![vec!["24", "TX1"]])],
440                        vec![group(
441                            "SG8",
442                            vec![
443                                instance(vec![make_seg("SEQ", vec![vec!["Z01"]])], vec![]),
444                                instance(
445                                    vec![make_seg("SEQ", vec![vec!["Z03"]])],
446                                    vec![sg10("E13")],
447                                ),
448                            ],
449                        )],
450                    ),
451                ],
452            )],
453            post_group_start: 0,
454            inter_group_segments: std::collections::BTreeMap::new(),
455        }
456    }
457
458    #[test]
459    fn an_instance_is_addressed_at_every_level() {
460        let tree = tree_with_two_sg4();
461        let nav = AssembledTreeNavigator::new(&tree);
462        let seq = |path: &[(&str, usize)]| -> Vec<String> {
463            nav.find_segments_in_instance("SEQ", path)
464                .iter()
465                .map(|s| s.get_element(0).to_string())
466                .collect()
467        };
468        assert_eq!(seq(&[("SG4", 0), ("SG8", 0)]), ["Z98"]);
469        assert_eq!(seq(&[("SG4", 1), ("SG8", 0)]), ["Z01"]);
470        assert_eq!(seq(&[("SG4", 1), ("SG8", 1)]), ["Z03"]);
471        assert!(seq(&[("SG4", 0), ("SG8", 1)]).is_empty());
472    }
473
474    #[test]
475    fn a_subtree_includes_the_nested_groups() {
476        let tree = tree_with_two_sg4();
477        let nav = AssembledTreeNavigator::new(&tree);
478        let cci = |path: &[(&str, usize)]| -> Vec<String> {
479            nav.find_segments_in_subtree("CCI", path)
480                .iter()
481                .map(|s| s.get_component(2, 0).to_string())
482                .collect()
483        };
484        assert_eq!(cci(&[("SG4", 1), ("SG8", 1)]), ["E13"]);
485        assert!(cci(&[("SG4", 1), ("SG8", 0)]).is_empty());
486        assert_eq!(cci(&[("SG4", 1)]), ["E13"], "the whole SG4");
487        assert!(
488            nav.find_segments_in_instance("CCI", &[("SG4", 1), ("SG8", 1)])
489                .is_empty(),
490            "the instance alone holds no CCI"
491        );
492    }
493
494    #[test]
495    fn test_find_in_sg8_instance_0() {
496        let tree = tree_with_sg4_sg8();
497        let nav = AssembledTreeNavigator::new(&tree);
498        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
499        assert_eq!(segs.len(), 1);
500        assert_eq!(segs[0].get_element(0), "Z98");
501    }
502
503    #[test]
504    fn test_find_in_sg8_instance_1() {
505        let tree = tree_with_sg4_sg8();
506        let nav = AssembledTreeNavigator::new(&tree);
507        let segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], 1);
508        assert_eq!(segs.len(), 1);
509        assert_eq!(segs[0].get_element(0), "Z01");
510    }
511
512    #[test]
513    fn test_qualifier_in_group_scoped() {
514        let tree = tree_with_sg4_sg8();
515        let nav = AssembledTreeNavigator::new(&tree);
516        let segs = nav.find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 1);
517        assert_eq!(segs.len(), 1);
518        // NOT in instance 0
519        assert!(nav
520            .find_segments_with_qualifier_in_group("CCI", 2, "ZC0", &["SG4", "SG8"], 0)
521            .is_empty());
522    }
523
524    #[test]
525    fn test_group_instance_count() {
526        let tree = tree_with_sg4_sg8();
527        let nav = AssembledTreeNavigator::new(&tree);
528        assert_eq!(nav.group_instance_count(&["SG4"]), 1);
529        assert_eq!(nav.group_instance_count(&["SG4", "SG8"]), 2);
530        assert_eq!(nav.group_instance_count(&["SG4", "SG5"]), 0);
531    }
532
533    #[test]
534    fn test_find_in_sg4_directly() {
535        let tree = tree_with_sg4_sg8();
536        let nav = AssembledTreeNavigator::new(&tree);
537        let segs = nav.find_segments_in_group("STS", &["SG4"], 0);
538        assert_eq!(segs.len(), 1);
539    }
540
541    #[test]
542    fn test_invalid_path_returns_empty() {
543        let tree = tree_with_sg4_sg8();
544        let nav = AssembledTreeNavigator::new(&tree);
545        assert!(nav.find_segments_in_group("SEQ", &["SG99"], 0).is_empty());
546        assert!(nav
547            .find_segments_in_group("SEQ", &["SG4", "SG8"], 99)
548            .is_empty());
549        assert!(nav.find_segments_in_group("SEQ", &[], 0).is_empty());
550    }
551
552    #[test]
553    fn test_child_group_instance_count() {
554        let tree = tree_with_sg4_sg8();
555        let nav = AssembledTreeNavigator::new(&tree);
556        // SG8[0] has 2 SG10 children
557        assert_eq!(
558            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
559            2
560        );
561        // SG8[1] has no SG10 children
562        assert_eq!(
563            nav.child_group_instance_count(&["SG4", "SG8"], 1, "SG10"),
564            0
565        );
566        // Non-existent child group
567        assert_eq!(
568            nav.child_group_instance_count(&["SG4", "SG8"], 0, "SG12"),
569            0
570        );
571    }
572
573    #[test]
574    fn test_find_segments_in_child_group() {
575        let tree = tree_with_sg4_sg8();
576        let nav = AssembledTreeNavigator::new(&tree);
577        // SG8[0] -> SG10[0] has CCI+Z23
578        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0);
579        assert_eq!(segs.len(), 1);
580        assert_eq!(segs[0].get_element(0), "Z23");
581        // SG8[0] -> SG10[1] has CCI with empty qualifier
582        let segs = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 1);
583        assert_eq!(segs.len(), 1);
584        assert_eq!(segs[0].get_element(0), "");
585        // SG8[0] -> SG10[0] has CAV
586        let segs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], 0, "SG10", 0);
587        assert_eq!(segs.len(), 1);
588    }
589
590    #[test]
591    fn test_child_group_invalid_path() {
592        let tree = tree_with_sg4_sg8();
593        let nav = AssembledTreeNavigator::new(&tree);
594        // Invalid parent path
595        assert_eq!(nav.child_group_instance_count(&["SG99"], 0, "SG10"), 0);
596        // Invalid parent instance
597        assert!(nav
598            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 99, "SG10", 0)
599            .is_empty());
600        // Invalid child instance
601        assert!(nav
602            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 99)
603            .is_empty());
604    }
605
606    #[test]
607    fn test_extract_value_in_group() {
608        let tree = tree_with_sg4_sg8();
609        let nav = AssembledTreeNavigator::new(&tree);
610        // SEQ qualifier in SG8[0]
611        assert_eq!(
612            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
613            Some("Z98".to_string()),
614        );
615        // SEQ qualifier in SG8[1]
616        assert_eq!(
617            nav.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 1),
618            Some("Z01".to_string()),
619        );
620    }
621
622    #[test]
623    fn test_extract_value_missing() {
624        let tree = tree_with_sg4_sg8();
625        let nav = AssembledTreeNavigator::new(&tree);
626        // Non-existent segment
627        assert_eq!(
628            nav.extract_value_in_group("LOC", 0, 0, &["SG4", "SG8"], 0),
629            None
630        );
631        // Element index out of bounds
632        assert_eq!(
633            nav.extract_value_in_group("SEQ", 5, 0, &["SG4", "SG8"], 0),
634            None
635        );
636        // Component index out of bounds
637        assert_eq!(
638            nav.extract_value_in_group("SEQ", 0, 5, &["SG4", "SG8"], 0),
639            None
640        );
641        // Invalid group path
642        assert_eq!(nav.extract_value_in_group("SEQ", 0, 0, &["SG99"], 0), None);
643    }
644}