use mig_types::schema::common::Cardinality;
use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};
use crate::assembler::{AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree};
use crate::diagnostic::{StructureDiagnostic, StructureDiagnosticKind};
pub fn check_structure(tree: &AssembledTree, pid_mig: &MigSchema) -> Vec<StructureDiagnostic> {
let mut out = Vec::new();
let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
root.extend(tree.inter_group_segments.values().flatten());
check_segments(&root, &pid_mig.segments, "message", 0, &mut out);
check_groups(
&tree.groups,
&pid_mig.segment_groups,
"message",
0,
&mut out,
);
out.sort_by_key(|d| d.position);
out
}
fn is_required(status_spec: Option<&str>, status_std: Option<&str>) -> bool {
let status = status_spec.or(status_std).unwrap_or("C");
matches!(
Cardinality::from_status(status),
Cardinality::Mandatory | Cardinality::Required
)
}
fn max_rep(spec: i32, std: i32) -> Option<usize> {
[spec, std].into_iter().find(|&n| n > 0).map(|n| n as usize)
}
fn is_placeholder(code: &mig_types::schema::common::CodeDefinition) -> bool {
code.name == "Beispielcode" || code.value == "XYZ"
}
fn qualifier(seg: &MigSegment) -> Option<(usize, usize, Vec<String>)> {
let simple = seg.data_elements.iter().map(|d| (d.position, 0, d));
let composite = seg.composites.iter().flat_map(|c| {
c.data_elements
.iter()
.map(move |d| (c.position, d.position, d))
});
simple
.chain(composite)
.filter(|(_, _, d)| d.codes.iter().any(|c| !c.value.is_empty()))
.min_by_key(|(e, c, _)| (*e, *c))
.and_then(|(e, c, d)| {
let codes: Vec<String> = d
.codes
.iter()
.filter(|c| !c.value.is_empty() && !is_placeholder(c))
.map(|c| c.value.clone())
.collect();
(!codes.is_empty()).then_some((e, c, codes))
})
}
fn value_at<'s>(elements: &'s [Vec<String>], q: &(usize, usize, Vec<String>)) -> Option<&'s str> {
let (e, c, _) = q;
elements
.get(*e)
.and_then(|el| el.get(*c))
.map(String::as_str)
.filter(|v| !v.is_empty())
}
fn carries(elements: &[Vec<String>], q: &(usize, usize, Vec<String>)) -> bool {
let (e, c, codes) = q;
elements
.get(*e)
.and_then(|el| el.get(*c))
.is_some_and(|v| codes.iter().any(|code| code == v))
}
struct Pool<'m> {
tag: &'m str,
qualifier: Option<(usize, usize, Vec<String>)>,
required: usize,
max: Option<usize>,
count: usize,
first_excess: Option<usize>,
}
fn position(seg: &AssembledSegment) -> usize {
seg.segment_number.map_or(0, |n| n as usize)
}
fn count(p: &mut Pool, seg: &AssembledSegment) {
p.count += 1;
if p.max.is_some_and(|m| p.count > m) && p.first_excess.is_none() {
p.first_excess = Some(position(seg));
}
}
fn check_segments(
present: &[&AssembledSegment],
slots: &[MigSegment],
scope: &str,
scope_position: usize,
out: &mut Vec<StructureDiagnostic>,
) {
let mut pools: Vec<Pool> = Vec::new();
for slot in slots {
let q = qualifier(slot);
let required = usize::from(is_required(
slot.status_spec.as_deref(),
slot.status_std.as_deref(),
));
let max = max_rep(slot.max_rep_spec, slot.max_rep_std);
match pools
.iter_mut()
.find(|p| p.tag == slot.id && p.qualifier == q)
{
Some(p) => {
p.required += required;
p.max = p.max.zip(max).map(|(a, b)| a + b);
}
None => pools.push(Pool {
tag: &slot.id,
qualifier: q,
required,
max,
count: 0,
first_excess: None,
}),
}
}
for seg in present {
let pool = pools
.iter()
.position(|p| {
p.tag == seg.tag
&& p.qualifier
.as_ref()
.is_some_and(|q| carries(&seg.elements, q))
})
.or_else(|| {
pools
.iter()
.position(|p| p.tag == seg.tag && p.qualifier.is_none())
});
let Some(i) = pool else {
let tagged: Vec<&Pool> = pools.iter().filter(|p| p.tag == seg.tag).collect();
let carried = tagged.iter().find_map(|p| {
p.qualifier
.as_ref()
.and_then(|q| value_at(&seg.elements, q))
});
match carried {
Some(value) => {
let mut allowed: Vec<&str> = Vec::new();
for code in tagged
.iter()
.filter_map(|p| p.qualifier.as_ref())
.flat_map(|(_, _, codes)| codes.iter().map(String::as_str))
{
if !allowed.contains(&code) {
allowed.push(code);
}
}
out.push(StructureDiagnostic {
kind: StructureDiagnosticKind::UnrecognizedQualifier,
segment_id: seg.tag.clone(),
position: position(seg),
message: format!(
"Segment '{}+{value}' is not allowed in {scope}: the MIG allows {} here",
seg.tag,
allowed
.iter()
.map(|c| format!("{}+{c}", seg.tag))
.collect::<Vec<_>>()
.join(", ")
),
});
}
None => {
if let Some(i) = pools.iter().position(|p| p.tag == seg.tag) {
count(&mut pools[i], seg);
}
}
}
continue;
};
count(&mut pools[i], seg);
}
for p in &pools {
let name = match &p.qualifier {
Some((_, _, codes)) if codes.len() == 1 => format!("{}+{}", p.tag, codes[0]),
_ => p.tag.to_string(),
};
if p.count < p.required {
out.push(StructureDiagnostic {
kind: StructureDiagnosticKind::MissingRequiredSegment,
segment_id: p.tag.to_string(),
position: scope_position,
message: format!(
"Mandatory segment '{name}' is missing in {scope} (found {}, required {})",
p.count, p.required
),
});
}
if let (Some(max), Some(at)) = (p.max, p.first_excess) {
out.push(StructureDiagnostic {
kind: StructureDiagnosticKind::MaxRepetitionsExceeded,
segment_id: p.tag.to_string(),
position: at,
message: format!(
"Segment '{name}' repeats {} times in {scope}, the MIG allows {max}",
p.count
),
});
}
}
}
fn variant_of<'m>(
rep: &AssembledGroupInstance,
variants: &[&'m MigSegmentGroup],
) -> Option<&'m MigSegmentGroup> {
if variants.len() == 1 {
return Some(variants[0]);
}
let entry = rep.segments.first()?;
variants.iter().copied().find(|v| {
v.entry_qualifier()
.is_some_and(|q| q.tag == entry.tag && q.matches(&entry.elements))
})
}
fn describe(variant: &MigSegmentGroup) -> String {
match variant.entry_qualifier() {
Some(q) if q.codes.len() == 1 => format!("{} ({}+{})", variant.id, q.tag, q.codes[0]),
Some(q) => format!("{} ({}+{})", variant.id, q.tag, q.codes.join("|")),
None => variant.id.clone(),
}
}
fn check_groups(
present: &[AssembledGroup],
variants: &[MigSegmentGroup],
scope: &str,
scope_position: usize,
out: &mut Vec<StructureDiagnostic>,
) {
let mut counts = vec![0usize; variants.len()];
let mut first_excess: Vec<Option<usize>> = vec![None; variants.len()];
for group in present {
let same_id: Vec<&MigSegmentGroup> =
variants.iter().filter(|v| v.id == group.group_id).collect();
for rep in &group.repetitions {
let Some(variant) = variant_of(rep, &same_id) else {
continue;
};
let i = variants
.iter()
.position(|v| std::ptr::eq(v, variant))
.expect("variant is one of `variants`");
counts[i] += 1;
let max = max_rep(variant.max_rep_spec, variant.max_rep_std);
let at = rep.segments.first().map_or(scope_position, position);
if max.is_some_and(|m| counts[i] > m) && first_excess[i].is_none() {
first_excess[i] = Some(at);
}
let inner = describe(variant);
let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
check_segments(&own, &variant.segments, &inner, at, out);
check_groups(&rep.child_groups, &variant.nested_groups, &inner, at, out);
}
}
for (i, variant) in variants.iter().enumerate() {
let name = describe(variant);
if counts[i] == 0
&& is_required(
variant.status_spec.as_deref(),
variant.status_std.as_deref(),
)
{
out.push(StructureDiagnostic {
kind: StructureDiagnosticKind::MissingRequiredGroup,
segment_id: variant.id.clone(),
position: scope_position,
message: format!("Mandatory group {name} is missing in {scope}"),
});
}
if let (Some(max), Some(at)) = (
max_rep(variant.max_rep_spec, variant.max_rep_std),
first_excess[i],
) {
out.push(StructureDiagnostic {
kind: StructureDiagnosticKind::GroupMaxRepetitionsExceeded,
segment_id: variant.id.clone(),
position: at,
message: format!(
"Group {name} repeats {} times in {scope}, the MIG allows {max}",
counts[i]
),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{make_mig_group, make_mig_segment};
use mig_types::schema::common::CodeDefinition;
use mig_types::schema::mig::MigDataElement;
fn slot(tag: &str, status: &str, max: i32, codes: &[&str]) -> MigSegment {
let mut s = make_mig_segment(tag);
s.status_spec = Some(status.into());
s.max_rep_spec = max;
if !codes.is_empty() {
s.data_elements = vec![MigDataElement {
id: "q".into(),
name: "q".into(),
description: None,
status_std: None,
status_spec: None,
format_std: None,
format_spec: None,
codes: codes
.iter()
.map(|c| CodeDefinition {
value: (*c).into(),
name: if *c == "XYZ" { "Beispielcode" } else { "code" }.into(),
description: None,
})
.collect(),
position: 0,
}];
}
s
}
fn group(id: &str, status: &str, max: i32, segments: Vec<MigSegment>) -> MigSegmentGroup {
let mut g = make_mig_group(id, vec![], vec![]);
g.status_spec = Some(status.into());
g.max_rep_spec = max;
g.segments = segments;
g
}
fn mig() -> MigSchema {
let seq_a = group(
"SG8",
"D",
99,
vec![
slot("SEQ", "M", 1, &["A"]),
slot("RFF", "R", 1, &["Z13"]),
slot("DTM", "D", 2, &[]),
],
);
let seq_b = group("SG8", "D", 99, vec![slot("SEQ", "M", 1, &["B"])]);
let mut sg4 = group("SG4", "M", 1, vec![slot("IDE", "M", 1, &[])]);
sg4.nested_groups = vec![seq_a, seq_b];
MigSchema {
message_type: "TEST".into(),
variant: None,
version: String::new(),
publication_date: String::new(),
author: String::new(),
format_version: "FV".into(),
source_file: String::new(),
segments: vec![slot("UNH", "M", 1, &[]), slot("BGM", "M", 1, &[])],
segment_groups: vec![
group("SG2", "R", 1, vec![slot("NAD", "M", 1, &["MS"])]),
group("SG2", "R", 1, vec![slot("NAD", "M", 1, &["MR"])]),
sg4,
],
}
}
fn seg(tag: &str, qualifier: &str) -> AssembledSegment {
AssembledSegment {
tag: tag.into(),
elements: vec![vec![qualifier.into()]],
mig_number: None,
segment_number: Some(1),
}
}
fn rep(
segments: Vec<AssembledSegment>,
children: Vec<AssembledGroup>,
) -> AssembledGroupInstance {
AssembledGroupInstance {
segments,
child_groups: children,
entry_mig_number: None,
variant_mig_numbers: vec![],
skipped_segments: vec![],
skipped_positions: vec![],
}
}
fn grp(id: &str, reps: Vec<AssembledGroupInstance>) -> AssembledGroup {
AssembledGroup {
group_id: id.into(),
repetitions: reps,
}
}
fn tree(sg8: Vec<AssembledGroupInstance>) -> AssembledTree {
let sg2 = grp(
"SG2",
vec![
rep(vec![seg("NAD", "MS")], vec![]),
rep(vec![seg("NAD", "MR")], vec![]),
],
);
let sg4 = grp(
"SG4",
vec![rep(vec![seg("IDE", "24")], vec![grp("SG8", sg8)])],
);
AssembledTree {
segments: vec![seg("UNH", "1"), seg("BGM", "E01")],
groups: vec![sg2, sg4],
post_group_start: 2,
inter_group_segments: Default::default(),
}
}
fn kinds(t: &AssembledTree) -> Vec<(StructureDiagnosticKind, String)> {
check_structure(t, &mig())
.into_iter()
.map(|d| (d.kind, d.message))
.collect()
}
#[test]
fn a_complete_message_is_clean() {
let t = tree(vec![
rep(vec![seg("SEQ", "A"), seg("RFF", "Z13")], vec![]),
rep(vec![seg("SEQ", "B")], vec![]),
]);
assert!(kinds(&t).is_empty(), "{:?}", kinds(&t));
}
#[test]
fn each_repetition_is_judged_by_its_own_variant() {
let t = tree(vec![
rep(vec![seg("SEQ", "A")], vec![]),
rep(vec![seg("SEQ", "B")], vec![]),
]);
let found = kinds(&t);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].0, StructureDiagnosticKind::MissingRequiredSegment);
assert!(
found[0].1.contains("RFF+Z13") && found[0].1.contains("SEQ+A"),
"{found:?}"
);
}
#[test]
fn a_missing_root_segment_and_a_missing_group_variant() {
let mut t = tree(vec![]);
t.segments.retain(|s| s.tag != "BGM");
t.groups[0].repetitions.remove(1); let found = kinds(&t);
assert!(found.iter().any(
|(k, m)| *k == StructureDiagnosticKind::MissingRequiredSegment && m.contains("'BGM'")
));
assert!(found
.iter()
.any(|(k, m)| *k == StructureDiagnosticKind::MissingRequiredGroup
&& m.contains("NAD+MR")));
assert_eq!(found.len(), 2, "{found:?}");
}
#[test]
fn too_many_repetitions_of_a_segment_and_of_a_group_variant() {
let mut t = tree(vec![rep(
vec![
seg("SEQ", "A"),
seg("RFF", "Z13"),
seg("DTM", "1"),
seg("DTM", "2"),
seg("DTM", "3"),
],
vec![],
)]);
t.groups[0]
.repetitions
.push(rep(vec![seg("NAD", "MS")], vec![]));
let found = kinds(&t);
assert!(found.iter().any(
|(k, m)| *k == StructureDiagnosticKind::MaxRepetitionsExceeded
&& m.contains("'DTM' repeats 3 times")
));
assert!(found.iter().any(|(k, m)| *k
== StructureDiagnosticKind::GroupMaxRepetitionsExceeded
&& m.contains("NAD+MS")
&& m.contains("2 times")));
assert_eq!(found.len(), 2, "{found:?}");
}
#[test]
fn a_qualifier_no_slot_accepts_is_unrecognized() {
let t = tree(vec![rep(
vec![seg("SEQ", "A"), seg("RFF", "Z13"), seg("RFF", "Z99")],
vec![],
)]);
let found = kinds(&t);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].0, StructureDiagnosticKind::UnrecognizedQualifier);
assert!(
found[0].1.contains("RFF+Z99") && found[0].1.contains("RFF+Z13"),
"{found:?}"
);
}
#[test]
fn an_example_code_names_no_qualifier() {
let mut m = mig();
m.segments.push(slot("CAV", "R", 1, &["XYZ"]));
let mut t = tree(vec![]);
t.segments.push(seg("CAV", "Z90"));
assert!(check_structure(&t, &m).is_empty());
}
#[test]
fn an_empty_qualifier_element_is_not_a_wrong_one() {
let mut m = mig();
m.segments.push(slot("UCS", "R", 1, &["13", "15"]));
let mut t = tree(vec![]);
t.segments.push(seg("UCS", ""));
assert!(check_structure(&t, &m).is_empty());
}
}