1use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ValidationError {
13 pub path: String,
15 pub expected: String,
17 pub found: String,
19 pub message: Option<String>,
21 pub error_type: String,
23 pub schema_info: Option<SchemaInfo>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SchemaInfo {
30 pub data_type: String,
32 pub required: Option<bool>,
34 pub format: Option<String>,
36 pub minimum: Option<f64>,
38 pub maximum: Option<f64>,
40 pub min_length: Option<usize>,
42 pub max_length: Option<usize>,
44 pub pattern: Option<String>,
46 pub enum_values: Option<Vec<Value>>,
48 pub additional_properties: Option<bool>,
50}
51
52impl ValidationError {
53 pub fn new(path: String, expected: String, found: String, error_type: &str) -> Self {
61 Self {
62 path,
63 expected,
64 found,
65 message: None,
66 error_type: error_type.to_string(),
67 schema_info: None,
68 }
69 }
70
71 pub fn with_message(mut self, message: String) -> Self {
73 self.message = Some(message);
74 self
75 }
76
77 pub fn with_schema_info(mut self, schema_info: SchemaInfo) -> Self {
79 self.schema_info = Some(schema_info);
80 self
81 }
82}
83
84#[derive(Debug, Clone)]
88pub struct FieldError {
89 pub path: String,
91 pub expected: String,
93 pub found: String,
95 pub message: Option<String>,
97}
98
99impl From<ValidationError> for FieldError {
100 fn from(error: ValidationError) -> Self {
101 Self {
102 path: error.path,
103 expected: error.expected,
104 found: error.found,
105 message: error.message,
106 }
107 }
108}
109
110pub fn diff(expected_schema: &Value, actual: &Value) -> Vec<FieldError> {
122 let mut out = Vec::new();
123 walk(expected_schema, actual, "", &mut out);
124 out
125}
126
127fn walk(expected: &Value, actual: &Value, path: &str, out: &mut Vec<FieldError>) {
128 match (expected, actual) {
129 (Value::Object(eo), Value::Object(ao)) => {
130 for (k, ev) in eo {
131 let np = format!("{}/{}", path, k);
132 if let Some(av) = ao.get(k) {
133 walk(ev, av, &np, out);
134 } else {
135 out.push(FieldError {
136 path: np,
137 expected: type_of(ev),
138 found: "missing".into(),
139 message: Some("required".into()),
140 });
141 }
142 }
143 }
144 (Value::Array(ea), Value::Array(aa)) => {
145 if let Some(esample) = ea.first() {
146 for (i, av) in aa.iter().enumerate() {
147 let np = format!("{}/{}", path, i);
148 walk(esample, av, &np, out);
149 }
150 }
151 }
152 (e, a) => {
153 let et = type_of(e);
154 let at = type_of(a);
155 if et != at {
156 out.push(FieldError {
157 path: path.into(),
158 expected: et,
159 found: at,
160 message: None,
161 });
162 }
163 }
164 }
165}
166
167fn type_of(v: &Value) -> String {
168 match v {
169 Value::Null => "null".to_string(),
170 Value::Bool(_) => "bool".to_string(),
171 Value::Number(n) => if n.is_i64() { "integer" } else { "number" }.to_string(),
172 Value::String(_) => "string".to_string(),
173 Value::Array(_) => "array".to_string(),
174 Value::Object(_) => "object".to_string(),
175 }
176}
177
178pub fn to_422_json(errors: Vec<FieldError>) -> Value {
186 json!({
187 "error": "Schema validation failed",
188 "details": errors.into_iter().map(|e| json!({
189 "path": e.path,
190 "expected": e.expected,
191 "found": e.found,
192 "message": e.message
193 })).collect::<Vec<_>>()
194 })
195}
196
197pub fn validation_diff(expected_schema: &Value, actual: &Value) -> Vec<ValidationError> {
201 let mut out = Vec::new();
202 validation_walk(expected_schema, actual, "", &mut out);
203 out
204}
205
206fn validation_walk(expected: &Value, actual: &Value, path: &str, out: &mut Vec<ValidationError>) {
207 match (expected, actual) {
208 (Value::Object(eo), Value::Object(ao)) => {
209 for (k, ev) in eo {
211 let np = format!("{}/{}", path, k);
212 if let Some(av) = ao.get(k) {
213 validation_walk(ev, av, &np, out);
215 } else {
216 let schema_info = SchemaInfo {
218 data_type: type_of(ev).clone(),
219 required: Some(true),
220 format: None,
221 minimum: None,
222 maximum: None,
223 min_length: None,
224 max_length: None,
225 pattern: None,
226 enum_values: None,
227 additional_properties: None,
228 };
229
230 let error_msg =
231 format!("Missing required field '{}' of type {}", k, schema_info.data_type);
232
233 out.push(
234 ValidationError::new(
235 path.to_string(),
236 schema_info.data_type.clone(),
237 "missing".to_string(),
238 "missing_required",
239 )
240 .with_message(error_msg)
241 .with_schema_info(schema_info),
242 );
243 }
244 }
245
246 for k in ao.keys() {
248 if !eo.contains_key(k) {
249 let np = format!("{}/{}", path, k);
250 let error_msg = format!("Unexpected additional field '{}' found", k);
251
252 out.push(
253 ValidationError::new(
254 np,
255 "not_allowed".to_string(),
256 type_of(&ao[k]).clone(),
257 "additional_property",
258 )
259 .with_message(error_msg),
260 );
261 }
262 }
263 }
264 (Value::Array(ea), Value::Array(aa)) => {
265 if let Some(esample) = ea.first() {
267 for (i, av) in aa.iter().enumerate() {
268 let np = format!("{}/{}", path, i);
269 validation_walk(esample, av, &np, out);
270 }
271
272 if let Some(arr_size) = esample.as_array().map(|a| a.len()) {
274 if aa.len() != arr_size {
275 let schema_info = SchemaInfo {
276 data_type: "array".to_string(),
277 required: None,
278 format: None,
279 minimum: None,
280 maximum: None,
281 min_length: Some(arr_size),
282 max_length: Some(arr_size),
283 pattern: None,
284 enum_values: None,
285 additional_properties: None,
286 };
287
288 let error_msg = format!(
289 "Array size mismatch: expected {} items, found {}",
290 arr_size,
291 aa.len()
292 );
293
294 out.push(
295 ValidationError::new(
296 path.to_string(),
297 format!("array[{}]", arr_size),
298 format!("array[{}]", aa.len()),
299 "length_mismatch",
300 )
301 .with_message(error_msg)
302 .with_schema_info(schema_info),
303 );
304 }
305 }
306 } else {
307 if !aa.is_empty() {
309 let error_msg = format!("Expected empty array, but found {} items", aa.len());
310
311 out.push(
312 ValidationError::new(
313 path.to_string(),
314 "empty_array".to_string(),
315 format!("array[{}]", aa.len()),
316 "unexpected_items",
317 )
318 .with_message(error_msg),
319 );
320 }
321 }
322 }
323 (e, a) => {
324 let et = type_of(e);
325 let at = type_of(a);
326
327 if et != at {
328 let schema_info = SchemaInfo {
330 data_type: et.clone(),
331 required: None,
332 format: None, minimum: None,
334 maximum: None,
335 min_length: None,
336 max_length: None,
337 pattern: None,
338 enum_values: None,
339 additional_properties: None,
340 };
341
342 let error_msg = format!("Type mismatch: expected {}, found {}", et, at);
343
344 out.push(
345 ValidationError::new(path.to_string(), et, at, "type_mismatch")
346 .with_message(error_msg)
347 .with_schema_info(schema_info),
348 );
349 } else {
350 match (e, a) {
352 (Value::String(es), Value::String(actual_str)) => {
353 if es.is_empty() && !actual_str.is_empty() {
355 }
357 }
358 (Value::Number(en), Value::Number(an)) => {
359 if let (Some(_en_val), Some(_an_val)) = (en.as_f64(), an.as_f64()) {
361 }
363 }
364 _ => {} }
366 }
367 }
368 }
369}
370
371pub fn to_enhanced_422_json(errors: Vec<ValidationError>) -> Value {
385 json!({
386 "error": "Schema validation failed",
387 "message": "Request data doesn't match expected schema. See details below for specific issues.",
388 "validation_errors": errors.iter().map(|e| {
389 json!({
390 "path": e.path,
391 "expected": e.expected,
392 "found": e.found,
393 "error_type": e.error_type,
394 "message": e.message,
395 "schema_info": e.schema_info
396 })
397 }).collect::<Vec<_>>(),
398 "help": {
399 "tips": [
400 "Check that all required fields are present",
401 "Ensure field types match the expected schema",
402 "Verify string formats and patterns",
403 "Confirm number values are within required ranges",
404 "Remove any unexpected fields"
405 ],
406 "documentation": "Refer to API specification for complete field definitions"
407 },
408 "timestamp": chrono::Utc::now().to_rfc3339()
409 })
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn test_validation_error_new() {
418 let error = ValidationError::new(
419 "/user/name".to_string(),
420 "string".to_string(),
421 "number".to_string(),
422 "type_mismatch",
423 );
424
425 assert_eq!(error.path, "/user/name");
426 assert_eq!(error.expected, "string");
427 assert_eq!(error.found, "number");
428 assert_eq!(error.error_type, "type_mismatch");
429 assert!(error.message.is_none());
430 assert!(error.schema_info.is_none());
431 }
432
433 #[test]
434 fn test_validation_error_with_message() {
435 let error = ValidationError::new(
436 "/user/age".to_string(),
437 "integer".to_string(),
438 "string".to_string(),
439 "type_mismatch",
440 )
441 .with_message("Expected integer, got string".to_string());
442
443 assert_eq!(error.message, Some("Expected integer, got string".to_string()));
444 }
445
446 #[test]
447 fn test_validation_error_with_schema_info() {
448 let schema_info = SchemaInfo {
449 data_type: "string".to_string(),
450 required: Some(true),
451 format: Some("email".to_string()),
452 minimum: None,
453 maximum: None,
454 min_length: Some(5),
455 max_length: Some(100),
456 pattern: None,
457 enum_values: None,
458 additional_properties: None,
459 };
460
461 let error = ValidationError::new(
462 "/user/email".to_string(),
463 "string".to_string(),
464 "missing".to_string(),
465 "missing_required",
466 )
467 .with_schema_info(schema_info.clone());
468
469 assert!(error.schema_info.is_some());
470 let info = error.schema_info.unwrap();
471 assert_eq!(info.data_type, "string");
472 assert_eq!(info.required, Some(true));
473 assert_eq!(info.format, Some("email".to_string()));
474 }
475
476 #[test]
477 fn test_field_error_from_validation_error() {
478 let validation_error = ValidationError::new(
479 "/user/id".to_string(),
480 "integer".to_string(),
481 "string".to_string(),
482 "type_mismatch",
483 )
484 .with_message("Type mismatch".to_string());
485
486 let field_error: FieldError = validation_error.into();
487
488 assert_eq!(field_error.path, "/user/id");
489 assert_eq!(field_error.expected, "integer");
490 assert_eq!(field_error.found, "string");
491 assert_eq!(field_error.message, Some("Type mismatch".to_string()));
492 }
493
494 #[test]
495 fn test_type_of_null() {
496 let value = json!(null);
497 assert_eq!(type_of(&value), "null");
498 }
499
500 #[test]
501 fn test_type_of_bool() {
502 let value = json!(true);
503 assert_eq!(type_of(&value), "bool");
504 }
505
506 #[test]
507 fn test_type_of_integer() {
508 let value = json!(42);
509 assert_eq!(type_of(&value), "integer");
510 }
511
512 #[test]
513 fn test_type_of_number() {
514 let value = json!(42.5);
515 assert_eq!(type_of(&value), "number");
516 }
517
518 #[test]
519 fn test_type_of_string() {
520 let value = json!("hello");
521 assert_eq!(type_of(&value), "string");
522 }
523
524 #[test]
525 fn test_type_of_array() {
526 let value = json!([1, 2, 3]);
527 assert_eq!(type_of(&value), "array");
528 }
529
530 #[test]
531 fn test_type_of_object() {
532 let value = json!({"key": "value"});
533 assert_eq!(type_of(&value), "object");
534 }
535
536 #[test]
537 fn test_diff_matching_objects() {
538 let expected = json!({"name": "John", "age": 30});
539 let actual = json!({"name": "John", "age": 30});
540
541 let errors = diff(&expected, &actual);
542 assert_eq!(errors.len(), 0);
543 }
544
545 #[test]
546 fn test_diff_missing_field() {
547 let expected = json!({"name": "John", "age": 30});
548 let actual = json!({"name": "John"});
549
550 let errors = diff(&expected, &actual);
551 assert_eq!(errors.len(), 1);
552 assert_eq!(errors[0].path, "/age");
553 assert_eq!(errors[0].expected, "integer");
554 assert_eq!(errors[0].found, "missing");
555 }
556
557 #[test]
558 fn test_diff_type_mismatch() {
559 let expected = json!({"name": "John", "age": 30});
560 let actual = json!({"name": "John", "age": "thirty"});
561
562 let errors = diff(&expected, &actual);
563 assert_eq!(errors.len(), 1);
564 assert_eq!(errors[0].path, "/age");
565 assert_eq!(errors[0].expected, "integer");
566 assert_eq!(errors[0].found, "string");
567 }
568
569 #[test]
570 fn test_diff_nested_objects() {
571 let expected = json!({
572 "user": {
573 "name": "John",
574 "address": {
575 "city": "NYC"
576 }
577 }
578 });
579 let actual = json!({
580 "user": {
581 "name": "John",
582 "address": {
583 "city": 123
584 }
585 }
586 });
587
588 let errors = diff(&expected, &actual);
589 assert_eq!(errors.len(), 1);
590 assert_eq!(errors[0].path, "/user/address/city");
591 assert_eq!(errors[0].expected, "string");
592 assert_eq!(errors[0].found, "integer");
593 }
594
595 #[test]
596 fn test_diff_arrays() {
597 let expected = json!([{"id": 1}]);
598 let actual = json!([{"id": 1}, {"id": 2}]);
599
600 let errors = diff(&expected, &actual);
601 assert_eq!(errors.len(), 0); }
603
604 #[test]
605 fn test_diff_array_type_mismatch() {
606 let expected = json!([{"id": 1}]);
607 let actual = json!([{"id": "one"}]);
608
609 let errors = diff(&expected, &actual);
610 assert_eq!(errors.len(), 1);
611 assert_eq!(errors[0].path, "/0/id");
612 assert_eq!(errors[0].expected, "integer");
613 assert_eq!(errors[0].found, "string");
614 }
615
616 #[test]
617 fn test_to_422_json() {
618 let errors = vec![
619 FieldError {
620 path: "/name".to_string(),
621 expected: "string".to_string(),
622 found: "number".to_string(),
623 message: None,
624 },
625 FieldError {
626 path: "/email".to_string(),
627 expected: "string".to_string(),
628 found: "missing".to_string(),
629 message: Some("required".to_string()),
630 },
631 ];
632
633 let result = to_422_json(errors);
634 assert_eq!(result["error"], "Schema validation failed");
635 assert_eq!(result["details"].as_array().unwrap().len(), 2);
636 assert_eq!(result["details"][0]["path"], "/name");
637 assert_eq!(result["details"][1]["path"], "/email");
638 }
639
640 #[test]
641 fn test_validation_diff_matching_objects() {
642 let expected = json!({"name": "John", "age": 30});
643 let actual = json!({"name": "John", "age": 30});
644
645 let errors = validation_diff(&expected, &actual);
646 assert_eq!(errors.len(), 0);
647 }
648
649 #[test]
650 fn test_validation_diff_missing_required_field() {
651 let expected = json!({"name": "John", "age": 30});
652 let actual = json!({"name": "John"});
653
654 let errors = validation_diff(&expected, &actual);
655 assert_eq!(errors.len(), 1);
656 assert_eq!(errors[0].error_type, "missing_required");
657 assert!(errors[0].message.as_ref().unwrap().contains("Missing required field"));
658 assert!(errors[0].schema_info.is_some());
659 }
660
661 #[test]
662 fn test_validation_diff_additional_property() {
663 let expected = json!({"name": "John"});
664 let actual = json!({"name": "John", "age": 30});
665
666 let errors = validation_diff(&expected, &actual);
667 assert_eq!(errors.len(), 1);
668 assert_eq!(errors[0].error_type, "additional_property");
669 assert!(errors[0].message.as_ref().unwrap().contains("Unexpected additional field"));
670 }
671
672 #[test]
673 fn test_validation_diff_type_mismatch() {
674 let expected = json!({"age": 30});
675 let actual = json!({"age": "thirty"});
676
677 let errors = validation_diff(&expected, &actual);
678 assert_eq!(errors.len(), 1);
679 assert_eq!(errors[0].error_type, "type_mismatch");
680 assert_eq!(errors[0].expected, "integer");
681 assert_eq!(errors[0].found, "string");
682 assert!(errors[0].schema_info.is_some());
683 }
684
685 #[test]
686 fn test_validation_diff_array_items() {
687 let expected = json!([{"id": 1}]);
688 let actual = json!([{"id": "one"}]);
689
690 let errors = validation_diff(&expected, &actual);
691 assert_eq!(errors.len(), 1);
692 assert_eq!(errors[0].path, "/0/id");
693 assert_eq!(errors[0].error_type, "type_mismatch");
694 }
695
696 #[test]
697 fn test_validation_diff_empty_array_with_items() {
698 let expected = json!([]);
699 let actual = json!([1, 2, 3]);
700
701 let errors = validation_diff(&expected, &actual);
702 assert_eq!(errors.len(), 1);
703 assert_eq!(errors[0].error_type, "unexpected_items");
704 assert!(errors[0].message.as_ref().unwrap().contains("Expected empty array"));
705 }
706
707 #[test]
708 fn test_to_enhanced_422_json() {
709 let errors = vec![ValidationError::new(
710 "/name".to_string(),
711 "string".to_string(),
712 "number".to_string(),
713 "type_mismatch",
714 )
715 .with_message("Type mismatch: expected string, found number".to_string())];
716
717 let result = to_enhanced_422_json(errors);
718 assert_eq!(result["error"], "Schema validation failed");
719 assert!(result["message"].as_str().unwrap().contains("doesn't match expected schema"));
720 assert_eq!(result["validation_errors"].as_array().unwrap().len(), 1);
721 assert!(result["help"]["tips"].is_array());
722 assert!(result["timestamp"].is_string());
723 }
724
725 #[test]
726 fn test_validation_diff_nested_objects() {
727 let expected = json!({
728 "user": {
729 "profile": {
730 "name": "John",
731 "age": 30
732 }
733 }
734 });
735 let actual = json!({
736 "user": {
737 "profile": {
738 "name": "John"
739 }
740 }
741 });
742
743 let errors = validation_diff(&expected, &actual);
744 assert_eq!(errors.len(), 1);
745 assert!(errors[0].path.contains("/user/profile"));
746 assert_eq!(errors[0].error_type, "missing_required");
747 }
748
749 #[test]
750 fn test_validation_diff_multiple_errors() {
751 let expected = json!({
752 "name": "John",
753 "age": 30,
754 "email": "john@example.com"
755 });
756 let actual = json!({
757 "name": 123,
758 "extra": "field"
759 });
760
761 let errors = validation_diff(&expected, &actual);
762 assert!(errors.len() >= 3);
764
765 let error_types: Vec<_> = errors.iter().map(|e| e.error_type.as_str()).collect();
766 assert!(error_types.contains(&"type_mismatch"));
767 assert!(error_types.contains(&"missing_required"));
768 assert!(error_types.contains(&"additional_property"));
769 }
770}