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
118impl ValidationIssue {
119 pub fn new(
121 severity: Severity,
122 category: ValidationCategory,
123 code: impl Into<String>,
124 message: impl Into<String>,
125 ) -> Self {
126 Self {
127 severity,
128 category,
129 code: code.into(),
130 message: message.into(),
131 segment_position: None,
132 field_path: None,
133 rule: None,
134 actual_value: None,
135 expected_value: None,
136 bo4e_path: None,
137 instance_index: None,
138 }
139 }
140
141 pub fn with_position(mut self, position: impl Into<SegmentPosition>) -> Self {
143 self.segment_position = Some(position.into());
144 self
145 }
146
147 pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
149 self.field_path = Some(path.into());
150 self
151 }
152
153 pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
155 self.rule = Some(rule.into());
156 self
157 }
158
159 pub fn with_actual(mut self, value: impl Into<String>) -> Self {
161 self.actual_value = Some(value.into());
162 self
163 }
164
165 pub fn with_expected(mut self, value: impl Into<String>) -> Self {
167 self.expected_value = Some(value.into());
168 self
169 }
170
171 pub fn with_bo4e_path(mut self, path: impl Into<String>) -> Self {
173 self.bo4e_path = Some(path.into());
174 self
175 }
176
177 pub fn with_instance_index(mut self, index: usize) -> Self {
179 self.instance_index = Some(index);
180 self
181 }
182
183 pub fn is_error(&self) -> bool {
185 self.severity == Severity::Error
186 }
187
188 pub fn is_warning(&self) -> bool {
190 self.severity == Severity::Warning
191 }
192}
193
194impl std::fmt::Display for ValidationIssue {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
197 if let Some(ref path) = self.field_path {
198 write!(f, " at {path}")?;
199 }
200 if let Some(ref pos) = self.segment_position {
201 write!(
202 f,
203 " (segment #{}, byte {})",
204 pos.segment_number, pos.byte_offset
205 )?;
206 }
207 Ok(())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_severity_ordering() {
217 assert!(Severity::Info < Severity::Warning);
218 assert!(Severity::Warning < Severity::Error);
219 }
220
221 #[test]
222 fn test_issue_builder() {
223 let issue = ValidationIssue::new(
224 Severity::Error,
225 ValidationCategory::Ahb,
226 "AHB001",
227 "Required field missing",
228 )
229 .with_field_path("SG2/NAD/C082/3039")
230 .with_rule("Muss [182] ∧ [152]")
231 .with_position(SegmentPosition {
232 segment_number: 5,
233 byte_offset: 234,
234 message_number: 1,
235 });
236
237 assert!(issue.is_error());
238 assert!(!issue.is_warning());
239 assert_eq!(issue.code, "AHB001");
240 assert_eq!(issue.field_path.as_deref(), Some("SG2/NAD/C082/3039"));
241 assert_eq!(issue.rule.as_deref(), Some("Muss [182] ∧ [152]"));
242 assert_eq!(issue.segment_position.unwrap().segment_number, 5);
243 }
244
245 #[test]
246 fn test_issue_display() {
247 let issue = ValidationIssue::new(
248 Severity::Error,
249 ValidationCategory::Ahb,
250 "AHB001",
251 "Required field missing",
252 )
253 .with_field_path("NAD");
254
255 let display = format!("{issue}");
256 assert!(display.contains("[ERROR]"));
257 assert!(display.contains("AHB001"));
258 assert!(display.contains("Required field missing"));
259 assert!(display.contains("at NAD"));
260 }
261
262 #[test]
263 fn test_issue_serialization() {
264 let issue = ValidationIssue::new(
265 Severity::Warning,
266 ValidationCategory::Code,
267 "COD002",
268 "Code not allowed for PID",
269 );
270
271 let json = serde_json::to_string_pretty(&issue).unwrap();
272 assert!(!json.contains("bo4e_path"));
274 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
275 assert_eq!(deserialized.code, "COD002");
276 assert_eq!(deserialized.severity, Severity::Warning);
277 assert!(deserialized.bo4e_path.is_none());
278 }
279
280 #[test]
281 fn test_bo4e_path_builder_and_serialization() {
282 let issue = ValidationIssue::new(
283 Severity::Error,
284 ValidationCategory::Ahb,
285 "AHB001",
286 "Required field missing",
287 )
288 .with_field_path("SG4/SG5/LOC/C517/3225")
289 .with_bo4e_path("stammdaten.Marktlokation.marktlokationsId");
290
291 assert_eq!(
292 issue.bo4e_path.as_deref(),
293 Some("stammdaten.Marktlokation.marktlokationsId")
294 );
295
296 let json = serde_json::to_string_pretty(&issue).unwrap();
297 assert!(json.contains("bo4e_path"));
298 assert!(json.contains("stammdaten.Marktlokation.marktlokationsId"));
299
300 let deserialized: ValidationIssue = serde_json::from_str(&json).unwrap();
301 assert_eq!(
302 deserialized.bo4e_path.as_deref(),
303 Some("stammdaten.Marktlokation.marktlokationsId")
304 );
305 }
306
307 #[test]
308 fn test_category_display() {
309 assert_eq!(format!("{}", ValidationCategory::Structure), "Structure");
310 assert_eq!(format!("{}", ValidationCategory::Ahb), "AHB");
311 }
312
313 #[test]
314 fn test_position_from_edifact_primitives() {
315 let edifact_pos = edifact_primitives::SegmentPosition::new(3, 100, 1);
316 let pos: SegmentPosition = edifact_pos.into();
317 assert_eq!(pos.segment_number, 3);
318 assert_eq!(pos.byte_offset, 100);
319 assert_eq!(pos.message_number, 1);
320 }
321
322 #[test]
323 fn issue_instance_index_round_trip() {
324 let issue = ValidationIssue::new(
325 Severity::Error,
326 ValidationCategory::Ahb,
327 "AHB001",
328 "x",
329 )
330 .with_instance_index(3);
331 assert_eq!(issue.instance_index, Some(3));
332 }
333
334 #[test]
335 fn issue_instance_index_defaults_to_none() {
336 let issue = ValidationIssue::new(
337 Severity::Error,
338 ValidationCategory::Ahb,
339 "AHB001",
340 "x",
341 );
342 assert_eq!(issue.instance_index, None);
343 }
344}