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