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}