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