automapper_validation/validator/
issue.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
7pub enum Severity {
8 Info,
10 Warning,
12 Error,
14}
15
16impl std::fmt::Display for Severity {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Severity::Info => write!(f, "INFO"),
20 Severity::Warning => write!(f, "WARN"),
21 Severity::Error => write!(f, "ERROR"),
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum ValidationCategory {
29 Structure,
31 Format,
33 Code,
35 Ahb,
37}
38
39impl std::fmt::Display for ValidationCategory {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 ValidationCategory::Structure => write!(f, "Structure"),
43 ValidationCategory::Format => write!(f, "Format"),
44 ValidationCategory::Code => write!(f, "Code"),
45 ValidationCategory::Ahb => write!(f, "AHB"),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub struct SegmentPosition {
56 pub segment_number: u32,
58 pub byte_offset: usize,
60 pub message_number: u32,
62}
63
64impl From<edifact_primitives::SegmentPosition> for SegmentPosition {
65 fn from(pos: edifact_primitives::SegmentPosition) -> Self {
66 Self {
67 segment_number: pos.segment_number,
68 byte_offset: pos.byte_offset,
69 message_number: pos.message_number,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ValidationIssue {
77 pub severity: Severity,
79
80 pub category: ValidationCategory,
82
83 pub code: String,
85
86 pub message: String,
88
89 pub segment_position: Option<SegmentPosition>,
91
92 pub field_path: Option<String>,
94
95 pub rule: Option<String>,
97
98 pub actual_value: Option<String>,
100
101 pub expected_value: Option<String>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
108 pub bo4e_path: Option<String>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
115 pub instance_index: Option<usize>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
122 pub field_element_position: Option<u32>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
128 pub field_component_position: Option<u32>,
129}
130
131impl ValidationIssue {
132 pub fn new(
134 severity: Severity,
135 category: ValidationCategory,
136 code: impl Into<String>,
137 message: impl Into<String>,
138 ) -> Self {
139 Self {
140 severity,
141 category,
142 code: code.into(),
143 message: message.into(),
144 segment_position: None,
145 field_path: None,
146 rule: None,
147 actual_value: None,
148 expected_value: None,
149 bo4e_path: None,
150 instance_index: None,
151 field_element_position: None,
152 field_component_position: None,
153 }
154 }
155
156 pub fn with_field_position(
162 mut self,
163 element_pos: u32,
164 component_pos: Option<u32>,
165 ) -> Self {
166 self.field_element_position = Some(element_pos);
167 self.field_component_position = component_pos;
168 self
169 }
170
171 pub fn with_position(mut self, position: impl Into<SegmentPosition>) -> Self {
173 self.segment_position = Some(position.into());
174 self
175 }
176
177 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
179 self.field_path = Some(path.into());
180 self
181 }
182
183 pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
185 self.rule = Some(rule.into());
186 self
187 }
188
189 pub fn with_actual(mut self, value: impl Into<String>) -> Self {
191 self.actual_value = Some(value.into());
192 self
193 }
194
195 pub fn with_expected(mut self, value: impl Into<String>) -> Self {
197 self.expected_value = Some(value.into());
198 self
199 }
200
201 pub fn with_bo4e_path(mut self, path: impl Into<String>) -> Self {
203 self.bo4e_path = Some(path.into());
204 self
205 }
206
207 pub fn with_instance_index(mut self, index: usize) -> Self {
209 self.instance_index = Some(index);
210 self
211 }
212
213 pub fn is_error(&self) -> bool {
215 self.severity == Severity::Error
216 }
217
218 pub fn is_warning(&self) -> bool {
220 self.severity == Severity::Warning
221 }
222}
223
224impl std::fmt::Display for ValidationIssue {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
227 if let Some(ref path) = self.field_path {
228 write!(f, " at {path}")?;
229 }
230 if let Some(ref pos) = self.segment_position {
231 write!(
232 f,
233 " (segment #{}, byte {})",
234 pos.segment_number, pos.byte_offset
235 )?;
236 }
237 Ok(())
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_severity_ordering() {
247 assert!(Severity::Info < Severity::Warning);
248 assert!(Severity::Warning < Severity::Error);
249 }
250
251 #[test]
252 fn test_issue_builder() {
253 let issue = ValidationIssue::new(
254 Severity::Error,
255 ValidationCategory::Ahb,
256 "AHB001",
257 "Required field missing",
258 )
259 .with_field_path("SG2/NAD/C082/3039")
260 .with_rule("Muss [182] ∧ [152]")
261 .with_position(SegmentPosition {
262 segment_number: 5,
263 byte_offset: 234,
264 message_number: 1,
265 });
266
267 assert!(issue.is_error());
268 assert!(!issue.is_warning());
269 assert_eq!(issue.code, "AHB001");
270 assert_eq!(issue.field_path.as_deref(), Some("SG2/NAD/C082/3039"));
271 assert_eq!(issue.rule.as_deref(), Some("Muss [182] ∧ [152]"));
272 assert_eq!(issue.segment_position.unwrap().segment_number, 5);
273 }
274
275 #[test]
276 fn test_issue_display() {
277 let issue = ValidationIssue::new(
278 Severity::Error,
279 ValidationCategory::Ahb,
280 "AHB001",
281 "Required field missing",
282 )
283 .with_field_path("NAD");
284
285 let display = format!("{issue}");
286 assert!(display.contains("[ERROR]"));
287 assert!(display.contains("AHB001"));
288 assert!(display.contains("Required field missing"));
289 assert!(display.contains("at NAD"));
290 }
291
292 #[test]
293 fn test_issue_serialization() {
294 let issue = ValidationIssue::new(
295 Severity::Warning,
296 ValidationCategory::Code,
297 "COD002",
298 "Code not allowed for PID",
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 = ValidationIssue::new(
313 Severity::Error,
314 ValidationCategory::Ahb,
315 "AHB001",
316 "Required field missing",
317 )
318 .with_field_path("SG4/SG5/LOC/C517/3225")
319 .with_bo4e_path("stammdaten.Marktlokation.marktlokationsId");
320
321 assert_eq!(
322 issue.bo4e_path.as_deref(),
323 Some("stammdaten.Marktlokation.marktlokationsId")
324 );
325
326 let json = serde_json::to_string_pretty(&issue).unwrap();
327 assert!(json.contains("bo4e_path"));
328 assert!(json.contains("stammdaten.Marktlokation.marktlokationsId"));
329
330 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
331 assert_eq!(
332 deserialized.bo4e_path.as_deref(),
333 Some("stammdaten.Marktlokation.marktlokationsId")
334 );
335 }
336
337 #[test]
338 fn test_category_display() {
339 assert_eq!(format!("{}", ValidationCategory::Structure), "Structure");
340 assert_eq!(format!("{}", ValidationCategory::Ahb), "AHB");
341 }
342
343 #[test]
344 fn test_position_from_edifact_primitives() {
345 let edifact_pos = edifact_primitives::SegmentPosition::new(3, 100, 1);
346 let pos: SegmentPosition = edifact_pos.into();
347 assert_eq!(pos.segment_number, 3);
348 assert_eq!(pos.byte_offset, 100);
349 assert_eq!(pos.message_number, 1);
350 }
351
352 #[test]
353 fn issue_instance_index_round_trip() {
354 let issue = ValidationIssue::new(
355 Severity::Error,
356 ValidationCategory::Ahb,
357 "AHB001",
358 "x",
359 )
360 .with_instance_index(3);
361 assert_eq!(issue.instance_index, Some(3));
362 }
363
364 #[test]
365 fn issue_instance_index_defaults_to_none() {
366 let issue = ValidationIssue::new(
367 Severity::Error,
368 ValidationCategory::Ahb,
369 "AHB001",
370 "x",
371 );
372 assert_eq!(issue.instance_index, None);
373 }
374}