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 element_index,
490 component_index,
491 mig_number: mig_number.map(|s| s.to_string()),
492 }
493 }
494
495 #[test]
496 fn test_empty_workflow_empty_tree() {
497 let workflow = empty_workflow();
498 let tree = empty_tree();
499 let result = build_validated_tree(&workflow, &tree);
500
501 assert_eq!(result.pruefidentifikator, "11001");
502 assert!(result.root_fields.is_empty());
503 assert!(result.groups.is_empty());
504 }
505
506 #[test]
507 fn test_root_field_matches_root_segment() {
508 let mut workflow = empty_workflow();
509 workflow.fields.push(make_rule(
510 "BGM/C002/1001",
511 "Nachrichtentyp",
512 "X",
513 Some("0001"),
514 Some(0),
515 Some(0),
516 ));
517
518 let tree = AssembledTree {
519 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
520 groups: vec![],
521 post_group_start: 1,
522 inter_group_segments: BTreeMap::new(),
523 };
524
525 let result = build_validated_tree(&workflow, &tree);
526
527 assert_eq!(result.root_fields.len(), 1);
528 let node = &result.root_fields[0];
529 assert_eq!(node.value, Some("E01"));
530 assert!(node.segment_elements.is_some());
531 assert_eq!(node.rule.name, "Nachrichtentyp");
532 }
533
534 #[test]
535 fn test_sg4_dtm_mig_number_matching() {
536 let mut workflow = empty_workflow();
539
540 workflow.fields.push(make_rule(
542 "SG4/DTM/C507/2380",
543 "Eingangsdatum",
544 "X",
545 Some("0082"),
546 Some(1), Some(0),
548 ));
549
550 workflow.fields.push(make_rule(
552 "SG4/DTM/C507/2380",
553 "Dokumentendatum",
554 "X",
555 Some("0083"),
556 Some(1),
557 Some(0),
558 ));
559
560 let tree = AssembledTree {
561 segments: vec![],
562 groups: vec![AssembledGroup {
563 group_id: "SG4".to_string(),
564 repetitions: vec![AssembledGroupInstance {
565 segments: vec![
566 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
567 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
568 ],
569 child_groups: vec![],
570 entry_mig_number: None,
571 variant_mig_numbers: vec![],
572 skipped_segments: vec![],
573 }],
574 }],
575 post_group_start: 0,
576 inter_group_segments: BTreeMap::new(),
577 };
578
579 let result = build_validated_tree(&workflow, &tree);
580
581 assert_eq!(result.groups.len(), 1);
582 let sg4 = &result.groups[0];
583 assert_eq!(sg4.group_id, "SG4");
584 assert_eq!(sg4.fields.len(), 2);
585
586 let eingangsdatum = &sg4.fields[0];
588 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
589 assert_eq!(eingangsdatum.value, Some("20260101"));
590
591 let dokumentendatum = &sg4.fields[1];
593 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
594 assert_eq!(dokumentendatum.value, Some("20260401"));
595 }
596
597 #[test]
598 fn test_rule_filtered_to_correct_variant() {
599 let mut workflow = empty_workflow();
603 workflow.fields.push(make_rule(
604 "SG4/RFF/C506/1154",
605 "Referenz",
606 "X",
607 Some("0099"),
608 Some(0),
609 Some(1),
610 ));
611
612 let tree = AssembledTree {
613 segments: vec![],
614 groups: vec![AssembledGroup {
615 group_id: "SG4".to_string(),
616 repetitions: vec![AssembledGroupInstance {
617 segments: vec![], child_groups: vec![],
619 entry_mig_number: None,
620 variant_mig_numbers: vec![],
621 skipped_segments: vec![],
622 }],
623 }],
624 post_group_start: 0,
625 inter_group_segments: BTreeMap::new(),
626 };
627
628 let result = build_validated_tree(&workflow, &tree);
629 assert_eq!(result.groups.len(), 1);
630 assert_eq!(result.groups[0].fields.len(), 0);
632 }
633
634 #[test]
635 fn test_missing_segment_within_correct_variant() {
636 let mut workflow = empty_workflow();
640 workflow.fields.push(make_rule(
642 "SG4/SEQ/1229",
643 "Qualifier",
644 "X",
645 Some("0098"),
646 Some(0),
647 Some(0),
648 ));
649 workflow.fields.push(make_rule(
651 "SG4/RFF/C506/1154",
652 "Referenz",
653 "X",
654 Some("0099"),
655 Some(0),
656 Some(1),
657 ));
658
659 let tree = AssembledTree {
660 segments: vec![],
661 groups: vec![AssembledGroup {
662 group_id: "SG4".to_string(),
663 repetitions: vec![AssembledGroupInstance {
664 segments: vec![
665 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
667 ],
668 child_groups: vec![],
669 entry_mig_number: Some("0098".to_string()),
670 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
672 skipped_segments: vec![],
673 }],
674 }],
675 post_group_start: 0,
676 inter_group_segments: BTreeMap::new(),
677 };
678
679 let result = build_validated_tree(&workflow, &tree);
680 assert_eq!(result.groups.len(), 1);
681 assert_eq!(result.groups[0].fields.len(), 2);
683 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
684 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
685 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
687 assert_eq!(result.groups[0].fields[1].value, None);
688 }
689
690 #[test]
691 fn test_missing_group_variant_populates_unmatched_rules() {
692 let mut workflow = empty_workflow();
696
697 workflow.fields.push(make_rule(
699 "SG2/NAD/3035",
700 "MP-ID Absender Qualifier",
701 "X",
702 Some("0010"),
703 Some(0),
704 Some(0),
705 ));
706 workflow.fields.push(make_rule(
708 "SG2/NAD/C082/3039",
709 "MP-ID Absender",
710 "X",
711 Some("0010"),
712 Some(1),
713 Some(0),
714 ));
715 workflow.fields.push(make_rule(
717 "SG2/NAD/3035",
718 "MP-ID Empfänger Qualifier",
719 "X",
720 Some("0011"),
721 Some(0),
722 Some(0),
723 ));
724 workflow.fields.push(make_rule(
726 "SG2/NAD/C082/3039",
727 "MP-ID Empfänger",
728 "X",
729 Some("0011"),
730 Some(1),
731 Some(0),
732 ));
733
734 let tree = AssembledTree {
736 segments: vec![],
737 groups: vec![AssembledGroup {
738 group_id: "SG2".to_string(),
739 repetitions: vec![AssembledGroupInstance {
740 segments: vec![make_segment(
741 "NAD",
742 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
743 Some("0011"),
744 )],
745 child_groups: vec![],
746 entry_mig_number: Some("0011".to_string()),
747 variant_mig_numbers: vec!["0011".to_string()],
748 skipped_segments: vec![],
749 }],
750 }],
751 post_group_start: 0,
752 inter_group_segments: BTreeMap::new(),
753 };
754
755 let result = build_validated_tree(&workflow, &tree);
756
757 assert_eq!(result.groups.len(), 1);
759 assert_eq!(result.groups[0].fields.len(), 2);
760 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
761
762 assert_eq!(
764 result.unmatched_rules.len(),
765 2,
766 "Expected 2 unmatched rules (NAD+MS), got {}",
767 result.unmatched_rules.len()
768 );
769 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
770 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
771 }
772
773 #[test]
774 fn test_entirely_absent_group_populates_unmatched_rules() {
775 let mut workflow = empty_workflow();
777 workflow.fields.push(make_rule(
778 "SG2/NAD/3035",
779 "MP-ID Absender Qualifier",
780 "X",
781 Some("0010"),
782 Some(0),
783 Some(0),
784 ));
785
786 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
789
790 assert!(result.groups.is_empty());
792
793 assert_eq!(
795 result.unmatched_rules.len(),
796 1,
797 "Expected 1 unmatched rule, got {}",
798 result.unmatched_rules.len()
799 );
800 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
801 }
802
803 #[test]
804 fn test_fallback_to_tag_when_no_mig_number() {
805 let mut workflow = empty_workflow();
806 workflow.fields.push(make_rule(
807 "BGM/C002/1001",
808 "Nachrichtentyp",
809 "X",
810 None, Some(0),
812 Some(0),
813 ));
814
815 let tree = AssembledTree {
816 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
817 groups: vec![],
818 post_group_start: 1,
819 inter_group_segments: BTreeMap::new(),
820 };
821
822 let result = build_validated_tree(&workflow, &tree);
823 assert_eq!(result.root_fields.len(), 1);
824 assert_eq!(result.root_fields[0].value, Some("E01"));
825 }
826
827 #[test]
828 fn test_nested_child_groups() {
829 let mut workflow = empty_workflow();
830 workflow.fields.push(make_rule(
832 "SG4/SG5/LOC/C517/3225",
833 "Marktlokations-ID",
834 "X",
835 Some("0050"),
836 Some(0),
837 Some(0),
838 ));
839
840 let tree = AssembledTree {
841 segments: vec![],
842 groups: vec![AssembledGroup {
843 group_id: "SG4".to_string(),
844 repetitions: vec![AssembledGroupInstance {
845 segments: vec![],
846 entry_mig_number: None,
847 child_groups: vec![AssembledGroup {
848 group_id: "SG5".to_string(),
849 repetitions: vec![AssembledGroupInstance {
850 segments: vec![make_segment(
851 "LOC",
852 vec![vec!["DE00012345678"]],
853 Some("0050"),
854 )],
855 child_groups: vec![],
856 entry_mig_number: None,
857 variant_mig_numbers: vec![],
858 skipped_segments: vec![],
859 }],
860 }],
861 variant_mig_numbers: vec![],
862 skipped_segments: vec![],
863 }],
864 }],
865 post_group_start: 0,
866 inter_group_segments: BTreeMap::new(),
867 };
868
869 let result = build_validated_tree(&workflow, &tree);
870 assert_eq!(result.groups.len(), 1);
871 let sg4 = &result.groups[0];
872 assert_eq!(sg4.children.len(), 1);
873
874 let sg5 = &sg4.children[0];
875 assert_eq!(sg5.group_id, "SG5");
876 assert_eq!(sg5.fields.len(), 1);
877 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
878 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
879 }
880
881 #[test]
882 fn test_qualifier_aware_segment_matching_same_tag_variants() {
883 let mut workflow = empty_workflow();
897
898 let mut z33 = make_rule(
900 "SG10/STS/C601/9015",
901 "Statuskategorie Z33",
902 "X",
903 Some("00035"),
904 Some(0),
905 Some(0),
906 );
907 z33.codes = vec![super::super::validate::AhbCodeRule {
908 value: "Z33".into(),
909 description: String::new(),
910 ahb_status: "X".into(),
911 }];
912 workflow.fields.push(z33);
913
914 let mut z32 = make_rule(
916 "SG10/STS/C601/9015",
917 "Statuskategorie Z32",
918 "X",
919 Some("00036"),
920 Some(0),
921 Some(0),
922 );
923 z32.codes = vec![super::super::validate::AhbCodeRule {
924 value: "Z32".into(),
925 description: String::new(),
926 ahb_status: "X".into(),
927 }];
928 workflow.fields.push(z32);
929
930 let mut z40 = make_rule(
932 "SG10/STS/C601/9015",
933 "Statuskategorie Z40",
934 "X",
935 Some("00038"),
936 Some(0),
937 Some(0),
938 );
939 z40.codes = vec![super::super::validate::AhbCodeRule {
940 value: "Z40".into(),
941 description: String::new(),
942 ahb_status: "X".into(),
943 }];
944 workflow.fields.push(z40);
945
946 let tree = AssembledTree {
949 segments: vec![],
950 groups: vec![AssembledGroup {
951 group_id: "SG10".to_string(),
952 repetitions: vec![AssembledGroupInstance {
953 segments: vec![
954 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
955 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
956 ],
957 child_groups: vec![],
958 entry_mig_number: None,
959 variant_mig_numbers: vec![
960 "00035".into(),
961 "00036".into(),
962 "00037".into(),
963 "00038".into(),
964 ],
965 skipped_segments: vec![],
966 }],
967 }],
968 post_group_start: 0,
969 inter_group_segments: BTreeMap::new(),
970 };
971
972 let result = build_validated_tree(&workflow, &tree);
973 assert_eq!(result.groups.len(), 1);
974 let sg10 = &result.groups[0];
975 assert_eq!(sg10.fields.len(), 3, "all three rules should pass the variant filter");
976
977 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
978
979 assert_eq!(
981 by_name("Statuskategorie Z33").value,
982 None,
983 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
984 );
985
986 assert_eq!(
989 by_name("Statuskategorie Z32").value,
990 Some("Z32"),
991 "rule for Z32 must attach to the STS segment with qualifier Z32"
992 );
993
994 assert_eq!(
996 by_name("Statuskategorie Z40").value,
997 Some("Z40"),
998 "rule for Z40 must attach to the STS segment with qualifier Z40"
999 );
1000 }
1001}