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    /// AHB007 — a group or segment the AHB allows once per transaction
64    /// (`[2061]` "genau einmal je SG4 IDE (Vorgang)") is sent `count` times in
65    /// one. `path` is the group or segment (`SG4/SG8`, `SG4/STS`).
66    RepeatedInTransaction { path: String, count: usize },
67    /// AHB008 — a group or segment sent another number of times in its group
68    /// than a repetition condition allows there: too often (ORDERS `[2005]`
69    /// "Pro SG29 LIN ist die SG34 RFF+Z09 (Gerätenummer) genau einmal
70    /// anzugeben") or too rarely (UTILMD `[2317]` "mindestens zweimal").
71    /// `path` is the group or segment (`SG29/SG34`), `count` how often the
72    /// group instance holds it.
73    RepetitionCount { path: String, count: usize },
74    /// COD002 — the value is a valid code but not allowed for this PID.
75    CodeNotAllowedForPid { value: String, allowed: Vec<String> },
76    /// COD003 — a response's answer (AJT 4465, STS C556/9013) is not one of the
77    /// codes of the EBD or Codeliste it names beside it (AJT 1082, C556/1131):
78    /// `AJT+ZB5+G_0059`. The AHB lists which lists may be named; the EBD
79    /// document says what they hold.
80    AnswerNotInList {
81        answer: String,
82        list: String,
83        allowed: Vec<String>,
84    },
85    /// FMT001 — the value is longer than its MIG format allows (`an..512`).
86    ValueTooLong {
87        field_name: String,
88        length: usize,
89        max: usize,
90    },
91    /// FMT002 — a data element of numeric format (`n..10`) holds something
92    /// that is not a number (`O5D2DXBG1A`).
93    ValueNotNumeric { field_name: String },
94    /// STR007 — UNT's declared segment count disagrees with the actual count.
95    UntSegmentCountMismatch { declared: usize, actual: usize },
96    /// STR007 — the input holds several messages, so the count is not checkable.
97    UntCountNotVerifiable { unh_count: usize },
98    /// STR003 / STR008 — a structure diagnostic raised during assembly.
99    ///
100    /// `detail` carries the diagnostic's own message verbatim: some
101    /// diagnostics (e.g. `mig_assembly::Assembler::assemble_with_diagnostics`'s
102    /// `"Assembly failed: {e}"` case) say something not derivable from
103    /// `(kind, segment_id, position)` alone.
104    StructureDiagnostic {
105        kind: StructureDiagnosticKind,
106        segment_id: String,
107        position: usize,
108        detail: String,
109    },
110}
111
112impl IssueKind {
113    /// The stable machine identifier, unchanged from the former `code` field.
114    pub fn code(&self) -> &'static str {
115        match self {
116            Self::MissingRequiredField { .. } => "AHB001",
117            Self::GroupNotAllowed { .. } | Self::SegmentNotAllowed { .. } => "AHB002",
118            Self::FieldConditionNotSatisfied { .. } => "AHB003",
119            Self::ConditionUnknown { .. } => "AHB005",
120            Self::PackageCardinality { .. } => "AHB006",
121            Self::RepeatedInTransaction { .. } => "AHB007",
122            Self::RepetitionCount { .. } => "AHB008",
123            Self::CodeNotAllowedForPid { .. } => "COD002",
124            Self::AnswerNotInList { .. } => "COD003",
125            Self::ValueTooLong { .. } => "FMT001",
126            Self::ValueNotNumeric { .. } => "FMT002",
127            Self::UntSegmentCountMismatch { .. } | Self::UntCountNotVerifiable { .. } => "STR007",
128            // One code per kind (#81). An orphaned group segment is a skipped
129            // segment too (its content is equally absent); the kind and
130            // message tell the missing entry segment apart. A segment whose
131            // qualifier no slot at its place accepts is unexpected there.
132            Self::StructureDiagnostic { kind, .. } => match kind {
133                StructureDiagnosticKind::MissingRequiredSegment => "STR001",
134                StructureDiagnosticKind::MaxRepetitionsExceeded => "STR002",
135                StructureDiagnosticKind::UnexpectedSegment
136                | StructureDiagnosticKind::UnrecognizedQualifier => "STR003",
137                StructureDiagnosticKind::MissingRequiredGroup => "STR005",
138                StructureDiagnosticKind::GroupMaxRepetitionsExceeded => "STR006",
139                StructureDiagnosticKind::SkippedUnknownSegment
140                | StructureDiagnosticKind::OrphanedGroupSegment => "STR008",
141            },
142        }
143    }
144
145    /// The issue's category. Derived, so it can never disagree with the kind.
146    pub fn category(&self) -> ValidationCategory {
147        match self {
148            Self::MissingRequiredField { .. }
149            | Self::GroupNotAllowed { .. }
150            | Self::SegmentNotAllowed { .. }
151            | Self::FieldConditionNotSatisfied { .. }
152            | Self::ConditionUnknown { .. }
153            | Self::PackageCardinality { .. }
154            | Self::RepeatedInTransaction { .. }
155            | Self::RepetitionCount { .. } => ValidationCategory::Ahb,
156            Self::CodeNotAllowedForPid { .. } | Self::AnswerNotInList { .. } => {
157                ValidationCategory::Code
158            }
159            Self::UntSegmentCountMismatch { .. }
160            | Self::UntCountNotVerifiable { .. }
161            | Self::StructureDiagnostic { .. } => ValidationCategory::Structure,
162            Self::ValueTooLong { .. } | Self::ValueNotNumeric { .. } => ValidationCategory::Format,
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use mig_assembly::StructureDiagnosticKind;
171
172    #[test]
173    fn code_matches_the_legacy_error_code_for_every_kind() {
174        let cases: Vec<(IssueKind, &str)> = vec![
175            (
176                IssueKind::MissingRequiredField {
177                    field_name: "f".into(),
178                },
179                "AHB001",
180            ),
181            (
182                IssueKind::GroupNotAllowed {
183                    group: "SG4/SG5".into(),
184                },
185                "AHB002",
186            ),
187            (
188                IssueKind::SegmentNotAllowed {
189                    segment: "SG4/SG8/SG10/CAV".into(),
190                },
191                "AHB002",
192            ),
193            (
194                IssueKind::FieldConditionNotSatisfied {
195                    field_name: "f".into(),
196                },
197                "AHB003",
198            ),
199            (
200                IssueKind::ConditionUnknown {
201                    field_name: "f".into(),
202                    unresolved: UnresolvedConditions::default(),
203                },
204                "AHB005",
205            ),
206            (
207                IssueKind::PackageCardinality {
208                    package_id: "1P0..1".into(),
209                    present: 2,
210                    min: 0,
211                    max: 1,
212                    codes: vec![],
213                },
214                "AHB006",
215            ),
216            (
217                IssueKind::RepetitionCount {
218                    path: "SG29/SG34".into(),
219                    count: 2,
220                },
221                "AHB008",
222            ),
223            (
224                IssueKind::CodeNotAllowedForPid {
225                    value: "X".into(),
226                    allowed: vec![],
227                },
228                "COD002",
229            ),
230            (
231                IssueKind::AnswerNotInList {
232                    answer: "ZB5".into(),
233                    list: "G_0059".into(),
234                    allowed: vec![],
235                },
236                "COD003",
237            ),
238            (
239                IssueKind::ValueTooLong {
240                    field_name: "f".into(),
241                    length: 600,
242                    max: 512,
243                },
244                "FMT001",
245            ),
246            (
247                IssueKind::UntSegmentCountMismatch {
248                    declared: 30,
249                    actual: 31,
250                },
251                "STR007",
252            ),
253            (IssueKind::UntCountNotVerifiable { unh_count: 2 }, "STR007"),
254            (
255                IssueKind::StructureDiagnostic {
256                    kind: StructureDiagnosticKind::SkippedUnknownSegment,
257                    segment_id: "CCI".into(),
258                    position: 15,
259                    detail: "irrelevant to the code".into(),
260                },
261                "STR008",
262            ),
263            (
264                IssueKind::StructureDiagnostic {
265                    kind: StructureDiagnosticKind::UnexpectedSegment,
266                    segment_id: "CCI".into(),
267                    position: 15,
268                    detail: "irrelevant to the code".into(),
269                },
270                "STR003",
271            ),
272        ];
273        for (kind, expected) in cases {
274            assert_eq!(kind.code(), expected, "wrong code for {kind:?}");
275        }
276    }
277
278    /// Every structure diagnostic keeps its own code (#81): a repetition
279    /// overrun must not surface as "unexpected segment".
280    #[test]
281    fn each_structure_kind_has_its_own_code() {
282        use StructureDiagnosticKind as K;
283        for (kind, expected) in [
284            (K::MissingRequiredSegment, "STR001"),
285            (K::MaxRepetitionsExceeded, "STR002"),
286            (K::UnexpectedSegment, "STR003"),
287            (K::UnrecognizedQualifier, "STR003"),
288            (K::MissingRequiredGroup, "STR005"),
289            (K::GroupMaxRepetitionsExceeded, "STR006"),
290            (K::SkippedUnknownSegment, "STR008"),
291            (K::OrphanedGroupSegment, "STR008"),
292        ] {
293            let issue = IssueKind::StructureDiagnostic {
294                kind,
295                segment_id: "RFF".into(),
296                position: 1,
297                detail: String::new(),
298            };
299            assert_eq!(issue.code(), expected, "{kind:?}");
300        }
301    }
302
303    #[test]
304    fn category_is_derived_from_the_kind() {
305        assert_eq!(
306            IssueKind::MissingRequiredField {
307                field_name: "f".into()
308            }
309            .category(),
310            ValidationCategory::Ahb
311        );
312        assert_eq!(
313            IssueKind::CodeNotAllowedForPid {
314                value: "X".into(),
315                allowed: vec![]
316            }
317            .category(),
318            ValidationCategory::Code
319        );
320        assert_eq!(
321            IssueKind::UntSegmentCountMismatch {
322                declared: 1,
323                actual: 2
324            }
325            .category(),
326            ValidationCategory::Structure
327        );
328    }
329}