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 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub message_segment_number: Option<u32>,
70}
71
72impl SegmentPosition {
73 pub fn at(segment_number: u32) -> Self {
76 Self {
77 segment_number,
78 byte_offset: 0,
79 message_number: 1,
80 message_segment_number: None,
81 }
82 }
83}
84
85impl From<edifact_primitives::SegmentPosition> for SegmentPosition {
86 fn from(pos: edifact_primitives::SegmentPosition) -> Self {
87 Self {
88 segment_number: pos.segment_number,
89 byte_offset: pos.byte_offset,
90 message_number: pos.message_number,
91 message_segment_number: None,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ValidationIssue {
99 pub severity: Severity,
101
102 pub kind: IssueKind,
105
106 pub segment_position: Option<SegmentPosition>,
108
109 pub field_path: Option<String>,
111
112 pub rule: Option<String>,
114
115 pub actual_value: Option<String>,
117
118 pub expected_value: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub bo4e_path: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
132 pub instance_index: Option<usize>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
139 pub field_element_position: Option<u32>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
145 pub field_component_position: Option<u32>,
146
147 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub mig_number: Option<String>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub transaction_index: Option<usize>,
158}
159
160impl ValidationIssue {
161 pub fn new(severity: Severity, kind: IssueKind) -> Self {
163 Self {
164 severity,
165 kind,
166 segment_position: None,
167 field_path: None,
168 rule: None,
169 actual_value: None,
170 expected_value: None,
171 bo4e_path: None,
172 instance_index: None,
173 field_element_position: None,
174 field_component_position: None,
175 mig_number: None,
176 transaction_index: None,
177 }
178 }
179
180 pub fn with_mig_number(mut self, number: Option<&str>) -> Self {
182 if let Some(number) = number {
183 self.mig_number = Some(number.to_string());
184 }
185 self
186 }
187
188 pub fn code(&self) -> &'static str {
190 self.kind.code()
191 }
192
193 pub fn category(&self) -> ValidationCategory {
195 self.kind.category()
196 }
197
198 pub fn with_field_position(mut self, element_pos: u32, component_pos: Option<u32>) -> Self {
204 self.field_element_position = Some(element_pos);
205 self.field_component_position = component_pos;
206 self
207 }
208
209 pub fn with_position(mut self, position: impl Into<SegmentPosition>) -> Self {
211 self.segment_position = Some(position.into());
212 self
213 }
214
215 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
217 self.field_path = Some(path.into());
218 self
219 }
220
221 pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
223 self.rule = Some(rule.into());
224 self
225 }
226
227 pub fn with_actual(mut self, value: impl Into<String>) -> Self {
229 self.actual_value = Some(value.into());
230 self
231 }
232
233 pub fn with_expected(mut self, value: impl Into<String>) -> Self {
235 self.expected_value = Some(value.into());
236 self
237 }
238
239 pub fn with_bo4e_path(mut self, path: impl Into<String>) -> Self {
241 self.bo4e_path = Some(path.into());
242 self
243 }
244
245 pub fn with_instance_index(mut self, index: usize) -> Self {
247 self.instance_index = Some(index);
248 self
249 }
250
251 pub fn is_error(&self) -> bool {
253 self.severity == Severity::Error
254 }
255
256 pub fn is_warning(&self) -> bool {
258 self.severity == Severity::Warning
259 }
260}
261
262impl std::fmt::Display for ValidationIssue {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 let location = self.field_path.as_deref();
265 write!(
266 f,
267 "[{}] {}: {}",
268 self.severity,
269 self.code(),
270 crate::display::TechnicalNarrator.describe(self, location)
271 )?;
272 if let Some(ref pos) = self.segment_position {
273 write!(
274 f,
275 " (segment #{}, byte {})",
276 pos.segment_number, pos.byte_offset
277 )?;
278 }
279 Ok(())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_severity_ordering() {
289 assert!(Severity::Info < Severity::Warning);
290 assert!(Severity::Warning < Severity::Error);
291 }
292
293 fn missing_field_issue() -> ValidationIssue {
294 ValidationIssue::new(
295 Severity::Error,
296 IssueKind::MissingRequiredField {
297 field_name: "Merkmal, Code".into(),
298 },
299 )
300 }
301
302 #[test]
303 fn test_issue_builder() {
304 let issue = missing_field_issue()
305 .with_field_path("SG2/NAD/C082/3039")
306 .with_rule("Muss [182] ∧ [152]")
307 .with_position(SegmentPosition {
308 byte_offset: 234,
309 ..SegmentPosition::at(5)
310 });
311
312 assert!(issue.is_error());
313 assert!(!issue.is_warning());
314 assert_eq!(issue.code(), "AHB001");
315 assert_eq!(issue.field_path.as_deref(), Some("SG2/NAD/C082/3039"));
316 assert_eq!(issue.rule.as_deref(), Some("Muss [182] ∧ [152]"));
317 assert_eq!(issue.segment_position.unwrap().segment_number, 5);
318 }
319
320 #[test]
321 fn test_issue_display() {
322 let issue = missing_field_issue().with_field_path("NAD");
323
324 let display = format!("{issue}");
325 assert!(display.contains("[ERROR]"));
326 assert!(display.contains("AHB001"));
327 assert!(display.contains("Merkmal, Code"));
328 assert!(display.contains("at NAD"));
329 }
330
331 #[test]
332 fn test_issue_serialization() {
333 let issue = ValidationIssue::new(
334 Severity::Warning,
335 IssueKind::CodeNotAllowedForPid {
336 value: "X".into(),
337 allowed: vec!["A".into()],
338 },
339 );
340
341 let json = serde_json::to_string_pretty(&issue).unwrap();
342 assert!(!json.contains("bo4e_path"));
344 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
345 assert_eq!(deserialized.code(), "COD002");
346 assert_eq!(deserialized.severity, Severity::Warning);
347 assert!(deserialized.bo4e_path.is_none());
348 }
349
350 #[test]
351 fn test_bo4e_path_builder_and_serialization() {
352 let issue = missing_field_issue()
353 .with_field_path("SG4/SG5/LOC/C517/3225")
354 .with_bo4e_path("stammdaten.Marktlokation.marktlokationsId");
355
356 assert_eq!(
357 issue.bo4e_path.as_deref(),
358 Some("stammdaten.Marktlokation.marktlokationsId")
359 );
360
361 let json = serde_json::to_string_pretty(&issue).unwrap();
362 assert!(json.contains("bo4e_path"));
363 assert!(json.contains("stammdaten.Marktlokation.marktlokationsId"));
364
365 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
366 assert_eq!(
367 deserialized.bo4e_path.as_deref(),
368 Some("stammdaten.Marktlokation.marktlokationsId")
369 );
370 }
371
372 #[test]
373 fn test_category_display() {
374 assert_eq!(format!("{}", ValidationCategory::Structure), "Structure");
375 assert_eq!(format!("{}", ValidationCategory::Ahb), "AHB");
376 }
377
378 #[test]
379 fn test_position_from_edifact_primitives() {
380 let edifact_pos = edifact_primitives::SegmentPosition::new(3, 100, 1);
381 let pos: SegmentPosition = edifact_pos.into();
382 assert_eq!(pos.segment_number, 3);
383 assert_eq!(pos.byte_offset, 100);
384 assert_eq!(pos.message_number, 1);
385 }
386
387 #[test]
388 fn issue_instance_index_round_trip() {
389 let issue = missing_field_issue().with_instance_index(3);
390 assert_eq!(issue.instance_index, Some(3));
391 }
392
393 #[test]
394 fn issue_instance_index_defaults_to_none() {
395 let issue = missing_field_issue();
396 assert_eq!(issue.instance_index, None);
397 }
398
399 #[test]
400 fn code_and_category_are_derived_and_display_has_no_duplicate_path() {
401 let issue = ValidationIssue::new(
402 Severity::Error,
403 IssueKind::MissingRequiredField {
404 field_name: "Merkmal, Code".into(),
405 },
406 )
407 .with_field_path("SG4/SG8/SG10/CCI/C240/7037");
408
409 assert_eq!(issue.code(), "AHB001");
410 assert_eq!(issue.category(), ValidationCategory::Ahb);
411 let shown = issue.to_string();
414 assert_eq!(
415 shown.matches("SG4/SG8/SG10/CCI/C240/7037").count(),
416 1,
417 "{shown}"
418 );
419 }
420}