1use std::collections::{HashMap, HashSet};
28
29use mig_assembly::assembler::{AssembledGroup, AssembledSegment, Assembler, AssemblerConfig};
30use mig_assembly::{pid_filter, structure_check, StructureDiagnosticKind};
31use mig_types::schema::common::CodeDefinition;
32use mig_types::schema::mig::{MigDataElement, MigSchema, MigSegment, MigSegmentGroup};
33use mig_types::segment::OwnedSegment;
34use serde::{Deserialize, Serialize};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum SyntaxScope {
39 Message,
41 Segment,
43 Element,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct SyntaxFinding {
50 pub scope: SyntaxScope,
52 pub code: String,
54 pub segment_tag: String,
57 pub message_segment_number: u32,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub element_position: Option<u32>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub component_position: Option<u32>,
66 pub detail: String,
68}
69
70pub type AcceptedCodes = HashMap<(String, usize, usize), Accepted>;
76
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
79pub struct Accepted {
80 pub any: bool,
82 pub codes: HashSet<String>,
84}
85
86impl Accepted {
87 fn accepts(&self, value: &str) -> bool {
88 self.any || self.codes.contains(value)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Charset {
95 Unoa,
97 Unob,
99 Unoc,
101 Other,
103}
104
105impl Charset {
106 pub fn from_syntax_identifier(id: &str) -> Self {
108 match id {
109 "UNOA" => Self::Unoa,
110 "UNOB" => Self::Unob,
111 "UNOC" => Self::Unoc,
112 _ => Self::Other,
113 }
114 }
115
116 fn allows(self, c: char) -> bool {
117 match self {
118 Self::Unoa => {
119 c.is_ascii_uppercase() || c.is_ascii_digit() || " .,-()/='+:?!\"%&*;<>".contains(c)
120 }
121 Self::Unob => (' '..='~').contains(&c),
122 Self::Unoc => (' '..='~').contains(&c) || ('\u{A0}'..='\u{FF}').contains(&c),
123 Self::Other => !c.is_control(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct SyntaxSettings {
131 pub charset: Charset,
132 pub decimal_mark: char,
134}
135
136impl Default for SyntaxSettings {
137 fn default() -> Self {
138 Self {
139 charset: Charset::Unoc,
140 decimal_mark: '.',
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
151pub struct MigPlacements {
152 unmerged: MigSchema,
153 merged: MigSchema,
154 apart: MigSchema,
155}
156
157impl MigPlacements {
158 pub fn new(mig: &MigSchema) -> Self {
159 let numbers = all_numbers(mig);
160 Self {
161 unmerged: pid_filter::filter_mig_for_pid_unmerged(mig, &numbers),
162 merged: pid_filter::filter_mig_for_pid(mig, &numbers),
163 apart: pid_filter::filter_mig_variant_aware(mig, &numbers),
164 }
165 }
166}
167
168pub fn check_message_syntax(
174 segments: &[OwnedSegment],
175 mig: &MigSchema,
176 settings: SyntaxSettings,
177 accepted: &AcceptedCodes,
178) -> Vec<SyntaxFinding> {
179 check_message_syntax_prepared(segments, mig, &MigPlacements::new(mig), settings, accepted)
180}
181
182pub fn check_message_syntax_prepared(
185 segments: &[OwnedSegment],
186 mig: &MigSchema,
187 placements: &MigPlacements,
188 settings: SyntaxSettings,
189 accepted: &AcceptedCodes,
190) -> Vec<SyntaxFinding> {
191 let Some(unh_index) = segments.iter().position(|s| s.id == "UNH") else {
192 return Vec::new();
193 };
194 let unh = &segments[unh_index];
195 let unh_number = unh.segment_number;
196 let counter = |segment_number: u32| segment_number.saturating_sub(unh_number) + 1;
197 let unt_index = segments.iter().rposition(|s| s.id == "UNT");
198
199 let mut out = Vec::new();
201 for seg in [Some(unh_index), unt_index]
202 .into_iter()
203 .flatten()
204 .map(|i| &segments[i])
205 {
206 if let Some(slot) = mig.segments.iter().find(|s| s.id == seg.id) {
207 check_elements(
208 seg,
209 slot,
210 counter(seg.segment_number),
211 settings,
212 accepted,
213 &mut out,
214 );
215 }
216 }
217 match unt_index {
218 Some(i) => {
219 let unt = &segments[i];
220 let count = (i - unh_index + 1).to_string();
221 if !unt.get_element(0).is_empty() && unt.get_element(0) != count {
222 out.push(finding(
223 SyntaxScope::Message,
224 "29",
225 unt,
226 counter(unt.segment_number),
227 Some((2, None)),
228 format!(
229 "UNT counts {} segments, the message has {count}",
230 unt.get_element(0)
231 ),
232 ));
233 }
234 if !unt.get_element(1).is_empty() && unt.get_element(1) != unh.get_element(0) {
235 out.push(finding(
236 SyntaxScope::Message,
237 "28",
238 unt,
239 counter(unt.segment_number),
240 Some((3, None)),
241 format!(
242 "UNT names message '{}', UNH '{}'",
243 unt.get_element(1),
244 unh.get_element(0)
245 ),
246 ));
247 }
248 }
249 None => out.push(SyntaxFinding {
250 scope: SyntaxScope::Message,
251 code: "13".into(),
252 segment_tag: "UNT".into(),
253 message_segment_number: counter(
254 segments.last().map_or(unh_number, |s| s.segment_number),
255 ),
256 element_position: None,
257 component_position: None,
258 detail: "UNT is missing".into(),
259 }),
260 }
261 for f in &mut out {
262 f.scope = SyntaxScope::Message;
263 }
264 if !out.is_empty() {
265 return out;
266 }
267
268 let unmerged = &placements.unmerged;
274 let place = |assembly_mig: &MigSchema| {
275 let assembler = Assembler::with_config(
276 assembly_mig,
277 AssemblerConfig {
278 strict_code_matching: true,
279 skip_unknown_segments: true,
280 ..Default::default()
281 },
282 );
283 let (tree, mut diagnostics) = assembler.assemble_with_diagnostics(segments);
284 diagnostics.extend(structure_check::check_structure(&tree, unmerged));
285 (tree, diagnostics)
286 };
287 let merged = place(&placements.merged);
288 let apart = place(&placements.apart);
289 let (tree, diagnostics) = if apart.1.len() < merged.1.len() {
290 apart
291 } else {
292 merged
293 };
294
295 let first_segments = group_first_segments(mig);
296 let within =
297 |n: u32| n >= unh_number && unt_index.map_or(true, |i| n <= segments[i].segment_number);
298 let mut reported = HashSet::new();
299 for diag in diagnostics {
300 let n = u32::try_from(diag.position).unwrap_or(0);
301 if !within(n) {
302 continue;
303 }
304 let (code, tag) = match diag.kind {
305 StructureDiagnosticKind::MissingRequiredSegment => ("13", diag.segment_id.clone()),
306 StructureDiagnosticKind::MissingRequiredGroup => (
307 "13",
308 first_segments
309 .get(diag.segment_id.as_str())
310 .cloned()
311 .unwrap_or(diag.segment_id.clone()),
312 ),
313 StructureDiagnosticKind::MaxRepetitionsExceeded => ("35", diag.segment_id.clone()),
314 StructureDiagnosticKind::GroupMaxRepetitionsExceeded => (
315 "36",
316 first_segments
317 .get(diag.segment_id.as_str())
318 .cloned()
319 .unwrap_or(diag.segment_id.clone()),
320 ),
321 _ => ("15", diag.segment_id.clone()),
322 };
323 if !reported.insert((code, tag.clone(), n)) {
325 continue;
326 }
327 out.push(SyntaxFinding {
328 scope: SyntaxScope::Segment,
329 code: code.into(),
330 segment_tag: tag,
331 message_segment_number: counter(n),
332 element_position: None,
333 component_position: None,
334 detail: diag.message,
335 });
336 }
337
338 let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
341 root.extend(tree.inter_group_segments.values().flatten());
342 let mut walk = ElementWalk {
343 settings,
344 accepted,
345 within: &within,
346 counter: &counter,
347 out: &mut out,
348 };
349 walk.segments(&root, &unmerged.segments);
350 walk.groups(&tree.groups, &unmerged.segment_groups);
351
352 let wrong_codes: HashSet<u32> = out
355 .iter()
356 .filter(|f| f.scope == SyntaxScope::Element && f.code == "12")
357 .map(|f| f.message_segment_number)
358 .collect();
359 out.retain(|f| {
360 !(f.scope == SyntaxScope::Segment
361 && f.code == "15"
362 && wrong_codes.contains(&f.message_segment_number))
363 });
364
365 out.sort_by_key(|f| {
366 (
367 f.message_segment_number,
368 f.element_position,
369 f.component_position,
370 )
371 });
372 out
373}
374
375fn finding(
376 scope: SyntaxScope,
377 code: &str,
378 seg: &OwnedSegment,
379 message_segment_number: u32,
380 at: Option<(u32, Option<u32>)>,
381 detail: String,
382) -> SyntaxFinding {
383 SyntaxFinding {
384 scope,
385 code: code.into(),
386 segment_tag: seg.id.clone(),
387 message_segment_number,
388 element_position: at.map(|(e, _)| e),
389 component_position: at.and_then(|(_, c)| c),
390 detail,
391 }
392}
393
394fn all_numbers(mig: &MigSchema) -> HashSet<String> {
396 fn groups(list: &[MigSegmentGroup], out: &mut HashSet<String>) {
397 for g in list {
398 out.extend(g.segments.iter().filter_map(|s| s.number.clone()));
399 groups(&g.nested_groups, out);
400 }
401 }
402 let mut out: HashSet<String> = mig
403 .segments
404 .iter()
405 .filter_map(|s| s.number.clone())
406 .collect();
407 groups(&mig.segment_groups, &mut out);
408 out
409}
410
411fn group_first_segments(mig: &MigSchema) -> HashMap<&str, String> {
414 fn walk<'m>(list: &'m [MigSegmentGroup], out: &mut HashMap<&'m str, String>) {
415 for g in list {
416 if let Some(first) = g.segments.first() {
417 out.entry(g.id.as_str()).or_insert_with(|| first.id.clone());
418 }
419 walk(&g.nested_groups, out);
420 }
421 }
422 let mut out = HashMap::new();
423 walk(&mig.segment_groups, &mut out);
424 out
425}
426
427struct ElementWalk<'a> {
429 settings: SyntaxSettings,
430 accepted: &'a AcceptedCodes,
431 within: &'a dyn Fn(u32) -> bool,
432 counter: &'a dyn Fn(u32) -> u32,
433 out: &'a mut Vec<SyntaxFinding>,
434}
435
436impl ElementWalk<'_> {
437 fn groups(&mut self, present: &[AssembledGroup], variants: &[MigSegmentGroup]) {
438 for group in present {
439 let same_id: Vec<&MigSegmentGroup> =
440 variants.iter().filter(|v| v.id == group.group_id).collect();
441 for rep in &group.repetitions {
442 let candidates: Vec<&MigSegmentGroup> = match same_id.as_slice() {
447 [only] => vec![*only],
448 _ => rep.segments.first().map_or(Vec::new(), |entry| {
449 same_id
450 .iter()
451 .copied()
452 .filter(|v| {
453 v.entry_qualifier().is_some_and(|q| {
454 q.tag == entry.tag && q.matches(&entry.elements)
455 })
456 })
457 .collect()
458 }),
459 };
460 let mut best: Option<Vec<SyntaxFinding>> = None;
461 for variant in candidates {
462 let mut found = Vec::new();
463 let mut walk = ElementWalk {
464 settings: self.settings,
465 accepted: self.accepted,
466 within: self.within,
467 counter: self.counter,
468 out: &mut found,
469 };
470 let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
471 walk.segments(&own, &variant.segments);
472 walk.groups(&rep.child_groups, &variant.nested_groups);
473 if best.as_ref().map_or(true, |b| found.len() < b.len()) {
474 best = Some(found);
475 }
476 }
477 self.out.extend(best.unwrap_or_default());
478 }
479 }
480 }
481
482 fn segments(&mut self, present: &[&AssembledSegment], slots: &[MigSegment]) {
483 for seg in present {
484 let Some(n) = seg.segment_number else {
485 continue;
486 };
487 if seg.tag == "UNH" || seg.tag == "UNT" || !(self.within)(n) {
488 continue;
489 }
490 let owned = OwnedSegment {
491 id: seg.tag.clone(),
492 elements: seg.elements.clone(),
493 segment_number: n,
494 };
495 let mut best: Option<Vec<SyntaxFinding>> = None;
499 for slot in slots_for(seg, slots) {
500 let mut found = Vec::new();
501 check_elements(
502 &owned,
503 slot,
504 (self.counter)(n),
505 self.settings,
506 self.accepted,
507 &mut found,
508 );
509 if best.as_ref().map_or(true, |b| found.len() < b.len()) {
510 best = Some(found);
511 }
512 }
513 self.out.extend(best.unwrap_or_default());
514 }
515 }
516}
517
518fn slots_for<'m>(seg: &AssembledSegment, slots: &'m [MigSegment]) -> Vec<&'m MigSegment> {
524 let tagged: Vec<&MigSegment> = slots.iter().filter(|s| s.id == seg.tag).collect();
525 let carries = |slot: &MigSegment| {
526 qualifier(slot).map(|(e, c, codes)| {
527 seg.elements
528 .get(e)
529 .and_then(|el| el.get(c))
530 .is_some_and(|v| codes.contains(v))
531 })
532 };
533 let qualified: Vec<&MigSegment> = tagged
534 .iter()
535 .copied()
536 .filter(|slot| carries(slot) == Some(true))
537 .collect();
538 if !qualified.is_empty() {
539 return qualified;
540 }
541 let open: Vec<&MigSegment> = tagged
542 .iter()
543 .copied()
544 .filter(|slot| carries(slot).is_none())
545 .collect();
546 if !open.is_empty() {
547 return open;
548 }
549 match tagged.as_slice() {
550 [only] => vec![*only],
551 _ => Vec::new(),
552 }
553}
554
555fn is_placeholder(code: &CodeDefinition) -> bool {
557 code.name == "Beispielcode" || code.value == "XYZ"
558}
559
560fn codes(d: &MigDataElement) -> Vec<&str> {
562 if d.codes.iter().any(is_placeholder) {
563 return Vec::new();
564 }
565 d.codes
566 .iter()
567 .map(|c| c.value.as_str())
568 .filter(|v| !v.is_empty())
569 .collect()
570}
571
572fn qualifier(slot: &MigSegment) -> Option<(usize, usize, Vec<String>)> {
574 let simple = slot.data_elements.iter().map(|d| (d.position, 0, d));
575 let composite = slot.composites.iter().flat_map(|c| {
576 c.data_elements
577 .iter()
578 .map(move |d| (c.position, d.position, d))
579 });
580 simple
581 .chain(composite)
582 .filter(|(_, _, d)| !codes(d).is_empty())
583 .min_by_key(|(e, c, _)| (*e, *c))
584 .map(|(e, c, d)| (e, c, codes(d).into_iter().map(String::from).collect()))
585}
586
587fn required(spec: Option<&str>, std: Option<&str>) -> bool {
588 matches!(spec.or(std).map(str::trim), Some("M" | "R"))
589}
590
591fn check_elements(
593 seg: &OwnedSegment,
594 slot: &MigSegment,
595 counter: u32,
596 settings: SyntaxSettings,
597 accepted: &AcceptedCodes,
598 out: &mut Vec<SyntaxFinding>,
599) {
600 let number = slot.number.as_deref().unwrap_or("");
601 let also = |e: usize, c: usize| accepted.get(&(number.to_string(), e, c));
602 let defined = slot
603 .data_elements
604 .iter()
605 .map(|d| d.position)
606 .chain(slot.composites.iter().map(|c| c.position))
607 .max()
608 .map_or(0, |p| p + 1);
609 let last_filled = seg
610 .elements
611 .iter()
612 .rposition(|e| e.iter().any(|c| !c.is_empty()))
613 .map_or(0, |p| p + 1);
614 if last_filled > defined {
615 out.push(finding(
616 SyntaxScope::Segment,
617 "16",
618 seg,
619 counter,
620 None,
621 format!(
622 "{} has {last_filled} data elements, the MIG defines {defined}",
623 seg.id
624 ),
625 ));
626 }
627
628 for d in &slot.data_elements {
629 let element = seg.elements.get(d.position);
630 let at = d.position as u32 + 2;
631 let value = element.and_then(|e| e.first()).map_or("", String::as_str);
632 if element.is_some_and(|e| e.iter().skip(1).any(|c| !c.is_empty())) {
633 out.push(finding(
634 SyntaxScope::Element,
635 "16",
636 seg,
637 counter,
638 Some((at, None)),
639 format!(
640 "{} {} is a simple data element but has components",
641 seg.id, d.id
642 ),
643 ));
644 }
645 check_value(
646 seg,
647 d,
648 value,
649 counter,
650 (at, None),
651 settings,
652 also(d.position, 0),
653 out,
654 );
655 }
656
657 for c in &slot.composites {
658 let at = c.position as u32 + 2;
659 let components: &[String] = seg.elements.get(c.position).map_or(&[], Vec::as_slice);
660 if components.iter().all(String::is_empty) {
661 if required(c.status_spec.as_deref(), c.status_std.as_deref()) {
662 out.push(finding(
663 SyntaxScope::Element,
664 "13",
665 seg,
666 counter,
667 Some((at, None)),
668 format!("{} {} ({}) is missing", seg.id, c.id, c.name),
669 ));
670 }
671 continue;
672 }
673 let defined = c
674 .data_elements
675 .iter()
676 .map(|d| d.position)
677 .max()
678 .map_or(0, |p| p + 1);
679 let last_filled = components
680 .iter()
681 .rposition(|v| !v.is_empty())
682 .map_or(0, |p| p + 1);
683 if last_filled > defined {
684 out.push(finding(
685 SyntaxScope::Element,
686 "16",
687 seg,
688 counter,
689 Some((at, None)),
690 format!(
691 "{} {} has {last_filled} components, the MIG defines {defined}",
692 seg.id, c.id
693 ),
694 ));
695 }
696 for d in &c.data_elements {
697 let value = components.get(d.position).map_or("", String::as_str);
698 check_value(
699 seg,
700 d,
701 value,
702 counter,
703 (at, Some(d.position as u32 + 1)),
704 settings,
705 also(c.position, d.position),
706 out,
707 );
708 }
709 }
710}
711
712fn check_value(
714 seg: &OwnedSegment,
715 d: &MigDataElement,
716 value: &str,
717 counter: u32,
718 at: (u32, Option<u32>),
719 settings: SyntaxSettings,
720 also: Option<&Accepted>,
721 out: &mut Vec<SyntaxFinding>,
722) {
723 let mut push = |code: &str, detail: String| {
724 out.push(finding(
725 SyntaxScope::Element,
726 code,
727 seg,
728 counter,
729 Some(at),
730 detail,
731 ));
732 };
733 let name = format!("{} {} ({})", seg.id, d.id, d.name);
734 if value.is_empty() {
735 if required(d.status_spec.as_deref(), d.status_std.as_deref()) {
736 push("13", format!("{name} is missing"));
737 }
738 return;
739 }
740 if let Some(bad) = value.chars().find(|&c| !settings.charset.allows(c)) {
741 push(
742 "21",
743 format!("{name} holds '{bad}', which its syntax level does not allow"),
744 );
745 return;
746 }
747 if let Some(format) = d.format_spec.as_deref().or(d.format_std.as_deref()) {
748 if let Some((code, detail)) = check_format(value, format, settings.decimal_mark) {
749 push(code, format!("{name} '{value}': {detail}"));
750 return;
751 }
752 }
753 let allowed = codes(d);
754 if !allowed.is_empty() && !allowed.contains(&value) && !also.is_some_and(|a| a.accepts(value)) {
755 push(
756 "12",
757 format!(
758 "{name} '{value}' is none of the MIG's codes {}",
759 allowed.join(", ")
760 ),
761 );
762 }
763}
764
765fn parse_format(format: &str) -> Option<(&str, usize, bool)> {
768 let format = format.trim();
769 let kind_end = format.find(|c: char| !c.is_ascii_alphabetic())?;
770 let (kind, rest) = format.split_at(kind_end);
771 let (upto, digits) = match rest.strip_prefix("..") {
772 Some(d) => (true, d),
773 None => (false, rest),
774 };
775 Some((kind, digits.parse().ok()?, upto))
776}
777
778fn check_format(value: &str, format: &str, decimal_mark: char) -> Option<(&'static str, String)> {
780 let (kind, length, upto) = parse_format(format)?;
781 let counted = match kind {
782 "n" => {
783 let unsigned = value.strip_prefix('-').unwrap_or(value);
784 if let Some(c) = unsigned
785 .chars()
786 .find(|c| !c.is_ascii_digit() && *c != decimal_mark)
787 {
788 return Some(match c {
789 '.' | ',' => (
790 "19",
791 format!("'{c}' is not the decimal mark '{decimal_mark}'"),
792 ),
793 _ => ("37", format!("'{c}' in a numeric data element ({format})")),
794 });
795 }
796 let marks = unsigned.matches(decimal_mark).count();
797 if marks > 1 || unsigned.ends_with(decimal_mark) {
798 return Some(("19", format!("not a decimal number ({format})")));
799 }
800 if unsigned.starts_with(decimal_mark) {
801 return Some(("38", "no digit before the decimal mark".to_string()));
802 }
803 unsigned.chars().filter(char::is_ascii_digit).count()
804 }
805 "a" => {
806 if let Some(c) = value.chars().find(char::is_ascii_digit) {
807 return Some((
808 "37",
809 format!("'{c}' in an alphabetic data element ({format})"),
810 ));
811 }
812 value.chars().count()
813 }
814 _ => value.chars().count(),
815 };
816 if counted > length {
817 return Some((
818 "39",
819 format!("{counted} characters, {format} allows {length}"),
820 ));
821 }
822 if !upto && counted < length {
823 return Some((
824 "40",
825 format!("{counted} characters, {format} requires {length}"),
826 ));
827 }
828 None
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn formats() {
837 assert_eq!(check_format("12345", "n5", '.'), None);
838 assert_eq!(check_format("1234", "n5", '.').map(|f| f.0), Some("40"));
839 assert_eq!(check_format("123456", "n..5", '.').map(|f| f.0), Some("39"));
840 assert_eq!(check_format("-12.5", "n..3", '.'), None);
841 assert_eq!(check_format(".5", "n..3", '.').map(|f| f.0), Some("38"));
842 assert_eq!(check_format("5.", "n..3", '.').map(|f| f.0), Some("19"));
843 assert_eq!(check_format("1,5", "n..3", '.').map(|f| f.0), Some("19"));
844 assert_eq!(check_format("1A", "n..3", '.').map(|f| f.0), Some("37"));
845 assert_eq!(check_format("Z1", "a..3", '.').map(|f| f.0), Some("37"));
846 assert_eq!(check_format("ÄBC", "an3", '.'), None);
847 assert_eq!(check_format("AB", "an3", '.').map(|f| f.0), Some("40"));
848 }
849
850 #[test]
851 fn charsets() {
852 assert!(Charset::Unoc.allows('ä'));
853 assert!(!Charset::Unoc.allows('€'));
854 assert!(!Charset::Unoc.allows('\n'));
855 assert!(!Charset::Unoa.allows('a'));
856 assert!(Charset::Unob.allows('a'));
857 }
858}