automapper_validation/validator/
issue.rs1use serde::{Deserialize, Serialize};
4
5use super::kind::IssueKind;
6use crate::display::IssueNarrator;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
10pub enum Severity {
11 Info,
13 Warning,
15 Error,
17}
18
19impl std::fmt::Display for Severity {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Severity::Info => write!(f, "INFO"),
23 Severity::Warning => write!(f, "WARN"),
24 Severity::Error => write!(f, "ERROR"),
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub enum ValidationCategory {
32 Structure,
34 Format,
36 Code,
38 Ahb,
40}
41
42impl std::fmt::Display for ValidationCategory {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 ValidationCategory::Structure => write!(f, "Structure"),
46 ValidationCategory::Format => write!(f, "Format"),
47 ValidationCategory::Code => write!(f, "Code"),
48 ValidationCategory::Ahb => write!(f, "AHB"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub struct SegmentPosition {
59 pub segment_number: u32,
61 pub byte_offset: usize,
63 pub message_number: u32,
65}
66
67impl From<edifact_primitives::SegmentPosition> for SegmentPosition {
68 fn from(pos: edifact_primitives::SegmentPosition) -> Self {
69 Self {
70 segment_number: pos.segment_number,
71 byte_offset: pos.byte_offset,
72 message_number: pos.message_number,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ValidationIssue {
80 pub severity: Severity,
82
83 pub kind: IssueKind,
86
87 pub segment_position: Option<SegmentPosition>,
89
90 pub field_path: Option<String>,
92
93 pub rule: Option<String>,
95
96 pub actual_value: Option<String>,
98
99 pub expected_value: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
106 pub bo4e_path: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
113 pub instance_index: Option<usize>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
120 pub field_element_position: Option<u32>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
126 pub field_component_position: Option<u32>,
127}
128
129impl ValidationIssue {
130 pub fn new(severity: Severity, kind: IssueKind) -> Self {
132 Self {
133 severity,
134 kind,
135 segment_position: None,
136 field_path: None,
137 rule: None,
138 actual_value: None,
139 expected_value: None,
140 bo4e_path: None,
141 instance_index: None,
142 field_element_position: None,
143 field_component_position: None,
144 }
145 }
146
147 pub fn code(&self) -> &'static str {
149 self.kind.code()
150 }
151
152 pub fn category(&self) -> ValidationCategory {
154 self.kind.category()
155 }
156
157 pub fn with_field_position(mut self, element_pos: u32, component_pos: Option<u32>) -> Self {
163 self.field_element_position = Some(element_pos);
164 self.field_component_position = component_pos;
165 self
166 }
167
168 pub fn with_position(mut self, position: impl Into<SegmentPosition>) -> Self {
170 self.segment_position = Some(position.into());
171 self
172 }
173
174 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
176 self.field_path = Some(path.into());
177 self
178 }
179
180 pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
182 self.rule = Some(rule.into());
183 self
184 }
185
186 pub fn with_actual(mut self, value: impl Into<String>) -> Self {
188 self.actual_value = Some(value.into());
189 self
190 }
191
192 pub fn with_expected(mut self, value: impl Into<String>) -> Self {
194 self.expected_value = Some(value.into());
195 self
196 }
197
198 pub fn with_bo4e_path(mut self, path: impl Into<String>) -> Self {
200 self.bo4e_path = Some(path.into());
201 self
202 }
203
204 pub fn with_instance_index(mut self, index: usize) -> Self {
206 self.instance_index = Some(index);
207 self
208 }
209
210 pub fn is_error(&self) -> bool {
212 self.severity == Severity::Error
213 }
214
215 pub fn is_warning(&self) -> bool {
217 self.severity == Severity::Warning
218 }
219}
220
221impl std::fmt::Display for ValidationIssue {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 let location = self.field_path.as_deref();
224 write!(
225 f,
226 "[{}] {}: {}",
227 self.severity,
228 self.code(),
229 crate::display::TechnicalNarrator.describe(self, location)
230 )?;
231 if let Some(ref pos) = self.segment_position {
232 write!(
233 f,
234 " (segment #{}, byte {})",
235 pos.segment_number, pos.byte_offset
236 )?;
237 }
238 Ok(())
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_severity_ordering() {
248 assert!(Severity::Info < Severity::Warning);
249 assert!(Severity::Warning < Severity::Error);
250 }
251
252 fn missing_field_issue() -> ValidationIssue {
253 ValidationIssue::new(
254 Severity::Error,
255 IssueKind::MissingRequiredField {
256 field_name: "Merkmal, Code".into(),
257 },
258 )
259 }
260
261 #[test]
262 fn test_issue_builder() {
263 let issue = missing_field_issue()
264 .with_field_path("SG2/NAD/C082/3039")
265 .with_rule("Muss [182] ∧ [152]")
266 .with_position(SegmentPosition {
267 segment_number: 5,
268 byte_offset: 234,
269 message_number: 1,
270 });
271
272 assert!(issue.is_error());
273 assert!(!issue.is_warning());
274 assert_eq!(issue.code(), "AHB001");
275 assert_eq!(issue.field_path.as_deref(), Some("SG2/NAD/C082/3039"));
276 assert_eq!(issue.rule.as_deref(), Some("Muss [182] ∧ [152]"));
277 assert_eq!(issue.segment_position.unwrap().segment_number, 5);
278 }
279
280 #[test]
281 fn test_issue_display() {
282 let issue = missing_field_issue().with_field_path("NAD");
283
284 let display = format!("{issue}");
285 assert!(display.contains("[ERROR]"));
286 assert!(display.contains("AHB001"));
287 assert!(display.contains("Merkmal, Code"));
288 assert!(display.contains("at NAD"));
289 }
290
291 #[test]
292 fn test_issue_serialization() {
293 let issue = ValidationIssue::new(
294 Severity::Warning,
295 IssueKind::CodeNotAllowedForPid {
296 value: "X".into(),
297 allowed: vec!["A".into()],
298 },
299 );
300
301 let json = serde_json::to_string_pretty(&issue).unwrap();
302 assert!(!json.contains("bo4e_path"));
304 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
305 assert_eq!(deserialized.code(), "COD002");
306 assert_eq!(deserialized.severity, Severity::Warning);
307 assert!(deserialized.bo4e_path.is_none());
308 }
309
310 #[test]
311 fn test_bo4e_path_builder_and_serialization() {
312 let issue = missing_field_issue()
313 .with_field_path("SG4/SG5/LOC/C517/3225")
314 .with_bo4e_path("stammdaten.Marktlokation.marktlokationsId");
315
316 assert_eq!(
317 issue.bo4e_path.as_deref(),
318 Some("stammdaten.Marktlokation.marktlokationsId")
319 );
320
321 let json = serde_json::to_string_pretty(&issue).unwrap();
322 assert!(json.contains("bo4e_path"));
323 assert!(json.contains("stammdaten.Marktlokation.marktlokationsId"));
324
325 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
326 assert_eq!(
327 deserialized.bo4e_path.as_deref(),
328 Some("stammdaten.Marktlokation.marktlokationsId")
329 );
330 }
331
332 #[test]
333 fn test_category_display() {
334 assert_eq!(format!("{}", ValidationCategory::Structure), "Structure");
335 assert_eq!(format!("{}", ValidationCategory::Ahb), "AHB");
336 }
337
338 #[test]
339 fn test_position_from_edifact_primitives() {
340 let edifact_pos = edifact_primitives::SegmentPosition::new(3, 100, 1);
341 let pos: SegmentPosition = edifact_pos.into();
342 assert_eq!(pos.segment_number, 3);
343 assert_eq!(pos.byte_offset, 100);
344 assert_eq!(pos.message_number, 1);
345 }
346
347 #[test]
348 fn issue_instance_index_round_trip() {
349 let issue = missing_field_issue().with_instance_index(3);
350 assert_eq!(issue.instance_index, Some(3));
351 }
352
353 #[test]
354 fn issue_instance_index_defaults_to_none() {
355 let issue = missing_field_issue();
356 assert_eq!(issue.instance_index, None);
357 }
358
359 #[test]
360 fn code_and_category_are_derived_and_display_has_no_duplicate_path() {
361 let issue = ValidationIssue::new(
362 Severity::Error,
363 IssueKind::MissingRequiredField {
364 field_name: "Merkmal, Code".into(),
365 },
366 )
367 .with_field_path("SG4/SG8/SG10/CCI/C240/7037");
368
369 assert_eq!(issue.code(), "AHB001");
370 assert_eq!(issue.category(), ValidationCategory::Ahb);
371 let shown = issue.to_string();
374 assert_eq!(
375 shown.matches("SG4/SG8/SG10/CCI/C240/7037").count(),
376 1,
377 "{shown}"
378 );
379 }
380}