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    /// STR007 — UNT's declared segment count disagrees with the actual count.
66    UntSegmentCountMismatch { declared: usize, actual: usize },
67    /// STR007 — the input holds several messages, so the count is not checkable.
68    UntCountNotVerifiable { unh_count: usize },
69    /// STR003 / STR008 — a structure diagnostic raised during assembly.
70    ///
71    /// `detail` carries the diagnostic's own message verbatim: some
72    /// diagnostics (e.g. `mig_assembly::Assembler::assemble_with_diagnostics`'s
73    /// `"Assembly failed: {e}"` case) say something not derivable from
74    /// `(kind, segment_id, position)` alone.
75    StructureDiagnostic {
76        kind: StructureDiagnosticKind,
77        segment_id: String,
78        position: usize,
79        detail: String,
80    },
81}
82
83impl IssueKind {
84    /// The stable machine identifier, unchanged from the former `code` field.
85    pub fn code(&self) -> &'static str {
86        match self {
87            Self::MissingRequiredField { .. } => "AHB001",
88            Self::GroupNotAllowed { .. } | Self::SegmentNotAllowed { .. } => "AHB002",
89            Self::FieldConditionNotSatisfied { .. } => "AHB003",
90            Self::ConditionUnknown { .. } => "AHB005",
91            Self::PackageCardinality { .. } => "AHB006",
92            Self::CodeNotAllowedForPid { .. } => "COD002",
93            Self::UntSegmentCountMismatch { .. } | Self::UntCountNotVerifiable { .. } => "STR007",
94            // One code per kind (#81). An orphaned group segment is a skipped
95            // segment too (its content is equally absent); the kind and
96            // message tell the missing entry segment apart. A segment whose
97            // qualifier no slot at its place accepts is unexpected there.
98            Self::StructureDiagnostic { kind, .. } => match kind {
99                StructureDiagnosticKind::MissingRequiredSegment => "STR001",
100                StructureDiagnosticKind::MaxRepetitionsExceeded => "STR002",
101                StructureDiagnosticKind::UnexpectedSegment
102                | StructureDiagnosticKind::UnrecognizedQualifier => "STR003",
103                StructureDiagnosticKind::MissingRequiredGroup => "STR005",
104                StructureDiagnosticKind::GroupMaxRepetitionsExceeded => "STR006",
105                StructureDiagnosticKind::SkippedUnknownSegment
106                | StructureDiagnosticKind::OrphanedGroupSegment => "STR008",
107            },
108        }
109    }
110
111    /// The issue's category. Derived, so it can never disagree with the kind.
112    pub fn category(&self) -> ValidationCategory {
113        match self {
114            Self::MissingRequiredField { .. }
115            | Self::GroupNotAllowed { .. }
116            | Self::SegmentNotAllowed { .. }
117            | Self::FieldConditionNotSatisfied { .. }
118            | Self::ConditionUnknown { .. }
119            | Self::PackageCardinality { .. } => ValidationCategory::Ahb,
120            Self::CodeNotAllowedForPid { .. } => ValidationCategory::Code,
121            Self::UntSegmentCountMismatch { .. }
122            | Self::UntCountNotVerifiable { .. }
123            | Self::StructureDiagnostic { .. } => ValidationCategory::Structure,
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use mig_assembly::StructureDiagnosticKind;
132
133    #[test]
134    fn code_matches_the_legacy_error_code_for_every_kind() {
135        let cases: Vec<(IssueKind, &str)> = vec![
136            (
137                IssueKind::MissingRequiredField {
138                    field_name: "f".into(),
139                },
140                "AHB001",
141            ),
142            (
143                IssueKind::GroupNotAllowed {
144                    group: "SG4/SG5".into(),
145                },
146                "AHB002",
147            ),
148            (
149                IssueKind::SegmentNotAllowed {
150                    segment: "SG4/SG8/SG10/CAV".into(),
151                },
152                "AHB002",
153            ),
154            (
155                IssueKind::FieldConditionNotSatisfied {
156                    field_name: "f".into(),
157                },
158                "AHB003",
159            ),
160            (
161                IssueKind::ConditionUnknown {
162                    field_name: "f".into(),
163                    unresolved: UnresolvedConditions::default(),
164                },
165                "AHB005",
166            ),
167            (
168                IssueKind::PackageCardinality {
169                    package_id: "1P0..1".into(),
170                    present: 2,
171                    min: 0,
172                    max: 1,
173                    codes: vec![],
174                },
175                "AHB006",
176            ),
177            (
178                IssueKind::CodeNotAllowedForPid {
179                    value: "X".into(),
180                    allowed: vec![],
181                },
182                "COD002",
183            ),
184            (
185                IssueKind::UntSegmentCountMismatch {
186                    declared: 30,
187                    actual: 31,
188                },
189                "STR007",
190            ),
191            (IssueKind::UntCountNotVerifiable { unh_count: 2 }, "STR007"),
192            (
193                IssueKind::StructureDiagnostic {
194                    kind: StructureDiagnosticKind::SkippedUnknownSegment,
195                    segment_id: "CCI".into(),
196                    position: 15,
197                    detail: "irrelevant to the code".into(),
198                },
199                "STR008",
200            ),
201            (
202                IssueKind::StructureDiagnostic {
203                    kind: StructureDiagnosticKind::UnexpectedSegment,
204                    segment_id: "CCI".into(),
205                    position: 15,
206                    detail: "irrelevant to the code".into(),
207                },
208                "STR003",
209            ),
210        ];
211        for (kind, expected) in cases {
212            assert_eq!(kind.code(), expected, "wrong code for {kind:?}");
213        }
214    }
215
216    /// Every structure diagnostic keeps its own code (#81): a repetition
217    /// overrun must not surface as "unexpected segment".
218    #[test]
219    fn each_structure_kind_has_its_own_code() {
220        use StructureDiagnosticKind as K;
221        for (kind, expected) in [
222            (K::MissingRequiredSegment, "STR001"),
223            (K::MaxRepetitionsExceeded, "STR002"),
224            (K::UnexpectedSegment, "STR003"),
225            (K::UnrecognizedQualifier, "STR003"),
226            (K::MissingRequiredGroup, "STR005"),
227            (K::GroupMaxRepetitionsExceeded, "STR006"),
228            (K::SkippedUnknownSegment, "STR008"),
229            (K::OrphanedGroupSegment, "STR008"),
230        ] {
231            let issue = IssueKind::StructureDiagnostic {
232                kind,
233                segment_id: "RFF".into(),
234                position: 1,
235                detail: String::new(),
236            };
237            assert_eq!(issue.code(), expected, "{kind:?}");
238        }
239    }
240
241    #[test]
242    fn category_is_derived_from_the_kind() {
243        assert_eq!(
244            IssueKind::MissingRequiredField {
245                field_name: "f".into()
246            }
247            .category(),
248            ValidationCategory::Ahb
249        );
250        assert_eq!(
251            IssueKind::CodeNotAllowedForPid {
252                value: "X".into(),
253                allowed: vec![]
254            }
255            .category(),
256            ValidationCategory::Code
257        );
258        assert_eq!(
259            IssueKind::UntSegmentCountMismatch {
260                declared: 1,
261                actual: 2
262            }
263            .category(),
264            ValidationCategory::Structure
265        );
266    }
267}