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