1use serde::Serialize;
9use std::collections::HashMap;
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Serialize)]
36pub struct ValidationError {
37 pub path: Vec<PathSegment>,
42
43 pub code: String,
49
50 pub message: String,
52
53 #[serde(skip_serializing_if = "HashMap::is_empty")]
58 pub params: HashMap<String, serde_json::Value>,
59}
60
61impl ValidationError {
62 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
64 Self {
65 path: Vec::new(),
66 code: code.into(),
67 message: message.into(),
68 params: HashMap::new(),
69 }
70 }
71
72 pub fn with_param(
74 mut self,
75 key: impl Into<String>,
76 value: impl Into<serde_json::Value>,
77 ) -> Self {
78 self.params.insert(key.into(), value.into());
79 self
80 }
81
82 pub fn with_path(mut self, path: Vec<PathSegment>) -> Self {
84 self.path = path;
85 self
86 }
87
88 pub fn path_string(&self) -> String {
93 let mut result = String::new();
94 for (i, segment) in self.path.iter().enumerate() {
95 match segment {
96 PathSegment::Field(name) => {
97 if i > 0 {
98 result.push('.');
99 }
100 result.push_str(name);
101 }
102 PathSegment::Index(idx) => {
103 result.push('[');
104 result.push_str(&idx.to_string());
105 result.push(']');
106 }
107 }
108 }
109 result
110 }
111}
112
113impl fmt::Display for ValidationError {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 let path = self.path_string();
116 if path.is_empty() {
117 write!(f, "{} ({})", self.message, self.code)
118 } else {
119 write!(f, "{}: {} ({})", path, self.message, self.code)
120 }
121 }
122}
123
124impl std::error::Error for ValidationError {}
125
126#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
131#[serde(untagged)]
132pub enum PathSegment {
133 Field(String),
135 Index(usize),
137}
138
139#[derive(Debug, Clone, Serialize)]
165pub struct ValidationErrors {
166 errors: Vec<ValidationError>,
168}
169
170impl ValidationErrors {
171 pub fn new() -> Self {
173 Self {
174 errors: Vec::new(),
175 }
176 }
177
178 pub fn add(&mut self, error: ValidationError) {
180 self.errors.push(error);
181 }
182
183 pub fn merge(&mut self, other: ValidationErrors) {
186 self.errors.extend(other.errors);
187 }
188
189 pub fn is_empty(&self) -> bool {
191 self.errors.is_empty()
192 }
193
194 pub fn len(&self) -> usize {
196 self.errors.len()
197 }
198
199 pub fn errors(&self) -> &[ValidationError] {
201 &self.errors
202 }
203
204 pub fn into_errors(self) -> Vec<ValidationError> {
206 self.errors
207 }
208
209 pub fn field_errors(&self, field_name: &str) -> Vec<&ValidationError> {
211 self.errors
212 .iter()
213 .filter(|e| {
214 e.path
215 .first()
216 .map(|s| matches!(s, PathSegment::Field(name) if name == field_name))
217 .unwrap_or(false)
218 })
219 .collect()
220 }
221}
222
223impl Default for ValidationErrors {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl fmt::Display for ValidationErrors {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 writeln!(
240 f,
241 "Validation failed with {} error(s):",
242 self.errors.len()
243 )?;
244 for error in &self.errors {
245 writeln!(f, " - {}", error)?;
246 }
247 Ok(())
248 }
249}
250
251impl std::error::Error for ValidationErrors {}
252
253impl IntoIterator for ValidationErrors {
255 type Item = ValidationError;
256 type IntoIter = std::vec::IntoIter<ValidationError>;
257
258 fn into_iter(self) -> Self::IntoIter {
259 self.errors.into_iter()
260 }
261}
262
263impl<'a> IntoIterator for &'a ValidationErrors {
264 type Item = &'a ValidationError;
265 type IntoIter = std::slice::Iter<'a, ValidationError>;
266
267 fn into_iter(self) -> Self::IntoIter {
268 self.errors.iter()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_validation_error_display_with_path() {
278 let error = ValidationError {
279 path: vec![
280 PathSegment::Field("user".to_string()),
281 PathSegment::Field("email".to_string()),
282 ],
283 code: "email".to_string(),
284 message: "invalid email format".to_string(),
285 params: HashMap::new(),
286 };
287 assert_eq!(
288 error.to_string(),
289 "user.email: invalid email format (email)"
290 );
291 }
292
293 #[test]
294 fn test_validation_error_display_with_index() {
295 let error = ValidationError {
296 path: vec![
297 PathSegment::Field("users".to_string()),
298 PathSegment::Index(3),
299 PathSegment::Field("name".to_string()),
300 ],
301 code: "length_min".to_string(),
302 message: "must be at least 1 character".to_string(),
303 params: HashMap::new(),
304 };
305 assert_eq!(
306 error.to_string(),
307 "users[3].name: must be at least 1 character (length_min)"
308 );
309 }
310
311 #[test]
312 fn test_validation_error_display_without_path() {
313 let error = ValidationError::new("custom", "cross-field validation failed");
314 assert_eq!(
315 error.to_string(),
316 "cross-field validation failed (custom)"
317 );
318 }
319
320 #[test]
321 fn test_path_string_empty() {
322 let error = ValidationError::new("test", "test");
323 assert_eq!(error.path_string(), "");
324 }
325
326 #[test]
327 fn test_path_string_nested() {
328 let error = ValidationError {
329 path: vec![
330 PathSegment::Field("a".to_string()),
331 PathSegment::Field("b".to_string()),
332 PathSegment::Index(0),
333 PathSegment::Field("c".to_string()),
334 ],
335 code: "test".to_string(),
336 message: "test".to_string(),
337 params: HashMap::new(),
338 };
339 assert_eq!(error.path_string(), "a.b[0].c");
340 }
341
342 #[test]
343 fn test_validation_errors_empty() {
344 let errors = ValidationErrors::new();
345 assert!(errors.is_empty());
346 assert_eq!(errors.len(), 0);
347 }
348
349 #[test]
350 fn test_validation_errors_add_and_len() {
351 let mut errors = ValidationErrors::new();
352 errors.add(ValidationError::new("a", "error a"));
353 errors.add(ValidationError::new("b", "error b"));
354 assert_eq!(errors.len(), 2);
355 assert!(!errors.is_empty());
356 }
357
358 #[test]
359 fn test_validation_errors_merge() {
360 let mut errors1 = ValidationErrors::new();
361 errors1.add(ValidationError::new("a", "error a"));
362
363 let mut errors2 = ValidationErrors::new();
364 errors2.add(ValidationError::new("b", "error b"));
365 errors2.add(ValidationError::new("c", "error c"));
366
367 errors1.merge(errors2);
368 assert_eq!(errors1.len(), 3);
369 }
370
371 #[test]
372 fn test_validation_errors_field_errors() {
373 let mut errors = ValidationErrors::new();
374 errors.add(
375 ValidationError::new("email", "invalid")
376 .with_path(vec![PathSegment::Field("email".to_string())]),
377 );
378 errors.add(
379 ValidationError::new("length", "too short")
380 .with_path(vec![PathSegment::Field("name".to_string())]),
381 );
382 errors.add(
383 ValidationError::new("email", "duplicate")
384 .with_path(vec![PathSegment::Field("email".to_string())]),
385 );
386
387 let email_errors = errors.field_errors("email");
388 assert_eq!(email_errors.len(), 2);
389
390 let name_errors = errors.field_errors("name");
391 assert_eq!(name_errors.len(), 1);
392
393 let unknown_errors = errors.field_errors("unknown");
394 assert_eq!(unknown_errors.len(), 0);
395 }
396
397 #[test]
398 fn test_validation_errors_json_serialization() {
399 let mut errors = ValidationErrors::new();
400 errors.add(
401 ValidationError::new("length_min", "must be at least 3 characters")
402 .with_path(vec![PathSegment::Field("username".to_string())])
403 .with_param("min", serde_json::json!(3))
404 .with_param("actual", serde_json::json!(1)),
405 );
406
407 let json = serde_json::to_string(&errors).unwrap();
408 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
409
410 let first_error = &parsed["errors"][0];
411 assert_eq!(first_error["code"], "length_min");
412 assert_eq!(first_error["path"][0], "username");
413 assert_eq!(first_error["params"]["min"], 3);
414 }
415
416 #[test]
417 fn test_validation_errors_display() {
418 let mut errors = ValidationErrors::new();
419 errors.add(
420 ValidationError::new("email", "invalid email format")
421 .with_path(vec![PathSegment::Field("email".to_string())]),
422 );
423 let display = errors.to_string();
424 assert!(display.contains("Validation failed with 1 error(s)"));
425 assert!(display.contains("email: invalid email format (email)"));
426 }
427
428 #[test]
429 fn test_validation_errors_into_iterator() {
430 let mut errors = ValidationErrors::new();
431 errors.add(ValidationError::new("a", "A"));
432 errors.add(ValidationError::new("b", "B"));
433
434 let codes: Vec<String> = errors.into_iter().map(|e| e.code).collect();
435 assert_eq!(codes, vec!["a", "b"]);
436 }
437
438 #[test]
439 fn test_validation_error_with_param() {
440 let error = ValidationError::new("length_min", "too short")
441 .with_param("min", serde_json::json!(3))
442 .with_param("actual", serde_json::json!(1));
443
444 assert_eq!(error.params.len(), 2);
445 assert_eq!(error.params["min"], serde_json::json!(3));
446 assert_eq!(error.params["actual"], serde_json::json!(1));
447 }
448
449 #[test]
450 fn test_params_skip_serializing_if_empty() {
451 let error = ValidationError::new("email", "invalid");
452 let json = serde_json::to_string(&error).unwrap();
453 assert!(!json.contains("params"));
455 }
456}