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