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