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