use std::collections::{HashMap, HashSet};
use mig_assembly::assembler::{AssembledGroup, AssembledSegment, Assembler, AssemblerConfig};
use mig_assembly::{pid_filter, structure_check, StructureDiagnosticKind};
use mig_types::schema::common::CodeDefinition;
use mig_types::schema::mig::{MigDataElement, MigSchema, MigSegment, MigSegmentGroup};
use mig_types::segment::OwnedSegment;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SyntaxScope {
Message,
Segment,
Element,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyntaxFinding {
pub scope: SyntaxScope,
pub code: String,
pub segment_tag: String,
pub message_segment_number: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub element_position: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub component_position: Option<u32>,
pub detail: String,
}
pub type AcceptedCodes = HashMap<(String, usize, usize), Accepted>;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Accepted {
pub any: bool,
pub codes: HashSet<String>,
}
impl Accepted {
fn accepts(&self, value: &str) -> bool {
self.any || self.codes.contains(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Charset {
Unoa,
Unob,
Unoc,
Other,
}
impl Charset {
pub fn from_syntax_identifier(id: &str) -> Self {
match id {
"UNOA" => Self::Unoa,
"UNOB" => Self::Unob,
"UNOC" => Self::Unoc,
_ => Self::Other,
}
}
fn allows(self, c: char) -> bool {
match self {
Self::Unoa => {
c.is_ascii_uppercase() || c.is_ascii_digit() || " .,-()/='+:?!\"%&*;<>".contains(c)
}
Self::Unob => (' '..='~').contains(&c),
Self::Unoc => (' '..='~').contains(&c) || ('\u{A0}'..='\u{FF}').contains(&c),
Self::Other => !c.is_control(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyntaxSettings {
pub charset: Charset,
pub decimal_mark: char,
}
impl Default for SyntaxSettings {
fn default() -> Self {
Self {
charset: Charset::Unoc,
decimal_mark: '.',
}
}
}
pub fn check_message_syntax(
segments: &[OwnedSegment],
mig: &MigSchema,
settings: SyntaxSettings,
accepted: &AcceptedCodes,
) -> Vec<SyntaxFinding> {
let Some(unh_index) = segments.iter().position(|s| s.id == "UNH") else {
return Vec::new();
};
let unh = &segments[unh_index];
let unh_number = unh.segment_number;
let counter = |segment_number: u32| segment_number.saturating_sub(unh_number) + 1;
let unt_index = segments.iter().rposition(|s| s.id == "UNT");
let mut out = Vec::new();
for seg in [Some(unh_index), unt_index]
.into_iter()
.flatten()
.map(|i| &segments[i])
{
if let Some(slot) = mig.segments.iter().find(|s| s.id == seg.id) {
check_elements(
seg,
slot,
counter(seg.segment_number),
settings,
accepted,
&mut out,
);
}
}
match unt_index {
Some(i) => {
let unt = &segments[i];
let count = (i - unh_index + 1).to_string();
if !unt.get_element(0).is_empty() && unt.get_element(0) != count {
out.push(finding(
SyntaxScope::Message,
"29",
unt,
counter(unt.segment_number),
Some((2, None)),
format!(
"UNT counts {} segments, the message has {count}",
unt.get_element(0)
),
));
}
if !unt.get_element(1).is_empty() && unt.get_element(1) != unh.get_element(0) {
out.push(finding(
SyntaxScope::Message,
"28",
unt,
counter(unt.segment_number),
Some((3, None)),
format!(
"UNT names message '{}', UNH '{}'",
unt.get_element(1),
unh.get_element(0)
),
));
}
}
None => out.push(SyntaxFinding {
scope: SyntaxScope::Message,
code: "13".into(),
segment_tag: "UNT".into(),
message_segment_number: counter(
segments.last().map_or(unh_number, |s| s.segment_number),
),
element_position: None,
component_position: None,
detail: "UNT is missing".into(),
}),
}
for f in &mut out {
f.scope = SyntaxScope::Message;
}
if !out.is_empty() {
return out;
}
let numbers = all_numbers(mig);
let unmerged = pid_filter::filter_mig_for_pid_unmerged(mig, &numbers);
let place = |assembly_mig: &MigSchema| {
let assembler = Assembler::with_config(
assembly_mig,
AssemblerConfig {
strict_code_matching: true,
skip_unknown_segments: true,
..Default::default()
},
);
let (tree, mut diagnostics) = assembler.assemble_with_diagnostics(segments);
diagnostics.extend(structure_check::check_structure(&tree, &unmerged));
(tree, diagnostics)
};
let merged = place(&pid_filter::filter_mig_for_pid(mig, &numbers));
let apart = place(&pid_filter::filter_mig_variant_aware(mig, &numbers));
let (tree, diagnostics) = if apart.1.len() < merged.1.len() {
apart
} else {
merged
};
let first_segments = group_first_segments(mig);
let within =
|n: u32| n >= unh_number && unt_index.map_or(true, |i| n <= segments[i].segment_number);
let mut reported = HashSet::new();
for diag in diagnostics {
let n = u32::try_from(diag.position).unwrap_or(0);
if !within(n) {
continue;
}
let (code, tag) = match diag.kind {
StructureDiagnosticKind::MissingRequiredSegment => ("13", diag.segment_id.clone()),
StructureDiagnosticKind::MissingRequiredGroup => (
"13",
first_segments
.get(diag.segment_id.as_str())
.cloned()
.unwrap_or(diag.segment_id.clone()),
),
StructureDiagnosticKind::MaxRepetitionsExceeded => ("35", diag.segment_id.clone()),
StructureDiagnosticKind::GroupMaxRepetitionsExceeded => (
"36",
first_segments
.get(diag.segment_id.as_str())
.cloned()
.unwrap_or(diag.segment_id.clone()),
),
_ => ("15", diag.segment_id.clone()),
};
if !reported.insert((code, tag.clone(), n)) {
continue;
}
out.push(SyntaxFinding {
scope: SyntaxScope::Segment,
code: code.into(),
segment_tag: tag,
message_segment_number: counter(n),
element_position: None,
component_position: None,
detail: diag.message,
});
}
let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
root.extend(tree.inter_group_segments.values().flatten());
let mut walk = ElementWalk {
settings,
accepted,
within: &within,
counter: &counter,
out: &mut out,
};
walk.segments(&root, &unmerged.segments);
walk.groups(&tree.groups, &unmerged.segment_groups);
let wrong_codes: HashSet<u32> = out
.iter()
.filter(|f| f.scope == SyntaxScope::Element && f.code == "12")
.map(|f| f.message_segment_number)
.collect();
out.retain(|f| {
!(f.scope == SyntaxScope::Segment
&& f.code == "15"
&& wrong_codes.contains(&f.message_segment_number))
});
out.sort_by_key(|f| {
(
f.message_segment_number,
f.element_position,
f.component_position,
)
});
out
}
fn finding(
scope: SyntaxScope,
code: &str,
seg: &OwnedSegment,
message_segment_number: u32,
at: Option<(u32, Option<u32>)>,
detail: String,
) -> SyntaxFinding {
SyntaxFinding {
scope,
code: code.into(),
segment_tag: seg.id.clone(),
message_segment_number,
element_position: at.map(|(e, _)| e),
component_position: at.and_then(|(_, c)| c),
detail,
}
}
fn all_numbers(mig: &MigSchema) -> HashSet<String> {
fn groups(list: &[MigSegmentGroup], out: &mut HashSet<String>) {
for g in list {
out.extend(g.segments.iter().filter_map(|s| s.number.clone()));
groups(&g.nested_groups, out);
}
}
let mut out: HashSet<String> = mig
.segments
.iter()
.filter_map(|s| s.number.clone())
.collect();
groups(&mig.segment_groups, &mut out);
out
}
fn group_first_segments(mig: &MigSchema) -> HashMap<&str, String> {
fn walk<'m>(list: &'m [MigSegmentGroup], out: &mut HashMap<&'m str, String>) {
for g in list {
if let Some(first) = g.segments.first() {
out.entry(g.id.as_str()).or_insert_with(|| first.id.clone());
}
walk(&g.nested_groups, out);
}
}
let mut out = HashMap::new();
walk(&mig.segment_groups, &mut out);
out
}
struct ElementWalk<'a> {
settings: SyntaxSettings,
accepted: &'a AcceptedCodes,
within: &'a dyn Fn(u32) -> bool,
counter: &'a dyn Fn(u32) -> u32,
out: &'a mut Vec<SyntaxFinding>,
}
impl ElementWalk<'_> {
fn groups(&mut self, present: &[AssembledGroup], variants: &[MigSegmentGroup]) {
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 candidates: Vec<&MigSegmentGroup> = match same_id.as_slice() {
[only] => vec![*only],
_ => rep.segments.first().map_or(Vec::new(), |entry| {
same_id
.iter()
.copied()
.filter(|v| {
v.entry_qualifier().is_some_and(|q| {
q.tag == entry.tag && q.matches(&entry.elements)
})
})
.collect()
}),
};
let mut best: Option<Vec<SyntaxFinding>> = None;
for variant in candidates {
let mut found = Vec::new();
let mut walk = ElementWalk {
settings: self.settings,
accepted: self.accepted,
within: self.within,
counter: self.counter,
out: &mut found,
};
let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
walk.segments(&own, &variant.segments);
walk.groups(&rep.child_groups, &variant.nested_groups);
if best.as_ref().map_or(true, |b| found.len() < b.len()) {
best = Some(found);
}
}
self.out.extend(best.unwrap_or_default());
}
}
}
fn segments(&mut self, present: &[&AssembledSegment], slots: &[MigSegment]) {
for seg in present {
let Some(n) = seg.segment_number else {
continue;
};
if seg.tag == "UNH" || seg.tag == "UNT" || !(self.within)(n) {
continue;
}
let owned = OwnedSegment {
id: seg.tag.clone(),
elements: seg.elements.clone(),
segment_number: n,
};
let mut best: Option<Vec<SyntaxFinding>> = None;
for slot in slots_for(seg, slots) {
let mut found = Vec::new();
check_elements(
&owned,
slot,
(self.counter)(n),
self.settings,
self.accepted,
&mut found,
);
if best.as_ref().map_or(true, |b| found.len() < b.len()) {
best = Some(found);
}
}
self.out.extend(best.unwrap_or_default());
}
}
}
fn slots_for<'m>(seg: &AssembledSegment, slots: &'m [MigSegment]) -> Vec<&'m MigSegment> {
let tagged: Vec<&MigSegment> = slots.iter().filter(|s| s.id == seg.tag).collect();
let carries = |slot: &MigSegment| {
qualifier(slot).map(|(e, c, codes)| {
seg.elements
.get(e)
.and_then(|el| el.get(c))
.is_some_and(|v| codes.contains(v))
})
};
let qualified: Vec<&MigSegment> = tagged
.iter()
.copied()
.filter(|slot| carries(slot) == Some(true))
.collect();
if !qualified.is_empty() {
return qualified;
}
let open: Vec<&MigSegment> = tagged
.iter()
.copied()
.filter(|slot| carries(slot).is_none())
.collect();
if !open.is_empty() {
return open;
}
match tagged.as_slice() {
[only] => vec![*only],
_ => Vec::new(),
}
}
fn is_placeholder(code: &CodeDefinition) -> bool {
code.name == "Beispielcode" || code.value == "XYZ"
}
fn codes(d: &MigDataElement) -> Vec<&str> {
if d.codes.iter().any(is_placeholder) {
return Vec::new();
}
d.codes
.iter()
.map(|c| c.value.as_str())
.filter(|v| !v.is_empty())
.collect()
}
fn qualifier(slot: &MigSegment) -> Option<(usize, usize, Vec<String>)> {
let simple = slot.data_elements.iter().map(|d| (d.position, 0, d));
let composite = slot.composites.iter().flat_map(|c| {
c.data_elements
.iter()
.map(move |d| (c.position, d.position, d))
});
simple
.chain(composite)
.filter(|(_, _, d)| !codes(d).is_empty())
.min_by_key(|(e, c, _)| (*e, *c))
.map(|(e, c, d)| (e, c, codes(d).into_iter().map(String::from).collect()))
}
fn required(spec: Option<&str>, std: Option<&str>) -> bool {
matches!(spec.or(std).map(str::trim), Some("M" | "R"))
}
fn check_elements(
seg: &OwnedSegment,
slot: &MigSegment,
counter: u32,
settings: SyntaxSettings,
accepted: &AcceptedCodes,
out: &mut Vec<SyntaxFinding>,
) {
let number = slot.number.as_deref().unwrap_or("");
let also = |e: usize, c: usize| accepted.get(&(number.to_string(), e, c));
let defined = slot
.data_elements
.iter()
.map(|d| d.position)
.chain(slot.composites.iter().map(|c| c.position))
.max()
.map_or(0, |p| p + 1);
let last_filled = seg
.elements
.iter()
.rposition(|e| e.iter().any(|c| !c.is_empty()))
.map_or(0, |p| p + 1);
if last_filled > defined {
out.push(finding(
SyntaxScope::Segment,
"16",
seg,
counter,
None,
format!(
"{} has {last_filled} data elements, the MIG defines {defined}",
seg.id
),
));
}
for d in &slot.data_elements {
let element = seg.elements.get(d.position);
let at = d.position as u32 + 2;
let value = element.and_then(|e| e.first()).map_or("", String::as_str);
if element.is_some_and(|e| e.iter().skip(1).any(|c| !c.is_empty())) {
out.push(finding(
SyntaxScope::Element,
"16",
seg,
counter,
Some((at, None)),
format!(
"{} {} is a simple data element but has components",
seg.id, d.id
),
));
}
check_value(
seg,
d,
value,
counter,
(at, None),
settings,
also(d.position, 0),
out,
);
}
for c in &slot.composites {
let at = c.position as u32 + 2;
let components: &[String] = seg.elements.get(c.position).map_or(&[], Vec::as_slice);
if components.iter().all(String::is_empty) {
if required(c.status_spec.as_deref(), c.status_std.as_deref()) {
out.push(finding(
SyntaxScope::Element,
"13",
seg,
counter,
Some((at, None)),
format!("{} {} ({}) is missing", seg.id, c.id, c.name),
));
}
continue;
}
let defined = c
.data_elements
.iter()
.map(|d| d.position)
.max()
.map_or(0, |p| p + 1);
let last_filled = components
.iter()
.rposition(|v| !v.is_empty())
.map_or(0, |p| p + 1);
if last_filled > defined {
out.push(finding(
SyntaxScope::Element,
"16",
seg,
counter,
Some((at, None)),
format!(
"{} {} has {last_filled} components, the MIG defines {defined}",
seg.id, c.id
),
));
}
for d in &c.data_elements {
let value = components.get(d.position).map_or("", String::as_str);
check_value(
seg,
d,
value,
counter,
(at, Some(d.position as u32 + 1)),
settings,
also(c.position, d.position),
out,
);
}
}
}
fn check_value(
seg: &OwnedSegment,
d: &MigDataElement,
value: &str,
counter: u32,
at: (u32, Option<u32>),
settings: SyntaxSettings,
also: Option<&Accepted>,
out: &mut Vec<SyntaxFinding>,
) {
let mut push = |code: &str, detail: String| {
out.push(finding(
SyntaxScope::Element,
code,
seg,
counter,
Some(at),
detail,
));
};
let name = format!("{} {} ({})", seg.id, d.id, d.name);
if value.is_empty() {
if required(d.status_spec.as_deref(), d.status_std.as_deref()) {
push("13", format!("{name} is missing"));
}
return;
}
if let Some(bad) = value.chars().find(|&c| !settings.charset.allows(c)) {
push(
"21",
format!("{name} holds '{bad}', which its syntax level does not allow"),
);
return;
}
if let Some(format) = d.format_spec.as_deref().or(d.format_std.as_deref()) {
if let Some((code, detail)) = check_format(value, format, settings.decimal_mark) {
push(code, format!("{name} '{value}': {detail}"));
return;
}
}
let allowed = codes(d);
if !allowed.is_empty() && !allowed.contains(&value) && !also.is_some_and(|a| a.accepts(value)) {
push(
"12",
format!(
"{name} '{value}' is none of the MIG's codes {}",
allowed.join(", ")
),
);
}
}
fn parse_format(format: &str) -> Option<(&str, usize, bool)> {
let format = format.trim();
let kind_end = format.find(|c: char| !c.is_ascii_alphabetic())?;
let (kind, rest) = format.split_at(kind_end);
let (upto, digits) = match rest.strip_prefix("..") {
Some(d) => (true, d),
None => (false, rest),
};
Some((kind, digits.parse().ok()?, upto))
}
fn check_format(value: &str, format: &str, decimal_mark: char) -> Option<(&'static str, String)> {
let (kind, length, upto) = parse_format(format)?;
let counted = match kind {
"n" => {
let unsigned = value.strip_prefix('-').unwrap_or(value);
if let Some(c) = unsigned
.chars()
.find(|c| !c.is_ascii_digit() && *c != decimal_mark)
{
return Some(match c {
'.' | ',' => (
"19",
format!("'{c}' is not the decimal mark '{decimal_mark}'"),
),
_ => ("37", format!("'{c}' in a numeric data element ({format})")),
});
}
let marks = unsigned.matches(decimal_mark).count();
if marks > 1 || unsigned.ends_with(decimal_mark) {
return Some(("19", format!("not a decimal number ({format})")));
}
if unsigned.starts_with(decimal_mark) {
return Some(("38", "no digit before the decimal mark".to_string()));
}
unsigned.chars().filter(char::is_ascii_digit).count()
}
"a" => {
if let Some(c) = value.chars().find(char::is_ascii_digit) {
return Some((
"37",
format!("'{c}' in an alphabetic data element ({format})"),
));
}
value.chars().count()
}
_ => value.chars().count(),
};
if counted > length {
return Some((
"39",
format!("{counted} characters, {format} allows {length}"),
));
}
if !upto && counted < length {
return Some((
"40",
format!("{counted} characters, {format} requires {length}"),
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats() {
assert_eq!(check_format("12345", "n5", '.'), None);
assert_eq!(check_format("1234", "n5", '.').map(|f| f.0), Some("40"));
assert_eq!(check_format("123456", "n..5", '.').map(|f| f.0), Some("39"));
assert_eq!(check_format("-12.5", "n..3", '.'), None);
assert_eq!(check_format(".5", "n..3", '.').map(|f| f.0), Some("38"));
assert_eq!(check_format("5.", "n..3", '.').map(|f| f.0), Some("19"));
assert_eq!(check_format("1,5", "n..3", '.').map(|f| f.0), Some("19"));
assert_eq!(check_format("1A", "n..3", '.').map(|f| f.0), Some("37"));
assert_eq!(check_format("Z1", "a..3", '.').map(|f| f.0), Some("37"));
assert_eq!(check_format("ÄBC", "an3", '.'), None);
assert_eq!(check_format("AB", "an3", '.').map(|f| f.0), Some("40"));
}
#[test]
fn charsets() {
assert!(Charset::Unoc.allows('ä'));
assert!(!Charset::Unoc.allows('€'));
assert!(!Charset::Unoc.allows('\n'));
assert!(!Charset::Unoa.allows('a'));
assert!(Charset::Unob.allows('a'));
}
}