1use std::collections::HashMap;
8
9use crate::expr::ConditionExpr;
10
11use super::validate::{AhbFieldRule, AhbWorkflow};
12use mig_assembly::assembler::{
13 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
14};
15
16#[derive(Debug)]
18pub struct AhbNode<'a> {
19 pub rule: &'a AhbFieldRule,
21 pub value: Option<&'a str>,
23 pub segment_elements: Option<&'a [Vec<String>]>,
25}
26
27#[derive(Debug)]
29pub struct AhbGroupNode<'a> {
30 pub group_id: &'a str,
32 pub ahb_status: Option<&'a str>,
34 pub fields: Vec<AhbNode<'a>>,
36 pub children: Vec<AhbGroupNode<'a>>,
38}
39
40#[derive(Debug)]
42pub struct ValidatedTree<'a> {
43 pub pruefidentifikator: &'a str,
45 pub ub_definitions: &'a HashMap<String, ConditionExpr>,
47 pub root_fields: Vec<AhbNode<'a>>,
49 pub groups: Vec<AhbGroupNode<'a>>,
51 pub unmatched_rules: Vec<&'a AhbFieldRule>,
58}
59
60pub fn build_validated_tree<'a>(
66 workflow: &'a AhbWorkflow,
67 tree: &'a AssembledTree,
68) -> ValidatedTree<'a> {
69 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
71 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
73
74 for rule in &workflow.fields {
75 match extract_top_group(&rule.segment_path) {
76 Some(group_id) => {
77 group_rules
78 .entry(group_id.to_owned())
79 .or_default()
80 .push(rule);
81 }
82 None => {
83 root_rules.push(rule);
84 }
85 }
86 }
87
88 let root_fields = resolve_fields(&root_rules, &tree.segments);
90
91 let groups = resolve_groups(&tree.groups, &group_rules, 0);
93
94 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
97 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
98 for seg in &tree.segments {
100 if let Some(ref num) = seg.mig_number {
101 matched_mig_numbers.insert(num.as_str());
102 }
103 }
104
105 let unmatched_rules: Vec<&AhbFieldRule> = workflow
106 .fields
107 .iter()
108 .filter(|rule| {
109 rule.mig_number
110 .as_ref()
111 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
112 })
113 .collect();
114
115 ValidatedTree {
116 pruefidentifikator: &workflow.pruefidentifikator,
117 ub_definitions: &workflow.ub_definitions,
118 root_fields,
119 groups,
120 unmatched_rules,
121 }
122}
123
124fn collect_matched_mig_numbers_from_groups<'a>(
130 groups: &[AhbGroupNode<'a>],
131 out: &mut std::collections::HashSet<&'a str>,
132) {
133 for group in groups {
134 for node in &group.fields {
135 if let Some(ref num) = node.rule.mig_number {
136 out.insert(num.as_str());
137 }
138 }
139 collect_matched_mig_numbers_from_groups(&group.children, out);
140 }
141}
142
143fn extract_top_group(segment_path: &str) -> Option<&str> {
149 let first = segment_path.split('/').next()?;
150 if first.starts_with("SG") {
151 Some(first)
152 } else {
153 None
154 }
155}
156
157fn extract_child_group(stripped_path: &str) -> Option<&str> {
162 let first = stripped_path.split('/').next()?;
163 if first.starts_with("SG") {
164 Some(first)
165 } else {
166 None
167 }
168}
169
170fn resolve_fields<'a>(
176 rules: &[&'a AhbFieldRule],
177 segments: &'a [AssembledSegment],
178) -> Vec<AhbNode<'a>> {
179 rules
180 .iter()
181 .map(|rule| {
182 let matched_segment = find_segment(rule, segments);
183 let (value, elements) = match matched_segment {
184 Some(seg) => {
185 let val = extract_value(seg, rule);
186 (val, Some(seg.elements.as_slice()))
187 }
188 None => (None, None),
189 };
190 AhbNode {
191 rule,
192 value,
193 segment_elements: elements,
194 }
195 })
196 .collect()
197}
198
199fn find_segment<'a>(
211 rule: &AhbFieldRule,
212 segments: &'a [AssembledSegment],
213) -> Option<&'a AssembledSegment> {
214 let tag = extract_segment_tag(&rule.segment_path)?;
215
216 if !rule.codes.is_empty() {
235 let el_idx = rule.element_index.unwrap_or(0);
236 let comp_idx = rule.component_index.unwrap_or(0);
237 let expected: std::collections::HashSet<&str> =
238 rule.codes.iter().map(|c| c.value.as_str()).collect();
239 if let Some(seg) = segments.iter().find(|s| {
240 s.tag == tag
241 && s.elements
242 .get(el_idx)
243 .and_then(|e| e.get(comp_idx))
244 .is_some_and(|v| expected.contains(v.as_str()))
245 }) {
246 return Some(seg);
247 }
248 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
249 if same_tag_count > 1 {
250 return None;
251 }
252 }
253
254 if let Some(ref mig_num) = rule.mig_number {
255 if let Some(seg) = segments
257 .iter()
258 .find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
259 {
260 return Some(seg);
261 }
262
263 return segments
265 .iter()
266 .find(|s| s.tag == tag && s.mig_number.as_ref().map_or(true, |m| m == mig_num));
267 }
268
269 segments.iter().find(|s| s.tag == tag)
271}
272
273fn extract_segment_tag(segment_path: &str) -> Option<&str> {
278 segment_path.split('/').find(|part| !part.starts_with("SG"))
279}
280
281fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
283 let elem_idx = rule.element_index.unwrap_or(0);
284 let comp_idx = rule.component_index.unwrap_or(0);
285
286 let element = segment.elements.get(elem_idx)?;
287 let component = element.get(comp_idx)?;
288
289 if component.is_empty() {
290 None
291 } else {
292 Some(component.as_str())
293 }
294}
295
296fn resolve_groups<'a>(
301 assembled_groups: &'a [AssembledGroup],
302 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
303 depth: usize,
304) -> Vec<AhbGroupNode<'a>> {
305 let mut result = Vec::new();
306
307 for assembled_group in assembled_groups {
308 let rules = group_rules.get(&assembled_group.group_id);
309
310 for instance in &assembled_group.repetitions {
311 let node = resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
312 result.push(node);
313 }
314 }
315
316 result
317}
318
319fn strip_n_groups(path: &str, n: usize) -> &str {
323 let mut rest = path;
324 for _ in 0..n {
325 match rest.find('/') {
326 Some(idx) => rest = &rest[idx + 1..],
327 None => return rest,
328 }
329 }
330 rest
331}
332
333fn resolve_group_instance<'a>(
342 group_id: &'a str,
343 instance: &'a AssembledGroupInstance,
344 rules: Option<&Vec<&'a AhbFieldRule>>,
345 depth: usize,
346) -> AhbGroupNode<'a> {
347 let strip_count = depth + 1;
349
350 let variant_numbers: std::collections::HashSet<&str> =
355 if !instance.variant_mig_numbers.is_empty() {
356 instance
357 .variant_mig_numbers
358 .iter()
359 .map(|s| s.as_str())
360 .collect()
361 } else {
362 collect_instance_mig_numbers(instance)
363 };
364
365 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
366 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
367 let mut ahb_status: Option<&'a str> = None;
368
369 if let Some(rules) = rules {
370 for rule in rules {
371 if let Some(ref rule_mig) = rule.mig_number {
374 if !variant_numbers.contains(rule_mig.as_str()) {
375 continue;
376 }
377 }
378
379 let stripped = strip_n_groups(&rule.segment_path, strip_count);
381
382 if let Some(child_group_id) = extract_child_group(stripped) {
383 child_group_rules
384 .entry(child_group_id.to_owned())
385 .or_default()
386 .push(rule);
387 } else {
388 direct_rules.push(rule);
389 }
390
391 if ahb_status.is_none() {
393 if let Some(ref status) = rule.parent_group_ahb_status {
394 ahb_status = Some(status.as_str());
395 }
396 }
397 }
398 }
399
400 let fields = resolve_fields(&direct_rules, &instance.segments);
402
403 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
405
406 AhbGroupNode {
407 group_id,
408 ahb_status,
409 fields,
410 children,
411 }
412}
413
414fn collect_instance_mig_numbers(
416 instance: &AssembledGroupInstance,
417) -> std::collections::HashSet<&str> {
418 let mut numbers = std::collections::HashSet::new();
419 for seg in &instance.segments {
420 if let Some(ref num) = seg.mig_number {
421 numbers.insert(num.as_str());
422 }
423 }
424 for child_group in &instance.child_groups {
425 for child_instance in &child_group.repetitions {
426 numbers.extend(collect_instance_mig_numbers(child_instance));
427 }
428 }
429 numbers
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
436 use mig_assembly::assembler::{
437 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
438 };
439 use std::collections::{BTreeMap, HashMap};
440
441 fn empty_workflow() -> AhbWorkflow {
442 AhbWorkflow {
443 pruefidentifikator: "11001".to_string(),
444 description: String::new(),
445 communication_direction: None,
446 fields: vec![],
447 ub_definitions: HashMap::new(),
448 }
449 }
450
451 fn empty_tree() -> AssembledTree {
452 AssembledTree {
453 segments: vec![],
454 groups: vec![],
455 post_group_start: 0,
456 inter_group_segments: BTreeMap::new(),
457 }
458 }
459
460 fn make_segment(
461 tag: &str,
462 elements: Vec<Vec<&str>>,
463 mig_number: Option<&str>,
464 ) -> AssembledSegment {
465 AssembledSegment {
466 tag: tag.to_string(),
467 elements: elements
468 .into_iter()
469 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
470 .collect(),
471 mig_number: mig_number.map(|s| s.to_string()),
472 }
473 }
474
475 fn make_rule(
476 segment_path: &str,
477 name: &str,
478 ahb_status: &str,
479 mig_number: Option<&str>,
480 element_index: Option<usize>,
481 component_index: Option<usize>,
482 ) -> AhbFieldRule {
483 AhbFieldRule {
484 segment_path: segment_path.to_string(),
485 name: name.to_string(),
486 ahb_status: ahb_status.to_string(),
487 codes: vec![],
488 parent_group_ahb_status: None,
489 segment_ahb_status: None,
490 element_index,
491 component_index,
492 mig_number: mig_number.map(|s| s.to_string()),
493 }
494 }
495
496 #[test]
497 fn test_empty_workflow_empty_tree() {
498 let workflow = empty_workflow();
499 let tree = empty_tree();
500 let result = build_validated_tree(&workflow, &tree);
501
502 assert_eq!(result.pruefidentifikator, "11001");
503 assert!(result.root_fields.is_empty());
504 assert!(result.groups.is_empty());
505 }
506
507 #[test]
508 fn test_root_field_matches_root_segment() {
509 let mut workflow = empty_workflow();
510 workflow.fields.push(make_rule(
511 "BGM/C002/1001",
512 "Nachrichtentyp",
513 "X",
514 Some("0001"),
515 Some(0),
516 Some(0),
517 ));
518
519 let tree = AssembledTree {
520 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
521 groups: vec![],
522 post_group_start: 1,
523 inter_group_segments: BTreeMap::new(),
524 };
525
526 let result = build_validated_tree(&workflow, &tree);
527
528 assert_eq!(result.root_fields.len(), 1);
529 let node = &result.root_fields[0];
530 assert_eq!(node.value, Some("E01"));
531 assert!(node.segment_elements.is_some());
532 assert_eq!(node.rule.name, "Nachrichtentyp");
533 }
534
535 #[test]
536 fn test_sg4_dtm_mig_number_matching() {
537 let mut workflow = empty_workflow();
540
541 workflow.fields.push(make_rule(
543 "SG4/DTM/C507/2380",
544 "Eingangsdatum",
545 "X",
546 Some("0082"),
547 Some(1), Some(0),
549 ));
550
551 workflow.fields.push(make_rule(
553 "SG4/DTM/C507/2380",
554 "Dokumentendatum",
555 "X",
556 Some("0083"),
557 Some(1),
558 Some(0),
559 ));
560
561 let tree = AssembledTree {
562 segments: vec![],
563 groups: vec![AssembledGroup {
564 group_id: "SG4".to_string(),
565 repetitions: vec![AssembledGroupInstance {
566 segments: vec![
567 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
568 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
569 ],
570 child_groups: vec![],
571 entry_mig_number: None,
572 variant_mig_numbers: vec![],
573 skipped_segments: vec![],
574 skipped_positions: Vec::new(),
575 }],
576 }],
577 post_group_start: 0,
578 inter_group_segments: BTreeMap::new(),
579 };
580
581 let result = build_validated_tree(&workflow, &tree);
582
583 assert_eq!(result.groups.len(), 1);
584 let sg4 = &result.groups[0];
585 assert_eq!(sg4.group_id, "SG4");
586 assert_eq!(sg4.fields.len(), 2);
587
588 let eingangsdatum = &sg4.fields[0];
590 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
591 assert_eq!(eingangsdatum.value, Some("20260101"));
592
593 let dokumentendatum = &sg4.fields[1];
595 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
596 assert_eq!(dokumentendatum.value, Some("20260401"));
597 }
598
599 #[test]
600 fn test_rule_filtered_to_correct_variant() {
601 let mut workflow = empty_workflow();
605 workflow.fields.push(make_rule(
606 "SG4/RFF/C506/1154",
607 "Referenz",
608 "X",
609 Some("0099"),
610 Some(0),
611 Some(1),
612 ));
613
614 let tree = AssembledTree {
615 segments: vec![],
616 groups: vec![AssembledGroup {
617 group_id: "SG4".to_string(),
618 repetitions: vec![AssembledGroupInstance {
619 segments: vec![], child_groups: vec![],
621 entry_mig_number: None,
622 variant_mig_numbers: vec![],
623 skipped_segments: vec![],
624 skipped_positions: Vec::new(),
625 }],
626 }],
627 post_group_start: 0,
628 inter_group_segments: BTreeMap::new(),
629 };
630
631 let result = build_validated_tree(&workflow, &tree);
632 assert_eq!(result.groups.len(), 1);
633 assert_eq!(result.groups[0].fields.len(), 0);
635 }
636
637 #[test]
638 fn test_missing_segment_within_correct_variant() {
639 let mut workflow = empty_workflow();
643 workflow.fields.push(make_rule(
645 "SG4/SEQ/1229",
646 "Qualifier",
647 "X",
648 Some("0098"),
649 Some(0),
650 Some(0),
651 ));
652 workflow.fields.push(make_rule(
654 "SG4/RFF/C506/1154",
655 "Referenz",
656 "X",
657 Some("0099"),
658 Some(0),
659 Some(1),
660 ));
661
662 let tree = AssembledTree {
663 segments: vec![],
664 groups: vec![AssembledGroup {
665 group_id: "SG4".to_string(),
666 repetitions: vec![AssembledGroupInstance {
667 segments: vec![
668 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
670 ],
671 child_groups: vec![],
672 entry_mig_number: Some("0098".to_string()),
673 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
675 skipped_segments: vec![],
676 skipped_positions: Vec::new(),
677 }],
678 }],
679 post_group_start: 0,
680 inter_group_segments: BTreeMap::new(),
681 };
682
683 let result = build_validated_tree(&workflow, &tree);
684 assert_eq!(result.groups.len(), 1);
685 assert_eq!(result.groups[0].fields.len(), 2);
687 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
688 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
689 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
691 assert_eq!(result.groups[0].fields[1].value, None);
692 }
693
694 #[test]
695 fn test_missing_group_variant_populates_unmatched_rules() {
696 let mut workflow = empty_workflow();
700
701 workflow.fields.push(make_rule(
703 "SG2/NAD/3035",
704 "MP-ID Absender Qualifier",
705 "X",
706 Some("0010"),
707 Some(0),
708 Some(0),
709 ));
710 workflow.fields.push(make_rule(
712 "SG2/NAD/C082/3039",
713 "MP-ID Absender",
714 "X",
715 Some("0010"),
716 Some(1),
717 Some(0),
718 ));
719 workflow.fields.push(make_rule(
721 "SG2/NAD/3035",
722 "MP-ID Empfänger Qualifier",
723 "X",
724 Some("0011"),
725 Some(0),
726 Some(0),
727 ));
728 workflow.fields.push(make_rule(
730 "SG2/NAD/C082/3039",
731 "MP-ID Empfänger",
732 "X",
733 Some("0011"),
734 Some(1),
735 Some(0),
736 ));
737
738 let tree = AssembledTree {
740 segments: vec![],
741 groups: vec![AssembledGroup {
742 group_id: "SG2".to_string(),
743 repetitions: vec![AssembledGroupInstance {
744 segments: vec![make_segment(
745 "NAD",
746 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
747 Some("0011"),
748 )],
749 child_groups: vec![],
750 entry_mig_number: Some("0011".to_string()),
751 variant_mig_numbers: vec!["0011".to_string()],
752 skipped_segments: vec![],
753 skipped_positions: Vec::new(),
754 }],
755 }],
756 post_group_start: 0,
757 inter_group_segments: BTreeMap::new(),
758 };
759
760 let result = build_validated_tree(&workflow, &tree);
761
762 assert_eq!(result.groups.len(), 1);
764 assert_eq!(result.groups[0].fields.len(), 2);
765 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
766
767 assert_eq!(
769 result.unmatched_rules.len(),
770 2,
771 "Expected 2 unmatched rules (NAD+MS), got {}",
772 result.unmatched_rules.len()
773 );
774 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
775 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
776 }
777
778 #[test]
779 fn test_entirely_absent_group_populates_unmatched_rules() {
780 let mut workflow = empty_workflow();
782 workflow.fields.push(make_rule(
783 "SG2/NAD/3035",
784 "MP-ID Absender Qualifier",
785 "X",
786 Some("0010"),
787 Some(0),
788 Some(0),
789 ));
790
791 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
794
795 assert!(result.groups.is_empty());
797
798 assert_eq!(
800 result.unmatched_rules.len(),
801 1,
802 "Expected 1 unmatched rule, got {}",
803 result.unmatched_rules.len()
804 );
805 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
806 }
807
808 #[test]
809 fn test_fallback_to_tag_when_no_mig_number() {
810 let mut workflow = empty_workflow();
811 workflow.fields.push(make_rule(
812 "BGM/C002/1001",
813 "Nachrichtentyp",
814 "X",
815 None, Some(0),
817 Some(0),
818 ));
819
820 let tree = AssembledTree {
821 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
822 groups: vec![],
823 post_group_start: 1,
824 inter_group_segments: BTreeMap::new(),
825 };
826
827 let result = build_validated_tree(&workflow, &tree);
828 assert_eq!(result.root_fields.len(), 1);
829 assert_eq!(result.root_fields[0].value, Some("E01"));
830 }
831
832 #[test]
833 fn test_nested_child_groups() {
834 let mut workflow = empty_workflow();
835 workflow.fields.push(make_rule(
837 "SG4/SG5/LOC/C517/3225",
838 "Marktlokations-ID",
839 "X",
840 Some("0050"),
841 Some(0),
842 Some(0),
843 ));
844
845 let tree = AssembledTree {
846 segments: vec![],
847 groups: vec![AssembledGroup {
848 group_id: "SG4".to_string(),
849 repetitions: vec![AssembledGroupInstance {
850 segments: vec![],
851 entry_mig_number: None,
852 child_groups: vec![AssembledGroup {
853 group_id: "SG5".to_string(),
854 repetitions: vec![AssembledGroupInstance {
855 segments: vec![make_segment(
856 "LOC",
857 vec![vec!["DE00012345678"]],
858 Some("0050"),
859 )],
860 child_groups: vec![],
861 entry_mig_number: None,
862 variant_mig_numbers: vec![],
863 skipped_segments: vec![],
864 skipped_positions: Vec::new(),
865 }],
866 }],
867 variant_mig_numbers: vec![],
868 skipped_segments: vec![],
869 skipped_positions: Vec::new(),
870 }],
871 }],
872 post_group_start: 0,
873 inter_group_segments: BTreeMap::new(),
874 };
875
876 let result = build_validated_tree(&workflow, &tree);
877 assert_eq!(result.groups.len(), 1);
878 let sg4 = &result.groups[0];
879 assert_eq!(sg4.children.len(), 1);
880
881 let sg5 = &sg4.children[0];
882 assert_eq!(sg5.group_id, "SG5");
883 assert_eq!(sg5.fields.len(), 1);
884 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
885 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
886 }
887
888 #[test]
889 fn test_qualifier_aware_segment_matching_same_tag_variants() {
890 let mut workflow = empty_workflow();
904
905 let mut z33 = make_rule(
907 "SG10/STS/C601/9015",
908 "Statuskategorie Z33",
909 "X",
910 Some("00035"),
911 Some(0),
912 Some(0),
913 );
914 z33.codes = vec![super::super::validate::AhbCodeRule {
915 value: "Z33".into(),
916 description: String::new(),
917 ahb_status: "X".into(),
918 }];
919 workflow.fields.push(z33);
920
921 let mut z32 = make_rule(
923 "SG10/STS/C601/9015",
924 "Statuskategorie Z32",
925 "X",
926 Some("00036"),
927 Some(0),
928 Some(0),
929 );
930 z32.codes = vec![super::super::validate::AhbCodeRule {
931 value: "Z32".into(),
932 description: String::new(),
933 ahb_status: "X".into(),
934 }];
935 workflow.fields.push(z32);
936
937 let mut z40 = make_rule(
939 "SG10/STS/C601/9015",
940 "Statuskategorie Z40",
941 "X",
942 Some("00038"),
943 Some(0),
944 Some(0),
945 );
946 z40.codes = vec![super::super::validate::AhbCodeRule {
947 value: "Z40".into(),
948 description: String::new(),
949 ahb_status: "X".into(),
950 }];
951 workflow.fields.push(z40);
952
953 let tree = AssembledTree {
956 segments: vec![],
957 groups: vec![AssembledGroup {
958 group_id: "SG10".to_string(),
959 repetitions: vec![AssembledGroupInstance {
960 segments: vec![
961 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
962 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
963 ],
964 child_groups: vec![],
965 entry_mig_number: None,
966 variant_mig_numbers: vec![
967 "00035".into(),
968 "00036".into(),
969 "00037".into(),
970 "00038".into(),
971 ],
972 skipped_segments: vec![],
973 skipped_positions: Vec::new(),
974 }],
975 }],
976 post_group_start: 0,
977 inter_group_segments: BTreeMap::new(),
978 };
979
980 let result = build_validated_tree(&workflow, &tree);
981 assert_eq!(result.groups.len(), 1);
982 let sg10 = &result.groups[0];
983 assert_eq!(
984 sg10.fields.len(),
985 3,
986 "all three rules should pass the variant filter"
987 );
988
989 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
990
991 assert_eq!(
993 by_name("Statuskategorie Z33").value,
994 None,
995 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
996 );
997
998 assert_eq!(
1001 by_name("Statuskategorie Z32").value,
1002 Some("Z32"),
1003 "rule for Z32 must attach to the STS segment with qualifier Z32"
1004 );
1005
1006 assert_eq!(
1008 by_name("Statuskategorie Z40").value,
1009 Some("Z40"),
1010 "rule for Z40 must attach to the STS segment with qualifier Z40"
1011 );
1012 }
1013}