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>,
40 pub own_status: Option<&'a str>,
44 pub fields: Vec<AhbNode<'a>>,
46 pub children: Vec<AhbGroupNode<'a>>,
48 pub variant_rules: Vec<&'a AhbFieldRule>,
50 pub absent_children: Vec<&'a AhbFieldRule>,
54}
55
56#[derive(Debug)]
58pub struct ValidatedTree<'a> {
59 pub pruefidentifikator: &'a str,
61 pub ub_definitions: &'a BTreeMap<String, ConditionExpr>,
63 pub package_conditions: &'a BTreeMap<u32, ConditionExpr>,
65 pub root_fields: Vec<AhbNode<'a>>,
67 pub groups: Vec<AhbGroupNode<'a>>,
69 pub unmatched_rules: Vec<&'a AhbFieldRule>,
76}
77
78pub fn build_validated_tree<'a>(
84 workflow: &'a AhbWorkflow,
85 tree: &'a AssembledTree,
86) -> ValidatedTree<'a> {
87 let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
89 let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
91
92 for rule in &workflow.fields {
93 match extract_top_group(&rule.segment_path) {
94 Some(group_id) => {
95 group_rules
96 .entry(group_id.to_owned())
97 .or_default()
98 .push(rule);
99 }
100 None => {
101 root_rules.push(rule);
102 }
103 }
104 }
105
106 let root_fields = resolve_fields(&root_rules, &tree.segments, None);
108
109 let groups = resolve_groups(&tree.groups, &group_rules, 0, false);
111
112 let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
115 collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
116 for seg in &tree.segments {
118 if let Some(ref num) = seg.mig_number {
119 matched_mig_numbers.insert(num.as_str());
120 }
121 }
122
123 let unmatched_rules: Vec<&AhbFieldRule> = workflow
124 .fields
125 .iter()
126 .filter(|rule| {
127 rule.mig_number
128 .as_ref()
129 .is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
130 })
131 .collect();
132
133 ValidatedTree {
134 pruefidentifikator: &workflow.pruefidentifikator,
135 ub_definitions: &workflow.ub_definitions,
136 package_conditions: &workflow.package_conditions,
137 root_fields,
138 groups,
139 unmatched_rules,
140 }
141}
142
143fn collect_matched_mig_numbers_from_groups<'a>(
149 groups: &[AhbGroupNode<'a>],
150 out: &mut std::collections::HashSet<&'a str>,
151) {
152 for group in groups {
153 for node in &group.fields {
154 if let Some(ref num) = node.rule.mig_number {
155 out.insert(num.as_str());
156 }
157 }
158 collect_matched_mig_numbers_from_groups(&group.children, out);
159 }
160}
161
162fn merged_variant<'a>(
168 rules: &[&'a AhbFieldRule],
169 instance: &AssembledGroupInstance,
170) -> Option<(&'a str, std::ops::Range<u32>)> {
171 let entry = instance.segments.first()?;
172 let recorded = instance.entry_mig_number.as_deref()?;
179 let first_tag = rules
180 .iter()
181 .filter_map(|r| Some((r.mig_number.as_deref()?.parse::<u32>().ok()?, *r)))
182 .min_by_key(|(n, _)| *n)
183 .and_then(|(_, r)| extract_segment_tag(&r.segment_path));
184 if !rules.iter().any(|r| {
185 r.mig_number.as_deref() == Some(recorded)
186 && extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
187 }) && first_tag != Some(entry.tag.as_str())
188 {
189 return None;
190 }
191 if instance
195 .segments
196 .iter()
197 .filter(|s| s.tag == entry.tag)
198 .count()
199 != 1
200 {
201 return None;
202 }
203 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
204 let mut positions: Vec<(usize, usize)> = Vec::new();
205 for r in rules {
206 let pos = (r.element_index.unwrap_or(0), r.component_index.unwrap_or(0));
207 if extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
208 && !r.codes.is_empty()
209 && !positions.contains(&pos)
210 {
211 positions.push(pos);
212 }
213 }
214 let all_entry_numbers: std::collections::BTreeSet<u32> = rules
216 .iter()
217 .filter(|r| extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str()))
218 .filter_map(|r| number(r))
219 .collect();
220 for every_variant in [true, false] {
226 if !every_variant && all_entry_numbers.len() < 2 {
228 break;
229 }
230 if let Some(found) =
231 variant_at_positions(rules, entry, &positions, &all_entry_numbers, every_variant)
232 {
233 return Some(found);
234 }
235 }
236 None
237}
238
239fn variant_at_positions<'a>(
243 rules: &[&'a AhbFieldRule],
244 entry: &mig_assembly::assembler::AssembledSegment,
245 positions: &[(usize, usize)],
246 all_entry_numbers: &std::collections::BTreeSet<u32>,
247 every_variant: bool,
248) -> Option<(&'a str, std::ops::Range<u32>)> {
249 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
250 for &(element, component) in positions {
251 let mut entries: Vec<(u32, &'a AhbFieldRule)> = rules
253 .iter()
254 .filter(|r| {
255 extract_segment_tag(&r.segment_path) == Some(entry.tag.as_str())
256 && r.element_index.unwrap_or(0) == element
257 && r.component_index.unwrap_or(0) == component
258 && !r.codes.is_empty()
259 })
260 .filter_map(|r| Some((number(r)?, *r)))
261 .collect();
262 entries.sort_by_key(|(n, _)| *n);
263 entries.dedup_by_key(|(n, _)| *n);
264 if every_variant && entries.len() != all_entry_numbers.len() {
269 continue;
270 }
271 let disjoint = (entries.len() >= 2 || !every_variant && !entries.is_empty())
272 && entries.iter().enumerate().all(|(i, (_, a))| {
273 entries[i + 1..].iter().all(|(_, b)| {
274 !a.codes
275 .iter()
276 .any(|c| b.codes.iter().any(|d| d.value == c.value))
277 })
278 });
279 if !disjoint {
280 continue;
281 }
282 let Some(value) = entry.elements.get(element).and_then(|e| e.get(component)) else {
283 if every_variant {
284 return None;
285 }
286 continue;
287 };
288 let Some(at) = entries
289 .iter()
290 .position(|(_, r)| r.codes.iter().any(|c| &c.value == value))
291 else {
292 if every_variant {
293 return None;
294 }
295 continue;
296 };
297 let start = entries[at].0;
298 let end = all_entry_numbers
299 .range(start + 1..)
300 .next()
301 .copied()
302 .unwrap_or(u32::MAX);
303 return Some((entries[at].1.mig_number.as_deref()?, start..end));
304 }
305 None
306}
307
308fn extract_top_group(segment_path: &str) -> Option<&str> {
314 let first = segment_path.split('/').next()?;
315 if first.starts_with("SG") {
316 Some(first)
317 } else {
318 None
319 }
320}
321
322fn extract_child_group(stripped_path: &str) -> Option<&str> {
327 let first = stripped_path.split('/').next()?;
328 if first.starts_with("SG") {
329 Some(first)
330 } else {
331 None
332 }
333}
334
335fn resolve_fields<'a>(
341 rules: &[&'a AhbFieldRule],
342 segments: &'a [AssembledSegment],
343 numbers: Option<&[Option<String>]>,
344) -> Vec<AhbNode<'a>> {
345 let mut qualifiers: HashMap<(&str, &str), HashSet<&str>> = HashMap::new();
350 for rule in rules {
351 if rule.element_index.unwrap_or(0) != 0 || rule.component_index.unwrap_or(0) != 0 {
352 continue;
353 }
354 let (Some(tag), Some(number)) = (
355 extract_segment_tag(&rule.segment_path),
356 rule.mig_number.as_deref(),
357 ) else {
358 continue;
359 };
360 if !rule.codes.is_empty() {
361 qualifiers
362 .entry((tag, number))
363 .or_default()
364 .extend(rule.codes.iter().map(|c| c.value.as_str()));
365 }
366 }
367 rules
368 .iter()
369 .map(|rule| {
370 let slot_qualifiers = match (
371 rule.codes.is_empty(),
372 extract_segment_tag(&rule.segment_path),
373 rule.mig_number.as_deref(),
374 ) {
375 (true, Some(tag), Some(number)) => qualifiers.get(&(tag, number)),
376 _ => None,
377 };
378 let matched_segment = match slot_qualifiers {
379 Some(q) => find_segment_by_qualifier(rule, segments, q)
380 .unwrap_or_else(|| find_segment(rule, segments, numbers)),
381 None => find_segment(rule, segments, numbers),
382 };
383 let (value, elements, segment_number) = match matched_segment {
384 Some(seg) => {
385 let val = extract_value(seg, rule);
386 (val, Some(seg.elements.as_slice()), seg.segment_number)
387 }
388 None => {
389 let fallback_num = extract_segment_tag(&rule.segment_path)
398 .and_then(|tag| segments.iter().find(|s| s.tag == tag))
399 .and_then(|s| s.segment_number);
400 (None, None, fallback_num)
401 }
402 };
403 AhbNode {
404 rule,
405 value,
406 segment_elements: elements,
407 matched_segment_number: segment_number,
408 }
409 })
410 .collect()
411}
412
413fn find_segment<'a>(
425 rule: &AhbFieldRule,
426 segments: &'a [AssembledSegment],
427 numbers: Option<&[Option<String>]>,
428) -> Option<&'a AssembledSegment> {
429 let number_of = |i: usize, s: &'a AssembledSegment| -> Option<&str> {
431 match numbers {
432 Some(n) => n.get(i).and_then(|n| n.as_deref()),
433 None => s.mig_number.as_deref(),
434 }
435 };
436 let tag = extract_segment_tag(&rule.segment_path)?;
437
438 if !rule.codes.is_empty() {
457 let el_idx = rule.element_index.unwrap_or(0);
458 let comp_idx = rule.component_index.unwrap_or(0);
459 let expected: std::collections::HashSet<&str> =
460 rule.codes.iter().map(|c| c.value.as_str()).collect();
461 if let Some(seg) = segments.iter().find(|s| {
462 s.tag == tag
463 && s.elements
464 .get(el_idx)
465 .and_then(|e| e.get(comp_idx))
466 .is_some_and(|v| expected.contains(v.as_str()))
467 }) {
468 return Some(seg);
469 }
470 let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
471 if same_tag_count > 1 {
472 return None;
473 }
474 let other_slot = segments.iter().enumerate().any(|(i, s)| {
480 i > 0
481 && s.tag == tag
482 && s.elements
483 .get(el_idx)
484 .and_then(|e| e.get(comp_idx))
485 .is_some_and(|v| !v.is_empty() && !expected.contains(v.as_str()))
486 });
487 if other_slot {
488 return None;
489 }
490 }
491
492 if let Some(mig_num) = rule.mig_number.as_ref() {
493 if let Some((_, seg)) = segments
495 .iter()
496 .enumerate()
497 .find(|(i, s)| number_of(*i, s) == Some(mig_num.as_str()))
498 {
499 return Some(seg);
500 }
501
502 return segments
504 .iter()
505 .enumerate()
506 .find(|(i, s)| s.tag == tag && number_of(*i, s).map_or(true, |m| m == mig_num))
507 .map(|(_, s)| s);
508 }
509
510 segments.iter().find(|s| s.tag == tag)
512}
513
514fn find_segment_by_qualifier<'a>(
521 rule: &AhbFieldRule,
522 segments: &'a [AssembledSegment],
523 qualifiers: &HashSet<&str>,
524) -> Option<Option<&'a AssembledSegment>> {
525 let tag = extract_segment_tag(&rule.segment_path)?;
526 let same_tag = || segments.iter().filter(|s| s.tag == tag);
527 if same_tag().count() < 2 {
528 return None;
529 }
530 Some(same_tag().find(|s| {
531 s.elements
532 .first()
533 .and_then(|e| e.first())
534 .is_some_and(|v| qualifiers.contains(v.as_str()))
535 }))
536}
537
538fn extract_segment_tag(segment_path: &str) -> Option<&str> {
543 segment_path.split('/').find(|part| !part.starts_with("SG"))
544}
545
546fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
548 let elem_idx = rule.element_index.unwrap_or(0);
549 let comp_idx = rule.component_index.unwrap_or(0);
550
551 let element = segment.elements.get(elem_idx)?;
552 let component = element.get(comp_idx)?;
553
554 if component.is_empty() {
555 None
556 } else {
557 Some(component.as_str())
558 }
559}
560
561fn resolve_groups<'a>(
566 assembled_groups: &'a [AssembledGroup],
567 group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
568 depth: usize,
569 narrowed: bool,
570) -> Vec<AhbGroupNode<'a>> {
571 let mut result = Vec::new();
572
573 for assembled_group in assembled_groups {
574 let rules = group_rules.get(&assembled_group.group_id);
575
576 for (instance_index, instance) in assembled_group.repetitions.iter().enumerate() {
577 let mut node =
578 resolve_group_instance(&assembled_group.group_id, instance, rules, depth, narrowed);
579 node.instance_index = instance_index;
580 result.push(node);
581 }
582 }
583
584 result
585}
586
587fn strip_n_groups(path: &str, n: usize) -> &str {
591 let mut rest = path;
592 for _ in 0..n {
593 match rest.find('/') {
594 Some(idx) => rest = &rest[idx + 1..],
595 None => return rest,
596 }
597 }
598 rest
599}
600
601fn resolve_group_instance<'a>(
610 group_id: &'a str,
611 instance: &'a AssembledGroupInstance,
612 rules: Option<&Vec<&'a AhbFieldRule>>,
613 depth: usize,
614 narrowed: bool,
615) -> AhbGroupNode<'a> {
616 let strip_count = depth + 1;
618
619 let variant_numbers: std::collections::HashSet<&str> =
624 if !instance.variant_mig_numbers.is_empty() {
625 instance
626 .variant_mig_numbers
627 .iter()
628 .map(|s| s.as_str())
629 .collect()
630 } else {
631 collect_instance_mig_numbers(instance)
632 };
633
634 let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
635 let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
636 let mut ahb_status: Option<&'a str> = None;
637
638 let merged = rules.and_then(|rules| {
643 let own: Vec<&'a AhbFieldRule> = rules
644 .iter()
645 .copied()
646 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none())
647 .collect();
648 merged_variant(&own, instance)
649 });
650
651 let span = variant_numbers
656 .iter()
657 .filter_map(|n| n.parse::<u32>().ok())
658 .fold(None, |acc: Option<(u32, u32)>, n| {
659 Some(acc.map_or((n, n), |(lo, hi)| (lo.min(n), hi.max(n))))
660 });
661
662 let handed_down = narrowed
675 && rules.is_some_and(|rules| {
676 let own: Vec<&&'a AhbFieldRule> = rules
677 .iter()
678 .filter(|r| {
679 extract_child_group(strip_n_groups(&r.segment_path, strip_count)).is_none()
680 })
681 .collect();
682 !own.is_empty()
683 && !own.iter().any(|r| {
684 r.mig_number
685 .as_deref()
686 .is_some_and(|n| variant_numbers.contains(n))
687 })
688 });
689 if let Some(rules) = rules {
690 for rule in rules {
691 let stripped = strip_n_groups(&rule.segment_path, strip_count);
693 let child = extract_child_group(stripped);
694
695 if let Some(ref rule_mig) = rule.mig_number {
698 let number = rule_mig.parse::<u32>().ok();
699 let belongs = match (&merged, child) {
700 (Some((_, range)), _) => number.is_some_and(|m| range.contains(&m)),
701 (None, _) if handed_down => true,
702 (None, Some(_)) => match (number, span) {
703 (Some(m), Some((lo, hi))) => (lo..=hi).contains(&m),
704 _ => variant_numbers.contains(rule_mig.as_str()),
705 },
706 (None, None) => variant_numbers.contains(rule_mig.as_str()),
707 };
708 if !belongs {
709 continue;
710 }
711 }
712
713 if let Some(child_group_id) = child {
714 child_group_rules
715 .entry(child_group_id.to_owned())
716 .or_default()
717 .push(rule);
718 } else {
719 direct_rules.push(rule);
720 }
721 }
722 }
723 let entry = merged
728 .as_ref()
729 .map(|(entry, _)| *entry)
730 .or(instance.entry_mig_number.as_deref());
731 let status_of = |read: fn(&'a AhbFieldRule) -> Option<&'a str>| {
736 let entry_rules: Vec<&&'a AhbFieldRule> = direct_rules
737 .iter()
738 .filter(|r| entry.is_some() && r.mig_number.as_deref() == entry)
739 .collect();
740 if !entry_rules.is_empty() {
741 return entry_rules.iter().find_map(|r| read(r));
742 }
743 direct_rules
744 .iter()
745 .filter(|r| r.mig_number.is_some())
746 .find_map(|r| read(r))
747 };
748 if let Some(status) = status_of(|r| r.parent_group_ahb_status.as_deref()) {
749 ahb_status = Some(status);
750 }
751 let own_status = status_of(|r| r.group_ahb_status.as_deref());
752
753 let numbers: Option<Vec<Option<String>>> = merged.as_ref().map(|_| {
758 let mut used: Vec<&str> = Vec::new();
759 instance
760 .segments
761 .iter()
762 .map(|seg| {
763 let of_tag = |r: &&&'a AhbFieldRule| {
764 extract_segment_tag(&r.segment_path) == Some(seg.tag.as_str())
765 };
766 let mut slots: Vec<&str> = direct_rules
767 .iter()
768 .filter(of_tag)
769 .filter_map(|r| r.mig_number.as_deref())
770 .filter(|n| !used.contains(n))
771 .collect();
772 slots.sort_by_key(|n| n.parse::<u32>().unwrap_or(u32::MAX));
773 slots.dedup();
774 let coded = |slot: &str| {
777 direct_rules
778 .iter()
779 .filter(of_tag)
780 .filter(|r| r.mig_number.as_deref() == Some(slot) && !r.codes.is_empty())
781 .collect::<Vec<_>>()
782 };
783 let carries = |slot: &&str| {
784 coded(slot).iter().any(|r| {
785 seg.elements
786 .get(r.element_index.unwrap_or(0))
787 .and_then(|e| e.get(r.component_index.unwrap_or(0)))
788 .is_some_and(|v| r.codes.iter().any(|c| &c.value == v))
789 })
790 };
791 let slot = slots
792 .iter()
793 .find(|s| carries(s))
794 .or_else(|| slots.iter().find(|s| coded(s).is_empty()))
795 .or_else(|| slots.first())
796 .copied();
797 if let Some(slot) = slot {
798 used.push(slot);
799 }
800 slot.map(str::to_string)
801 })
802 .collect()
803 });
804 let numbers = if handed_down && numbers.is_none() {
807 Some(vec![None; instance.segments.len()])
808 } else {
809 numbers
810 };
811 let fields = resolve_fields(&direct_rules, &instance.segments, numbers.as_deref());
812
813 let children = resolve_groups(
815 &instance.child_groups,
816 &child_group_rules,
817 strip_count,
818 merged.is_some(),
819 );
820
821 let mut absent_children = Vec::new();
822 for (child_id, rules) in &child_group_rules {
823 absent_children.extend(absent_variant_rules(
824 child_id,
825 rules,
826 &children,
827 strip_count + 1,
828 ));
829 }
830 absent_children.sort_by_key(|r| r.mig_number.as_deref().and_then(|n| n.parse::<u32>().ok()));
831 let mut variant_rules = direct_rules;
832 variant_rules.extend(child_group_rules.into_values().flatten());
833
834 AhbGroupNode {
835 group_id,
836 instance_index: 0,
837 ahb_status,
838 own_status,
839 fields,
840 children,
841 variant_rules,
842 absent_children,
843 }
844}
845
846type Profile<'a> = std::collections::BTreeMap<(usize, usize), Vec<&'a str>>;
848
849fn absent_variant_rules<'a>(
862 child_id: &str,
863 rules: &[&'a AhbFieldRule],
864 children: &[AhbGroupNode<'a>],
865 strip: usize,
866) -> Vec<&'a AhbFieldRule> {
867 let number = |r: &AhbFieldRule| r.mig_number.as_deref()?.parse::<u32>().ok();
868 let own: Vec<&'a AhbFieldRule> = rules
869 .iter()
870 .copied()
871 .filter(|r| extract_child_group(strip_n_groups(&r.segment_path, strip)).is_none())
872 .filter(|r| number(r).is_some())
873 .collect();
874 let Some(entry_tag) = own
875 .iter()
876 .min_by_key(|r| number(r))
877 .and_then(|r| extract_segment_tag(&r.segment_path))
878 else {
879 return Vec::new();
880 };
881 let is_entry_tag = |r: &AhbFieldRule| extract_segment_tag(&r.segment_path) == Some(entry_tag);
882 let starts: std::collections::BTreeSet<u32> = own
883 .iter()
884 .filter(|r| is_entry_tag(r))
885 .filter_map(|r| number(r))
886 .collect();
887 let mut entries: Vec<(u32, &'a AhbFieldRule, Profile<'a>)> = Vec::new();
890 for &start in &starts {
891 let mut profile = Profile::new();
892 let mut entry = None;
893 for r in own
894 .iter()
895 .copied()
896 .filter(|r| is_entry_tag(r) && number(r) == Some(start))
897 {
898 entry.get_or_insert(r);
899 profile
900 .entry((r.element_index.unwrap_or(0), r.component_index.unwrap_or(0)))
901 .or_default()
902 .extend(r.codes.iter().map(|c| c.value.as_str()));
903 }
904 if let Some(entry) = entry {
905 entries.push((start, entry, profile));
906 }
907 }
908 let coded = |p: &Profile| p.values().any(|codes| !codes.is_empty());
909 let apart = |a: &Profile, b: &Profile| {
911 a.iter().any(|(pos, codes)| {
912 !codes.is_empty()
913 && b.get(pos).map_or(true, |other| {
914 !other.is_empty() && !codes.iter().any(|c| other.contains(c))
915 })
916 })
917 };
918 let distinct = entries.iter().all(|(_, _, p)| coded(p))
919 && entries.iter().enumerate().all(|(i, (_, _, a))| {
920 entries[i + 1..]
921 .iter()
922 .all(|(_, _, b)| apart(a, b) || apart(b, a))
923 });
924 if !distinct {
925 return Vec::new();
926 }
927 if entries.len() == 1 && children.iter().any(|c| c.group_id == child_id) {
932 return Vec::new();
933 }
934 let claimed: std::collections::HashSet<&str> = children
935 .iter()
936 .filter(|c| c.group_id == child_id)
937 .flat_map(|c| c.fields.iter().filter_map(|n| n.rule.mig_number.as_deref()))
938 .collect();
939 let mut out = Vec::new();
940 for (i, (start, entry, _)) in entries.iter().enumerate() {
941 if claimed.contains(entry.mig_number.as_deref().unwrap_or_default()) {
942 continue;
943 }
944 let end = entries.get(i + 1).map_or(u32::MAX, |(n, _, _)| *n);
945 out.extend(
946 own.iter()
947 .copied()
948 .filter(|r| number(r).is_some_and(|n| (*start..end).contains(&n))),
949 );
950 }
951 out
952}
953
954fn collect_instance_mig_numbers(
956 instance: &AssembledGroupInstance,
957) -> std::collections::HashSet<&str> {
958 let mut numbers = std::collections::HashSet::new();
959 for seg in &instance.segments {
960 if let Some(ref num) = seg.mig_number {
961 numbers.insert(num.as_str());
962 }
963 }
964 for child_group in &instance.child_groups {
965 for child_instance in &child_group.repetitions {
966 numbers.extend(collect_instance_mig_numbers(child_instance));
967 }
968 }
969 numbers
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
976 use mig_assembly::assembler::{
977 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
978 };
979 use std::collections::BTreeMap;
980
981 fn empty_workflow() -> AhbWorkflow {
982 AhbWorkflow {
983 pruefidentifikator: "11001".to_string(),
984 description: String::new(),
985 communication_direction: None,
986 fields: vec![],
987 ub_definitions: BTreeMap::new(),
988 package_conditions: Default::default(),
989 }
990 }
991
992 fn empty_tree() -> AssembledTree {
993 AssembledTree {
994 segments: vec![],
995 groups: vec![],
996 post_group_start: 0,
997 inter_group_segments: BTreeMap::new(),
998 }
999 }
1000
1001 fn make_segment(
1002 tag: &str,
1003 elements: Vec<Vec<&str>>,
1004 mig_number: Option<&str>,
1005 ) -> AssembledSegment {
1006 AssembledSegment {
1007 tag: tag.to_string(),
1008 elements: elements
1009 .into_iter()
1010 .map(|e| e.into_iter().map(|s| s.to_string()).collect())
1011 .collect(),
1012 mig_number: mig_number.map(|s| s.to_string()),
1013 segment_number: None,
1014 }
1015 }
1016
1017 fn make_rule(
1018 segment_path: &str,
1019 name: &str,
1020 ahb_status: &str,
1021 mig_number: Option<&str>,
1022 element_index: Option<usize>,
1023 component_index: Option<usize>,
1024 ) -> AhbFieldRule {
1025 AhbFieldRule {
1026 group_ahb_status: None,
1027 segment_path: segment_path.to_string(),
1028 name: name.to_string(),
1029 ahb_status: ahb_status.to_string(),
1030 codes: vec![],
1031 parent_group_ahb_status: None,
1032 segment_ahb_status: None,
1033 element_index,
1034 component_index,
1035 mig_number: mig_number.map(|s| s.to_string()),
1036 format: None,
1037 }
1038 }
1039
1040 #[test]
1041 fn test_empty_workflow_empty_tree() {
1042 let workflow = empty_workflow();
1043 let tree = empty_tree();
1044 let result = build_validated_tree(&workflow, &tree);
1045
1046 assert_eq!(result.pruefidentifikator, "11001");
1047 assert!(result.root_fields.is_empty());
1048 assert!(result.groups.is_empty());
1049 }
1050
1051 #[test]
1052 fn test_root_field_matches_root_segment() {
1053 let mut workflow = empty_workflow();
1054 workflow.fields.push(make_rule(
1055 "BGM/C002/1001",
1056 "Nachrichtentyp",
1057 "X",
1058 Some("0001"),
1059 Some(0),
1060 Some(0),
1061 ));
1062
1063 let tree = AssembledTree {
1064 segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
1065 groups: vec![],
1066 post_group_start: 1,
1067 inter_group_segments: BTreeMap::new(),
1068 };
1069
1070 let result = build_validated_tree(&workflow, &tree);
1071
1072 assert_eq!(result.root_fields.len(), 1);
1073 let node = &result.root_fields[0];
1074 assert_eq!(node.value, Some("E01"));
1075 assert!(node.segment_elements.is_some());
1076 assert_eq!(node.rule.name, "Nachrichtentyp");
1077 }
1078
1079 #[test]
1080 fn test_sg4_dtm_mig_number_matching() {
1081 let mut workflow = empty_workflow();
1084
1085 workflow.fields.push(make_rule(
1087 "SG4/DTM/C507/2380",
1088 "Eingangsdatum",
1089 "X",
1090 Some("0082"),
1091 Some(1), Some(0),
1093 ));
1094
1095 workflow.fields.push(make_rule(
1097 "SG4/DTM/C507/2380",
1098 "Dokumentendatum",
1099 "X",
1100 Some("0083"),
1101 Some(1),
1102 Some(0),
1103 ));
1104
1105 let tree = AssembledTree {
1106 segments: vec![],
1107 groups: vec![AssembledGroup {
1108 group_id: "SG4".to_string(),
1109 repetitions: vec![AssembledGroupInstance {
1110 segments: vec![
1111 make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
1112 make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
1113 ],
1114 child_groups: vec![],
1115 entry_mig_number: None,
1116 variant_mig_numbers: vec![],
1117 skipped_segments: vec![],
1118 skipped_positions: Vec::new(),
1119 }],
1120 }],
1121 post_group_start: 0,
1122 inter_group_segments: BTreeMap::new(),
1123 };
1124
1125 let result = build_validated_tree(&workflow, &tree);
1126
1127 assert_eq!(result.groups.len(), 1);
1128 let sg4 = &result.groups[0];
1129 assert_eq!(sg4.group_id, "SG4");
1130 assert_eq!(sg4.fields.len(), 2);
1131
1132 let eingangsdatum = &sg4.fields[0];
1134 assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
1135 assert_eq!(eingangsdatum.value, Some("20260101"));
1136
1137 let dokumentendatum = &sg4.fields[1];
1139 assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
1140 assert_eq!(dokumentendatum.value, Some("20260401"));
1141 }
1142
1143 #[test]
1144 fn test_rule_filtered_to_correct_variant() {
1145 let mut workflow = empty_workflow();
1149 workflow.fields.push(make_rule(
1150 "SG4/RFF/C506/1154",
1151 "Referenz",
1152 "X",
1153 Some("0099"),
1154 Some(0),
1155 Some(1),
1156 ));
1157
1158 let tree = AssembledTree {
1159 segments: vec![],
1160 groups: vec![AssembledGroup {
1161 group_id: "SG4".to_string(),
1162 repetitions: vec![AssembledGroupInstance {
1163 segments: vec![], child_groups: vec![],
1165 entry_mig_number: None,
1166 variant_mig_numbers: vec![],
1167 skipped_segments: vec![],
1168 skipped_positions: Vec::new(),
1169 }],
1170 }],
1171 post_group_start: 0,
1172 inter_group_segments: BTreeMap::new(),
1173 };
1174
1175 let result = build_validated_tree(&workflow, &tree);
1176 assert_eq!(result.groups.len(), 1);
1177 assert_eq!(result.groups[0].fields.len(), 0);
1179 }
1180
1181 #[test]
1182 fn test_missing_segment_within_correct_variant() {
1183 let mut workflow = empty_workflow();
1187 workflow.fields.push(make_rule(
1189 "SG4/SEQ/1229",
1190 "Qualifier",
1191 "X",
1192 Some("0098"),
1193 Some(0),
1194 Some(0),
1195 ));
1196 workflow.fields.push(make_rule(
1198 "SG4/RFF/C506/1154",
1199 "Referenz",
1200 "X",
1201 Some("0099"),
1202 Some(0),
1203 Some(1),
1204 ));
1205
1206 let tree = AssembledTree {
1207 segments: vec![],
1208 groups: vec![AssembledGroup {
1209 group_id: "SG4".to_string(),
1210 repetitions: vec![AssembledGroupInstance {
1211 segments: vec![
1212 make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
1214 ],
1215 child_groups: vec![],
1216 entry_mig_number: Some("0098".to_string()),
1217 variant_mig_numbers: vec!["0098".to_string(), "0099".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 assert_eq!(result.groups.len(), 1);
1229 assert_eq!(result.groups[0].fields.len(), 2);
1231 assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
1232 assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
1233 assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
1235 assert_eq!(result.groups[0].fields[1].value, None);
1236 }
1237
1238 #[test]
1239 fn test_missing_group_variant_populates_unmatched_rules() {
1240 let mut workflow = empty_workflow();
1244
1245 workflow.fields.push(make_rule(
1247 "SG2/NAD/3035",
1248 "MP-ID Absender Qualifier",
1249 "X",
1250 Some("0010"),
1251 Some(0),
1252 Some(0),
1253 ));
1254 workflow.fields.push(make_rule(
1256 "SG2/NAD/C082/3039",
1257 "MP-ID Absender",
1258 "X",
1259 Some("0010"),
1260 Some(1),
1261 Some(0),
1262 ));
1263 workflow.fields.push(make_rule(
1265 "SG2/NAD/3035",
1266 "MP-ID Empfänger Qualifier",
1267 "X",
1268 Some("0011"),
1269 Some(0),
1270 Some(0),
1271 ));
1272 workflow.fields.push(make_rule(
1274 "SG2/NAD/C082/3039",
1275 "MP-ID Empfänger",
1276 "X",
1277 Some("0011"),
1278 Some(1),
1279 Some(0),
1280 ));
1281
1282 let tree = AssembledTree {
1284 segments: vec![],
1285 groups: vec![AssembledGroup {
1286 group_id: "SG2".to_string(),
1287 repetitions: vec![AssembledGroupInstance {
1288 segments: vec![make_segment(
1289 "NAD",
1290 vec![vec!["MR"], vec!["9900269000000", "", "293"]],
1291 Some("0011"),
1292 )],
1293 child_groups: vec![],
1294 entry_mig_number: Some("0011".to_string()),
1295 variant_mig_numbers: vec!["0011".to_string()],
1296 skipped_segments: vec![],
1297 skipped_positions: Vec::new(),
1298 }],
1299 }],
1300 post_group_start: 0,
1301 inter_group_segments: BTreeMap::new(),
1302 };
1303
1304 let result = build_validated_tree(&workflow, &tree);
1305
1306 assert_eq!(result.groups.len(), 1);
1308 assert_eq!(result.groups[0].fields.len(), 2);
1309 assert_eq!(result.groups[0].fields[0].value, Some("MR"));
1310
1311 assert_eq!(
1313 result.unmatched_rules.len(),
1314 2,
1315 "Expected 2 unmatched rules (NAD+MS), got {}",
1316 result.unmatched_rules.len()
1317 );
1318 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1319 assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
1320 }
1321
1322 #[test]
1323 fn test_entirely_absent_group_populates_unmatched_rules() {
1324 let mut workflow = empty_workflow();
1326 workflow.fields.push(make_rule(
1327 "SG2/NAD/3035",
1328 "MP-ID Absender Qualifier",
1329 "X",
1330 Some("0010"),
1331 Some(0),
1332 Some(0),
1333 ));
1334
1335 let tree = empty_tree(); let result = build_validated_tree(&workflow, &tree);
1338
1339 assert!(result.groups.is_empty());
1341
1342 assert_eq!(
1344 result.unmatched_rules.len(),
1345 1,
1346 "Expected 1 unmatched rule, got {}",
1347 result.unmatched_rules.len()
1348 );
1349 assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
1350 }
1351
1352 #[test]
1353 fn test_fallback_to_tag_when_no_mig_number() {
1354 let mut workflow = empty_workflow();
1355 workflow.fields.push(make_rule(
1356 "BGM/C002/1001",
1357 "Nachrichtentyp",
1358 "X",
1359 None, Some(0),
1361 Some(0),
1362 ));
1363
1364 let tree = AssembledTree {
1365 segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
1366 groups: vec![],
1367 post_group_start: 1,
1368 inter_group_segments: BTreeMap::new(),
1369 };
1370
1371 let result = build_validated_tree(&workflow, &tree);
1372 assert_eq!(result.root_fields.len(), 1);
1373 assert_eq!(result.root_fields[0].value, Some("E01"));
1374 }
1375
1376 #[test]
1377 fn test_nested_child_groups() {
1378 let mut workflow = empty_workflow();
1379 workflow.fields.push(make_rule(
1381 "SG4/SG5/LOC/C517/3225",
1382 "Marktlokations-ID",
1383 "X",
1384 Some("0050"),
1385 Some(0),
1386 Some(0),
1387 ));
1388
1389 let tree = AssembledTree {
1390 segments: vec![],
1391 groups: vec![AssembledGroup {
1392 group_id: "SG4".to_string(),
1393 repetitions: vec![AssembledGroupInstance {
1394 segments: vec![],
1395 entry_mig_number: None,
1396 child_groups: vec![AssembledGroup {
1397 group_id: "SG5".to_string(),
1398 repetitions: vec![AssembledGroupInstance {
1399 segments: vec![make_segment(
1400 "LOC",
1401 vec![vec!["DE00012345678"]],
1402 Some("0050"),
1403 )],
1404 child_groups: vec![],
1405 entry_mig_number: None,
1406 variant_mig_numbers: vec![],
1407 skipped_segments: vec![],
1408 skipped_positions: Vec::new(),
1409 }],
1410 }],
1411 variant_mig_numbers: vec![],
1412 skipped_segments: vec![],
1413 skipped_positions: Vec::new(),
1414 }],
1415 }],
1416 post_group_start: 0,
1417 inter_group_segments: BTreeMap::new(),
1418 };
1419
1420 let result = build_validated_tree(&workflow, &tree);
1421 assert_eq!(result.groups.len(), 1);
1422 let sg4 = &result.groups[0];
1423 assert_eq!(sg4.children.len(), 1);
1424
1425 let sg5 = &sg4.children[0];
1426 assert_eq!(sg5.group_id, "SG5");
1427 assert_eq!(sg5.fields.len(), 1);
1428 assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
1429 assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
1430 }
1431
1432 #[test]
1433 fn test_qualifier_aware_segment_matching_same_tag_variants() {
1434 let mut workflow = empty_workflow();
1448
1449 let mut z33 = make_rule(
1451 "SG10/STS/C601/9015",
1452 "Statuskategorie Z33",
1453 "X",
1454 Some("00035"),
1455 Some(0),
1456 Some(0),
1457 );
1458 z33.codes = vec![super::super::validate::AhbCodeRule {
1459 value: "Z33".into(),
1460 description: String::new(),
1461 ahb_status: "X".into(),
1462 }];
1463 workflow.fields.push(z33);
1464
1465 let mut z32 = make_rule(
1467 "SG10/STS/C601/9015",
1468 "Statuskategorie Z32",
1469 "X",
1470 Some("00036"),
1471 Some(0),
1472 Some(0),
1473 );
1474 z32.codes = vec![super::super::validate::AhbCodeRule {
1475 value: "Z32".into(),
1476 description: String::new(),
1477 ahb_status: "X".into(),
1478 }];
1479 workflow.fields.push(z32);
1480
1481 let mut z40 = make_rule(
1483 "SG10/STS/C601/9015",
1484 "Statuskategorie Z40",
1485 "X",
1486 Some("00038"),
1487 Some(0),
1488 Some(0),
1489 );
1490 z40.codes = vec![super::super::validate::AhbCodeRule {
1491 value: "Z40".into(),
1492 description: String::new(),
1493 ahb_status: "X".into(),
1494 }];
1495 workflow.fields.push(z40);
1496
1497 let tree = AssembledTree {
1500 segments: vec![],
1501 groups: vec![AssembledGroup {
1502 group_id: "SG10".to_string(),
1503 repetitions: vec![AssembledGroupInstance {
1504 segments: vec![
1505 make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
1506 make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
1507 ],
1508 child_groups: vec![],
1509 entry_mig_number: None,
1510 variant_mig_numbers: vec![
1511 "00035".into(),
1512 "00036".into(),
1513 "00037".into(),
1514 "00038".into(),
1515 ],
1516 skipped_segments: vec![],
1517 skipped_positions: Vec::new(),
1518 }],
1519 }],
1520 post_group_start: 0,
1521 inter_group_segments: BTreeMap::new(),
1522 };
1523
1524 let result = build_validated_tree(&workflow, &tree);
1525 assert_eq!(result.groups.len(), 1);
1526 let sg10 = &result.groups[0];
1527 assert_eq!(
1528 sg10.fields.len(),
1529 3,
1530 "all three rules should pass the variant filter"
1531 );
1532
1533 let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
1534
1535 assert_eq!(
1537 by_name("Statuskategorie Z33").value,
1538 None,
1539 "Z33 has no matching STS in the input; must not silently pick up Z32's segment"
1540 );
1541
1542 assert_eq!(
1545 by_name("Statuskategorie Z32").value,
1546 Some("Z32"),
1547 "rule for Z32 must attach to the STS segment with qualifier Z32"
1548 );
1549
1550 assert_eq!(
1552 by_name("Statuskategorie Z40").value,
1553 Some("Z40"),
1554 "rule for Z40 must attach to the STS segment with qualifier Z40"
1555 );
1556 }
1557}