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