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