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()?;
174 let first_tag = rules
175 .iter()
176 .filter_map(|r| Some((r.mig_number.as_deref()?.parse::<u32>().ok()?, *r)))
177 .min_by_key(|(n, _)| *n)
178 .and_then(|(_, r)| extract_segment_tag(&r.segment_path));
179 if !rules.iter().any(|r| {
180 r.mig_number.as_deref() == Some(recorded)
181 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
182 }) && first_tag != Some(entry.tag.as_str())
183 {
184 return None;
185 }
186 if instance
190 .segments
191 .iter()
192 .filter(|s| s.tag == entry.tag)
193 .count()
194 != 1
195 {
196 return None;
197 }
198 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
199 let mut positions: Vec<(usize, usize)> = Vec::new();
200 for r in rules {
201 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
202 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
203 && !r.codes.is_empty()
204 && !positions.contains(&pos)
205 {
206 positions.push(pos);
207 }
208 }
209 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
211 .iter()
212 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
213 .filter_map(|r| number(r))
214 .collect();
215 for every_variant in [true, false] {
221 if !every_variant && all_entry_numbers.len() < 2 {
223 break;
224 }
225 if let Some(found) =
226 variant_at_positions(rules, entry, &positions, &all_entry_numbers, every_variant)
227 {
228 return Some(found);
229 }
230 }
231 None
232}
233
234fn variant_at_positions<'a>(
238 rules: &[&'a AhbFieldRule],
239 entry: &mig_assembly::assembler::AssembledSegment,
240 positions: &[(usize, usize)],
241 all_entry_numbers: &std::collections::BTreeSet<u32>,
242 every_variant: bool,
243) -> Option<(&'a str, std::ops::Range<u32>)> {
244 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
245 for &(element, component) in positions {
246 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
248 .iter()
249 .filter(|r| {
250 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
251 && r.element_index.unwrap_or(0) == element
252 && r.component_index.unwrap_or(0) == component
253 && !r.codes.is_empty()
254 })
255 .filter_map(|r| Some((number(r)?, *r)))
256 .collect();
257 entries.sort_by_key(|(n, _)| *n);
258 entries.dedup_by_key(|(n, _)| *n);
259 if every_variant && entries.len() != all_entry_numbers.len() {
264 continue;
265 }
266 let disjoint = (entries.len() >= 2 || !every_variant && !entries.is_empty())
267 && entries.iter().enumerate().all(|(i, (_, a))| {
268 entries[i + 1..].iter().all(|(_, b)| {
269 !a.codes
270 .iter()
271 .any(|c| b.codes.iter().any(|d| d.value == c.value))
272 })
273 });
274 if !disjoint {
275 continue;
276 }
277 let Some(value) = entry.elements.get(element).and_then(|e| e.get(component)) else {
278 if every_variant {
279 return None;
280 }
281 continue;
282 };
283 let Some(at) = entries
284 .iter()
285 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))
286 else {
287 if every_variant {
288 return None;
289 }
290 continue;
291 };
292 let start = entries[at].0;
293 let end = all_entry_numbers
294 .range(start + 1..)
295 .next()
296 .copied()
297 .unwrap_or(u32::MAX);
298 return Some((entries[at].1.mig_number.as_deref()?, start..end));
299 }
300 None
301}
302
303fn extract_top_group(segment_path: &str) -> Option<&str> {
309 let first = segment_path.split('/').next()?;
310 if first.starts_with("SG") {
311 Some(first)
312 } else {
313 None
314 }
315}
316
317fn extract_child_group(stripped_path: &str) -> Option<&str> {
322 let first = stripped_path.split('/').next()?;
323 if first.starts_with("SG") {
324 Some(first)
325 } else {
326 None
327 }
328}
329
330fn resolve_fields<'a>(
336 rules: &[&'a AhbFieldRule],
337 segments: &'a [AssembledSegment],
338 numbers: Option<&[Option<String>]>,
339) -> Vec<AhbNode<'a>> {
340 let mut qualifiers: HashMap<(&str, &str), HashSet<&str>> = HashMap::new();
345 for rule in rules {
346 if rule.element_index.unwrap_or(0) != 0 || rule.component_index.unwrap_or(0) != 0 {
347 continue;
348 }
349 let (Some(tag), Some(number)) = (
350 extract_segment_tag(&rule.segment_path),
351 rule.mig_number.as_deref(),
352 ) else {
353 continue;
354 };
355 if !rule.codes.is_empty() {
356 qualifiers
357 .entry((tag, number))
358 .or_default()
359 .extend(rule.codes.iter().map(|c| c.value.as_str()));
360 }
361 }
362 rules
363 .iter()
364 .map(|rule| {
365 let slot_qualifiers = match (
366 rule.codes.is_empty(),
367 extract_segment_tag(&rule.segment_path),
368 rule.mig_number.as_deref(),
369 ) {
370 (true, Some(tag), Some(number)) => qualifiers.get(&(tag, number)),
371 _ => None,
372 };
373 let matched_segment = match slot_qualifiers {
374 Some(q) => find_segment_by_qualifier(rule, segments, q)
375 .unwrap_or_else(|| find_segment(rule, segments, numbers)),
376 None => find_segment(rule, segments, numbers),
377 };
378 let (value, elements, segment_number) = match matched_segment {
379 Some(seg) => {
380 let val = extract_value(seg, rule);
381 (val, Some(seg.elements.as_slice()), seg.segment_number)
382 }
383 None => {
384 let fallback_num = extract_segment_tag(&rule.segment_path)
393 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
394 .and_then(|s| s.segment_number);
395 (None, None, fallback_num)
396 }
397 };
398 AhbNode {
399 rule,
400 value,
401 segment_elements: elements,
402 matched_segment_number: segment_number,
403 }
404 })
405 .collect()
406}
407
408fn find_segment<'a>(
420 rule: &AhbFieldRule,
421 segments: &'a [AssembledSegment],
422 numbers: Option<&[Option<String>]>,
423) -> Option<&'a AssembledSegment> {
424 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
426 match numbers {
427 Some(n) => n.get(i).and_then(|n| n.as_deref()),
428 None => s.mig_number.as_deref(),
429 }
430 };
431 let tag = extract_segment_tag(&rule.segment_path)?;
432
433 if !rule.codes.is_empty() {
452 let el_idx = rule.element_index.unwrap_or(0);
453 let comp_idx = rule.component_index.unwrap_or(0);
454 let expected: std::collections::HashSet<&str> =
455 rule.codes.iter().map(|c| c.value.as_str()).collect();
456 if let Some(seg) = segments.iter().find(|s| {
457 s.tag == tag
458 && s.elements
459 .get(el_idx)
460 .and_then(|e| e.get(comp_idx))
461 .is_some_and(|v| expected.contains(v.as_str()))
462 }) {
463 return Some(seg);
464 }
465 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
466 if same_tag_count > 1 {
467 return None;
468 }
469 let other_slot = segments.iter().enumerate().any(|(i, s)| {
475 i > 0
476 && s.tag == tag
477 && s.elements
478 .get(el_idx)
479 .and_then(|e| e.get(comp_idx))
480 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
481 });
482 if other_slot {
483 return None;
484 }
485 }
486
487 if let Some(mig_num) = rule.mig_number.as_ref() {
488 if let Some((_, seg)) = segments
490 .iter()
491 .enumerate()
492 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
493 {
494 return Some(seg);
495 }
496
497 return segments
499 .iter()
500 .enumerate()
501 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
502 .map(|(_, s)| s);
503 }
504
505 segments.iter().find(|s| s.tag == tag)
507}
508
509fn find_segment_by_qualifier<'a>(
516 rule: &AhbFieldRule,
517 segments: &'a [AssembledSegment],
518 qualifiers: &HashSet<&str>,
519) -> Option<Option<&'a AssembledSegment>> {
520 let tag = extract_segment_tag(&rule.segment_path)?;
521 let same_tag = || segments.iter().filter(|s| s.tag == tag);
522 if same_tag().count() < 2 {
523 return None;
524 }
525 Some(same_tag().find(|s| {
526 s.elements
527 .first()
528 .and_then(|e| e.first())
529 .is_some_and(|v| qualifiers.contains(v.as_str()))
530 }))
531}
532
533fn extract_segment_tag(segment_path: &str) -> Option<&str> {
538 segment_path.split('/').find(|part| !part.starts_with("SG"))
539}
540
541fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
543 let elem_idx = rule.element_index.unwrap_or(0);
544 let comp_idx = rule.component_index.unwrap_or(0);
545
546 let element = segment.elements.get(elem_idx)?;
547 let component = element.get(comp_idx)?;
548
549 if component.is_empty() {
550 None
551 } else {
552 Some(component.as_str())
553 }
554}
555
556fn resolve_groups<'a>(
561 assembled_groups: &'a [AssembledGroup],
562 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
563 depth: usize,
564 narrowed: bool,
565) -> Vec<AhbGroupNode<'a>> {
566 let mut result = Vec::new();
567
568 for assembled_group in assembled_groups {
569 let rules = group_rules.get(&assembled_group.group_id);
570
571 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
572 let mut node =
573 resolve_group_instance(&assembled_group.group_id, instance, rules, depth, narrowed);
574 node.instance_index = instance_index;
575 result.push(node);
576 }
577 }
578
579 result
580}
581
582fn strip_n_groups(path: &str, n: usize) -> &str {
586 let mut rest = path;
587 for _ in 0..n {
588 match rest.find('/') {
589 Some(idx) => rest = &rest[idx + 1..],
590 None => return rest,
591 }
592 }
593 rest
594}
595
596fn resolve_group_instance<'a>(
605 group_id: &'a str,
606 instance: &'a AssembledGroupInstance,
607 rules: Option<&Vec<&'a AhbFieldRule>>,
608 depth: usize,
609 narrowed: bool,
610) -> AhbGroupNode<'a> {
611 let strip_count = depth + 1;
613
614 let variant_numbers: std::collections::HashSet<&str> =
619 if !instance.variant_mig_numbers.is_empty() {
620 instance
621 .variant_mig_numbers
622 .iter()
623 .map(|s| s.as_str())
624 .collect()
625 } else {
626 collect_instance_mig_numbers(instance)
627 };
628
629 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
630 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
631 let mut ahb_status: Option<&'a str> = None;
632
633 let merged = rules.and_then(|rules| {
638 let own: Vec<&'a AhbFieldRule> = rules
639 .iter()
640 .copied()
641 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
642 .collect();
643 merged_variant(&own, instance)
644 });
645
646 let span = variant_numbers
651 .iter()
652 .filter_map(|n| n.parse::<u32>().ok())
653 .fold(None, |acc: Option<(u32, u32)>, n| {
654 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
655 });
656
657 let handed_down = narrowed
670 && rules.is_some_and(|rules| {
671 let own: Vec<&&'a AhbFieldRule> = rules
672 .iter()
673 .filter(|r| {
674 extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none()
675 })
676 .collect();
677 !own.is_empty()
678 && !own.iter().any(|r| {
679 r.mig_number
680 .as_deref()
681 .is_some_and(|n| variant_numbers.contains(n))
682 })
683 });
684 if let Some(rules) = rules {
685 for rule in rules {
686 let stripped = strip_n_groups(&rule.segment_path, strip_count);
688 let child = extract_child_group(stripped);
689
690 if let Some(ref rule_mig) = rule.mig_number {
693 let number = rule_mig.parse::<u32>().ok();
694 let belongs = match (&merged, child) {
695 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
696 (None, _) if handed_down => true,
697 (None, Some(_)) => match (number, span) {
698 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
699 _ => variant_numbers.contains(rule_mig.as_str()),
700 },
701 (None, None) => variant_numbers.contains(rule_mig.as_str()),
702 };
703 if !belongs {
704 continue;
705 }
706 }
707
708 if let Some(child_group_id) = child {
709 child_group_rules
710 .entry(child_group_id.to_owned())
711 .or_default()
712 .push(rule);
713 } else {
714 direct_rules.push(rule);
715 }
716 }
717 }
718 let own_status = |rule: &&'a AhbFieldRule| rule.parent_group_ahb_status.as_deref();
723 let entry = merged
724 .as_ref()
725 .map(|(entry, _)| *entry)
726 .or(instance.entry_mig_number.as_deref());
727 if let Some(status) = direct_rules
728 .iter()
729 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
730 .find_map(own_status)
731 .or_else(|| {
732 direct_rules
733 .iter()
734 .filter(|r| r.mig_number.is_some())
735 .find_map(own_status)
736 })
737 {
738 ahb_status = Some(status);
739 }
740
741 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
746 let mut used: Vec<&str> = Vec::new();
747 instance
748 .segments
749 .iter()
750 .map(|seg| {
751 let of_tag = |r: &&&'a AhbFieldRule| {
752 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
753 };
754 let mut slots: Vec<&str> = direct_rules
755 .iter()
756 .filter(of_tag)
757 .filter_map(|r| r.mig_number.as_deref())
758 .filter(|n| !used.contains(n))
759 .collect();
760 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
761 slots.dedup();
762 let coded = |slot: &str| {
765 direct_rules
766 .iter()
767 .filter(of_tag)
768 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
769 .collect::<Vec<_>>()
770 };
771 let carries = |slot: &&str| {
772 coded(slot).iter().any(|r| {
773 seg.elements
774 .get(r.element_index.unwrap_or(0))
775 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
776 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
777 })
778 };
779 let slot = slots
780 .iter()
781 .find(|s| carries(s))
782 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
783 .or_else(|| slots.first())
784 .copied();
785 if let Some(slot) = slot {
786 used.push(slot);
787 }
788 slot.map(str::to_string)
789 })
790 .collect()
791 });
792 let numbers = if handed_down && numbers.is_none() {
795 Some(vec![None; instance.segments.len()])
796 } else {
797 numbers
798 };
799 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
800
801 let children = resolve_groups(
803 &instance.child_groups,
804 &child_group_rules,
805 strip_count,
806 merged.is_some(),
807 );
808
809 let mut absent_children = Vec::new();
810 for (child_id, rules) in &child_group_rules {
811 absent_children.extend(absent_variant_rules(
812 child_id,
813 rules,
814 &children,
815 strip_count + 1,
816 ));
817 }
818 absent_children.sort_by_key(|r| r.mig_number.as_deref().and_then(|n| n.parse::<u32>().ok()));
819 let mut variant_rules = direct_rules;
820 variant_rules.extend(child_group_rules.into_values().flatten());
821
822 AhbGroupNode {
823 group_id,
824 instance_index: 0,
825 ahb_status,
826 fields,
827 children,
828 variant_rules,
829 absent_children,
830 }
831}
832
833type Profile<'a> = std::collections::BTreeMap<(usize, usize), Vec<&'a str>>;
835
836fn absent_variant_rules<'a>(
849 child_id: &str,
850 rules: &[&'a AhbFieldRule],
851 children: &[AhbGroupNode<'a>],
852 strip: usize,
853) -> Vec<&'a AhbFieldRule> {
854 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
855 let own: Vec<&'a AhbFieldRule> = rules
856 .iter()
857 .copied()
858 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip)).is_none())
859 .filter(|r| number(r).is_some())
860 .collect();
861 let Some(entry_tag) = own
862 .iter()
863 .min_by_key(|r| number(r))
864 .and_then(|r| extract_segment_tag(&r.segment_path))
865 else {
866 return Vec::new();
867 };
868 let is_entry_tag = |r: &AhbFieldRule| extract_segment_tag(&r.segment_path) == Some(entry_tag);
869 let starts: std::collections::BTreeSet<u32> = own
870 .iter()
871 .filter(|r| is_entry_tag(r))
872 .filter_map(|r| number(r))
873 .collect();
874 let mut entries: Vec<(u32, &'a AhbFieldRule, Profile<'a>)> = Vec::new();
877 for &start in &starts {
878 let mut profile = Profile::new();
879 let mut entry = None;
880 for r in own
881 .iter()
882 .copied()
883 .filter(|r| is_entry_tag(r) && number(r) == Some(start))
884 {
885 entry.get_or_insert(r);
886 profile
887 .entry((r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))
888 .or_default()
889 .extend(r.codes.iter().map(|c| c.value.as_str()));
890 }
891 if let Some(entry) = entry {
892 entries.push((start, entry, profile));
893 }
894 }
895 let coded = |p: &Profile| p.values().any(|codes| !codes.is_empty());
896 let apart = |a: &Profile, b: &Profile| {
898 a.iter().any(|(pos, codes)| {
899 !codes.is_empty()
900 && b.get(pos).map_or(true, |other| {
901 !other.is_empty() && !codes.iter().any(|c| other.contains(c))
902 })
903 })
904 };
905 let distinct = entries.iter().all(|(_, _, p)| coded(p))
906 && entries.iter().enumerate().all(|(i, (_, _, a))| {
907 entries[i + 1..]
908 .iter()
909 .all(|(_, _, b)| apart(a, b) || apart(b, a))
910 });
911 if !distinct {
912 return Vec::new();
913 }
914 let claimed: std::collections::HashSet<&str> = children
915 .iter()
916 .filter(|c| c.group_id == child_id)
917 .flat_map(|c| c.fields.iter().filter_map(|n| n.rule.mig_number.as_deref()))
918 .collect();
919 let mut out = Vec::new();
920 for (i, (start, entry, _)) in entries.iter().enumerate() {
921 if claimed.contains(entry.mig_number.as_deref().unwrap_or_default()) {
922 continue;
923 }
924 let end = entries.get(i + 1).map_or(u32::MAX, |(n, _, _)| *n);
925 out.extend(
926 own.iter()
927 .copied()
928 .filter(|r| number(r).is_some_and(|n| (*start..end).contains(&n))),
929 );
930 }
931 out
932}
933
934fn collect_instance_mig_numbers(
936 instance: &AssembledGroupInstance,
937) -> std::collections::HashSet<&str> {
938 let mut numbers = std::collections::HashSet::new();
939 for seg in &instance.segments {
940 if let Some(ref num) = seg.mig_number {
941 numbers.insert(num.as_str());
942 }
943 }
944 for child_group in &instance.child_groups {
945 for child_instance in &child_group.repetitions {
946 numbers.extend(collect_instance_mig_numbers(child_instance));
947 }
948 }
949 numbers
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
956 use mig_assembly::assembler::{
957 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
958 };
959 use std::collections::BTreeMap;
960
961 fn empty_workflow() -> AhbWorkflow {
962 AhbWorkflow {
963 pruefidentifikator: "11001".to_string(),
964 description: String::new(),
965 communication_direction: None,
966 fields: vec![],
967 ub_definitions: BTreeMap::new(),
968 package_conditions: Default::default(),
969 }
970 }
971
972 fn empty_tree() -> AssembledTree {
973 AssembledTree {
974 segments: vec![],
975 groups: vec![],
976 post_group_start: 0,
977 inter_group_segments: BTreeMap::new(),
978 }
979 }
980
981 fn make_segment(
982 tag: &str,
983 elements: Vec<Vec<&str>>,
984 mig_number: Option<&str>,
985 ) -> AssembledSegment {
986 AssembledSegment {
987 tag: tag.to_string(),
988 elements: elements
989 .into_iter()
990 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
991 .collect(),
992 mig_number: mig_number.map(|s| s.to_string()),
993 segment_number: None,
994 }
995 }
996
997 fn make_rule(
998 segment_path: &str,
999 name: &str,
1000 ahb_status: &str,
1001 mig_number: Option<&str>,
1002 element_index: Option<usize>,
1003 component_index: Option<usize>,
1004 ) -> AhbFieldRule {
1005 AhbFieldRule {
1006 segment_path: segment_path.to_string(),
1007 name: name.to_string(),
1008 ahb_status: ahb_status.to_string(),
1009 codes: vec![],
1010 parent_group_ahb_status: None,
1011 segment_ahb_status: None,
1012 element_index,
1013 component_index,
1014 mig_number: mig_number.map(|s| s.to_string()),
1015 format: None,
1016 }
1017 }
1018
1019 #[test]
1020 fn test_empty_workflow_empty_tree() {
1021 let workflow = empty_workflow();
1022 let tree = empty_tree();
1023 let result = build_validated_tree(&workflow, &tree);
1024
1025 assert_eq!(result.pruefidentifikator, "11001");
1026 assert!(result.root_fields.is_empty());
1027 assert!(result.groups.is_empty());
1028 }
1029
1030 #[test]
1031 fn test_root_field_matches_root_segment() {
1032 let mut workflow = empty_workflow();
1033 workflow.fields.push(make_rule(
1034 "BGM/C002/1001",
1035 "Nachrichtentyp",
1036 "X",
1037 Some("0001"),
1038 Some(0),
1039 Some(0),
1040 ));
1041
1042 let tree = AssembledTree {
1043 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
1044 groups: vec![],
1045 post_group_start: 1,
1046 inter_group_segments: BTreeMap::new(),
1047 };
1048
1049 let result = build_validated_tree(&workflow, &tree);
1050
1051 assert_eq!(result.root_fields.len(), 1);
1052 let node = &result.root_fields[0];
1053 assert_eq!(node.value, Some("E01"));
1054 assert!(node.segment_elements.is_some());
1055 assert_eq!(node.rule.name, "Nachrichtentyp");
1056 }
1057
1058 #[test]
1059 fn test_sg4_dtm_mig_number_matching() {
1060 let mut workflow = empty_workflow();
1063
1064 workflow.fields.push(make_rule(
1066 "SG4/DTM/C507/2380",
1067 "Eingangsdatum",
1068 "X",
1069 Some("0082"),
1070 Some(1), Some(0),
1072 ));
1073
1074 workflow.fields.push(make_rule(
1076 "SG4/DTM/C507/2380",
1077 "Dokumentendatum",
1078 "X",
1079 Some("0083"),
1080 Some(1),
1081 Some(0),
1082 ));
1083
1084 let tree = AssembledTree {
1085 segments: vec![],
1086 groups: vec![AssembledGroup {
1087 group_id: "SG4".to_string(),
1088 repetitions: vec![AssembledGroupInstance {
1089 segments: vec![
1090 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
1091 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
1092 ],
1093 child_groups: vec![],
1094 entry_mig_number: None,
1095 variant_mig_numbers: vec![],
1096 skipped_segments: vec![],
1097 skipped_positions: Vec::new(),
1098 }],
1099 }],
1100 post_group_start: 0,
1101 inter_group_segments: BTreeMap::new(),
1102 };
1103
1104 let result = build_validated_tree(&workflow, &tree);
1105
1106 assert_eq!(result.groups.len(), 1);
1107 let sg4 = &result.groups[0];
1108 assert_eq!(sg4.group_id, "SG4");
1109 assert_eq!(sg4.fields.len(), 2);
1110
1111 let eingangsdatum = &sg4.fields[0];
1113 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
1114 assert_eq!(eingangsdatum.value, Some("20260101"));
1115
1116 let dokumentendatum = &sg4.fields[1];
1118 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
1119 assert_eq!(dokumentendatum.value, Some("20260401"));
1120 }
1121
1122 #[test]
1123 fn test_rule_filtered_to_correct_variant() {
1124 let mut workflow = empty_workflow();
1128 workflow.fields.push(make_rule(
1129 "SG4/RFF/C506/1154",
1130 "Referenz",
1131 "X",
1132 Some("0099"),
1133 Some(0),
1134 Some(1),
1135 ));
1136
1137 let tree = AssembledTree {
1138 segments: vec![],
1139 groups: vec![AssembledGroup {
1140 group_id: "SG4".to_string(),
1141 repetitions: vec![AssembledGroupInstance {
1142 segments: vec![], child_groups: vec![],
1144 entry_mig_number: None,
1145 variant_mig_numbers: vec![],
1146 skipped_segments: vec![],
1147 skipped_positions: Vec::new(),
1148 }],
1149 }],
1150 post_group_start: 0,
1151 inter_group_segments: BTreeMap::new(),
1152 };
1153
1154 let result = build_validated_tree(&workflow, &tree);
1155 assert_eq!(result.groups.len(), 1);
1156 assert_eq!(result.groups[0].fields.len(), 0);
1158 }
1159
1160 #[test]
1161 fn test_missing_segment_within_correct_variant() {
1162 let mut workflow = empty_workflow();
1166 workflow.fields.push(make_rule(
1168 "SG4/SEQ/1229",
1169 "Qualifier",
1170 "X",
1171 Some("0098"),
1172 Some(0),
1173 Some(0),
1174 ));
1175 workflow.fields.push(make_rule(
1177 "SG4/RFF/C506/1154",
1178 "Referenz",
1179 "X",
1180 Some("0099"),
1181 Some(0),
1182 Some(1),
1183 ));
1184
1185 let tree = AssembledTree {
1186 segments: vec![],
1187 groups: vec![AssembledGroup {
1188 group_id: "SG4".to_string(),
1189 repetitions: vec![AssembledGroupInstance {
1190 segments: vec![
1191 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
1193 ],
1194 child_groups: vec![],
1195 entry_mig_number: Some("0098".to_string()),
1196 variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
1198 skipped_segments: vec![],
1199 skipped_positions: Vec::new(),
1200 }],
1201 }],
1202 post_group_start: 0,
1203 inter_group_segments: BTreeMap::new(),
1204 };
1205
1206 let result = build_validated_tree(&workflow, &tree);
1207 assert_eq!(result.groups.len(), 1);
1208 assert_eq!(result.groups[0].fields.len(), 2);
1210 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
1211 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
1212 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
1214 assert_eq!(result.groups[0].fields[1].value, None);
1215 }
1216
1217 #[test]
1218 fn test_missing_group_variant_populates_unmatched_rules() {
1219 let mut workflow = empty_workflow();
1223
1224 workflow.fields.push(make_rule(
1226 "SG2/NAD/3035",
1227 "MP-ID Absender Qualifier",
1228 "X",
1229 Some("0010"),
1230 Some(0),
1231 Some(0),
1232 ));
1233 workflow.fields.push(make_rule(
1235 "SG2/NAD/C082/3039",
1236 "MP-ID Absender",
1237 "X",
1238 Some("0010"),
1239 Some(1),
1240 Some(0),
1241 ));
1242 workflow.fields.push(make_rule(
1244 "SG2/NAD/3035",
1245 "MP-ID Empfänger Qualifier",
1246 "X",
1247 Some("0011"),
1248 Some(0),
1249 Some(0),
1250 ));
1251 workflow.fields.push(make_rule(
1253 "SG2/NAD/C082/3039",
1254 "MP-ID Empfänger",
1255 "X",
1256 Some("0011"),
1257 Some(1),
1258 Some(0),
1259 ));
1260
1261 let tree = AssembledTree {
1263 segments: vec![],
1264 groups: vec![AssembledGroup {
1265 group_id: "SG2".to_string(),
1266 repetitions: vec![AssembledGroupInstance {
1267 segments: vec![make_segment(
1268 "NAD",
1269 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
1270 Some("0011"),
1271 )],
1272 child_groups: vec![],
1273 entry_mig_number: Some("0011".to_string()),
1274 variant_mig_numbers: vec!["0011".to_string()],
1275 skipped_segments: vec![],
1276 skipped_positions: Vec::new(),
1277 }],
1278 }],
1279 post_group_start: 0,
1280 inter_group_segments: BTreeMap::new(),
1281 };
1282
1283 let result = build_validated_tree(&workflow, &tree);
1284
1285 assert_eq!(result.groups.len(), 1);
1287 assert_eq!(result.groups[0].fields.len(), 2);
1288 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1289
1290 assert_eq!(
1292 result.unmatched_rules.len(),
1293 2,
1294 "Expected 2 unmatched rules (NAD+MS), got {}",
1295 result.unmatched_rules.len()
1296 );
1297 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1298 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1299 }
1300
1301 #[test]
1302 fn test_entirely_absent_group_populates_unmatched_rules() {
1303 let mut workflow = empty_workflow();
1305 workflow.fields.push(make_rule(
1306 "SG2/NAD/3035",
1307 "MP-ID Absender Qualifier",
1308 "X",
1309 Some("0010"),
1310 Some(0),
1311 Some(0),
1312 ));
1313
1314 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1317
1318 assert!(result.groups.is_empty());
1320
1321 assert_eq!(
1323 result.unmatched_rules.len(),
1324 1,
1325 "Expected 1 unmatched rule, got {}",
1326 result.unmatched_rules.len()
1327 );
1328 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1329 }
1330
1331 #[test]
1332 fn test_fallback_to_tag_when_no_mig_number() {
1333 let mut workflow = empty_workflow();
1334 workflow.fields.push(make_rule(
1335 "BGM/C002/1001",
1336 "Nachrichtentyp",
1337 "X",
1338 None, Some(0),
1340 Some(0),
1341 ));
1342
1343 let tree = AssembledTree {
1344 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1345 groups: vec![],
1346 post_group_start: 1,
1347 inter_group_segments: BTreeMap::new(),
1348 };
1349
1350 let result = build_validated_tree(&workflow, &tree);
1351 assert_eq!(result.root_fields.len(), 1);
1352 assert_eq!(result.root_fields[0].value, Some("E01"));
1353 }
1354
1355 #[test]
1356 fn test_nested_child_groups() {
1357 let mut workflow = empty_workflow();
1358 workflow.fields.push(make_rule(
1360 "SG4/SG5/LOC/C517/3225",
1361 "Marktlokations-ID",
1362 "X",
1363 Some("0050"),
1364 Some(0),
1365 Some(0),
1366 ));
1367
1368 let tree = AssembledTree {
1369 segments: vec![],
1370 groups: vec![AssembledGroup {
1371 group_id: "SG4".to_string(),
1372 repetitions: vec![AssembledGroupInstance {
1373 segments: vec![],
1374 entry_mig_number: None,
1375 child_groups: vec![AssembledGroup {
1376 group_id: "SG5".to_string(),
1377 repetitions: vec![AssembledGroupInstance {
1378 segments: vec![make_segment(
1379 "LOC",
1380 vec![vec!["DE00012345678"]],
1381 Some("0050"),
1382 )],
1383 child_groups: vec![],
1384 entry_mig_number: None,
1385 variant_mig_numbers: vec![],
1386 skipped_segments: vec![],
1387 skipped_positions: Vec::new(),
1388 }],
1389 }],
1390 variant_mig_numbers: vec![],
1391 skipped_segments: vec![],
1392 skipped_positions: Vec::new(),
1393 }],
1394 }],
1395 post_group_start: 0,
1396 inter_group_segments: BTreeMap::new(),
1397 };
1398
1399 let result = build_validated_tree(&workflow, &tree);
1400 assert_eq!(result.groups.len(), 1);
1401 let sg4 = &result.groups[0];
1402 assert_eq!(sg4.children.len(), 1);
1403
1404 let sg5 = &sg4.children[0];
1405 assert_eq!(sg5.group_id, "SG5");
1406 assert_eq!(sg5.fields.len(), 1);
1407 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1408 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1409 }
1410
1411 #[test]
1412 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1413 let mut workflow = empty_workflow();
1427
1428 let mut z33 = make_rule(
1430 "SG10/STS/C601/9015",
1431 "Statuskategorie Z33",
1432 "X",
1433 Some("00035"),
1434 Some(0),
1435 Some(0),
1436 );
1437 z33.codes = vec![super::super::validate::AhbCodeRule {
1438 value: "Z33".into(),
1439 description: String::new(),
1440 ahb_status: "X".into(),
1441 }];
1442 workflow.fields.push(z33);
1443
1444 let mut z32 = make_rule(
1446 "SG10/STS/C601/9015",
1447 "Statuskategorie Z32",
1448 "X",
1449 Some("00036"),
1450 Some(0),
1451 Some(0),
1452 );
1453 z32.codes = vec![super::super::validate::AhbCodeRule {
1454 value: "Z32".into(),
1455 description: String::new(),
1456 ahb_status: "X".into(),
1457 }];
1458 workflow.fields.push(z32);
1459
1460 let mut z40 = make_rule(
1462 "SG10/STS/C601/9015",
1463 "Statuskategorie Z40",
1464 "X",
1465 Some("00038"),
1466 Some(0),
1467 Some(0),
1468 );
1469 z40.codes = vec![super::super::validate::AhbCodeRule {
1470 value: "Z40".into(),
1471 description: String::new(),
1472 ahb_status: "X".into(),
1473 }];
1474 workflow.fields.push(z40);
1475
1476 let tree = AssembledTree {
1479 segments: vec![],
1480 groups: vec![AssembledGroup {
1481 group_id: "SG10".to_string(),
1482 repetitions: vec![AssembledGroupInstance {
1483 segments: vec![
1484 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1485 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1486 ],
1487 child_groups: vec![],
1488 entry_mig_number: None,
1489 variant_mig_numbers: vec![
1490 "00035".into(),
1491 "00036".into(),
1492 "00037".into(),
1493 "00038".into(),
1494 ],
1495 skipped_segments: vec![],
1496 skipped_positions: Vec::new(),
1497 }],
1498 }],
1499 post_group_start: 0,
1500 inter_group_segments: BTreeMap::new(),
1501 };
1502
1503 let result = build_validated_tree(&workflow, &tree);
1504 assert_eq!(result.groups.len(), 1);
1505 let sg10 = &result.groups[0];
1506 assert_eq!(
1507 sg10.fields.len(),
1508 3,
1509 "all three rules should pass the variant filter"
1510 );
1511
1512 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1513
1514 assert_eq!(
1516 by_name("Statuskategorie Z33").value,
1517 None,
1518 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1519 );
1520
1521 assert_eq!(
1524 by_name("Statuskategorie Z32").value,
1525 Some("Z32"),
1526 "rule for Z32 must attach to the STS segment with qualifier Z32"
1527 );
1528
1529 assert_eq!(
1531 by_name("Statuskategorie Z40").value,
1532 Some("Z40"),
1533 "rule for Z40 must attach to the STS segment with qualifier Z40"
1534 );
1535 }
1536}