Skip to main content

mig_assembly/
diagnostic.rs

1//! Structure diagnostics emitted during MIG-guided assembly.
2
3use serde::{Deserialize, Serialize};
4
5/// A structure-level issue found during MIG-guided assembly.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct StructureDiagnostic {
8    pub kind: StructureDiagnosticKind,
9    pub segment_id: String,
10    pub position: usize,
11    pub message: String,
12}
13
14/// Classification of structure-level diagnostic issues.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum StructureDiagnosticKind {
17    /// A segment appeared where it was not expected by the MIG schema.
18    UnexpectedSegment,
19    /// A segment the MIG marks mandatory (`M` / `R`) is absent from a
20    /// message, or from a group repetition of the variant that requires it.
21    MissingRequiredSegment,
22    /// A segment repeats more often than the MIG allows at its place.
23    MaxRepetitionsExceeded,
24    /// A segment's qualifier is none of the codes the MIG allows for that
25    /// segment in this group, so no slot could hold it; the assembler
26    /// advanced past it.
27    UnrecognizedQualifier,
28    /// A group variant the MIG marks mandatory (`M` / `R`) is absent.
29    MissingRequiredGroup,
30    /// A group variant repeats more often than the MIG allows.
31    GroupMaxRepetitionsExceeded,
32    /// A segment was present in the EDIFACT input but the PID-filtered MIG
33    /// has no slot for it, and the assembler ran with `skip_unknown_segments`
34    /// on. The segment was advanced past (preserved on `skipped_segments`)
35    /// so subsequent legitimate content still assembles, but the caller
36    /// should surface it — the message contains data outside the AHB.
37    SkippedUnknownSegment,
38    /// A segment the MIG defines as a non-entry segment of a group (e.g. `CAV`
39    /// in SG10) appeared without that group's entry segment (`CCI`), so no
40    /// group repetition could hold it. Like [`SkippedUnknownSegment`] the
41    /// assembler advanced past it and its content is absent from the result,
42    /// but the defect is a missing entry segment, not foreign content.
43    ///
44    /// [`SkippedUnknownSegment`]: Self::SkippedUnknownSegment
45    OrphanedGroupSegment,
46}
47
48impl std::fmt::Display for StructureDiagnostic {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(
51            f,
52            "[{:?}] {} at position {}: {}",
53            self.kind, self.segment_id, self.position, self.message
54        )
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn display_format_includes_all_fields() {
64        let diag = StructureDiagnostic {
65            kind: StructureDiagnosticKind::UnexpectedSegment,
66            segment_id: "BGM".to_string(),
67            position: 3,
68            message: "BGM not expected after UNH".to_string(),
69        };
70        let display = diag.to_string();
71        assert!(display.contains("BGM"), "display should contain segment_id");
72        assert!(display.contains("3"), "display should contain position");
73        assert!(
74            display.contains("BGM not expected after UNH"),
75            "display should contain message"
76        );
77        assert!(
78            display.contains("UnexpectedSegment"),
79            "display should contain kind"
80        );
81    }
82
83    #[test]
84    fn display_format_missing_required() {
85        let diag = StructureDiagnostic {
86            kind: StructureDiagnosticKind::MissingRequiredSegment,
87            segment_id: "IDE".to_string(),
88            position: 5,
89            message: "mandatory IDE segment missing in SG4".to_string(),
90        };
91        let display = diag.to_string();
92        assert_eq!(
93            display,
94            "[MissingRequiredSegment] IDE at position 5: mandatory IDE segment missing in SG4"
95        );
96    }
97
98    #[test]
99    fn serialization_roundtrip() {
100        let diag = StructureDiagnostic {
101            kind: StructureDiagnosticKind::MaxRepetitionsExceeded,
102            segment_id: "RFF".to_string(),
103            position: 12,
104            message: "RFF exceeded max repetitions of 5".to_string(),
105        };
106        let json = serde_json::to_string(&diag).expect("serialize");
107        let roundtripped: StructureDiagnostic = serde_json::from_str(&json).expect("deserialize");
108        assert_eq!(
109            roundtripped.kind,
110            StructureDiagnosticKind::MaxRepetitionsExceeded
111        );
112        assert_eq!(roundtripped.segment_id, "RFF");
113        assert_eq!(roundtripped.position, 12);
114        assert_eq!(roundtripped.message, "RFF exceeded max repetitions of 5");
115    }
116
117    #[test]
118    fn serialization_roundtrip_unrecognized_qualifier() {
119        let diag = StructureDiagnostic {
120            kind: StructureDiagnosticKind::UnrecognizedQualifier,
121            segment_id: "LOC".to_string(),
122            position: 7,
123            message: "qualifier Z99 not recognized for LOC in SG5".to_string(),
124        };
125        let json = serde_json::to_string(&diag).expect("serialize");
126        let roundtripped: StructureDiagnostic = serde_json::from_str(&json).expect("deserialize");
127        assert_eq!(
128            roundtripped.kind,
129            StructureDiagnosticKind::UnrecognizedQualifier
130        );
131        assert_eq!(roundtripped.segment_id, "LOC");
132        assert_eq!(roundtripped.position, 7);
133        assert_eq!(
134            roundtripped.message,
135            "qualifier Z99 not recognized for LOC in SG5"
136        );
137    }
138}