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 pub matched_segment_number: Option<u32>,
28}
29
30#[derive(Debug)]
32pub struct AhbGroupNode<'a> {
33 pub group_id: &'a str,
35 pub ahb_status: Option<&'a str>,
37 pub fields: Vec<AhbNode<'a>>,
39 pub children: Vec<AhbGroupNode<'a>>,
41}
42
43#[derive(Debug)]
45pub struct ValidatedTree<'a> {
46 pub pruefidentifikator: &'a str,
48 pub ub_definitions: &'a HashMap<String, ConditionExpr>,
50 pub root_fields: Vec<AhbNode<'a>>,
52 pub groups: Vec<AhbGroupNode<'a>>,
54 pub unmatched_rules: Vec<&'a AhbFieldRule>,
61}
62
63pub fn build_validated_tree<'a>(
69 workflow: &'a AhbWorkflow,
70 tree: &'a AssembledTree,
71) -> ValidatedTree<'a> {
72 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
74 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
76
77 for rule in &workflow.fields {
78 match extract_top_group(&rule.segment_path) {
79 Some(group_id) => {
80 group_rules
81 .entry(group_id.to_owned())
82 .or_default()
83 .push(rule);
84 }
85 None => {
86 root_rules.push(rule);
87 }
88 }
89 }
90
91 let root_fields = resolve_fields(&root_rules, &tree.segments);
93
94 let groups = resolve_groups(&tree.groups, &group_rules, 0);
96
97 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
100 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
101 for seg in &tree.segments {
103 if let Some(ref num) = seg.mig_number {
104 matched_mig_numbers.insert(num.as_str());
105 }
106 }
107
108 let unmatched_rules: Vec<&AhbFieldRule> = workflow
109 .fields
110 .iter()
111 .filter(|rule| {
112 rule.mig_number
113 .as_ref()
114 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
115 })
116 .collect();
117
118 ValidatedTree {
119 pruefidentifikator: &workflow.pruefidentifikator,
120 ub_definitions: &workflow.ub_definitions,
121 root_fields,
122 groups,
123 unmatched_rules,
124 }
125}
126
127fn collect_matched_mig_numbers_from_groups<'a>(
133 groups: &[AhbGroupNode<'a>],
134 out: &mut std::collections::HashSet<&'a str>,
135) {
136 for group in groups {
137 for node in &group.fields {
138 if let Some(ref num) = node.rule.mig_number {
139 out.insert(num.as_str());
140 }
141 }
142 collect_matched_mig_numbers_from_groups(&group.children, out);
143 }
144}
145
146fn extract_top_group(segment_path: &str) -> Option<&str> {
152 let first = segment_path.split('/').next()?;
153 if first.starts_with("SG") {
154 Some(first)
155 } else {
156 None
157 }
158}
159
160fn extract_child_group(stripped_path: &str) -> Option<&str> {
165 let first = stripped_path.split('/').next()?;
166 if first.starts_with("SG") {
167 Some(first)
168 } else {
169 None
170 }
171}
172
173fn resolve_fields<'a>(
179 rules: &[&'a AhbFieldRule],
180 segments: &'a [AssembledSegment],
181) -> Vec<AhbNode<'a>> {
182 rules
183 .iter()
184 .map(|rule| {
185 let matched_segment = find_segment(rule, segments);
186 let (value, elements, segment_number) = match matched_segment {
187 Some(seg) => {
188 let val = extract_value(seg, rule);
189 (val, Some(seg.elements.as_slice()), seg.segment_number)
190 }
191 None => {
192 let fallback_num = extract_segment_tag(&rule.segment_path)
201 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
202 .and_then(|s| s.segment_number);
203 (None, None, fallback_num)
204 }
205 };
206 AhbNode {
207 rule,
208 value,
209 segment_elements: elements,
210 matched_segment_number: segment_number,
211 }
212 })
213 .collect()
214}
215
216fn find_segment<'a>(
228 rule: &AhbFieldRule,
229 segments: &'a [AssembledSegment],
230) -> Option<&'a AssembledSegment> {
231 let tag = extract_segment_tag(&rule.segment_path)?;
232
233 if !rule.codes.is_empty() {
252 let el_idx = rule.element_index.unwrap_or(0);
253 let comp_idx = rule.component_index.unwrap_or(0);
254 let expected: std::collections::HashSet<&str> =
255 rule.codes.iter().map(|c| c.value.as_str()).collect();
256 if let Some(seg) = segments.iter().find(|s| {
257 s.tag == tag
258 && s.elements
259 .get(el_idx)
260 .and_then(|e| e.get(comp_idx))
261 .is_some_and(|v| expected.contains(v.as_str()))
262 }) {
263 return Some(seg);
264 }
265 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
266 if same_tag_count > 1 {
267 return None;
268 }
269 }
270
271 if let Some(ref mig_num) = rule.mig_number {
272 if let Some(seg) = segments
274 .iter()
275 .find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
276 {
277 return Some(seg);
278 }
279
280 return segments
282 .iter()
283 .find(|s| s.tag == tag && s.mig_number.as_ref().map_or(true, |m| m == mig_num));
284 }
285
286 segments.iter().find(|s| s.tag == tag)
288}
289
290fn extract_segment_tag(segment_path: &str) -> Option<&str> {
295 segment_path.split('/').find(|part| !part.starts_with("SG"))
296}
297
298fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
300 let elem_idx = rule.element_index.unwrap_or(0);
301 let comp_idx = rule.component_index.unwrap_or(0);
302
303 let element = segment.elements.get(elem_idx)?;
304 let component = element.get(comp_idx)?;
305
306 if component.is_empty() {
307 None
308 } else {
309 Some(component.as_str())
310 }
311}
312
313fn resolve_groups<'a>(
318 assembled_groups: &'a [AssembledGroup],
319 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
320 depth: usize,
321) -> Vec<AhbGroupNode<'a>> {
322 let mut result = Vec::new();
323
324 for assembled_group in assembled_groups {
325 let rules = group_rules.get(&assembled_group.group_id);
326
327 for instance in &assembled_group.repetitions {
328 let node = resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
329 result.push(node);
330 }
331 }
332
333 result
334}
335
336fn strip_n_groups(path: &str, n: usize) -> &str {
340 let mut rest = path;
341 for _ in 0..n {
342 match rest.find('/') {
343 Some(idx) => rest = &rest[idx + 1..],
344 None => return rest,
345 }
346 }
347 rest
348}
349
350fn resolve_group_instance<'a>(
359 group_id: &'a str,
360 instance: &'a AssembledGroupInstance,
361 rules: Option<&Vec<&'a AhbFieldRule>>,
362 depth: usize,
363) -> AhbGroupNode<'a> {
364 let strip_count = depth + 1;
366
367 let variant_numbers: std::collections::HashSet<&str> =
372 if !instance.variant_mig_numbers.is_empty() {
373 instance
374 .variant_mig_numbers
375 .iter()
376 .map(|s| s.as_str())
377 .collect()
378 } else {
379 collect_instance_mig_numbers(instance)
380 };
381
382 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
383 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
384 let mut ahb_status: Option<&'a str> = None;
385
386 if let Some(rules) = rules {
387 for rule in rules {
388 if let Some(ref rule_mig) = rule.mig_number {
391 if !variant_numbers.contains(rule_mig.as_str()) {
392 continue;
393 }
394 }
395
396 let stripped = strip_n_groups(&rule.segment_path, strip_count);
398
399 if let Some(child_group_id) = extract_child_group(stripped) {
400 child_group_rules
401 .entry(child_group_id.to_owned())
402 .or_default()
403 .push(rule);
404 } else {
405 direct_rules.push(rule);
406 }
407
408 if ahb_status.is_none() {
410 if let Some(ref status) = rule.parent_group_ahb_status {
411 ahb_status = Some(status.as_str());
412 }
413 }
414 }
415 }
416
417 let fields = resolve_fields(&direct_rules, &instance.segments);
419
420 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
422
423 AhbGroupNode {
424 group_id,
425 ahb_status,
426 fields,
427 children,
428 }
429}
430
431fn collect_instance_mig_numbers(
433 instance: &AssembledGroupInstance,
434) -> std::collections::HashSet<&str> {
435 let mut numbers = std::collections::HashSet::new();
436 for seg in &instance.segments {
437 if let Some(ref num) = seg.mig_number {
438 numbers.insert(num.as_str());
439 }
440 }
441 for child_group in &instance.child_groups {
442 for child_instance in &child_group.repetitions {
443 numbers.extend(collect_instance_mig_numbers(child_instance));
444 }
445 }
446 numbers
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
453 use mig_assembly::assembler::{
454 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
455 };
456 use std::collections::{BTreeMap, HashMap};
457
458 fn empty_workflow() -> AhbWorkflow {
459 AhbWorkflow {
460 pruefidentifikator: "11001".to_string(),
461 description: String::new(),
462 communication_direction: None,
463 fields: vec![],
464 ub_definitions: HashMap::new(),
465 }
466 }
467
468 fn empty_tree() -> AssembledTree {
469 AssembledTree {
470 segments: vec![],
471 groups: vec![],
472 post_group_start: 0,
473 inter_group_segments: BTreeMap::new(),
474 }
475 }
476
477 fn make_segment(
478 tag: &str,
479 elements: Vec<Vec<&str>>,
480 mig_number: Option<&str>,
481 ) -> AssembledSegment {
482 AssembledSegment {
483 tag: tag.to_string(),
484 elements: elements
485 .into_iter()
486 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
487 .collect(),
488 mig_number: mig_number.map(|s| s.to_string()),
489 segment_number: None,
490 }
491 }
492
493 fn make_rule(
494 segment_path: &str,
495 name: &str,
496 ahb_status: &str,
497 mig_number: Option<&str>,
498 element_index: Option<usize>,
499 component_index: Option<usize>,
500 ) -> AhbFieldRule {
501 AhbFieldRule {
502 segment_path: segment_path.to_string(),
503 name: name.to_string(),
504 ahb_status: ahb_status.to_string(),
505 codes: vec![],
506 parent_group_ahb_status: None,
507 segment_ahb_status: None,
508 element_index,
509 component_index,
510 mig_number: mig_number.map(|s| s.to_string()),
511 }
512 }
513
514 #[test]
515 fn test_empty_workflow_empty_tree() {
516 let workflow = empty_workflow();
517 let tree = empty_tree();
518 let result = build_validated_tree(&workflow, &tree);
519
520 assert_eq!(result.pruefidentifikator, "11001");
521 assert!(result.root_fields.is_empty());
522 assert!(result.groups.is_empty());
523 }
524
525 #[test]
526 fn test_root_field_matches_root_segment() {
527 let mut workflow = empty_workflow();
528 workflow.fields.push(make_rule(
529 "BGM/C002/1001",
530 "Nachrichtentyp",
531 "X",
532 Some("0001"),
533 Some(0),
534 Some(0),
535 ));
536
537 let tree = AssembledTree {
538 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
539 groups: vec![],
540 post_group_start: 1,
541 inter_group_segments: BTreeMap::new(),
542 };
543
544 let result = build_validated_tree(&workflow, &tree);
545
546 assert_eq!(result.root_fields.len(), 1);
547 let node = &result.root_fields[0];
548 assert_eq!(node.value, Some("E01"));
549 assert!(node.segment_elements.is_some());
550 assert_eq!(node.rule.name, "Nachrichtentyp");
551 }
552
553 #[test]
554 fn test_sg4_dtm_mig_number_matching() {
555 let mut workflow = empty_workflow();
558
559 workflow.fields.push(make_rule(
561 "SG4/DTM/C507/2380",
562 "Eingangsdatum",
563 "X",
564 Some("0082"),
565 Some(1), Some(0),
567 ));
568
569 workflow.fields.push(make_rule(
571 "SG4/DTM/C507/2380",
572 "Dokumentendatum",
573 "X",
574 Some("0083"),
575 Some(1),
576 Some(0),
577 ));
578
579 let tree = AssembledTree {
580 segments: vec![],
581 groups: vec![AssembledGroup {
582 group_id: "SG4".to_string(),
583 repetitions: vec![AssembledGroupInstance {
584 segments: vec![
585 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
586 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
587 ],
588 child_groups: vec![],
589 entry_mig_number: None,
590 variant_mig_numbers: vec![],
591 skipped_segments: vec![],
592 skipped_positions: Vec::new(),
593 }],
594 }],
595 post_group_start: 0,
596 inter_group_segments: BTreeMap::new(),
597 };
598
599 let result = build_validated_tree(&workflow, &tree);
600
601 assert_eq!(result.groups.len(), 1);
602 let sg4 = &result.groups[0];
603 assert_eq!(sg4.group_id, "SG4");
604 assert_eq!(sg4.fields.len(), 2);
605
606 let eingangsdatum = &sg4.fields[0];
608 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
609 assert_eq!(eingangsdatum.value, Some("20260101"));
610
611 let dokumentendatum = &sg4.fields[1];
613 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
614 assert_eq!(dokumentendatum.value, Some("20260401"));
615 }
616
617 #[test]
618 fn test_rule_filtered_to_correct_variant() {
619 let mut workflow = empty_workflow();
623 workflow.fields.push(make_rule(
624 "SG4/RFF/C506/1154",
625 "Referenz",
626 "X",
627 Some("0099"),
628 Some(0),
629 Some(1),
630 ));
631
632 let tree = AssembledTree {
633 segments: vec![],
634 groups: vec![AssembledGroup {
635 group_id: "SG4".to_string(),
636 repetitions: vec![AssembledGroupInstance {
637 segments: vec![], child_groups: vec![],
639 entry_mig_number: None,
640 variant_mig_numbers: vec![],
641 skipped_segments: vec![],
642 skipped_positions: Vec::new(),
643 }],
644 }],
645 post_group_start: 0,
646 inter_group_segments: BTreeMap::new(),
647 };
648
649 let result = build_validated_tree(&workflow, &tree);
650 assert_eq!(result.groups.len(), 1);
651 assert_eq!(result.groups[0].fields.len(), 0);
653 }
654
655 #[test]
656 fn test_missing_segment_within_correct_variant() {
657 let mut workflow = empty_workflow();
661 workflow.fields.push(make_rule(
663 "SG4/SEQ/1229",
664 "Qualifier",
665 "X",
666 Some("0098"),
667 Some(0),
668 Some(0),
669 ));
670 workflow.fields.push(make_rule(
672 "SG4/RFF/C506/1154",
673 "Referenz",
674 "X",
675 Some("0099"),
676 Some(0),
677 Some(1),
678 ));
679
680 let tree = AssembledTree {
681 segments: vec![],
682 groups: vec![AssembledGroup {
683 group_id: "SG4".to_string(),
684 repetitions: vec![AssembledGroupInstance {
685 segments: vec![
686 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
688 ],
689 child_groups: vec![],
690 entry_mig_number: Some("0098".to_string()),
691 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
693 skipped_segments: vec![],
694 skipped_positions: Vec::new(),
695 }],
696 }],
697 post_group_start: 0,
698 inter_group_segments: BTreeMap::new(),
699 };
700
701 let result = build_validated_tree(&workflow, &tree);
702 assert_eq!(result.groups.len(), 1);
703 assert_eq!(result.groups[0].fields.len(), 2);
705 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
706 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
707 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
709 assert_eq!(result.groups[0].fields[1].value, None);
710 }
711
712 #[test]
713 fn test_missing_group_variant_populates_unmatched_rules() {
714 let mut workflow = empty_workflow();
718
719 workflow.fields.push(make_rule(
721 "SG2/NAD/3035",
722 "MP-ID Absender Qualifier",
723 "X",
724 Some("0010"),
725 Some(0),
726 Some(0),
727 ));
728 workflow.fields.push(make_rule(
730 "SG2/NAD/C082/3039",
731 "MP-ID Absender",
732 "X",
733 Some("0010"),
734 Some(1),
735 Some(0),
736 ));
737 workflow.fields.push(make_rule(
739 "SG2/NAD/3035",
740 "MP-ID Empfänger Qualifier",
741 "X",
742 Some("0011"),
743 Some(0),
744 Some(0),
745 ));
746 workflow.fields.push(make_rule(
748 "SG2/NAD/C082/3039",
749 "MP-ID Empfänger",
750 "X",
751 Some("0011"),
752 Some(1),
753 Some(0),
754 ));
755
756 let tree = AssembledTree {
758 segments: vec![],
759 groups: vec![AssembledGroup {
760 group_id: "SG2".to_string(),
761 repetitions: vec![AssembledGroupInstance {
762 segments: vec![make_segment(
763 "NAD",
764 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
765 Some("0011"),
766 )],
767 child_groups: vec![],
768 entry_mig_number: Some("0011".to_string()),
769 variant_mig_numbers: vec!["0011".to_string()],
770 skipped_segments: vec![],
771 skipped_positions: Vec::new(),
772 }],
773 }],
774 post_group_start: 0,
775 inter_group_segments: BTreeMap::new(),
776 };
777
778 let result = build_validated_tree(&workflow, &tree);
779
780 assert_eq!(result.groups.len(), 1);
782 assert_eq!(result.groups[0].fields.len(), 2);
783 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
784
785 assert_eq!(
787 result.unmatched_rules.len(),
788 2,
789 "Expected 2 unmatched rules (NAD+MS), got {}",
790 result.unmatched_rules.len()
791 );
792 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
793 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
794 }
795
796 #[test]
797 fn test_entirely_absent_group_populates_unmatched_rules() {
798 let mut workflow = empty_workflow();
800 workflow.fields.push(make_rule(
801 "SG2/NAD/3035",
802 "MP-ID Absender Qualifier",
803 "X",
804 Some("0010"),
805 Some(0),
806 Some(0),
807 ));
808
809 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
812
813 assert!(result.groups.is_empty());
815
816 assert_eq!(
818 result.unmatched_rules.len(),
819 1,
820 "Expected 1 unmatched rule, got {}",
821 result.unmatched_rules.len()
822 );
823 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
824 }
825
826 #[test]
827 fn test_fallback_to_tag_when_no_mig_number() {
828 let mut workflow = empty_workflow();
829 workflow.fields.push(make_rule(
830 "BGM/C002/1001",
831 "Nachrichtentyp",
832 "X",
833 None, Some(0),
835 Some(0),
836 ));
837
838 let tree = AssembledTree {
839 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
840 groups: vec![],
841 post_group_start: 1,
842 inter_group_segments: BTreeMap::new(),
843 };
844
845 let result = build_validated_tree(&workflow, &tree);
846 assert_eq!(result.root_fields.len(), 1);
847 assert_eq!(result.root_fields[0].value, Some("E01"));
848 }
849
850 #[test]
851 fn test_nested_child_groups() {
852 let mut workflow = empty_workflow();
853 workflow.fields.push(make_rule(
855 "SG4/SG5/LOC/C517/3225",
856 "Marktlokations-ID",
857 "X",
858 Some("0050"),
859 Some(0),
860 Some(0),
861 ));
862
863 let tree = AssembledTree {
864 segments: vec![],
865 groups: vec![AssembledGroup {
866 group_id: "SG4".to_string(),
867 repetitions: vec![AssembledGroupInstance {
868 segments: vec![],
869 entry_mig_number: None,
870 child_groups: vec![AssembledGroup {
871 group_id: "SG5".to_string(),
872 repetitions: vec![AssembledGroupInstance {
873 segments: vec![make_segment(
874 "LOC",
875 vec![vec!["DE00012345678"]],
876 Some("0050"),
877 )],
878 child_groups: vec![],
879 entry_mig_number: None,
880 variant_mig_numbers: vec![],
881 skipped_segments: vec![],
882 skipped_positions: Vec::new(),
883 }],
884 }],
885 variant_mig_numbers: vec![],
886 skipped_segments: vec![],
887 skipped_positions: Vec::new(),
888 }],
889 }],
890 post_group_start: 0,
891 inter_group_segments: BTreeMap::new(),
892 };
893
894 let result = build_validated_tree(&workflow, &tree);
895 assert_eq!(result.groups.len(), 1);
896 let sg4 = &result.groups[0];
897 assert_eq!(sg4.children.len(), 1);
898
899 let sg5 = &sg4.children[0];
900 assert_eq!(sg5.group_id, "SG5");
901 assert_eq!(sg5.fields.len(), 1);
902 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
903 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
904 }
905
906 #[test]
907 fn test_qualifier_aware_segment_matching_same_tag_variants() {
908 let mut workflow = empty_workflow();
922
923 let mut z33 = make_rule(
925 "SG10/STS/C601/9015",
926 "Statuskategorie Z33",
927 "X",
928 Some("00035"),
929 Some(0),
930 Some(0),
931 );
932 z33.codes = vec![super::super::validate::AhbCodeRule {
933 value: "Z33".into(),
934 description: String::new(),
935 ahb_status: "X".into(),
936 }];
937 workflow.fields.push(z33);
938
939 let mut z32 = make_rule(
941 "SG10/STS/C601/9015",
942 "Statuskategorie Z32",
943 "X",
944 Some("00036"),
945 Some(0),
946 Some(0),
947 );
948 z32.codes = vec![super::super::validate::AhbCodeRule {
949 value: "Z32".into(),
950 description: String::new(),
951 ahb_status: "X".into(),
952 }];
953 workflow.fields.push(z32);
954
955 let mut z40 = make_rule(
957 "SG10/STS/C601/9015",
958 "Statuskategorie Z40",
959 "X",
960 Some("00038"),
961 Some(0),
962 Some(0),
963 );
964 z40.codes = vec![super::super::validate::AhbCodeRule {
965 value: "Z40".into(),
966 description: String::new(),
967 ahb_status: "X".into(),
968 }];
969 workflow.fields.push(z40);
970
971 let tree = AssembledTree {
974 segments: vec![],
975 groups: vec![AssembledGroup {
976 group_id: "SG10".to_string(),
977 repetitions: vec![AssembledGroupInstance {
978 segments: vec![
979 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
980 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
981 ],
982 child_groups: vec![],
983 entry_mig_number: None,
984 variant_mig_numbers: vec![
985 "00035".into(),
986 "00036".into(),
987 "00037".into(),
988 "00038".into(),
989 ],
990 skipped_segments: vec![],
991 skipped_positions: Vec::new(),
992 }],
993 }],
994 post_group_start: 0,
995 inter_group_segments: BTreeMap::new(),
996 };
997
998 let result = build_validated_tree(&workflow, &tree);
999 assert_eq!(result.groups.len(), 1);
1000 let sg10 = &result.groups[0];
1001 assert_eq!(
1002 sg10.fields.len(),
1003 3,
1004 "all three rules should pass the variant filter"
1005 );
1006
1007 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1008
1009 assert_eq!(
1011 by_name("Statuskategorie Z33").value,
1012 None,
1013 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1014 );
1015
1016 assert_eq!(
1019 by_name("Statuskategorie Z32").value,
1020 Some("Z32"),
1021 "rule for Z32 must attach to the STS segment with qualifier Z32"
1022 );
1023
1024 assert_eq!(
1026 by_name("Statuskategorie Z40").value,
1027 Some("Z40"),
1028 "rule for Z40 must attach to the STS segment with qualifier Z40"
1029 );
1030 }
1031}