1use std::collections::{BTreeMap, HashMap, HashSet};
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 pub variant_rules: Vec<&'a AhbFieldRule>,
45 pub absent_children: Vec<&'a AhbFieldRule>,
49}
50
51#[derive(Debug)]
53pub struct ValidatedTree<'a> {
54 pub pruefidentifikator: &'a str,
56 pub ub_definitions: &'a BTreeMap<String, ConditionExpr>,
58 pub package_conditions: &'a BTreeMap<u32, ConditionExpr>,
60 pub root_fields: Vec<AhbNode<'a>>,
62 pub groups: Vec<AhbGroupNode<'a>>,
64 pub unmatched_rules: Vec<&'a AhbFieldRule>,
71}
72
73pub fn build_validated_tree<'a>(
79 workflow: &'a AhbWorkflow,
80 tree: &'a AssembledTree,
81) -> ValidatedTree<'a> {
82 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
84 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
86
87 for rule in &workflow.fields {
88 match extract_top_group(&rule.segment_path) {
89 Some(group_id) => {
90 group_rules
91 .entry(group_id.to_owned())
92 .or_default()
93 .push(rule);
94 }
95 None => {
96 root_rules.push(rule);
97 }
98 }
99 }
100
101 let root_fields = resolve_fields(&root_rules, &tree.segments, None);
103
104 let groups = resolve_groups(&tree.groups, &group_rules, 0, false);
106
107 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
110 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
111 for seg in &tree.segments {
113 if let Some(ref num) = seg.mig_number {
114 matched_mig_numbers.insert(num.as_str());
115 }
116 }
117
118 let unmatched_rules: Vec<&AhbFieldRule> = workflow
119 .fields
120 .iter()
121 .filter(|rule| {
122 rule.mig_number
123 .as_ref()
124 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
125 })
126 .collect();
127
128 ValidatedTree {
129 pruefidentifikator: &workflow.pruefidentifikator,
130 ub_definitions: &workflow.ub_definitions,
131 package_conditions: &workflow.package_conditions,
132 root_fields,
133 groups,
134 unmatched_rules,
135 }
136}
137
138fn collect_matched_mig_numbers_from_groups<'a>(
144 groups: &[AhbGroupNode<'a>],
145 out: &mut std::collections::HashSet<&'a str>,
146) {
147 for group in groups {
148 for node in &group.fields {
149 if let Some(ref num) = node.rule.mig_number {
150 out.insert(num.as_str());
151 }
152 }
153 collect_matched_mig_numbers_from_groups(&group.children, out);
154 }
155}
156
157fn merged_variant<'a>(
163 rules: &[&'a AhbFieldRule],
164 instance: &AssembledGroupInstance,
165) -> Option<(&'a str, std::ops::Range<u32>)> {
166 let entry = instance.segments.first()?;
167 let recorded = instance.entry_mig_number.as_deref()?;
171 if !rules.iter().any(|r| {
172 r.mig_number.as_deref() == Some(recorded)
173 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
174 }) {
175 return None;
176 }
177 if instance
181 .segments
182 .iter()
183 .filter(|s| s.tag == entry.tag)
184 .count()
185 != 1
186 {
187 return None;
188 }
189 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
190 let mut positions: Vec<(usize, usize)> = Vec::new();
191 for r in rules {
192 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
193 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
194 && !r.codes.is_empty()
195 && !positions.contains(&pos)
196 {
197 positions.push(pos);
198 }
199 }
200 for (element, component) in positions {
201 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
203 .iter()
204 .filter(|r| {
205 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
206 && r.element_index.unwrap_or(0) == element
207 && r.component_index.unwrap_or(0) == component
208 && !r.codes.is_empty()
209 })
210 .filter_map(|r| Some((number(r)?, *r)))
211 .collect();
212 entries.sort_by_key(|(n, _)| *n);
213 entries.dedup_by_key(|(n, _)| *n);
214 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
218 .iter()
219 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
220 .filter_map(|r| number(r))
221 .collect();
222 if entries.len() != all_entry_numbers.len() {
223 continue;
224 }
225 let disjoint = entries.len() >= 2
226 && entries.iter().enumerate().all(|(i, (_, a))| {
227 entries[i + 1..].iter().all(|(_, b)| {
228 !a.codes
229 .iter()
230 .any(|c| b.codes.iter().any(|d| d.value == c.value))
231 })
232 });
233 if !disjoint {
234 continue;
235 }
236 let value = entry.elements.get(element)?.get(component)?;
237 let at = entries
238 .iter()
239 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))?;
240 let start = entries[at].0;
241 let end = entries.get(at + 1).map_or(u32::MAX, |(n, _)| *n);
242 return Some((entries[at].1.mig_number.as_deref()?, start..end));
243 }
244 None
245}
246
247fn extract_top_group(segment_path: &str) -> Option<&str> {
253 let first = segment_path.split('/').next()?;
254 if first.starts_with("SG") {
255 Some(first)
256 } else {
257 None
258 }
259}
260
261fn extract_child_group(stripped_path: &str) -> Option<&str> {
266 let first = stripped_path.split('/').next()?;
267 if first.starts_with("SG") {
268 Some(first)
269 } else {
270 None
271 }
272}
273
274fn resolve_fields<'a>(
280 rules: &[&'a AhbFieldRule],
281 segments: &'a [AssembledSegment],
282 numbers: Option<&[Option<String>]>,
283) -> Vec<AhbNode<'a>> {
284 let mut qualifiers: HashMap<(&str, &str), HashSet<&str>> = HashMap::new();
289 for rule in rules {
290 if rule.element_index.unwrap_or(0) != 0 || rule.component_index.unwrap_or(0) != 0 {
291 continue;
292 }
293 let (Some(tag), Some(number)) = (
294 extract_segment_tag(&rule.segment_path),
295 rule.mig_number.as_deref(),
296 ) else {
297 continue;
298 };
299 if !rule.codes.is_empty() {
300 qualifiers
301 .entry((tag, number))
302 .or_default()
303 .extend(rule.codes.iter().map(|c| c.value.as_str()));
304 }
305 }
306 rules
307 .iter()
308 .map(|rule| {
309 let slot_qualifiers = match (
310 rule.codes.is_empty(),
311 extract_segment_tag(&rule.segment_path),
312 rule.mig_number.as_deref(),
313 ) {
314 (true, Some(tag), Some(number)) => qualifiers.get(&(tag, number)),
315 _ => None,
316 };
317 let matched_segment = match slot_qualifiers {
318 Some(q) => find_segment_by_qualifier(rule, segments, q)
319 .unwrap_or_else(|| find_segment(rule, segments, numbers)),
320 None => find_segment(rule, segments, numbers),
321 };
322 let (value, elements, segment_number) = match matched_segment {
323 Some(seg) => {
324 let val = extract_value(seg, rule);
325 (val, Some(seg.elements.as_slice()), seg.segment_number)
326 }
327 None => {
328 let fallback_num = extract_segment_tag(&rule.segment_path)
337 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
338 .and_then(|s| s.segment_number);
339 (None, None, fallback_num)
340 }
341 };
342 AhbNode {
343 rule,
344 value,
345 segment_elements: elements,
346 matched_segment_number: segment_number,
347 }
348 })
349 .collect()
350}
351
352fn find_segment<'a>(
364 rule: &AhbFieldRule,
365 segments: &'a [AssembledSegment],
366 numbers: Option<&[Option<String>]>,
367) -> Option<&'a AssembledSegment> {
368 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
370 match numbers {
371 Some(n) => n.get(i).and_then(|n| n.as_deref()),
372 None => s.mig_number.as_deref(),
373 }
374 };
375 let tag = extract_segment_tag(&rule.segment_path)?;
376
377 if !rule.codes.is_empty() {
396 let el_idx = rule.element_index.unwrap_or(0);
397 let comp_idx = rule.component_index.unwrap_or(0);
398 let expected: std::collections::HashSet<&str> =
399 rule.codes.iter().map(|c| c.value.as_str()).collect();
400 if let Some(seg) = segments.iter().find(|s| {
401 s.tag == tag
402 && s.elements
403 .get(el_idx)
404 .and_then(|e| e.get(comp_idx))
405 .is_some_and(|v| expected.contains(v.as_str()))
406 }) {
407 return Some(seg);
408 }
409 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
410 if same_tag_count > 1 {
411 return None;
412 }
413 let other_slot = segments.iter().enumerate().any(|(i, s)| {
419 i > 0
420 && s.tag == tag
421 && s.elements
422 .get(el_idx)
423 .and_then(|e| e.get(comp_idx))
424 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
425 });
426 if other_slot {
427 return None;
428 }
429 }
430
431 if let Some(mig_num) = rule.mig_number.as_ref() {
432 if let Some((_, seg)) = segments
434 .iter()
435 .enumerate()
436 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
437 {
438 return Some(seg);
439 }
440
441 return segments
443 .iter()
444 .enumerate()
445 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
446 .map(|(_, s)| s);
447 }
448
449 segments.iter().find(|s| s.tag == tag)
451}
452
453fn find_segment_by_qualifier<'a>(
460 rule: &AhbFieldRule,
461 segments: &'a [AssembledSegment],
462 qualifiers: &HashSet<&str>,
463) -> Option<Option<&'a AssembledSegment>> {
464 let tag = extract_segment_tag(&rule.segment_path)?;
465 let same_tag = || segments.iter().filter(|s| s.tag == tag);
466 if same_tag().count() < 2 {
467 return None;
468 }
469 Some(same_tag().find(|s| {
470 s.elements
471 .first()
472 .and_then(|e| e.first())
473 .is_some_and(|v| qualifiers.contains(v.as_str()))
474 }))
475}
476
477fn extract_segment_tag(segment_path: &str) -> Option<&str> {
482 segment_path.split('/').find(|part| !part.starts_with("SG"))
483}
484
485fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
487 let elem_idx = rule.element_index.unwrap_or(0);
488 let comp_idx = rule.component_index.unwrap_or(0);
489
490 let element = segment.elements.get(elem_idx)?;
491 let component = element.get(comp_idx)?;
492
493 if component.is_empty() {
494 None
495 } else {
496 Some(component.as_str())
497 }
498}
499
500fn resolve_groups<'a>(
505 assembled_groups: &'a [AssembledGroup],
506 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
507 depth: usize,
508 narrowed: bool,
509) -> Vec<AhbGroupNode<'a>> {
510 let mut result = Vec::new();
511
512 for assembled_group in assembled_groups {
513 let rules = group_rules.get(&assembled_group.group_id);
514
515 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
516 let mut node =
517 resolve_group_instance(&assembled_group.group_id, instance, rules, depth, narrowed);
518 node.instance_index = instance_index;
519 result.push(node);
520 }
521 }
522
523 result
524}
525
526fn strip_n_groups(path: &str, n: usize) -> &str {
530 let mut rest = path;
531 for _ in 0..n {
532 match rest.find('/') {
533 Some(idx) => rest = &rest[idx + 1..],
534 None => return rest,
535 }
536 }
537 rest
538}
539
540fn resolve_group_instance<'a>(
549 group_id: &'a str,
550 instance: &'a AssembledGroupInstance,
551 rules: Option<&Vec<&'a AhbFieldRule>>,
552 depth: usize,
553 narrowed: bool,
554) -> AhbGroupNode<'a> {
555 let strip_count = depth + 1;
557
558 let variant_numbers: std::collections::HashSet<&str> =
563 if !instance.variant_mig_numbers.is_empty() {
564 instance
565 .variant_mig_numbers
566 .iter()
567 .map(|s| s.as_str())
568 .collect()
569 } else {
570 collect_instance_mig_numbers(instance)
571 };
572
573 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
574 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
575 let mut ahb_status: Option<&'a str> = None;
576
577 let merged = rules.and_then(|rules| {
582 let own: Vec<&'a AhbFieldRule> = rules
583 .iter()
584 .copied()
585 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
586 .collect();
587 merged_variant(&own, instance)
588 });
589
590 let span = variant_numbers
595 .iter()
596 .filter_map(|n| n.parse::<u32>().ok())
597 .fold(None, |acc: Option<(u32, u32)>, n| {
598 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
599 });
600
601 let handed_down = narrowed
614 && rules.is_some_and(|rules| {
615 let own: Vec<&&'a AhbFieldRule> = rules
616 .iter()
617 .filter(|r| {
618 extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none()
619 })
620 .collect();
621 !own.is_empty()
622 && !own.iter().any(|r| {
623 r.mig_number
624 .as_deref()
625 .is_some_and(|n| variant_numbers.contains(n))
626 })
627 });
628 if let Some(rules) = rules {
629 for rule in rules {
630 let stripped = strip_n_groups(&rule.segment_path, strip_count);
632 let child = extract_child_group(stripped);
633
634 if let Some(ref rule_mig) = rule.mig_number {
637 let number = rule_mig.parse::<u32>().ok();
638 let belongs = match (&merged, child) {
639 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
640 (None, _) if handed_down => true,
641 (None, Some(_)) => match (number, span) {
642 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
643 _ => variant_numbers.contains(rule_mig.as_str()),
644 },
645 (None, None) => variant_numbers.contains(rule_mig.as_str()),
646 };
647 if !belongs {
648 continue;
649 }
650 }
651
652 if let Some(child_group_id) = child {
653 child_group_rules
654 .entry(child_group_id.to_owned())
655 .or_default()
656 .push(rule);
657 } else {
658 direct_rules.push(rule);
659 }
660 }
661 }
662 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
667 let entry = merged
668 .as_ref()
669 .map(|(entry, _)| *entry)
670 .or(instance.entry_mig_number.as_deref());
671 if let Some(status) = direct_rules
672 .iter()
673 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
674 .find_map(own_status)
675 .or_else(|| {
676 direct_rules
677 .iter()
678 .filter(|r| r.mig_number.is_some())
679 .find_map(own_status)
680 })
681 {
682 ahb_status = Some(status);
683 }
684
685 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
690 let mut used: Vec<&str> = Vec::new();
691 instance
692 .segments
693 .iter()
694 .map(|seg| {
695 let of_tag = |r: &&&'a AhbFieldRule| {
696 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
697 };
698 let mut slots: Vec<&str> = direct_rules
699 .iter()
700 .filter(of_tag)
701 .filter_map(|r| r.mig_number.as_deref())
702 .filter(|n| !used.contains(n))
703 .collect();
704 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
705 slots.dedup();
706 let coded = |slot: &str| {
709 direct_rules
710 .iter()
711 .filter(of_tag)
712 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
713 .collect::<Vec<_>>()
714 };
715 let carries = |slot: &&str| {
716 coded(slot).iter().any(|r| {
717 seg.elements
718 .get(r.element_index.unwrap_or(0))
719 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
720 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
721 })
722 };
723 let slot = slots
724 .iter()
725 .find(|s| carries(s))
726 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
727 .or_else(|| slots.first())
728 .copied();
729 if let Some(slot) = slot {
730 used.push(slot);
731 }
732 slot.map(str::to_string)
733 })
734 .collect()
735 });
736 let numbers = if handed_down && numbers.is_none() {
739 Some(vec![None; instance.segments.len()])
740 } else {
741 numbers
742 };
743 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
744
745 let children = resolve_groups(
747 &instance.child_groups,
748 &child_group_rules,
749 strip_count,
750 merged.is_some(),
751 );
752
753 let mut absent_children = Vec::new();
754 for (child_id, rules) in &child_group_rules {
755 absent_children.extend(absent_variant_rules(
756 child_id,
757 rules,
758 &children,
759 strip_count + 1,
760 ));
761 }
762 absent_children.sort_by_key(|r| r.mig_number.as_deref().and_then(|n| n.parse::<u32>().ok()));
763 let mut variant_rules = direct_rules;
764 variant_rules.extend(child_group_rules.into_values().flatten());
765
766 AhbGroupNode {
767 group_id,
768 instance_index: 0,
769 ahb_status,
770 fields,
771 children,
772 variant_rules,
773 absent_children,
774 }
775}
776
777type Profile<'a> = std::collections::BTreeMap<(usize, usize), Vec<&'a str>>;
779
780fn absent_variant_rules<'a>(
793 child_id: &str,
794 rules: &[&'a AhbFieldRule],
795 children: &[AhbGroupNode<'a>],
796 strip: usize,
797) -> Vec<&'a AhbFieldRule> {
798 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
799 let own: Vec<&'a AhbFieldRule> = rules
800 .iter()
801 .copied()
802 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip)).is_none())
803 .filter(|r| number(r).is_some())
804 .collect();
805 let Some(entry_tag) = own
806 .iter()
807 .min_by_key(|r| number(r))
808 .and_then(|r| extract_segment_tag(&r.segment_path))
809 else {
810 return Vec::new();
811 };
812 let is_entry_tag = |r: &AhbFieldRule| extract_segment_tag(&r.segment_path) == Some(entry_tag);
813 let starts: std::collections::BTreeSet<u32> = own
814 .iter()
815 .filter(|r| is_entry_tag(r))
816 .filter_map(|r| number(r))
817 .collect();
818 let mut entries: Vec<(u32, &'a AhbFieldRule, Profile<'a>)> = Vec::new();
821 for &start in &starts {
822 let mut profile = Profile::new();
823 let mut entry = None;
824 for r in own
825 .iter()
826 .copied()
827 .filter(|r| is_entry_tag(r) && number(r) == Some(start))
828 {
829 entry.get_or_insert(r);
830 profile
831 .entry((r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))
832 .or_default()
833 .extend(r.codes.iter().map(|c| c.value.as_str()));
834 }
835 if let Some(entry) = entry {
836 entries.push((start, entry, profile));
837 }
838 }
839 let coded = |p: &Profile| p.values().any(|codes| !codes.is_empty());
840 let apart = |a: &Profile, b: &Profile| {
842 a.iter().any(|(pos, codes)| {
843 !codes.is_empty()
844 && b.get(pos).map_or(true, |other| {
845 !other.is_empty() && !codes.iter().any(|c| other.contains(c))
846 })
847 })
848 };
849 let distinct = entries.iter().all(|(_, _, p)| coded(p))
850 && entries.iter().enumerate().all(|(i, (_, _, a))| {
851 entries[i + 1..]
852 .iter()
853 .all(|(_, _, b)| apart(a, b) || apart(b, a))
854 });
855 if !distinct {
856 return Vec::new();
857 }
858 let claimed: std::collections::HashSet<&str> = children
859 .iter()
860 .filter(|c| c.group_id == child_id)
861 .flat_map(|c| c.fields.iter().filter_map(|n| n.rule.mig_number.as_deref()))
862 .collect();
863 let mut out = Vec::new();
864 for (i, (start, entry, _)) in entries.iter().enumerate() {
865 if claimed.contains(entry.mig_number.as_deref().unwrap_or_default()) {
866 continue;
867 }
868 let end = entries.get(i + 1).map_or(u32::MAX, |(n, _, _)| *n);
869 out.extend(
870 own.iter()
871 .copied()
872 .filter(|r| number(r).is_some_and(|n| (*start..end).contains(&n))),
873 );
874 }
875 out
876}
877
878fn collect_instance_mig_numbers(
880 instance: &AssembledGroupInstance,
881) -> std::collections::HashSet<&str> {
882 let mut numbers = std::collections::HashSet::new();
883 for seg in &instance.segments {
884 if let Some(ref num) = seg.mig_number {
885 numbers.insert(num.as_str());
886 }
887 }
888 for child_group in &instance.child_groups {
889 for child_instance in &child_group.repetitions {
890 numbers.extend(collect_instance_mig_numbers(child_instance));
891 }
892 }
893 numbers
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
900 use mig_assembly::assembler::{
901 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
902 };
903 use std::collections::BTreeMap;
904
905 fn empty_workflow() -> AhbWorkflow {
906 AhbWorkflow {
907 pruefidentifikator: "11001".to_string(),
908 description: String::new(),
909 communication_direction: None,
910 fields: vec![],
911 ub_definitions: BTreeMap::new(),
912 package_conditions: Default::default(),
913 }
914 }
915
916 fn empty_tree() -> AssembledTree {
917 AssembledTree {
918 segments: vec![],
919 groups: vec![],
920 post_group_start: 0,
921 inter_group_segments: BTreeMap::new(),
922 }
923 }
924
925 fn make_segment(
926 tag: &str,
927 elements: Vec<Vec<&str>>,
928 mig_number: Option<&str>,
929 ) -> AssembledSegment {
930 AssembledSegment {
931 tag: tag.to_string(),
932 elements: elements
933 .into_iter()
934 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
935 .collect(),
936 mig_number: mig_number.map(|s| s.to_string()),
937 segment_number: None,
938 }
939 }
940
941 fn make_rule(
942 segment_path: &str,
943 name: &str,
944 ahb_status: &str,
945 mig_number: Option<&str>,
946 element_index: Option<usize>,
947 component_index: Option<usize>,
948 ) -> AhbFieldRule {
949 AhbFieldRule {
950 segment_path: segment_path.to_string(),
951 name: name.to_string(),
952 ahb_status: ahb_status.to_string(),
953 codes: vec![],
954 parent_group_ahb_status: None,
955 segment_ahb_status: None,
956 element_index,
957 component_index,
958 mig_number: mig_number.map(|s| s.to_string()),
959 format: None,
960 }
961 }
962
963 #[test]
964 fn test_empty_workflow_empty_tree() {
965 let workflow = empty_workflow();
966 let tree = empty_tree();
967 let result = build_validated_tree(&workflow, &tree);
968
969 assert_eq!(result.pruefidentifikator, "11001");
970 assert!(result.root_fields.is_empty());
971 assert!(result.groups.is_empty());
972 }
973
974 #[test]
975 fn test_root_field_matches_root_segment() {
976 let mut workflow = empty_workflow();
977 workflow.fields.push(make_rule(
978 "BGM/C002/1001",
979 "Nachrichtentyp",
980 "X",
981 Some("0001"),
982 Some(0),
983 Some(0),
984 ));
985
986 let tree = AssembledTree {
987 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
988 groups: vec![],
989 post_group_start: 1,
990 inter_group_segments: BTreeMap::new(),
991 };
992
993 let result = build_validated_tree(&workflow, &tree);
994
995 assert_eq!(result.root_fields.len(), 1);
996 let node = &result.root_fields[0];
997 assert_eq!(node.value, Some("E01"));
998 assert!(node.segment_elements.is_some());
999 assert_eq!(node.rule.name, "Nachrichtentyp");
1000 }
1001
1002 #[test]
1003 fn test_sg4_dtm_mig_number_matching() {
1004 let mut workflow = empty_workflow();
1007
1008 workflow.fields.push(make_rule(
1010 "SG4/DTM/C507/2380",
1011 "Eingangsdatum",
1012 "X",
1013 Some("0082"),
1014 Some(1), Some(0),
1016 ));
1017
1018 workflow.fields.push(make_rule(
1020 "SG4/DTM/C507/2380",
1021 "Dokumentendatum",
1022 "X",
1023 Some("0083"),
1024 Some(1),
1025 Some(0),
1026 ));
1027
1028 let tree = AssembledTree {
1029 segments: vec![],
1030 groups: vec![AssembledGroup {
1031 group_id: "SG4".to_string(),
1032 repetitions: vec![AssembledGroupInstance {
1033 segments: vec![
1034 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
1035 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
1036 ],
1037 child_groups: vec![],
1038 entry_mig_number: None,
1039 variant_mig_numbers: vec![],
1040 skipped_segments: vec![],
1041 skipped_positions: Vec::new(),
1042 }],
1043 }],
1044 post_group_start: 0,
1045 inter_group_segments: BTreeMap::new(),
1046 };
1047
1048 let result = build_validated_tree(&workflow, &tree);
1049
1050 assert_eq!(result.groups.len(), 1);
1051 let sg4 = &result.groups[0];
1052 assert_eq!(sg4.group_id, "SG4");
1053 assert_eq!(sg4.fields.len(), 2);
1054
1055 let eingangsdatum = &sg4.fields[0];
1057 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
1058 assert_eq!(eingangsdatum.value, Some("20260101"));
1059
1060 let dokumentendatum = &sg4.fields[1];
1062 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
1063 assert_eq!(dokumentendatum.value, Some("20260401"));
1064 }
1065
1066 #[test]
1067 fn test_rule_filtered_to_correct_variant() {
1068 let mut workflow = empty_workflow();
1072 workflow.fields.push(make_rule(
1073 "SG4/RFF/C506/1154",
1074 "Referenz",
1075 "X",
1076 Some("0099"),
1077 Some(0),
1078 Some(1),
1079 ));
1080
1081 let tree = AssembledTree {
1082 segments: vec![],
1083 groups: vec![AssembledGroup {
1084 group_id: "SG4".to_string(),
1085 repetitions: vec![AssembledGroupInstance {
1086 segments: vec![], child_groups: vec![],
1088 entry_mig_number: None,
1089 variant_mig_numbers: vec![],
1090 skipped_segments: vec![],
1091 skipped_positions: Vec::new(),
1092 }],
1093 }],
1094 post_group_start: 0,
1095 inter_group_segments: BTreeMap::new(),
1096 };
1097
1098 let result = build_validated_tree(&workflow, &tree);
1099 assert_eq!(result.groups.len(), 1);
1100 assert_eq!(result.groups[0].fields.len(), 0);
1102 }
1103
1104 #[test]
1105 fn test_missing_segment_within_correct_variant() {
1106 let mut workflow = empty_workflow();
1110 workflow.fields.push(make_rule(
1112 "SG4/SEQ/1229",
1113 "Qualifier",
1114 "X",
1115 Some("0098"),
1116 Some(0),
1117 Some(0),
1118 ));
1119 workflow.fields.push(make_rule(
1121 "SG4/RFF/C506/1154",
1122 "Referenz",
1123 "X",
1124 Some("0099"),
1125 Some(0),
1126 Some(1),
1127 ));
1128
1129 let tree = AssembledTree {
1130 segments: vec![],
1131 groups: vec![AssembledGroup {
1132 group_id: "SG4".to_string(),
1133 repetitions: vec![AssembledGroupInstance {
1134 segments: vec![
1135 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
1137 ],
1138 child_groups: vec![],
1139 entry_mig_number: Some("0098".to_string()),
1140 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
1142 skipped_segments: vec![],
1143 skipped_positions: Vec::new(),
1144 }],
1145 }],
1146 post_group_start: 0,
1147 inter_group_segments: BTreeMap::new(),
1148 };
1149
1150 let result = build_validated_tree(&workflow, &tree);
1151 assert_eq!(result.groups.len(), 1);
1152 assert_eq!(result.groups[0].fields.len(), 2);
1154 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
1155 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
1156 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
1158 assert_eq!(result.groups[0].fields[1].value, None);
1159 }
1160
1161 #[test]
1162 fn test_missing_group_variant_populates_unmatched_rules() {
1163 let mut workflow = empty_workflow();
1167
1168 workflow.fields.push(make_rule(
1170 "SG2/NAD/3035",
1171 "MP-ID Absender Qualifier",
1172 "X",
1173 Some("0010"),
1174 Some(0),
1175 Some(0),
1176 ));
1177 workflow.fields.push(make_rule(
1179 "SG2/NAD/C082/3039",
1180 "MP-ID Absender",
1181 "X",
1182 Some("0010"),
1183 Some(1),
1184 Some(0),
1185 ));
1186 workflow.fields.push(make_rule(
1188 "SG2/NAD/3035",
1189 "MP-ID Empfänger Qualifier",
1190 "X",
1191 Some("0011"),
1192 Some(0),
1193 Some(0),
1194 ));
1195 workflow.fields.push(make_rule(
1197 "SG2/NAD/C082/3039",
1198 "MP-ID Empfänger",
1199 "X",
1200 Some("0011"),
1201 Some(1),
1202 Some(0),
1203 ));
1204
1205 let tree = AssembledTree {
1207 segments: vec![],
1208 groups: vec![AssembledGroup {
1209 group_id: "SG2".to_string(),
1210 repetitions: vec![AssembledGroupInstance {
1211 segments: vec![make_segment(
1212 "NAD",
1213 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
1214 Some("0011"),
1215 )],
1216 child_groups: vec![],
1217 entry_mig_number: Some("0011".to_string()),
1218 variant_mig_numbers: vec!["0011".to_string()],
1219 skipped_segments: vec![],
1220 skipped_positions: Vec::new(),
1221 }],
1222 }],
1223 post_group_start: 0,
1224 inter_group_segments: BTreeMap::new(),
1225 };
1226
1227 let result = build_validated_tree(&workflow, &tree);
1228
1229 assert_eq!(result.groups.len(), 1);
1231 assert_eq!(result.groups[0].fields.len(), 2);
1232 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1233
1234 assert_eq!(
1236 result.unmatched_rules.len(),
1237 2,
1238 "Expected 2 unmatched rules (NAD+MS), got {}",
1239 result.unmatched_rules.len()
1240 );
1241 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1242 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1243 }
1244
1245 #[test]
1246 fn test_entirely_absent_group_populates_unmatched_rules() {
1247 let mut workflow = empty_workflow();
1249 workflow.fields.push(make_rule(
1250 "SG2/NAD/3035",
1251 "MP-ID Absender Qualifier",
1252 "X",
1253 Some("0010"),
1254 Some(0),
1255 Some(0),
1256 ));
1257
1258 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1261
1262 assert!(result.groups.is_empty());
1264
1265 assert_eq!(
1267 result.unmatched_rules.len(),
1268 1,
1269 "Expected 1 unmatched rule, got {}",
1270 result.unmatched_rules.len()
1271 );
1272 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1273 }
1274
1275 #[test]
1276 fn test_fallback_to_tag_when_no_mig_number() {
1277 let mut workflow = empty_workflow();
1278 workflow.fields.push(make_rule(
1279 "BGM/C002/1001",
1280 "Nachrichtentyp",
1281 "X",
1282 None, Some(0),
1284 Some(0),
1285 ));
1286
1287 let tree = AssembledTree {
1288 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1289 groups: vec![],
1290 post_group_start: 1,
1291 inter_group_segments: BTreeMap::new(),
1292 };
1293
1294 let result = build_validated_tree(&workflow, &tree);
1295 assert_eq!(result.root_fields.len(), 1);
1296 assert_eq!(result.root_fields[0].value, Some("E01"));
1297 }
1298
1299 #[test]
1300 fn test_nested_child_groups() {
1301 let mut workflow = empty_workflow();
1302 workflow.fields.push(make_rule(
1304 "SG4/SG5/LOC/C517/3225",
1305 "Marktlokations-ID",
1306 "X",
1307 Some("0050"),
1308 Some(0),
1309 Some(0),
1310 ));
1311
1312 let tree = AssembledTree {
1313 segments: vec![],
1314 groups: vec![AssembledGroup {
1315 group_id: "SG4".to_string(),
1316 repetitions: vec![AssembledGroupInstance {
1317 segments: vec![],
1318 entry_mig_number: None,
1319 child_groups: vec![AssembledGroup {
1320 group_id: "SG5".to_string(),
1321 repetitions: vec![AssembledGroupInstance {
1322 segments: vec![make_segment(
1323 "LOC",
1324 vec![vec!["DE00012345678"]],
1325 Some("0050"),
1326 )],
1327 child_groups: vec![],
1328 entry_mig_number: None,
1329 variant_mig_numbers: vec![],
1330 skipped_segments: vec![],
1331 skipped_positions: Vec::new(),
1332 }],
1333 }],
1334 variant_mig_numbers: vec![],
1335 skipped_segments: vec![],
1336 skipped_positions: Vec::new(),
1337 }],
1338 }],
1339 post_group_start: 0,
1340 inter_group_segments: BTreeMap::new(),
1341 };
1342
1343 let result = build_validated_tree(&workflow, &tree);
1344 assert_eq!(result.groups.len(), 1);
1345 let sg4 = &result.groups[0];
1346 assert_eq!(sg4.children.len(), 1);
1347
1348 let sg5 = &sg4.children[0];
1349 assert_eq!(sg5.group_id, "SG5");
1350 assert_eq!(sg5.fields.len(), 1);
1351 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1352 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1353 }
1354
1355 #[test]
1356 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1357 let mut workflow = empty_workflow();
1371
1372 let mut z33 = make_rule(
1374 "SG10/STS/C601/9015",
1375 "Statuskategorie Z33",
1376 "X",
1377 Some("00035"),
1378 Some(0),
1379 Some(0),
1380 );
1381 z33.codes = vec![super::super::validate::AhbCodeRule {
1382 value: "Z33".into(),
1383 description: String::new(),
1384 ahb_status: "X".into(),
1385 }];
1386 workflow.fields.push(z33);
1387
1388 let mut z32 = make_rule(
1390 "SG10/STS/C601/9015",
1391 "Statuskategorie Z32",
1392 "X",
1393 Some("00036"),
1394 Some(0),
1395 Some(0),
1396 );
1397 z32.codes = vec![super::super::validate::AhbCodeRule {
1398 value: "Z32".into(),
1399 description: String::new(),
1400 ahb_status: "X".into(),
1401 }];
1402 workflow.fields.push(z32);
1403
1404 let mut z40 = make_rule(
1406 "SG10/STS/C601/9015",
1407 "Statuskategorie Z40",
1408 "X",
1409 Some("00038"),
1410 Some(0),
1411 Some(0),
1412 );
1413 z40.codes = vec![super::super::validate::AhbCodeRule {
1414 value: "Z40".into(),
1415 description: String::new(),
1416 ahb_status: "X".into(),
1417 }];
1418 workflow.fields.push(z40);
1419
1420 let tree = AssembledTree {
1423 segments: vec![],
1424 groups: vec![AssembledGroup {
1425 group_id: "SG10".to_string(),
1426 repetitions: vec![AssembledGroupInstance {
1427 segments: vec![
1428 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1429 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1430 ],
1431 child_groups: vec![],
1432 entry_mig_number: None,
1433 variant_mig_numbers: vec![
1434 "00035".into(),
1435 "00036".into(),
1436 "00037".into(),
1437 "00038".into(),
1438 ],
1439 skipped_segments: vec![],
1440 skipped_positions: Vec::new(),
1441 }],
1442 }],
1443 post_group_start: 0,
1444 inter_group_segments: BTreeMap::new(),
1445 };
1446
1447 let result = build_validated_tree(&workflow, &tree);
1448 assert_eq!(result.groups.len(), 1);
1449 let sg10 = &result.groups[0];
1450 assert_eq!(
1451 sg10.fields.len(),
1452 3,
1453 "all three rules should pass the variant filter"
1454 );
1455
1456 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1457
1458 assert_eq!(
1460 by_name("Statuskategorie Z33").value,
1461 None,
1462 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1463 );
1464
1465 assert_eq!(
1468 by_name("Statuskategorie Z32").value,
1469 Some("Z32"),
1470 "rule for Z32 must attach to the STS segment with qualifier Z32"
1471 );
1472
1473 assert_eq!(
1475 by_name("Statuskategorie Z40").value,
1476 Some("Z40"),
1477 "rule for Z40 must attach to the STS segment with qualifier Z40"
1478 );
1479 }
1480}