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 (tree, diagnostics) = if merged.1.is_empty() {
291 merged
292 } else {
293 let apart = place(&placements.apart);
294 if apart.1.len() < merged.1.len() {
295 apart
296 } else {
297 merged
298 }
299 };
300
301 let first_segments = group_first_segments(mig);
302 let within =
303 |n: u32| n >= unh_number && unt_index.map_or(true, |i| n <= segments[i].segment_number);
304 let mut reported = HashSet::new();
305 for diag in diagnostics {
306 let n = u32::try_from(diag.position).unwrap_or(0);
307 if !within(n) {
308 continue;
309 }
310 let (code, tag) = match diag.kind {
311 StructureDiagnosticKind::MissingRequiredSegment => ("13", diag.segment_id.clone()),
312 StructureDiagnosticKind::MissingRequiredGroup => (
313 "13",
314 first_segments
315 .get(diag.segment_id.as_str())
316 .cloned()
317 .unwrap_or(diag.segment_id.clone()),
318 ),
319 StructureDiagnosticKind::MaxRepetitionsExceeded => ("35", diag.segment_id.clone()),
320 StructureDiagnosticKind::GroupMaxRepetitionsExceeded => (
321 "36",
322 first_segments
323 .get(diag.segment_id.as_str())
324 .cloned()
325 .unwrap_or(diag.segment_id.clone()),
326 ),
327 _ => ("15", diag.segment_id.clone()),
328 };
329 if !reported.insert((code, tag.clone(), n)) {
331 continue;
332 }
333 out.push(SyntaxFinding {
334 scope: SyntaxScope::Segment,
335 code: code.into(),
336 segment_tag: tag,
337 message_segment_number: counter(n),
338 element_position: None,
339 component_position: None,
340 detail: diag.message,
341 });
342 }
343
344 let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
347 root.extend(tree.inter_group_segments.values().flatten());
348 let mut walk = ElementWalk {
349 settings,
350 accepted,
351 within: &within,
352 counter: &counter,
353 out: &mut out,
354 };
355 walk.segments(&root, &unmerged.segments);
356 walk.groups(&tree.groups, &unmerged.segment_groups);
357
358 let wrong_codes: HashSet<u32> = out
361 .iter()
362 .filter(|f| f.scope == SyntaxScope::Element && f.code == "12")
363 .map(|f| f.message_segment_number)
364 .collect();
365 out.retain(|f| {
366 !(f.scope == SyntaxScope::Segment
367 && f.code == "15"
368 && wrong_codes.contains(&f.message_segment_number))
369 });
370
371 out.sort_by_key(|f| {
372 (
373 f.message_segment_number,
374 f.element_position,
375 f.component_position,
376 )
377 });
378 out
379}
380
381fn finding(
382 scope: SyntaxScope,
383 code: &str,
384 seg: &OwnedSegment,
385 message_segment_number: u32,
386 at: Option<(u32, Option<u32>)>,
387 detail: String,
388) -> SyntaxFinding {
389 SyntaxFinding {
390 scope,
391 code: code.into(),
392 segment_tag: seg.id.clone(),
393 message_segment_number,
394 element_position: at.map(|(e, _)| e),
395 component_position: at.and_then(|(_, c)| c),
396 detail,
397 }
398}
399
400fn all_numbers(mig: &MigSchema) -> HashSet<String> {
402 fn groups(list: &[MigSegmentGroup], out: &mut HashSet<String>) {
403 for g in list {
404 out.extend(g.segments.iter().filter_map(|s| s.number.clone()));
405 groups(&g.nested_groups, out);
406 }
407 }
408 let mut out: HashSet<String> = mig
409 .segments
410 .iter()
411 .filter_map(|s| s.number.clone())
412 .collect();
413 groups(&mig.segment_groups, &mut out);
414 out
415}
416
417fn group_first_segments(mig: &MigSchema) -> HashMap<&str, String> {
420 fn walk<'m>(list: &'m [MigSegmentGroup], out: &mut HashMap<&'m str, String>) {
421 for g in list {
422 if let Some(first) = g.segments.first() {
423 out.entry(g.id.as_str()).or_insert_with(|| first.id.clone());
424 }
425 walk(&g.nested_groups, out);
426 }
427 }
428 let mut out = HashMap::new();
429 walk(&mig.segment_groups, &mut out);
430 out
431}
432
433struct ElementWalk<'a> {
435 settings: SyntaxSettings,
436 accepted: &'a AcceptedCodes,
437 within: &'a dyn Fn(u32) -> bool,
438 counter: &'a dyn Fn(u32) -> u32,
439 out: &'a mut Vec<SyntaxFinding>,
440}
441
442impl ElementWalk<'_> {
443 fn groups(&mut self, present: &[AssembledGroup], variants: &[MigSegmentGroup]) {
444 for group in present {
445 let same_id: Vec<&MigSegmentGroup> =
446 variants.iter().filter(|v| v.id == group.group_id).collect();
447 for rep in &group.repetitions {
448 let candidates: Vec<&MigSegmentGroup> = match same_id.as_slice() {
453 [only] => vec![*only],
454 _ => rep.segments.first().map_or(Vec::new(), |entry| {
455 same_id
456 .iter()
457 .copied()
458 .filter(|v| {
459 v.entry_qualifier().is_some_and(|q| {
460 q.tag == entry.tag && q.matches(&entry.elements)
461 })
462 })
463 .collect()
464 }),
465 };
466 let mut best: Option<Vec<SyntaxFinding>> = None;
467 for variant in candidates {
468 let mut found = Vec::new();
469 let mut walk = ElementWalk {
470 settings: self.settings,
471 accepted: self.accepted,
472 within: self.within,
473 counter: self.counter,
474 out: &mut found,
475 };
476 let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
477 walk.segments(&own, &variant.segments);
478 walk.groups(&rep.child_groups, &variant.nested_groups);
479 if best.as_ref().map_or(true, |b| found.len() < b.len()) {
480 best = Some(found);
481 }
482 }
483 self.out.extend(best.unwrap_or_default());
484 }
485 }
486 }
487
488 fn segments(&mut self, present: &[&AssembledSegment], slots: &[MigSegment]) {
489 for seg in present {
490 let Some(n) = seg.segment_number else {
491 continue;
492 };
493 if seg.tag == "UNH" || seg.tag == "UNT" || !(self.within)(n) {
494 continue;
495 }
496 let owned = OwnedSegment {
497 id: seg.tag.clone(),
498 elements: seg.elements.clone(),
499 segment_number: n,
500 };
501 let mut best: Option<Vec<SyntaxFinding>> = None;
505 for slot in slots_for(seg, slots) {
506 let mut found = Vec::new();
507 check_elements(
508 &owned,
509 slot,
510 (self.counter)(n),
511 self.settings,
512 self.accepted,
513 &mut found,
514 );
515 if best.as_ref().map_or(true, |b| found.len() < b.len()) {
516 best = Some(found);
517 }
518 }
519 self.out.extend(best.unwrap_or_default());
520 }
521 }
522}
523
524fn slots_for<'m>(seg: &AssembledSegment, slots: &'m [MigSegment]) -> Vec<&'m MigSegment> {
530 let tagged: Vec<&MigSegment> = slots.iter().filter(|s| s.id == seg.tag).collect();
531 let carries = |slot: &MigSegment| {
532 qualifier(slot).map(|(e, c, d)| {
533 seg.elements
534 .get(e)
535 .and_then(|el| el.get(c))
536 .is_some_and(|v| lists_code(d, v))
537 })
538 };
539 let qualified: Vec<&MigSegment> = tagged
540 .iter()
541 .copied()
542 .filter(|slot| carries(slot) == Some(true))
543 .collect();
544 if !qualified.is_empty() {
545 return qualified;
546 }
547 let open: Vec<&MigSegment> = tagged
548 .iter()
549 .copied()
550 .filter(|slot| carries(slot).is_none())
551 .collect();
552 if !open.is_empty() {
553 return open;
554 }
555 match tagged.as_slice() {
556 [only] => vec![*only],
557 _ => Vec::new(),
558 }
559}
560
561fn is_placeholder(code: &CodeDefinition) -> bool {
563 code.name == "Beispielcode" || code.value == "XYZ"
564}
565
566fn codes(d: &MigDataElement) -> Vec<&str> {
568 if d.codes.iter().any(is_placeholder) {
569 return Vec::new();
570 }
571 d.codes
572 .iter()
573 .map(|c| c.value.as_str())
574 .filter(|v| !v.is_empty())
575 .collect()
576}
577
578fn qualifier(slot: &MigSegment) -> Option<(usize, usize, &MigDataElement)> {
580 let simple = slot.data_elements.iter().map(|d| (d.position, 0, d));
581 let composite = slot.composites.iter().flat_map(|c| {
582 c.data_elements
583 .iter()
584 .map(move |d| (c.position, d.position, d))
585 });
586 simple
587 .chain(composite)
588 .filter(|(_, _, d)| has_codes(d))
589 .min_by_key(|(e, c, _)| (*e, *c))
590}
591
592fn has_codes(d: &MigDataElement) -> bool {
594 !d.codes.iter().any(is_placeholder) && d.codes.iter().any(|c| !c.value.is_empty())
595}
596
597fn lists_code(d: &MigDataElement, value: &str) -> bool {
599 has_codes(d) && !value.is_empty() && d.codes.iter().any(|c| c.value == value)
600}
601
602fn required(spec: Option<&str>, std: Option<&str>) -> bool {
603 matches!(spec.or(std).map(str::trim), Some("M" | "R"))
604}
605
606fn check_elements(
608 seg: &OwnedSegment,
609 slot: &MigSegment,
610 counter: u32,
611 settings: SyntaxSettings,
612 accepted: &AcceptedCodes,
613 out: &mut Vec<SyntaxFinding>,
614) {
615 let number = slot.number.as_deref().unwrap_or("");
616 let also = |e: usize, c: usize| accepted.get(&(number.to_string(), e, c));
617 let defined = slot
618 .data_elements
619 .iter()
620 .map(|d| d.position)
621 .chain(slot.composites.iter().map(|c| c.position))
622 .max()
623 .map_or(0, |p| p + 1);
624 let last_filled = seg
625 .elements
626 .iter()
627 .rposition(|e| e.iter().any(|c| !c.is_empty()))
628 .map_or(0, |p| p + 1);
629 if last_filled > defined {
630 out.push(finding(
631 SyntaxScope::Segment,
632 "16",
633 seg,
634 counter,
635 None,
636 format!(
637 "{} has {last_filled} data elements, the MIG defines {defined}",
638 seg.id
639 ),
640 ));
641 }
642
643 for d in &slot.data_elements {
644 let element = seg.elements.get(d.position);
645 let at = d.position as u32 + 2;
646 let value = element.and_then(|e| e.first()).map_or("", String::as_str);
647 if element.is_some_and(|e| e.iter().skip(1).any(|c| !c.is_empty())) {
648 out.push(finding(
649 SyntaxScope::Element,
650 "16",
651 seg,
652 counter,
653 Some((at, None)),
654 format!(
655 "{} {} is a simple data element but has components",
656 seg.id, d.id
657 ),
658 ));
659 }
660 check_value(
661 seg,
662 d,
663 value,
664 counter,
665 (at, None),
666 settings,
667 &|| also(d.position, 0),
668 out,
669 );
670 }
671
672 for c in &slot.composites {
673 let at = c.position as u32 + 2;
674 let components: &[String] = seg.elements.get(c.position).map_or(&[], Vec::as_slice);
675 if components.iter().all(String::is_empty) {
676 if required(c.status_spec.as_deref(), c.status_std.as_deref()) {
677 out.push(finding(
678 SyntaxScope::Element,
679 "13",
680 seg,
681 counter,
682 Some((at, None)),
683 format!("{} {} ({}) is missing", seg.id, c.id, c.name),
684 ));
685 }
686 continue;
687 }
688 let defined = c
689 .data_elements
690 .iter()
691 .map(|d| d.position)
692 .max()
693 .map_or(0, |p| p + 1);
694 let last_filled = components
695 .iter()
696 .rposition(|v| !v.is_empty())
697 .map_or(0, |p| p + 1);
698 if last_filled > defined {
699 out.push(finding(
700 SyntaxScope::Element,
701 "16",
702 seg,
703 counter,
704 Some((at, None)),
705 format!(
706 "{} {} has {last_filled} components, the MIG defines {defined}",
707 seg.id, c.id
708 ),
709 ));
710 }
711 for d in &c.data_elements {
712 let value = components.get(d.position).map_or("", String::as_str);
713 check_value(
714 seg,
715 d,
716 value,
717 counter,
718 (at, Some(d.position as u32 + 1)),
719 settings,
720 &|| also(c.position, d.position),
721 out,
722 );
723 }
724 }
725}
726
727fn check_value<'a>(
729 seg: &OwnedSegment,
730 d: &MigDataElement,
731 value: &str,
732 counter: u32,
733 at: (u32, Option<u32>),
734 settings: SyntaxSettings,
735 also: &dyn Fn() -> Option<&'a Accepted>,
737 out: &mut Vec<SyntaxFinding>,
738) {
739 let mut push = |code: &str, detail: String| {
740 out.push(finding(
741 SyntaxScope::Element,
742 code,
743 seg,
744 counter,
745 Some(at),
746 detail,
747 ));
748 };
749 let name = || format!("{} {} ({})", seg.id, d.id, d.name);
751 if value.is_empty() {
752 if required(d.status_spec.as_deref(), d.status_std.as_deref()) {
753 push("13", format!("{} is missing", name()));
754 }
755 return;
756 }
757 if let Some(bad) = value.chars().find(|&c| !settings.charset.allows(c)) {
758 push(
759 "21",
760 format!(
761 "{} holds '{bad}', which its syntax level does not allow",
762 name()
763 ),
764 );
765 return;
766 }
767 if let Some(format) = d.format_spec.as_deref().or(d.format_std.as_deref()) {
768 if let Some((code, detail)) = check_format(value, format, settings.decimal_mark) {
769 push(code, format!("{} '{value}': {detail}", name()));
770 return;
771 }
772 }
773 if has_codes(d) && !lists_code(d, value) && !also().is_some_and(|a| a.accepts(value)) {
774 push(
775 "12",
776 format!(
777 "{} '{value}' is none of the MIG's codes {}",
778 name(),
779 codes(d).join(", ")
780 ),
781 );
782 }
783}
784
785fn parse_format(format: &str) -> Option<(&str, usize, bool)> {
788 let format = format.trim();
789 let kind_end = format.find(|c: char| !c.is_ascii_alphabetic())?;
790 let (kind, rest) = format.split_at(kind_end);
791 let (upto, digits) = match rest.strip_prefix("..") {
792 Some(d) => (true, d),
793 None => (false, rest),
794 };
795 Some((kind, digits.parse().ok()?, upto))
796}
797
798fn check_format(value: &str, format: &str, decimal_mark: char) -> Option<(&'static str, String)> {
800 let (kind, length, upto) = parse_format(format)?;
801 let counted = match kind {
802 "n" => {
803 let unsigned = value.strip_prefix('-').unwrap_or(value);
804 if let Some(c) = unsigned
805 .chars()
806 .find(|c| !c.is_ascii_digit() && *c != decimal_mark)
807 {
808 return Some(match c {
809 '.' | ',' => (
810 "19",
811 format!("'{c}' is not the decimal mark '{decimal_mark}'"),
812 ),
813 _ => ("37", format!("'{c}' in a numeric data element ({format})")),
814 });
815 }
816 let marks = unsigned.matches(decimal_mark).count();
817 if marks > 1 || unsigned.ends_with(decimal_mark) {
818 return Some(("19", format!("not a decimal number ({format})")));
819 }
820 if unsigned.starts_with(decimal_mark) {
821 return Some(("38", "no digit before the decimal mark".to_string()));
822 }
823 unsigned.chars().filter(char::is_ascii_digit).count()
824 }
825 "a" => {
826 if let Some(c) = value.chars().find(char::is_ascii_digit) {
827 return Some((
828 "37",
829 format!("'{c}' in an alphabetic data element ({format})"),
830 ));
831 }
832 value.chars().count()
833 }
834 _ => value.chars().count(),
835 };
836 if counted > length {
837 return Some((
838 "39",
839 format!("{counted} characters, {format} allows {length}"),
840 ));
841 }
842 if !upto && counted < length {
843 return Some((
844 "40",
845 format!("{counted} characters, {format} requires {length}"),
846 ));
847 }
848 None
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 #[test]
856 fn formats() {
857 assert_eq!(check_format("12345", "n5", '.'), None);
858 assert_eq!(check_format("1234", "n5", '.').map(|f| f.0), Some("40"));
859 assert_eq!(check_format("123456", "n..5", '.').map(|f| f.0), Some("39"));
860 assert_eq!(check_format("-12.5", "n..3", '.'), None);
861 assert_eq!(check_format(".5", "n..3", '.').map(|f| f.0), Some("38"));
862 assert_eq!(check_format("5.", "n..3", '.').map(|f| f.0), Some("19"));
863 assert_eq!(check_format("1,5", "n..3", '.').map(|f| f.0), Some("19"));
864 assert_eq!(check_format("1A", "n..3", '.').map(|f| f.0), Some("37"));
865 assert_eq!(check_format("Z1", "a..3", '.').map(|f| f.0), Some("37"));
866 assert_eq!(check_format("ÄBC", "an3", '.'), None);
867 assert_eq!(check_format("AB", "an3", '.').map(|f| f.0), Some("40"));
868 }
869
870 #[test]
871 fn charsets() {
872 assert!(Charset::Unoc.allows('ä'));
873 assert!(!Charset::Unoc.allows('€'));
874 assert!(!Charset::Unoc.allows('\n'));
875 assert!(!Charset::Unoa.allows('a'));
876 assert!(Charset::Unob.allows('a'));
877 }
878}