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>(
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 if node.value.is_some() || node.segment_elements.is_some() {
137 out.insert(num.as_str());
139 }
140 }
141 if let Some(ref num) = node.rule.mig_number {
144 out.insert(num.as_str());
145 }
146 }
147 collect_matched_mig_numbers_from_groups(&group.children, out);
148 }
149}
150
151fn extract_top_group(segment_path: &str) -> Option<&str> {
157 let first = segment_path.split('/').next()?;
158 if first.starts_with("SG") {
159 Some(first)
160 } else {
161 None
162 }
163}
164
165fn extract_child_group(stripped_path: &str) -> Option<&str> {
170 let first = stripped_path.split('/').next()?;
171 if first.starts_with("SG") {
172 Some(first)
173 } else {
174 None
175 }
176}
177
178fn resolve_fields<'a>(
184 rules: &[&'a AhbFieldRule],
185 segments: &'a [AssembledSegment],
186) -> Vec<AhbNode<'a>> {
187 rules
188 .iter()
189 .map(|rule| {
190 let matched_segment = find_segment(rule, segments);
191 let (value, elements) = match matched_segment {
192 Some(seg) => {
193 let val = extract_value(seg, rule);
194 (val, Some(seg.elements.as_slice()))
195 }
196 None => (None, None),
197 };
198 AhbNode {
199 rule,
200 value,
201 segment_elements: elements,
202 }
203 })
204 .collect()
205}
206
207fn find_segment<'a>(
219 rule: &AhbFieldRule,
220 segments: &'a [AssembledSegment],
221) -> Option<&'a AssembledSegment> {
222 if let Some(ref mig_num) = rule.mig_number {
223 if let Some(seg) = segments
225 .iter()
226 .find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
227 {
228 return Some(seg);
229 }
230
231 let tag = extract_segment_tag(&rule.segment_path)?;
233 return segments.iter().find(|s| {
234 s.tag == tag && !s.mig_number.as_ref().is_some_and(|m| m.as_str() != mig_num.as_str())
235 });
236 }
237
238 let tag = extract_segment_tag(&rule.segment_path)?;
240 segments.iter().find(|s| s.tag == tag)
241}
242
243fn extract_segment_tag(segment_path: &str) -> Option<&str> {
248 segment_path
249 .split('/')
250 .find(|part| !part.starts_with("SG"))
251}
252
253fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
255 let elem_idx = rule.element_index.unwrap_or(0);
256 let comp_idx = rule.component_index.unwrap_or(0);
257
258 let element = segment.elements.get(elem_idx)?;
259 let component = element.get(comp_idx)?;
260
261 if component.is_empty() {
262 None
263 } else {
264 Some(component.as_str())
265 }
266}
267
268fn resolve_groups<'a>(
273 assembled_groups: &'a [AssembledGroup],
274 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
275 depth: usize,
276) -> Vec<AhbGroupNode<'a>> {
277 let mut result = Vec::new();
278
279 for assembled_group in assembled_groups {
280 let rules = group_rules.get(&assembled_group.group_id);
281
282 for instance in &assembled_group.repetitions {
283 let node =
284 resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
285 result.push(node);
286 }
287 }
288
289 result
290}
291
292fn strip_n_groups(path: &str, n: usize) -> &str {
296 let mut rest = path;
297 for _ in 0..n {
298 match rest.find('/') {
299 Some(idx) => rest = &rest[idx + 1..],
300 None => return rest,
301 }
302 }
303 rest
304}
305
306fn resolve_group_instance<'a>(
315 group_id: &'a str,
316 instance: &'a AssembledGroupInstance,
317 rules: Option<&Vec<&'a AhbFieldRule>>,
318 depth: usize,
319) -> AhbGroupNode<'a> {
320 let strip_count = depth + 1;
322
323 let variant_numbers: std::collections::HashSet<&str> = if !instance.variant_mig_numbers.is_empty() {
328 instance.variant_mig_numbers.iter().map(|s| s.as_str()).collect()
329 } else {
330 collect_instance_mig_numbers(instance)
331 };
332
333 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
334 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
335 let mut ahb_status: Option<&'a str> = None;
336
337 if let Some(rules) = rules {
338 for rule in rules {
339 if let Some(ref rule_mig) = rule.mig_number {
342 if !variant_numbers.contains(rule_mig.as_str()) {
343 continue;
344 }
345 }
346
347 let stripped = strip_n_groups(&rule.segment_path, strip_count);
349
350 if let Some(child_group_id) = extract_child_group(stripped) {
351 child_group_rules
352 .entry(child_group_id.to_owned())
353 .or_default()
354 .push(rule);
355 } else {
356 direct_rules.push(rule);
357 }
358
359 if ahb_status.is_none() {
361 if let Some(ref status) = rule.parent_group_ahb_status {
362 ahb_status = Some(status.as_str());
363 }
364 }
365 }
366 }
367
368 let fields = resolve_fields(&direct_rules, &instance.segments);
370
371 let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
373
374 AhbGroupNode {
375 group_id,
376 ahb_status,
377 fields,
378 children,
379 }
380}
381
382fn collect_instance_mig_numbers(instance: &AssembledGroupInstance) -> std::collections::HashSet<&str> {
384 let mut numbers = std::collections::HashSet::new();
385 for seg in &instance.segments {
386 if let Some(ref num) = seg.mig_number {
387 numbers.insert(num.as_str());
388 }
389 }
390 for child_group in &instance.child_groups {
391 for child_instance in &child_group.repetitions {
392 numbers.extend(collect_instance_mig_numbers(child_instance));
393 }
394 }
395 numbers
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
402 use mig_assembly::assembler::{
403 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
404 };
405 use std::collections::{BTreeMap, HashMap};
406
407 fn empty_workflow() -> AhbWorkflow {
408 AhbWorkflow {
409 pruefidentifikator: "11001".to_string(),
410 description: String::new(),
411 communication_direction: None,
412 fields: vec![],
413 ub_definitions: HashMap::new(),
414 }
415 }
416
417 fn empty_tree() -> AssembledTree {
418 AssembledTree {
419 segments: vec![],
420 groups: vec![],
421 post_group_start: 0,
422 inter_group_segments: BTreeMap::new(),
423 }
424 }
425
426 fn make_segment(tag: &str, elements: Vec<Vec<&str>>, mig_number: Option<&str>) -> AssembledSegment {
427 AssembledSegment {
428 tag: tag.to_string(),
429 elements: elements
430 .into_iter()
431 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
432 .collect(),
433 mig_number: mig_number.map(|s| s.to_string()),
434 }
435 }
436
437 fn make_rule(
438 segment_path: &str,
439 name: &str,
440 ahb_status: &str,
441 mig_number: Option<&str>,
442 element_index: Option<usize>,
443 component_index: Option<usize>,
444 ) -> AhbFieldRule {
445 AhbFieldRule {
446 segment_path: segment_path.to_string(),
447 name: name.to_string(),
448 ahb_status: ahb_status.to_string(),
449 codes: vec![],
450 parent_group_ahb_status: None,
451 element_index,
452 component_index,
453 mig_number: mig_number.map(|s| s.to_string()),
454 }
455 }
456
457 #[test]
458 fn test_empty_workflow_empty_tree() {
459 let workflow = empty_workflow();
460 let tree = empty_tree();
461 let result = build_validated_tree(&workflow, &tree);
462
463 assert_eq!(result.pruefidentifikator, "11001");
464 assert!(result.root_fields.is_empty());
465 assert!(result.groups.is_empty());
466 }
467
468 #[test]
469 fn test_root_field_matches_root_segment() {
470 let mut workflow = empty_workflow();
471 workflow.fields.push(make_rule(
472 "BGM/C002/1001",
473 "Nachrichtentyp",
474 "X",
475 Some("0001"),
476 Some(0),
477 Some(0),
478 ));
479
480 let tree = AssembledTree {
481 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
482 groups: vec![],
483 post_group_start: 1,
484 inter_group_segments: BTreeMap::new(),
485 };
486
487 let result = build_validated_tree(&workflow, &tree);
488
489 assert_eq!(result.root_fields.len(), 1);
490 let node = &result.root_fields[0];
491 assert_eq!(node.value, Some("E01"));
492 assert!(node.segment_elements.is_some());
493 assert_eq!(node.rule.name, "Nachrichtentyp");
494 }
495
496 #[test]
497 fn test_sg4_dtm_mig_number_matching() {
498 let mut workflow = empty_workflow();
501
502 workflow.fields.push(make_rule(
504 "SG4/DTM/C507/2380",
505 "Eingangsdatum",
506 "X",
507 Some("0082"),
508 Some(1), Some(0),
510 ));
511
512 workflow.fields.push(make_rule(
514 "SG4/DTM/C507/2380",
515 "Dokumentendatum",
516 "X",
517 Some("0083"),
518 Some(1),
519 Some(0),
520 ));
521
522 let tree = AssembledTree {
523 segments: vec![],
524 groups: vec![AssembledGroup {
525 group_id: "SG4".to_string(),
526 repetitions: vec![AssembledGroupInstance {
527 segments: vec![
528 make_segment(
529 "DTM",
530 vec![vec!["92"], vec!["20260101"]],
531 Some("0082"),
532 ),
533 make_segment(
534 "DTM",
535 vec![vec!["137"], vec!["20260401"]],
536 Some("0083"),
537 ),
538 ],
539 child_groups: vec![],
540 entry_mig_number: None,
541 variant_mig_numbers: vec![],
542 skipped_segments: vec![],
543 }],
544 }],
545 post_group_start: 0,
546 inter_group_segments: BTreeMap::new(),
547 };
548
549 let result = build_validated_tree(&workflow, &tree);
550
551 assert_eq!(result.groups.len(), 1);
552 let sg4 = &result.groups[0];
553 assert_eq!(sg4.group_id, "SG4");
554 assert_eq!(sg4.fields.len(), 2);
555
556 let eingangsdatum = &sg4.fields[0];
558 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
559 assert_eq!(eingangsdatum.value, Some("20260101"));
560
561 let dokumentendatum = &sg4.fields[1];
563 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
564 assert_eq!(dokumentendatum.value, Some("20260401"));
565 }
566
567 #[test]
568 fn test_rule_filtered_to_correct_variant() {
569 let mut workflow = empty_workflow();
573 workflow.fields.push(make_rule(
574 "SG4/RFF/C506/1154",
575 "Referenz",
576 "X",
577 Some("0099"),
578 Some(0),
579 Some(1),
580 ));
581
582 let tree = AssembledTree {
583 segments: vec![],
584 groups: vec![AssembledGroup {
585 group_id: "SG4".to_string(),
586 repetitions: vec![AssembledGroupInstance {
587 segments: vec![], child_groups: vec![],
589 entry_mig_number: None,
590 variant_mig_numbers: vec![],
591 skipped_segments: vec![],
592 }],
593 }],
594 post_group_start: 0,
595 inter_group_segments: BTreeMap::new(),
596 };
597
598 let result = build_validated_tree(&workflow, &tree);
599 assert_eq!(result.groups.len(), 1);
600 assert_eq!(result.groups[0].fields.len(), 0);
602 }
603
604 #[test]
605 fn test_missing_segment_within_correct_variant() {
606 let mut workflow = empty_workflow();
610 workflow.fields.push(make_rule(
612 "SG4/SEQ/1229",
613 "Qualifier",
614 "X",
615 Some("0098"),
616 Some(0),
617 Some(0),
618 ));
619 workflow.fields.push(make_rule(
621 "SG4/RFF/C506/1154",
622 "Referenz",
623 "X",
624 Some("0099"),
625 Some(0),
626 Some(1),
627 ));
628
629 let tree = AssembledTree {
630 segments: vec![],
631 groups: vec![AssembledGroup {
632 group_id: "SG4".to_string(),
633 repetitions: vec![AssembledGroupInstance {
634 segments: vec![
635 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
637 ],
638 child_groups: vec![],
639 entry_mig_number: Some("0098".to_string()),
640 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
642 skipped_segments: vec![],
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(), 2);
653 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
654 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
655 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
657 assert_eq!(result.groups[0].fields[1].value, None);
658 }
659
660 #[test]
661 fn test_missing_group_variant_populates_unmatched_rules() {
662 let mut workflow = empty_workflow();
666
667 workflow.fields.push(make_rule(
669 "SG2/NAD/3035",
670 "MP-ID Absender Qualifier",
671 "X",
672 Some("0010"),
673 Some(0),
674 Some(0),
675 ));
676 workflow.fields.push(make_rule(
678 "SG2/NAD/C082/3039",
679 "MP-ID Absender",
680 "X",
681 Some("0010"),
682 Some(1),
683 Some(0),
684 ));
685 workflow.fields.push(make_rule(
687 "SG2/NAD/3035",
688 "MP-ID Empfänger Qualifier",
689 "X",
690 Some("0011"),
691 Some(0),
692 Some(0),
693 ));
694 workflow.fields.push(make_rule(
696 "SG2/NAD/C082/3039",
697 "MP-ID Empfänger",
698 "X",
699 Some("0011"),
700 Some(1),
701 Some(0),
702 ));
703
704 let tree = AssembledTree {
706 segments: vec![],
707 groups: vec![AssembledGroup {
708 group_id: "SG2".to_string(),
709 repetitions: vec![AssembledGroupInstance {
710 segments: vec![make_segment(
711 "NAD",
712 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
713 Some("0011"),
714 )],
715 child_groups: vec![],
716 entry_mig_number: Some("0011".to_string()),
717 variant_mig_numbers: vec!["0011".to_string()],
718 skipped_segments: vec![],
719 }],
720 }],
721 post_group_start: 0,
722 inter_group_segments: BTreeMap::new(),
723 };
724
725 let result = build_validated_tree(&workflow, &tree);
726
727 assert_eq!(result.groups.len(), 1);
729 assert_eq!(result.groups[0].fields.len(), 2);
730 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
731
732 assert_eq!(
734 result.unmatched_rules.len(),
735 2,
736 "Expected 2 unmatched rules (NAD+MS), got {}",
737 result.unmatched_rules.len()
738 );
739 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
740 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
741 }
742
743 #[test]
744 fn test_entirely_absent_group_populates_unmatched_rules() {
745 let mut workflow = empty_workflow();
747 workflow.fields.push(make_rule(
748 "SG2/NAD/3035",
749 "MP-ID Absender Qualifier",
750 "X",
751 Some("0010"),
752 Some(0),
753 Some(0),
754 ));
755
756 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
759
760 assert!(result.groups.is_empty());
762
763 assert_eq!(
765 result.unmatched_rules.len(),
766 1,
767 "Expected 1 unmatched rule, got {}",
768 result.unmatched_rules.len()
769 );
770 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
771 }
772
773 #[test]
774 fn test_fallback_to_tag_when_no_mig_number() {
775 let mut workflow = empty_workflow();
776 workflow.fields.push(make_rule(
777 "BGM/C002/1001",
778 "Nachrichtentyp",
779 "X",
780 None, Some(0),
782 Some(0),
783 ));
784
785 let tree = AssembledTree {
786 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
787 groups: vec![],
788 post_group_start: 1,
789 inter_group_segments: BTreeMap::new(),
790 };
791
792 let result = build_validated_tree(&workflow, &tree);
793 assert_eq!(result.root_fields.len(), 1);
794 assert_eq!(result.root_fields[0].value, Some("E01"));
795 }
796
797 #[test]
798 fn test_nested_child_groups() {
799 let mut workflow = empty_workflow();
800 workflow.fields.push(make_rule(
802 "SG4/SG5/LOC/C517/3225",
803 "Marktlokations-ID",
804 "X",
805 Some("0050"),
806 Some(0),
807 Some(0),
808 ));
809
810 let tree = AssembledTree {
811 segments: vec![],
812 groups: vec![AssembledGroup {
813 group_id: "SG4".to_string(),
814 repetitions: vec![AssembledGroupInstance {
815 segments: vec![],
816 entry_mig_number: None,
817 child_groups: vec![AssembledGroup {
818 group_id: "SG5".to_string(),
819 repetitions: vec![AssembledGroupInstance {
820 segments: vec![make_segment(
821 "LOC",
822 vec![vec!["DE00012345678"]],
823 Some("0050"),
824 )],
825 child_groups: vec![],
826 entry_mig_number: None,
827 variant_mig_numbers: vec![],
828 skipped_segments: vec![],
829 }],
830 }],
831 variant_mig_numbers: vec![],
832 skipped_segments: vec![],
833 }],
834 }],
835 post_group_start: 0,
836 inter_group_segments: BTreeMap::new(),
837 };
838
839 let result = build_validated_tree(&workflow, &tree);
840 assert_eq!(result.groups.len(), 1);
841 let sg4 = &result.groups[0];
842 assert_eq!(sg4.children.len(), 1);
843
844 let sg5 = &sg4.children[0];
845 assert_eq!(sg5.group_id, "SG5");
846 assert_eq!(sg5.fields.len(), 1);
847 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
848 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
849 }
850}