Skip to main content

automapper_validation/validator/
kind.rs

1//! The machine-readable description of what a validation issue *is*.
2//!
3//! Wording lives in [`crate::display`]. A kind carries every fact the wording
4//! needs, so a consumer can say something other than the sentence we ship.
5
6use mig_assembly::StructureDiagnosticKind;
7use serde::{Deserialize, Serialize};
8
9use super::issue::ValidationCategory;
10
11/// Why a condition could not be evaluated.
12///
13/// Any combination of these may be non-empty; each is a list of AHB condition ids.
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15pub struct UnresolvedConditions {
16    /// Need an external provider to answer.
17    pub external: Vec<u32>,
18    /// Not determinable from the message data.
19    pub undetermined: Vec<u32>,
20    /// Not present in the rulebook at all.
21    pub missing: Vec<u32>,
22}
23
24impl UnresolvedConditions {
25    /// True when nothing at all could be attributed — the issue has no detail.
26    pub fn is_empty(&self) -> bool {
27        self.external.is_empty() && self.undetermined.is_empty() && self.missing.is_empty()
28    }
29}
30
31/// What kind of problem a [`ValidationIssue`](super::issue::ValidationIssue) reports.
32///
33/// One variant per problem the validator actually emits. `ErrorCodes` lists 22
34/// code strings, but only these are ever constructed; adding a new check means
35/// adding a variant, which the compiler then forces every narrator to handle.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(tag = "type", rename_all = "camelCase")]
38pub enum IssueKind {
39    /// AHB001 — a field the AHB marks mandatory for this PID is absent.
40    MissingRequiredField { field_name: String },
41    /// AHB002 — a segment group is present although its AHB condition does not
42    /// hold (`Muss [96]` without STS+7 ZAP). `group` is its path (`SG4/SG5`).
43    GroupNotAllowed { group: String },
44    /// AHB002 — a segment is present although its own AHB condition does not
45    /// hold (`CAV+Z56` Wärmenutzung, `Muss [239]`, without CAV+Z65 in its
46    /// SG10). `segment` is its path (`SG4/SG8/SG10/CAV`).
47    SegmentNotAllowed { segment: String },
48    /// AHB003 — the field is present but its AHB condition is not satisfied.
49    FieldConditionNotSatisfied { field_name: String },
50    /// AHB005 — the condition expression could not be fully evaluated.
51    ConditionUnknown {
52        field_name: String,
53        unresolved: UnresolvedConditions,
54    },
55    /// AHB006 — number of codes present in a package is outside `min..max`.
56    PackageCardinality {
57        package_id: String,
58        present: usize,
59        min: usize,
60        max: usize,
61        codes: Vec<String>,
62    },
63    /// COD002 — the value is a valid code but not allowed for this PID.
64    CodeNotAllowedForPid { value: String, allowed: Vec<String> },
65    /// COD003 — a response's answer (AJT 4465, STS C556/9013) is not one of the
66    /// codes of the EBD or Codeliste it names beside it (AJT 1082, C556/1131):
67    /// `AJT+ZB5+G_0059`. The AHB lists which lists may be named; the EBD
68    /// document says what they hold.
69    AnswerNotInList {
70        answer: String,
71        list: String,
72        allowed: Vec<String>,
73    },
74    /// FMT001 — the value is longer than its MIG format allows (`an..512`).
75    ValueTooLong {
76        field_name: String,
77        length: usize,
78        max: usize,
79    },
80    /// STR007 — UNT's declared segment count disagrees with the actual count.
81    UntSegmentCountMismatch { declared: usize, actual: usize },
82    /// STR007 — the input holds several messages, so the count is not checkable.
83    UntCountNotVerifiable { unh_count: usize },
84    /// STR003 / STR008 — a structure diagnostic raised during assembly.
85    ///
86    /// `detail` carries the diagnostic's own message verbatim: some
87    /// diagnostics (e.g. `mig_assembly::Assembler::assemble_with_diagnostics`'s
88    /// `"Assembly failed: {e}"` case) say something not derivable from
89    /// `(kind, segment_id, position)` alone.
90    StructureDiagnostic {
91        kind: StructureDiagnosticKind,
92        segment_id: String,
93        position: usize,
94        detail: String,
95    },
96}
97
98impl IssueKind {
99    /// The stable machine identifier, unchanged from the former `code` field.
100    pub fn code(&self) -> &'static str {
101        match self {
102            Self::MissingRequiredField { .. } => "AHB001",
103            Self::GroupNotAllowed { .. } | Self::SegmentNotAllowed { .. } => "AHB002",
104            Self::FieldConditionNotSatisfied { .. } => "AHB003",
105            Self::ConditionUnknown { .. } => "AHB005",
106            Self::PackageCardinality { .. } => "AHB006",
107            Self::CodeNotAllowedForPid { .. } => "COD002",
108            Self::AnswerNotInList { .. } => "COD003",
109            Self::ValueTooLong { .. } => "FMT001",
110            Self::UntSegmentCountMismatch { .. } | Self::UntCountNotVerifiable { .. } => "STR007",
111            // One code per kind (#81). An orphaned group segment is a skipped
112            // segment too (its content is equally absent); the kind and
113            // message tell the missing entry segment apart. A segment whose
114            // qualifier no slot at its place accepts is unexpected there.
115            Self::StructureDiagnostic { kind, .. } => match kind {
116                StructureDiagnosticKind::MissingRequiredSegment => "STR001",
117                StructureDiagnosticKind::MaxRepetitionsExceeded => "STR002",
118                StructureDiagnosticKind::UnexpectedSegment
119                | StructureDiagnosticKind::UnrecognizedQualifier => "STR003",
120                StructureDiagnosticKind::MissingRequiredGroup => "STR005",
121                StructureDiagnosticKind::GroupMaxRepetitionsExceeded => "STR006",
122                StructureDiagnosticKind::SkippedUnknownSegment
123                | StructureDiagnosticKind::OrphanedGroupSegment => "STR008",
124            },
125        }
126    }
127
128    /// The issue's category. Derived, so it can never disagree with the kind.
129    pub fn category(&self) -> ValidationCategory {
130        match self {
131            Self::MissingRequiredField { .. }
132            | Self::GroupNotAllowed { .. }
133            | Self::SegmentNotAllowed { .. }
134            | Self::FieldConditionNotSatisfied { .. }
135            | Self::ConditionUnknown { .. }
136            | Self::PackageCardinality { .. } => ValidationCategory::Ahb,
137            Self::CodeNotAllowedForPid { .. } | Self::AnswerNotInList { .. } => {
138                ValidationCategory::Code
139            }
140            Self::UntSegmentCountMismatch { .. }
141            | Self::UntCountNotVerifiable { .. }
142            | Self::StructureDiagnostic { .. } => ValidationCategory::Structure,
143            Self::ValueTooLong { .. } => ValidationCategory::Format,
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use mig_assembly::StructureDiagnosticKind;
152
153    #[test]
154    fn code_matches_the_legacy_error_code_for_every_kind() {
155        let cases: Vec<(IssueKind, &str)> = vec![
156            (
157                IssueKind::MissingRequiredField {
158                    field_name: "f".into(),
159                },
160                "AHB001",
161            ),
162            (
163                IssueKind::GroupNotAllowed {
164                    group: "SG4/SG5".into(),
165                },
166                "AHB002",
167            ),
168            (
169                IssueKind::SegmentNotAllowed {
170                    segment: "SG4/SG8/SG10/CAV".into(),
171                },
172                "AHB002",
173            ),
174            (
175                IssueKind::FieldConditionNotSatisfied {
176                    field_name: "f".into(),
177                },
178                "AHB003",
179            ),
180            (
181                IssueKind::ConditionUnknown {
182                    field_name: "f".into(),
183                    unresolved: UnresolvedConditions::default(),
184                },
185                "AHB005",
186            ),
187            (
188                IssueKind::PackageCardinality {
189                    package_id: "1P0..1".into(),
190                    present: 2,
191                    min: 0,
192                    max: 1,
193                    codes: vec![],
194                },
195                "AHB006",
196            ),
197            (
198                IssueKind::CodeNotAllowedForPid {
199                    value: "X".into(),
200                    allowed: vec![],
201                },
202                "COD002",
203            ),
204            (
205                IssueKind::AnswerNotInList {
206                    answer: "ZB5".into(),
207                    list: "G_0059".into(),
208                    allowed: vec![],
209                },
210                "COD003",
211            ),
212            (
213                IssueKind::ValueTooLong {
214                    field_name: "f".into(),
215                    length: 600,
216                    max: 512,
217                },
218                "FMT001",
219            ),
220            (
221                IssueKind::UntSegmentCountMismatch {
222                    declared: 30,
223                    actual: 31,
224                },
225                "STR007",
226            ),
227            (IssueKind::UntCountNotVerifiable { unh_count: 2 }, "STR007"),
228            (
229                IssueKind::StructureDiagnostic {
230                    kind: StructureDiagnosticKind::SkippedUnknownSegment,
231                    segment_id: "CCI".into(),
232                    position: 15,
233                    detail: "irrelevant to the code".into(),
234                },
235                "STR008",
236            ),
237            (
238                IssueKind::StructureDiagnostic {
239                    kind: StructureDiagnosticKind::UnexpectedSegment,
240                    segment_id: "CCI".into(),
241                    position: 15,
242                    detail: "irrelevant to the code".into(),
243                },
244                "STR003",
245            ),
246        ];
247        for (kind, expected) in cases {
248            assert_eq!(kind.code(), expected, "wrong code for {kind:?}");
249        }
250    }
251
252    /// Every structure diagnostic keeps its own code (#81): a repetition
253    /// overrun must not surface as "unexpected segment".
254    #[test]
255    fn each_structure_kind_has_its_own_code() {
256        use StructureDiagnosticKind as K;
257        for (kind, expected) in [
258            (K::MissingRequiredSegment, "STR001"),
259            (K::MaxRepetitionsExceeded, "STR002"),
260            (K::UnexpectedSegment, "STR003"),
261            (K::UnrecognizedQualifier, "STR003"),
262            (K::MissingRequiredGroup, "STR005"),
263            (K::GroupMaxRepetitionsExceeded, "STR006"),
264            (K::SkippedUnknownSegment, "STR008"),
265            (K::OrphanedGroupSegment, "STR008"),
266        ] {
267            let issue = IssueKind::StructureDiagnostic {
268                kind,
269                segment_id: "RFF".into(),
270                position: 1,
271                detail: String::new(),
272            };
273            assert_eq!(issue.code(), expected, "{kind:?}");
274        }
275    }
276
277    #[test]
278    fn category_is_derived_from_the_kind() {
279        assert_eq!(
280            IssueKind::MissingRequiredField {
281                field_name: "f".into()
282            }
283            .category(),
284            ValidationCategory::Ahb
285        );
286        assert_eq!(
287            IssueKind::CodeNotAllowedForPid {
288                value: "X".into(),
289                allowed: vec![]
290            }
291            .category(),
292            ValidationCategory::Code
293        );
294        assert_eq!(
295            IssueKind::UntSegmentCountMismatch {
296                declared: 1,
297                actual: 2
298            }
299            .category(),
300            ValidationCategory::Structure
301        );
302    }
303}