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