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> =
97 std::collections::HashSet::new();
98 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
99 for seg in &tree.segments {
101 if let Some(ref num) = seg.mig_number {
102 matched_mig_numbers.insert(num.as_str());
103 }
104 }
105
106 let unmatched_rules: Vec<&AhbFieldRule> = workflow
107 .fields
108 .iter()
109 .filter(|rule| {
110 rule.mig_number
111 .as_ref()
112 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
113 })
114 .collect();
115
116 ValidatedTree {
117 pruefidentifikator: &workflow.pruefidentifikator,
118 ub_definitions: &workflow.ub_definitions,
119 root_fields,
120 groups,
121 unmatched_rules,
122 }
123}
124
125fn collect_matched_mig_numbers_from_groups<'a>(
131 groups: &[AhbGroupNode<'a>],
132 out: &mut std::collections::HashSet<&'a str>,
133) {
134 for group in groups {
135 for node in &group.fields {
136 if let Some(ref num) = node.rule.mig_number {
137 out.insert(num.as_str());
138 }
139 }
140 collect_matched_mig_numbers_from_groups(&group.children, out);
141 }
142}
143
144fn extract_top_group(segment_path: &str) -> Option<&str> {
150 let first = segment_path.split('/').next()?;
151 if first.starts_with("SG") {
152 Some(first)
153 } else {
154 None
155 }
156}
157
158fn extract_child_group(stripped_path: &str) -> Option<&str> {
163 let first = stripped_path.split('/').next()?;
164 if first.starts_with("SG") {
165 Some(first)
166 } else {
167 None
168 }
169}
170
171fn resolve_fields<'a>(
177 rules: &[&'a AhbFieldRule],
178 segments: &'a [AssembledSegment],
179) -> Vec<AhbNode<'a>> {
180 rules
181 .iter()
182 .map(|rule| {
183 let matched_segment = find_segment(rule, segments);
184 let (value, elements) = match matched_segment {
185 Some(seg) => {
186 let val = extract_value(seg, rule);
187 (val, Some(seg.elements.as_slice()))
188 }
189 None => (None, None),
190 };
191 AhbNode {
192 rule,
193 value,
194 segment_elements: elements,
195 }
196 })
197 .collect()
198}
199
200fn find_segment<'a>(
212 rule: &AhbFieldRule,
213 segments: &'a [AssembledSegment],
214) -> Option<&'a AssembledSegment> {
215 if let Some(ref mig_num) = rule.mig_number {
216 if let Some(seg) = segments
218 .iter()
219 .find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
220 {
221 return Some(seg);
222 }
223
224 let tag = extract_segment_tag(&rule.segment_path)?;
226 return segments
227 .iter()
228 .find(|s| s.tag == tag && s.mig_number.as_ref().map_or(true, |m| m == mig_num));
229 }
230
231 let tag = extract_segment_tag(&rule.segment_path)?;
233 segments.iter().find(|s| s.tag == tag)
234}
235
236fn extract_segment_tag(segment_path: &str) -> Option<&str> {
241 segment_path
242 .split('/')
243 .find(|part| !part.starts_with("SG"))
244}
245
246fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
248 let elem_idx = rule.element_index.unwrap_or(0);
249 let comp_idx = rule.component_index.unwrap_or(0);
250
251 let element = segment.elements.get(elem_idx)?;
252 let component = element.get(comp_idx)?;
253
254 if component.is_empty() {
255 None
256 } else {
257 Some(component.as_str())
258 }
259}
260
261fn resolve_groups<'a>(
266 assembled_groups: &'a [AssembledGroup],
267 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
268 depth: usize,
269) -> Vec<AhbGroupNode<'a>> {
270 let mut result = Vec::new();
271
272 for assembled_group in assembled_groups {
273 let rules = group_rules.get(&assembled_group.group_id);
274
275 for instance in &assembled_group.repetitions {
276 let node =
277 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
278 result.push(node);
279 }
280 }
281
282 result
283}
284
285fn strip_n_groups(path: &str, n: usize) -> &str {
289 let mut rest = path;
290 for _ in 0..n {
291 match rest.find('/') {
292 Some(idx) => rest = &rest[idx + 1..],
293 None => return rest,
294 }
295 }
296 rest
297}
298
299fn resolve_group_instance<'a>(
308 group_id: &'a str,
309 instance: &'a AssembledGroupInstance,
310 rules: Option<&Vec<&'a AhbFieldRule>>,
311 depth: usize,
312) -> AhbGroupNode<'a> {
313 let strip_count = depth + 1;
315
316 let variant_numbers: std::collections::HashSet<&str> = if !instance.variant_mig_numbers.is_empty() {
321 instance.variant_mig_numbers.iter().map(|s| s.as_str()).collect()
322 } else {
323 collect_instance_mig_numbers(instance)
324 };
325
326 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
327 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
328 let mut ahb_status: Option<&'a str> = None;
329
330 if let Some(rules) = rules {
331 for rule in rules {
332 if let Some(ref rule_mig) = rule.mig_number {
335 if !variant_numbers.contains(rule_mig.as_str()) {
336 continue;
337 }
338 }
339
340 let stripped = strip_n_groups(&rule.segment_path, strip_count);
342
343 if let Some(child_group_id) = extract_child_group(stripped) {
344 child_group_rules
345 .entry(child_group_id.to_owned())
346 .or_default()
347 .push(rule);
348 } else {
349 direct_rules.push(rule);
350 }
351
352 if ahb_status.is_none() {
354 if let Some(ref status) = rule.parent_group_ahb_status {
355 ahb_status = Some(status.as_str());
356 }
357 }
358 }
359 }
360
361 let fields = resolve_fields(&direct_rules, &instance.segments);
363
364 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
366
367 AhbGroupNode {
368 group_id,
369 ahb_status,
370 fields,
371 children,
372 }
373}
374
375fn collect_instance_mig_numbers(instance: &AssembledGroupInstance) -> std::collections::HashSet<&str> {
377 let mut numbers = std::collections::HashSet::new();
378 for seg in &instance.segments {
379 if let Some(ref num) = seg.mig_number {
380 numbers.insert(num.as_str());
381 }
382 }
383 for child_group in &instance.child_groups {
384 for child_instance in &child_group.repetitions {
385 numbers.extend(collect_instance_mig_numbers(child_instance));
386 }
387 }
388 numbers
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
395 use mig_assembly::assembler::{
396 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
397 };
398 use std::collections::{BTreeMap, HashMap};
399
400 fn empty_workflow() -> AhbWorkflow {
401 AhbWorkflow {
402 pruefidentifikator: "11001".to_string(),
403 description: String::new(),
404 communication_direction: None,
405 fields: vec![],
406 ub_definitions: HashMap::new(),
407 }
408 }
409
410 fn empty_tree() -> AssembledTree {
411 AssembledTree {
412 segments: vec![],
413 groups: vec![],
414 post_group_start: 0,
415 inter_group_segments: BTreeMap::new(),
416 }
417 }
418
419 fn make_segment(tag: &str, elements: Vec<Vec<&str>>, mig_number: Option<&str>) -> AssembledSegment {
420 AssembledSegment {
421 tag: tag.to_string(),
422 elements: elements
423 .into_iter()
424 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
425 .collect(),
426 mig_number: mig_number.map(|s| s.to_string()),
427 }
428 }
429
430 fn make_rule(
431 segment_path: &str,
432 name: &str,
433 ahb_status: &str,
434 mig_number: Option<&str>,
435 element_index: Option<usize>,
436 component_index: Option<usize>,
437 ) -> AhbFieldRule {
438 AhbFieldRule {
439 segment_path: segment_path.to_string(),
440 name: name.to_string(),
441 ahb_status: ahb_status.to_string(),
442 codes: vec![],
443 parent_group_ahb_status: None,
444 element_index,
445 component_index,
446 mig_number: mig_number.map(|s| s.to_string()),
447 }
448 }
449
450 #[test]
451 fn test_empty_workflow_empty_tree() {
452 let workflow = empty_workflow();
453 let tree = empty_tree();
454 let result = build_validated_tree(&workflow, &tree);
455
456 assert_eq!(result.pruefidentifikator, "11001");
457 assert!(result.root_fields.is_empty());
458 assert!(result.groups.is_empty());
459 }
460
461 #[test]
462 fn test_root_field_matches_root_segment() {
463 let mut workflow = empty_workflow();
464 workflow.fields.push(make_rule(
465 "BGM/C002/1001",
466 "Nachrichtentyp",
467 "X",
468 Some("0001"),
469 Some(0),
470 Some(0),
471 ));
472
473 let tree = AssembledTree {
474 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
475 groups: vec![],
476 post_group_start: 1,
477 inter_group_segments: BTreeMap::new(),
478 };
479
480 let result = build_validated_tree(&workflow, &tree);
481
482 assert_eq!(result.root_fields.len(), 1);
483 let node = &result.root_fields[0];
484 assert_eq!(node.value, Some("E01"));
485 assert!(node.segment_elements.is_some());
486 assert_eq!(node.rule.name, "Nachrichtentyp");
487 }
488
489 #[test]
490 fn test_sg4_dtm_mig_number_matching() {
491 let mut workflow = empty_workflow();
494
495 workflow.fields.push(make_rule(
497 "SG4/DTM/C507/2380",
498 "Eingangsdatum",
499 "X",
500 Some("0082"),
501 Some(1), Some(0),
503 ));
504
505 workflow.fields.push(make_rule(
507 "SG4/DTM/C507/2380",
508 "Dokumentendatum",
509 "X",
510 Some("0083"),
511 Some(1),
512 Some(0),
513 ));
514
515 let tree = AssembledTree {
516 segments: vec![],
517 groups: vec![AssembledGroup {
518 group_id: "SG4".to_string(),
519 repetitions: vec![AssembledGroupInstance {
520 segments: vec![
521 make_segment(
522 "DTM",
523 vec![vec!["92"], vec!["20260101"]],
524 Some("0082"),
525 ),
526 make_segment(
527 "DTM",
528 vec![vec!["137"], vec!["20260401"]],
529 Some("0083"),
530 ),
531 ],
532 child_groups: vec![],
533 entry_mig_number: None,
534 variant_mig_numbers: vec![],
535 skipped_segments: vec![],
536 }],
537 }],
538 post_group_start: 0,
539 inter_group_segments: BTreeMap::new(),
540 };
541
542 let result = build_validated_tree(&workflow, &tree);
543
544 assert_eq!(result.groups.len(), 1);
545 let sg4 = &result.groups[0];
546 assert_eq!(sg4.group_id, "SG4");
547 assert_eq!(sg4.fields.len(), 2);
548
549 let eingangsdatum = &sg4.fields[0];
551 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
552 assert_eq!(eingangsdatum.value, Some("20260101"));
553
554 let dokumentendatum = &sg4.fields[1];
556 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
557 assert_eq!(dokumentendatum.value, Some("20260401"));
558 }
559
560 #[test]
561 fn test_rule_filtered_to_correct_variant() {
562 let mut workflow = empty_workflow();
566 workflow.fields.push(make_rule(
567 "SG4/RFF/C506/1154",
568 "Referenz",
569 "X",
570 Some("0099"),
571 Some(0),
572 Some(1),
573 ));
574
575 let tree = AssembledTree {
576 segments: vec![],
577 groups: vec![AssembledGroup {
578 group_id: "SG4".to_string(),
579 repetitions: vec![AssembledGroupInstance {
580 segments: vec![], child_groups: vec![],
582 entry_mig_number: None,
583 variant_mig_numbers: vec![],
584 skipped_segments: vec![],
585 }],
586 }],
587 post_group_start: 0,
588 inter_group_segments: BTreeMap::new(),
589 };
590
591 let result = build_validated_tree(&workflow, &tree);
592 assert_eq!(result.groups.len(), 1);
593 assert_eq!(result.groups[0].fields.len(), 0);
595 }
596
597 #[test]
598 fn test_missing_segment_within_correct_variant() {
599 let mut workflow = empty_workflow();
603 workflow.fields.push(make_rule(
605 "SG4/SEQ/1229",
606 "Qualifier",
607 "X",
608 Some("0098"),
609 Some(0),
610 Some(0),
611 ));
612 workflow.fields.push(make_rule(
614 "SG4/RFF/C506/1154",
615 "Referenz",
616 "X",
617 Some("0099"),
618 Some(0),
619 Some(1),
620 ));
621
622 let tree = AssembledTree {
623 segments: vec![],
624 groups: vec![AssembledGroup {
625 group_id: "SG4".to_string(),
626 repetitions: vec![AssembledGroupInstance {
627 segments: vec![
628 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
630 ],
631 child_groups: vec![],
632 entry_mig_number: Some("0098".to_string()),
633 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
635 skipped_segments: vec![],
636 }],
637 }],
638 post_group_start: 0,
639 inter_group_segments: BTreeMap::new(),
640 };
641
642 let result = build_validated_tree(&workflow, &tree);
643 assert_eq!(result.groups.len(), 1);
644 assert_eq!(result.groups[0].fields.len(), 2);
646 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
647 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
648 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
650 assert_eq!(result.groups[0].fields[1].value, None);
651 }
652
653 #[test]
654 fn test_missing_group_variant_populates_unmatched_rules() {
655 let mut workflow = empty_workflow();
659
660 workflow.fields.push(make_rule(
662 "SG2/NAD/3035",
663 "MP-ID Absender Qualifier",
664 "X",
665 Some("0010"),
666 Some(0),
667 Some(0),
668 ));
669 workflow.fields.push(make_rule(
671 "SG2/NAD/C082/3039",
672 "MP-ID Absender",
673 "X",
674 Some("0010"),
675 Some(1),
676 Some(0),
677 ));
678 workflow.fields.push(make_rule(
680 "SG2/NAD/3035",
681 "MP-ID Empfänger Qualifier",
682 "X",
683 Some("0011"),
684 Some(0),
685 Some(0),
686 ));
687 workflow.fields.push(make_rule(
689 "SG2/NAD/C082/3039",
690 "MP-ID Empfänger",
691 "X",
692 Some("0011"),
693 Some(1),
694 Some(0),
695 ));
696
697 let tree = AssembledTree {
699 segments: vec![],
700 groups: vec![AssembledGroup {
701 group_id: "SG2".to_string(),
702 repetitions: vec![AssembledGroupInstance {
703 segments: vec![make_segment(
704 "NAD",
705 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
706 Some("0011"),
707 )],
708 child_groups: vec![],
709 entry_mig_number: Some("0011".to_string()),
710 variant_mig_numbers: vec!["0011".to_string()],
711 skipped_segments: vec![],
712 }],
713 }],
714 post_group_start: 0,
715 inter_group_segments: BTreeMap::new(),
716 };
717
718 let result = build_validated_tree(&workflow, &tree);
719
720 assert_eq!(result.groups.len(), 1);
722 assert_eq!(result.groups[0].fields.len(), 2);
723 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
724
725 assert_eq!(
727 result.unmatched_rules.len(),
728 2,
729 "Expected 2 unmatched rules (NAD+MS), got {}",
730 result.unmatched_rules.len()
731 );
732 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
733 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
734 }
735
736 #[test]
737 fn test_entirely_absent_group_populates_unmatched_rules() {
738 let mut workflow = empty_workflow();
740 workflow.fields.push(make_rule(
741 "SG2/NAD/3035",
742 "MP-ID Absender Qualifier",
743 "X",
744 Some("0010"),
745 Some(0),
746 Some(0),
747 ));
748
749 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
752
753 assert!(result.groups.is_empty());
755
756 assert_eq!(
758 result.unmatched_rules.len(),
759 1,
760 "Expected 1 unmatched rule, got {}",
761 result.unmatched_rules.len()
762 );
763 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
764 }
765
766 #[test]
767 fn test_fallback_to_tag_when_no_mig_number() {
768 let mut workflow = empty_workflow();
769 workflow.fields.push(make_rule(
770 "BGM/C002/1001",
771 "Nachrichtentyp",
772 "X",
773 None, Some(0),
775 Some(0),
776 ));
777
778 let tree = AssembledTree {
779 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
780 groups: vec![],
781 post_group_start: 1,
782 inter_group_segments: BTreeMap::new(),
783 };
784
785 let result = build_validated_tree(&workflow, &tree);
786 assert_eq!(result.root_fields.len(), 1);
787 assert_eq!(result.root_fields[0].value, Some("E01"));
788 }
789
790 #[test]
791 fn test_nested_child_groups() {
792 let mut workflow = empty_workflow();
793 workflow.fields.push(make_rule(
795 "SG4/SG5/LOC/C517/3225",
796 "Marktlokations-ID",
797 "X",
798 Some("0050"),
799 Some(0),
800 Some(0),
801 ));
802
803 let tree = AssembledTree {
804 segments: vec![],
805 groups: vec![AssembledGroup {
806 group_id: "SG4".to_string(),
807 repetitions: vec![AssembledGroupInstance {
808 segments: vec![],
809 entry_mig_number: None,
810 child_groups: vec![AssembledGroup {
811 group_id: "SG5".to_string(),
812 repetitions: vec![AssembledGroupInstance {
813 segments: vec![make_segment(
814 "LOC",
815 vec![vec!["DE00012345678"]],
816 Some("0050"),
817 )],
818 child_groups: vec![],
819 entry_mig_number: None,
820 variant_mig_numbers: vec![],
821 skipped_segments: vec![],
822 }],
823 }],
824 variant_mig_numbers: vec![],
825 skipped_segments: vec![],
826 }],
827 }],
828 post_group_start: 0,
829 inter_group_segments: BTreeMap::new(),
830 };
831
832 let result = build_validated_tree(&workflow, &tree);
833 assert_eq!(result.groups.len(), 1);
834 let sg4 = &result.groups[0];
835 assert_eq!(sg4.children.len(), 1);
836
837 let sg5 = &sg4.children[0];
838 assert_eq!(sg5.group_id, "SG5");
839 assert_eq!(sg5.fields.len(), 1);
840 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
841 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
842 }
843}