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