use std::collections::HashMap;
use crate::expr::ConditionExpr;
use super::validate::{AhbFieldRule, AhbWorkflow};
use mig_assembly::assembler::{
AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
};
#[derive(Debug)]
pub struct AhbNode<'a> {
pub rule: &'a AhbFieldRule,
pub value: Option<&'a str>,
pub segment_elements: Option<&'a [Vec<String>]>,
}
#[derive(Debug)]
pub struct AhbGroupNode<'a> {
pub group_id: &'a str,
pub ahb_status: Option<&'a str>,
pub fields: Vec<AhbNode<'a>>,
pub children: Vec<AhbGroupNode<'a>>,
}
#[derive(Debug)]
pub struct ValidatedTree<'a> {
pub pruefidentifikator: &'a str,
pub ub_definitions: &'a HashMap<String, ConditionExpr>,
pub root_fields: Vec<AhbNode<'a>>,
pub groups: Vec<AhbGroupNode<'a>>,
pub unmatched_rules: Vec<&'a AhbFieldRule>,
}
pub fn build_validated_tree<'a>(
workflow: &'a AhbWorkflow,
tree: &'a AssembledTree,
) -> ValidatedTree<'a> {
let mut root_rules: Vec<&'a AhbFieldRule> = Vec::new();
let mut group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
for rule in &workflow.fields {
match extract_top_group(&rule.segment_path) {
Some(group_id) => {
group_rules
.entry(group_id.to_owned())
.or_default()
.push(rule);
}
None => {
root_rules.push(rule);
}
}
}
let root_fields = resolve_fields(&root_rules, &tree.segments);
let groups = resolve_groups(&tree.groups, &group_rules, 0);
let mut matched_mig_numbers: std::collections::HashSet<&str> = std::collections::HashSet::new();
collect_matched_mig_numbers_from_groups(&groups, &mut matched_mig_numbers);
for seg in &tree.segments {
if let Some(ref num) = seg.mig_number {
matched_mig_numbers.insert(num.as_str());
}
}
let unmatched_rules: Vec<&AhbFieldRule> = workflow
.fields
.iter()
.filter(|rule| {
rule.mig_number
.as_ref()
.is_some_and(|num| !matched_mig_numbers.contains(num.as_str()))
})
.collect();
ValidatedTree {
pruefidentifikator: &workflow.pruefidentifikator,
ub_definitions: &workflow.ub_definitions,
root_fields,
groups,
unmatched_rules,
}
}
fn collect_matched_mig_numbers_from_groups<'a>(
groups: &[AhbGroupNode<'a>],
out: &mut std::collections::HashSet<&'a str>,
) {
for group in groups {
for node in &group.fields {
if let Some(ref num) = node.rule.mig_number {
out.insert(num.as_str());
}
}
collect_matched_mig_numbers_from_groups(&group.children, out);
}
}
fn extract_top_group(segment_path: &str) -> Option<&str> {
let first = segment_path.split('/').next()?;
if first.starts_with("SG") {
Some(first)
} else {
None
}
}
fn extract_child_group(stripped_path: &str) -> Option<&str> {
let first = stripped_path.split('/').next()?;
if first.starts_with("SG") {
Some(first)
} else {
None
}
}
fn resolve_fields<'a>(
rules: &[&'a AhbFieldRule],
segments: &'a [AssembledSegment],
) -> Vec<AhbNode<'a>> {
rules
.iter()
.map(|rule| {
let matched_segment = find_segment(rule, segments);
let (value, elements) = match matched_segment {
Some(seg) => {
let val = extract_value(seg, rule);
(val, Some(seg.elements.as_slice()))
}
None => (None, None),
};
AhbNode {
rule,
value,
segment_elements: elements,
}
})
.collect()
}
fn find_segment<'a>(
rule: &AhbFieldRule,
segments: &'a [AssembledSegment],
) -> Option<&'a AssembledSegment> {
let tag = extract_segment_tag(&rule.segment_path)?;
if !rule.codes.is_empty() {
let el_idx = rule.element_index.unwrap_or(0);
let comp_idx = rule.component_index.unwrap_or(0);
let expected: std::collections::HashSet<&str> =
rule.codes.iter().map(|c| c.value.as_str()).collect();
if let Some(seg) = segments.iter().find(|s| {
s.tag == tag
&& s.elements
.get(el_idx)
.and_then(|e| e.get(comp_idx))
.is_some_and(|v| expected.contains(v.as_str()))
}) {
return Some(seg);
}
let same_tag_count = segments.iter().filter(|s| s.tag == tag).count();
if same_tag_count > 1 {
return None;
}
}
if let Some(ref mig_num) = rule.mig_number {
if let Some(seg) = segments
.iter()
.find(|s| s.mig_number.as_deref() == Some(mig_num.as_str()))
{
return Some(seg);
}
return segments
.iter()
.find(|s| s.tag == tag && s.mig_number.as_ref().map_or(true, |m| m == mig_num));
}
segments.iter().find(|s| s.tag == tag)
}
fn extract_segment_tag(segment_path: &str) -> Option<&str> {
segment_path.split('/').find(|part| !part.starts_with("SG"))
}
fn extract_value<'a>(segment: &'a AssembledSegment, rule: &AhbFieldRule) -> Option<&'a str> {
let elem_idx = rule.element_index.unwrap_or(0);
let comp_idx = rule.component_index.unwrap_or(0);
let element = segment.elements.get(elem_idx)?;
let component = element.get(comp_idx)?;
if component.is_empty() {
None
} else {
Some(component.as_str())
}
}
fn resolve_groups<'a>(
assembled_groups: &'a [AssembledGroup],
group_rules: &HashMap<String, Vec<&'a AhbFieldRule>>,
depth: usize,
) -> Vec<AhbGroupNode<'a>> {
let mut result = Vec::new();
for assembled_group in assembled_groups {
let rules = group_rules.get(&assembled_group.group_id);
for instance in &assembled_group.repetitions {
let node = resolve_group_instance(&assembled_group.group_id, instance, rules, depth);
result.push(node);
}
}
result
}
fn strip_n_groups(path: &str, n: usize) -> &str {
let mut rest = path;
for _ in 0..n {
match rest.find('/') {
Some(idx) => rest = &rest[idx + 1..],
None => return rest,
}
}
rest
}
fn resolve_group_instance<'a>(
group_id: &'a str,
instance: &'a AssembledGroupInstance,
rules: Option<&Vec<&'a AhbFieldRule>>,
depth: usize,
) -> AhbGroupNode<'a> {
let strip_count = depth + 1;
let variant_numbers: std::collections::HashSet<&str> =
if !instance.variant_mig_numbers.is_empty() {
instance
.variant_mig_numbers
.iter()
.map(|s| s.as_str())
.collect()
} else {
collect_instance_mig_numbers(instance)
};
let mut direct_rules: Vec<&'a AhbFieldRule> = Vec::new();
let mut child_group_rules: HashMap<String, Vec<&'a AhbFieldRule>> = HashMap::new();
let mut ahb_status: Option<&'a str> = None;
if let Some(rules) = rules {
for rule in rules {
if let Some(ref rule_mig) = rule.mig_number {
if !variant_numbers.contains(rule_mig.as_str()) {
continue;
}
}
let stripped = strip_n_groups(&rule.segment_path, strip_count);
if let Some(child_group_id) = extract_child_group(stripped) {
child_group_rules
.entry(child_group_id.to_owned())
.or_default()
.push(rule);
} else {
direct_rules.push(rule);
}
if ahb_status.is_none() {
if let Some(ref status) = rule.parent_group_ahb_status {
ahb_status = Some(status.as_str());
}
}
}
}
let fields = resolve_fields(&direct_rules, &instance.segments);
let children = resolve_groups(&instance.child_groups, &child_group_rules, strip_count);
AhbGroupNode {
group_id,
ahb_status,
fields,
children,
}
}
fn collect_instance_mig_numbers(
instance: &AssembledGroupInstance,
) -> std::collections::HashSet<&str> {
let mut numbers = std::collections::HashSet::new();
for seg in &instance.segments {
if let Some(ref num) = seg.mig_number {
numbers.insert(num.as_str());
}
}
for child_group in &instance.child_groups {
for child_instance in &child_group.repetitions {
numbers.extend(collect_instance_mig_numbers(child_instance));
}
}
numbers
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validator::validate::{AhbFieldRule, AhbWorkflow};
use mig_assembly::assembler::{
AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
};
use std::collections::{BTreeMap, HashMap};
fn empty_workflow() -> AhbWorkflow {
AhbWorkflow {
pruefidentifikator: "11001".to_string(),
description: String::new(),
communication_direction: None,
fields: vec![],
ub_definitions: HashMap::new(),
}
}
fn empty_tree() -> AssembledTree {
AssembledTree {
segments: vec![],
groups: vec![],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
}
}
fn make_segment(
tag: &str,
elements: Vec<Vec<&str>>,
mig_number: Option<&str>,
) -> AssembledSegment {
AssembledSegment {
tag: tag.to_string(),
elements: elements
.into_iter()
.map(|e| e.into_iter().map(|s| s.to_string()).collect())
.collect(),
mig_number: mig_number.map(|s| s.to_string()),
}
}
fn make_rule(
segment_path: &str,
name: &str,
ahb_status: &str,
mig_number: Option<&str>,
element_index: Option<usize>,
component_index: Option<usize>,
) -> AhbFieldRule {
AhbFieldRule {
segment_path: segment_path.to_string(),
name: name.to_string(),
ahb_status: ahb_status.to_string(),
codes: vec![],
parent_group_ahb_status: None,
element_index,
component_index,
mig_number: mig_number.map(|s| s.to_string()),
}
}
#[test]
fn test_empty_workflow_empty_tree() {
let workflow = empty_workflow();
let tree = empty_tree();
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.pruefidentifikator, "11001");
assert!(result.root_fields.is_empty());
assert!(result.groups.is_empty());
}
#[test]
fn test_root_field_matches_root_segment() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"BGM/C002/1001",
"Nachrichtentyp",
"X",
Some("0001"),
Some(0),
Some(0),
));
let tree = AssembledTree {
segments: vec![make_segment("BGM", vec![vec!["E01"]], Some("0001"))],
groups: vec![],
post_group_start: 1,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.root_fields.len(), 1);
let node = &result.root_fields[0];
assert_eq!(node.value, Some("E01"));
assert!(node.segment_elements.is_some());
assert_eq!(node.rule.name, "Nachrichtentyp");
}
#[test]
fn test_sg4_dtm_mig_number_matching() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG4/DTM/C507/2380",
"Eingangsdatum",
"X",
Some("0082"),
Some(1), Some(0),
));
workflow.fields.push(make_rule(
"SG4/DTM/C507/2380",
"Dokumentendatum",
"X",
Some("0083"),
Some(1),
Some(0),
));
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![
make_segment("DTM", vec![vec!["92"], vec!["20260101"]], Some("0082")),
make_segment("DTM", vec![vec!["137"], vec!["20260401"]], Some("0083")),
],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
let sg4 = &result.groups[0];
assert_eq!(sg4.group_id, "SG4");
assert_eq!(sg4.fields.len(), 2);
let eingangsdatum = &sg4.fields[0];
assert_eq!(eingangsdatum.rule.name, "Eingangsdatum");
assert_eq!(eingangsdatum.value, Some("20260101"));
let dokumentendatum = &sg4.fields[1];
assert_eq!(dokumentendatum.rule.name, "Dokumentendatum");
assert_eq!(dokumentendatum.value, Some("20260401"));
}
#[test]
fn test_rule_filtered_to_correct_variant() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG4/RFF/C506/1154",
"Referenz",
"X",
Some("0099"),
Some(0),
Some(1),
));
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![], child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].fields.len(), 0);
}
#[test]
fn test_missing_segment_within_correct_variant() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG4/SEQ/1229",
"Qualifier",
"X",
Some("0098"),
Some(0),
Some(0),
));
workflow.fields.push(make_rule(
"SG4/RFF/C506/1154",
"Referenz",
"X",
Some("0099"),
Some(0),
Some(1),
));
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![
make_segment("SEQ", vec![vec!["Z01"]], Some("0098")),
],
child_groups: vec![],
entry_mig_number: Some("0098".to_string()),
variant_mig_numbers: vec!["0098".to_string(), "0099".to_string()],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].fields.len(), 2);
assert_eq!(result.groups[0].fields[0].rule.name, "Qualifier");
assert_eq!(result.groups[0].fields[0].value, Some("Z01"));
assert_eq!(result.groups[0].fields[1].rule.name, "Referenz");
assert_eq!(result.groups[0].fields[1].value, None);
}
#[test]
fn test_missing_group_variant_populates_unmatched_rules() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG2/NAD/3035",
"MP-ID Absender Qualifier",
"X",
Some("0010"),
Some(0),
Some(0),
));
workflow.fields.push(make_rule(
"SG2/NAD/C082/3039",
"MP-ID Absender",
"X",
Some("0010"),
Some(1),
Some(0),
));
workflow.fields.push(make_rule(
"SG2/NAD/3035",
"MP-ID Empfänger Qualifier",
"X",
Some("0011"),
Some(0),
Some(0),
));
workflow.fields.push(make_rule(
"SG2/NAD/C082/3039",
"MP-ID Empfänger",
"X",
Some("0011"),
Some(1),
Some(0),
));
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG2".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![make_segment(
"NAD",
vec![vec!["MR"], vec!["9900269000000", "", "293"]],
Some("0011"),
)],
child_groups: vec![],
entry_mig_number: Some("0011".to_string()),
variant_mig_numbers: vec!["0011".to_string()],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].fields.len(), 2);
assert_eq!(result.groups[0].fields[0].value, Some("MR"));
assert_eq!(
result.unmatched_rules.len(),
2,
"Expected 2 unmatched rules (NAD+MS), got {}",
result.unmatched_rules.len()
);
assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
assert_eq!(result.unmatched_rules[1].name, "MP-ID Absender");
}
#[test]
fn test_entirely_absent_group_populates_unmatched_rules() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG2/NAD/3035",
"MP-ID Absender Qualifier",
"X",
Some("0010"),
Some(0),
Some(0),
));
let tree = empty_tree();
let result = build_validated_tree(&workflow, &tree);
assert!(result.groups.is_empty());
assert_eq!(
result.unmatched_rules.len(),
1,
"Expected 1 unmatched rule, got {}",
result.unmatched_rules.len()
);
assert_eq!(result.unmatched_rules[0].name, "MP-ID Absender Qualifier");
}
#[test]
fn test_fallback_to_tag_when_no_mig_number() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"BGM/C002/1001",
"Nachrichtentyp",
"X",
None, Some(0),
Some(0),
));
let tree = AssembledTree {
segments: vec![make_segment("BGM", vec![vec!["E01"]], None)],
groups: vec![],
post_group_start: 1,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.root_fields.len(), 1);
assert_eq!(result.root_fields[0].value, Some("E01"));
}
#[test]
fn test_nested_child_groups() {
let mut workflow = empty_workflow();
workflow.fields.push(make_rule(
"SG4/SG5/LOC/C517/3225",
"Marktlokations-ID",
"X",
Some("0050"),
Some(0),
Some(0),
));
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG4".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![],
entry_mig_number: None,
child_groups: vec![AssembledGroup {
group_id: "SG5".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![make_segment(
"LOC",
vec![vec!["DE00012345678"]],
Some("0050"),
)],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
}],
}],
variant_mig_numbers: vec![],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
let sg4 = &result.groups[0];
assert_eq!(sg4.children.len(), 1);
let sg5 = &sg4.children[0];
assert_eq!(sg5.group_id, "SG5");
assert_eq!(sg5.fields.len(), 1);
assert_eq!(sg5.fields[0].value, Some("DE00012345678"));
assert_eq!(sg5.fields[0].rule.name, "Marktlokations-ID");
}
#[test]
fn test_qualifier_aware_segment_matching_same_tag_variants() {
let mut workflow = empty_workflow();
let mut z33 = make_rule(
"SG10/STS/C601/9015",
"Statuskategorie Z33",
"X",
Some("00035"),
Some(0),
Some(0),
);
z33.codes = vec![super::super::validate::AhbCodeRule {
value: "Z33".into(),
description: String::new(),
ahb_status: "X".into(),
}];
workflow.fields.push(z33);
let mut z32 = make_rule(
"SG10/STS/C601/9015",
"Statuskategorie Z32",
"X",
Some("00036"),
Some(0),
Some(0),
);
z32.codes = vec![super::super::validate::AhbCodeRule {
value: "Z32".into(),
description: String::new(),
ahb_status: "X".into(),
}];
workflow.fields.push(z32);
let mut z40 = make_rule(
"SG10/STS/C601/9015",
"Statuskategorie Z40",
"X",
Some("00038"),
Some(0),
Some(0),
);
z40.codes = vec![super::super::validate::AhbCodeRule {
value: "Z40".into(),
description: String::new(),
ahb_status: "X".into(),
}];
workflow.fields.push(z40);
let tree = AssembledTree {
segments: vec![],
groups: vec![AssembledGroup {
group_id: "SG10".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![
make_segment("STS", vec![vec!["Z32"], vec![], vec!["Z92"]], Some("00035")),
make_segment("STS", vec![vec!["Z40"], vec![], vec!["Z75"]], Some("00036")),
],
child_groups: vec![],
entry_mig_number: None,
variant_mig_numbers: vec![
"00035".into(),
"00036".into(),
"00037".into(),
"00038".into(),
],
skipped_segments: vec![],
}],
}],
post_group_start: 0,
inter_group_segments: BTreeMap::new(),
};
let result = build_validated_tree(&workflow, &tree);
assert_eq!(result.groups.len(), 1);
let sg10 = &result.groups[0];
assert_eq!(sg10.fields.len(), 3, "all three rules should pass the variant filter");
let by_name = |name: &str| sg10.fields.iter().find(|f| f.rule.name == name).unwrap();
assert_eq!(
by_name("Statuskategorie Z33").value,
None,
"Z33 has no matching STS in the input; must not silently pick up Z32's segment"
);
assert_eq!(
by_name("Statuskategorie Z32").value,
Some("Z32"),
"rule for Z32 must attach to the STS segment with qualifier Z32"
);
assert_eq!(
by_name("Statuskategorie Z40").value,
Some("Z40"),
"rule for Z40 must attach to the STS segment with qualifier Z40"
);
}
}