1use pjson_rs_domain::value_objects::enter_deserialize_depth;
6use serde::{Deserialize, Deserializer, Serialize};
7use std::cell::Cell;
8use std::collections::HashMap;
9
10use crate::domain::value_objects::Schema;
11
12thread_local! {
13 static SCHEMA_DTO_DESERIALIZE_DEPTH: Cell<usize> = const { Cell::new(0) };
38}
39
40fn deserialize_boxed_schema_dto_option<'de, D>(
42 deserializer: D,
43) -> Result<Option<Box<SchemaDefinitionDto>>, D::Error>
44where
45 D: Deserializer<'de>,
46{
47 let _guard =
48 enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
49 Option::<Box<SchemaDefinitionDto>>::deserialize(deserializer)
50}
51
52fn deserialize_schema_dto_properties<'de, D>(
54 deserializer: D,
55) -> Result<HashMap<String, SchemaDefinitionDto>, D::Error>
56where
57 D: Deserializer<'de>,
58{
59 let _guard =
60 enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
61 HashMap::<String, SchemaDefinitionDto>::deserialize(deserializer)
62}
63
64fn deserialize_schema_dto_list<'de, D>(
66 deserializer: D,
67) -> Result<Vec<SchemaDefinitionDto>, D::Error>
68where
69 D: Deserializer<'de>,
70{
71 let _guard =
72 enter_deserialize_depth::<D::Error>(&SCHEMA_DTO_DESERIALIZE_DEPTH, "SchemaDefinitionDto")?;
73 Vec::<SchemaDefinitionDto>::deserialize(deserializer)
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SchemaRegistrationDto {
81 pub id: String,
83 pub schema: SchemaDefinitionDto,
85 pub metadata: Option<SchemaMetadataDto>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SchemaMetadataDto {
92 pub version: String,
94 pub description: Option<String>,
96 pub author: Option<String>,
98 pub created_at: Option<i64>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum SchemaDefinitionDto {
114 String {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 min_length: Option<usize>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 max_length: Option<usize>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pattern: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 enum_values: Option<Vec<String>>,
128 },
129 Integer {
131 #[serde(skip_serializing_if = "Option::is_none")]
133 minimum: Option<i64>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 maximum: Option<i64>,
137 },
138 Number {
140 #[serde(skip_serializing_if = "Option::is_none")]
142 minimum: Option<f64>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 maximum: Option<f64>,
146 },
147 Boolean,
149 Null,
151 Array {
153 #[serde(
155 default,
156 skip_serializing_if = "Option::is_none",
157 deserialize_with = "deserialize_boxed_schema_dto_option"
158 )]
159 items: Option<Box<SchemaDefinitionDto>>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 min_items: Option<usize>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 max_items: Option<usize>,
166 #[serde(default)]
168 unique_items: bool,
169 },
170 Object {
172 #[serde(deserialize_with = "deserialize_schema_dto_properties")]
174 properties: HashMap<String, SchemaDefinitionDto>,
175 #[serde(default)]
177 required: Vec<String>,
178 #[serde(default = "default_true")]
180 additional_properties: bool,
181 },
182 OneOf {
184 #[serde(deserialize_with = "deserialize_schema_dto_list")]
186 schemas: Vec<SchemaDefinitionDto>,
187 },
188 AllOf {
190 #[serde(deserialize_with = "deserialize_schema_dto_list")]
192 schemas: Vec<SchemaDefinitionDto>,
193 },
194 Any,
196}
197
198fn default_true() -> bool {
199 true
200}
201
202impl From<SchemaDefinitionDto> for Schema {
203 fn from(dto: SchemaDefinitionDto) -> Self {
204 match dto {
205 SchemaDefinitionDto::String {
206 min_length,
207 max_length,
208 pattern,
209 enum_values,
210 } => Self::String {
211 min_length,
212 max_length,
213 pattern,
214 allowed_values: enum_values
215 .map(|values| values.into_iter().collect::<smallvec::SmallVec<[_; 8]>>()),
216 },
217 SchemaDefinitionDto::Integer { minimum, maximum } => Self::Integer { minimum, maximum },
218 SchemaDefinitionDto::Number { minimum, maximum } => Self::Number { minimum, maximum },
219 SchemaDefinitionDto::Boolean => Self::Boolean,
220 SchemaDefinitionDto::Null => Self::Null,
221 SchemaDefinitionDto::Array {
222 items,
223 min_items,
224 max_items,
225 unique_items,
226 } => Self::Array {
227 items: items.map(|i| Box::new((*i).into())),
228 min_items,
229 max_items,
230 unique_items,
231 },
232 SchemaDefinitionDto::Object {
233 properties,
234 required,
235 additional_properties,
236 } => Self::Object {
237 properties: properties.into_iter().map(|(k, v)| (k, v.into())).collect(),
238 required,
239 additional_properties,
240 },
241 SchemaDefinitionDto::OneOf { schemas } => Self::OneOf {
242 schemas: schemas
243 .into_iter()
244 .map(|s| Box::new(s.into()))
245 .collect::<smallvec::SmallVec<[_; 4]>>(),
246 },
247 SchemaDefinitionDto::AllOf { schemas } => Self::AllOf {
248 schemas: schemas
249 .into_iter()
250 .map(|s| Box::new(s.into()))
251 .collect::<smallvec::SmallVec<[_; 4]>>(),
252 },
253 SchemaDefinitionDto::Any => Self::Any,
254 }
255 }
256}
257
258impl From<&Schema> for SchemaDefinitionDto {
259 fn from(schema: &Schema) -> Self {
260 match schema {
261 Schema::String {
262 min_length,
263 max_length,
264 pattern,
265 allowed_values,
266 } => Self::String {
267 min_length: *min_length,
268 max_length: *max_length,
269 pattern: pattern.as_ref().map(|p| p.to_string()),
270 enum_values: allowed_values
271 .as_ref()
272 .map(|v| v.iter().map(|s| s.to_string()).collect()),
273 },
274 Schema::Integer { minimum, maximum } => Self::Integer {
275 minimum: *minimum,
276 maximum: *maximum,
277 },
278 Schema::Number { minimum, maximum } => Self::Number {
279 minimum: *minimum,
280 maximum: *maximum,
281 },
282 Schema::Boolean => Self::Boolean,
283 Schema::Null => Self::Null,
284 Schema::Array {
285 items,
286 min_items,
287 max_items,
288 unique_items,
289 } => Self::Array {
290 items: items.as_ref().map(|i| Box::new(i.as_ref().into())),
291 min_items: *min_items,
292 max_items: *max_items,
293 unique_items: *unique_items,
294 },
295 Schema::Object {
296 properties,
297 required,
298 additional_properties,
299 } => Self::Object {
300 properties: properties
301 .iter()
302 .map(|(k, v)| (k.clone(), v.into()))
303 .collect(),
304 required: required.clone(),
305 additional_properties: *additional_properties,
306 },
307 Schema::OneOf { schemas } => Self::OneOf {
308 schemas: schemas.iter().map(|s| s.as_ref().into()).collect(),
309 },
310 Schema::AllOf { schemas } => Self::AllOf {
311 schemas: schemas.iter().map(|s| s.as_ref().into()).collect(),
312 },
313 Schema::Any => Self::Any,
314 _ => Self::Any,
315 }
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct ValidationRequestDto {
322 pub schema_id: String,
324 pub data: String,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct ValidationResultDto {
331 pub valid: bool,
333 #[serde(skip_serializing_if = "Vec::is_empty")]
335 pub errors: Vec<ValidationErrorDto>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct ValidationErrorDto {
341 pub path: String,
343 pub message: String,
345 pub error_type: String,
347}
348
349impl From<&crate::domain::value_objects::SchemaValidationError> for ValidationErrorDto {
350 fn from(error: &crate::domain::value_objects::SchemaValidationError) -> Self {
351 use crate::domain::value_objects::SchemaValidationError;
352
353 let (error_type, path, message) = match error {
354 SchemaValidationError::TypeMismatch {
355 path,
356 expected,
357 actual,
358 } => (
359 "type_mismatch".to_string(),
360 path.clone(),
361 format!("Expected {expected}, got {actual}"),
362 ),
363 SchemaValidationError::MissingRequired { path, field } => (
364 "missing_required".to_string(),
365 path.clone(),
366 format!("Missing required field: {field}"),
367 ),
368 SchemaValidationError::OutOfRange {
369 path,
370 value,
371 min,
372 max,
373 } => (
374 "out_of_range".to_string(),
375 path.clone(),
376 format!("Value {value} not in range [{min}, {max}]"),
377 ),
378 SchemaValidationError::StringLengthConstraint {
379 path,
380 actual,
381 min,
382 max,
383 } => (
384 "string_length".to_string(),
385 path.clone(),
386 format!("String length {actual} not in range [{min}, {max}]"),
387 ),
388 SchemaValidationError::PatternMismatch {
389 path,
390 value,
391 pattern,
392 } => (
393 "pattern_mismatch".to_string(),
394 path.clone(),
395 format!("Value '{value}' does not match pattern '{pattern}'"),
396 ),
397 SchemaValidationError::ArraySizeConstraint {
398 path,
399 actual,
400 min,
401 max,
402 } => (
403 "array_size".to_string(),
404 path.clone(),
405 format!("Array size {actual} not in range [{min}, {max}]"),
406 ),
407 SchemaValidationError::DuplicateItems { path } => (
408 "duplicate_items".to_string(),
409 path.clone(),
410 "Array contains duplicate items".to_string(),
411 ),
412 SchemaValidationError::InvalidEnumValue { path, value } => (
413 "invalid_enum".to_string(),
414 path.clone(),
415 format!("Value '{value}' not in allowed values"),
416 ),
417 SchemaValidationError::AdditionalPropertyNotAllowed { path, property } => (
418 "additional_property".to_string(),
419 path.clone(),
420 format!("Additional property '{property}' not allowed"),
421 ),
422 SchemaValidationError::NoMatchingOneOf { path } => (
423 "no_matching_one_of".to_string(),
424 path.clone(),
425 "No matching schema in OneOf".to_string(),
426 ),
427 SchemaValidationError::AllOfFailure { path, failures } => (
428 "all_of_failure".to_string(),
429 path.clone(),
430 format!("AllOf validation failed for schemas: {failures}"),
431 ),
432 SchemaValidationError::InvalidPattern {
433 path,
434 pattern,
435 reason,
436 } => (
437 "invalid_pattern".to_string(),
438 path.clone(),
439 format!("Pattern '{pattern}' is not valid regex: {reason}"),
440 ),
441 _ => ("unknown".to_string(), String::new(), error.to_string()),
442 };
443
444 Self {
445 path,
446 message,
447 error_type,
448 }
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::domain::value_objects::{Schema, SchemaValidationError};
456
457 #[test]
462 fn test_schema_dto_string_serialization() {
463 let dto = SchemaDefinitionDto::String {
464 min_length: Some(1),
465 max_length: Some(100),
466 pattern: Some("^[a-z]+$".to_string()),
467 enum_values: Some(vec!["hello".to_string(), "world".to_string()]),
468 };
469
470 let json = serde_json::to_string(&dto).unwrap();
471 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
472
473 assert!(matches!(
474 deserialized,
475 SchemaDefinitionDto::String {
476 min_length: Some(1),
477 max_length: Some(100),
478 pattern: Some(_),
479 enum_values: Some(_)
480 }
481 ));
482 }
483
484 #[test]
485 fn test_schema_dto_string_minimal_serialization() {
486 let dto = SchemaDefinitionDto::String {
487 min_length: None,
488 max_length: None,
489 pattern: None,
490 enum_values: None,
491 };
492
493 let json = serde_json::to_string(&dto).unwrap();
494 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
495
496 assert!(matches!(
497 deserialized,
498 SchemaDefinitionDto::String {
499 min_length: None,
500 max_length: None,
501 pattern: None,
502 enum_values: None
503 }
504 ));
505 }
506
507 #[test]
508 fn test_schema_dto_integer_serialization() {
509 let dto = SchemaDefinitionDto::Integer {
510 minimum: Some(-100),
511 maximum: Some(100),
512 };
513
514 let json = serde_json::to_string(&dto).unwrap();
515 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
516
517 assert!(matches!(
518 deserialized,
519 SchemaDefinitionDto::Integer {
520 minimum: Some(-100),
521 maximum: Some(100)
522 }
523 ));
524 }
525
526 #[test]
527 fn test_schema_dto_number_serialization() {
528 let dto = SchemaDefinitionDto::Number {
529 minimum: Some(0.5),
530 maximum: Some(99.9),
531 };
532
533 let json = serde_json::to_string(&dto).unwrap();
534 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
535
536 assert!(matches!(deserialized, SchemaDefinitionDto::Number { .. }));
537 }
538
539 #[test]
540 fn test_schema_dto_boolean_serialization() {
541 let dto = SchemaDefinitionDto::Boolean;
542
543 let json = serde_json::to_string(&dto).unwrap();
544 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
545
546 assert!(matches!(deserialized, SchemaDefinitionDto::Boolean));
547 }
548
549 #[test]
550 fn test_schema_dto_null_serialization() {
551 let dto = SchemaDefinitionDto::Null;
552
553 let json = serde_json::to_string(&dto).unwrap();
554 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
555
556 assert!(matches!(deserialized, SchemaDefinitionDto::Null));
557 }
558
559 #[test]
560 fn test_schema_dto_array_serialization() {
561 let dto = SchemaDefinitionDto::Array {
562 items: Some(Box::new(SchemaDefinitionDto::String {
563 min_length: None,
564 max_length: None,
565 pattern: None,
566 enum_values: None,
567 })),
568 min_items: Some(1),
569 max_items: Some(10),
570 unique_items: true,
571 };
572
573 let json = serde_json::to_string(&dto).unwrap();
574 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
575
576 assert!(matches!(
577 deserialized,
578 SchemaDefinitionDto::Array {
579 items: Some(_),
580 min_items: Some(1),
581 max_items: Some(10),
582 unique_items: true
583 }
584 ));
585 }
586
587 #[test]
588 fn test_schema_dto_array_minimal_serialization() {
589 let dto = SchemaDefinitionDto::Array {
590 items: None,
591 min_items: None,
592 max_items: None,
593 unique_items: false,
594 };
595
596 let json = serde_json::to_string(&dto).unwrap();
597 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
598
599 assert!(matches!(
600 deserialized,
601 SchemaDefinitionDto::Array {
602 items: None,
603 min_items: None,
604 max_items: None,
605 unique_items: false
606 }
607 ));
608 }
609
610 #[test]
611 fn test_schema_dto_object_serialization() {
612 let mut properties = HashMap::new();
613 properties.insert(
614 "name".to_string(),
615 SchemaDefinitionDto::String {
616 min_length: Some(1),
617 max_length: None,
618 pattern: None,
619 enum_values: None,
620 },
621 );
622 properties.insert(
623 "age".to_string(),
624 SchemaDefinitionDto::Integer {
625 minimum: Some(0),
626 maximum: Some(150),
627 },
628 );
629
630 let dto = SchemaDefinitionDto::Object {
631 properties,
632 required: vec!["name".to_string()],
633 additional_properties: false,
634 };
635
636 let json = serde_json::to_string(&dto).unwrap();
637 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
638
639 assert!(matches!(deserialized, SchemaDefinitionDto::Object {
640 properties,
641 required,
642 additional_properties: false
643 } if properties.len() == 2 && required.len() == 1));
644 }
645
646 #[test]
647 fn test_schema_dto_object_allow_additional_properties() {
648 let properties = HashMap::new();
649 let dto = SchemaDefinitionDto::Object {
650 properties,
651 required: vec![],
652 additional_properties: true,
653 };
654
655 let json = serde_json::to_string(&dto).unwrap();
656 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
657
658 assert!(matches!(
659 deserialized,
660 SchemaDefinitionDto::Object {
661 additional_properties: true,
662 ..
663 }
664 ));
665 }
666
667 #[test]
668 fn test_schema_dto_oneof_serialization() {
669 let dto = SchemaDefinitionDto::OneOf {
670 schemas: vec![
671 SchemaDefinitionDto::String {
672 min_length: None,
673 max_length: None,
674 pattern: None,
675 enum_values: None,
676 },
677 SchemaDefinitionDto::Integer {
678 minimum: None,
679 maximum: None,
680 },
681 ],
682 };
683
684 let json = serde_json::to_string(&dto).unwrap();
685 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
686
687 assert!(
688 matches!(deserialized, SchemaDefinitionDto::OneOf { schemas } if schemas.len() == 2)
689 );
690 }
691
692 #[test]
693 fn test_schema_dto_allof_serialization() {
694 let dto = SchemaDefinitionDto::AllOf {
695 schemas: vec![
696 SchemaDefinitionDto::String {
697 min_length: Some(1),
698 max_length: None,
699 pattern: None,
700 enum_values: None,
701 },
702 SchemaDefinitionDto::String {
703 min_length: None,
704 max_length: Some(100),
705 pattern: None,
706 enum_values: None,
707 },
708 ],
709 };
710
711 let json = serde_json::to_string(&dto).unwrap();
712 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
713
714 assert!(
715 matches!(deserialized, SchemaDefinitionDto::AllOf { schemas } if schemas.len() == 2)
716 );
717 }
718
719 #[test]
720 fn test_schema_dto_any_serialization() {
721 let dto = SchemaDefinitionDto::Any;
722
723 let json = serde_json::to_string(&dto).unwrap();
724 let deserialized: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
725
726 assert!(matches!(deserialized, SchemaDefinitionDto::Any));
727 }
728
729 #[test]
734 fn test_schema_dto_to_domain_string() {
735 let dto = SchemaDefinitionDto::String {
736 min_length: Some(5),
737 max_length: Some(50),
738 pattern: Some("[a-z]+".to_string()),
739 enum_values: Some(vec!["foo".to_string(), "bar".to_string()]),
740 };
741
742 let schema: Schema = dto.into();
743
744 assert!(matches!(
745 schema,
746 Schema::String {
747 min_length: Some(5),
748 max_length: Some(50),
749 pattern: Some(_),
750 allowed_values: Some(_)
751 }
752 ));
753 }
754
755 #[test]
756 fn test_schema_dto_to_domain_integer() {
757 let dto = SchemaDefinitionDto::Integer {
758 minimum: Some(10),
759 maximum: Some(20),
760 };
761
762 let schema: Schema = dto.into();
763
764 assert!(matches!(
765 schema,
766 Schema::Integer {
767 minimum: Some(10),
768 maximum: Some(20)
769 }
770 ));
771 }
772
773 #[test]
774 fn test_schema_dto_to_domain_number() {
775 let dto = SchemaDefinitionDto::Number {
776 minimum: Some(1.5),
777 maximum: Some(9.9),
778 };
779
780 let schema: Schema = dto.into();
781
782 assert!(matches!(schema, Schema::Number {
783 minimum: Some(min),
784 maximum: Some(max)
785 } if (min - 1.5).abs() < 0.001 && (max - 9.9).abs() < 0.001));
786 }
787
788 #[test]
789 fn test_schema_dto_to_domain_boolean() {
790 let dto = SchemaDefinitionDto::Boolean;
791 let schema: Schema = dto.into();
792
793 assert!(matches!(schema, Schema::Boolean));
794 }
795
796 #[test]
797 fn test_schema_dto_to_domain_null() {
798 let dto = SchemaDefinitionDto::Null;
799 let schema: Schema = dto.into();
800
801 assert!(matches!(schema, Schema::Null));
802 }
803
804 #[test]
805 fn test_schema_dto_to_domain_array_with_items() {
806 let dto = SchemaDefinitionDto::Array {
807 items: Some(Box::new(SchemaDefinitionDto::Integer {
808 minimum: None,
809 maximum: None,
810 })),
811 min_items: Some(0),
812 max_items: Some(100),
813 unique_items: true,
814 };
815
816 let schema: Schema = dto.into();
817
818 assert!(matches!(
819 schema,
820 Schema::Array {
821 items: Some(_),
822 min_items: Some(0),
823 max_items: Some(100),
824 unique_items: true
825 }
826 ));
827 }
828
829 #[test]
830 fn test_schema_dto_to_domain_array_without_items() {
831 let dto = SchemaDefinitionDto::Array {
832 items: None,
833 min_items: None,
834 max_items: None,
835 unique_items: false,
836 };
837
838 let schema: Schema = dto.into();
839
840 assert!(matches!(
841 schema,
842 Schema::Array {
843 items: None,
844 min_items: None,
845 max_items: None,
846 unique_items: false
847 }
848 ));
849 }
850
851 #[test]
852 fn test_schema_dto_to_domain_object() {
853 let mut properties = HashMap::new();
854 properties.insert(
855 "id".to_string(),
856 SchemaDefinitionDto::Integer {
857 minimum: Some(1),
858 maximum: None,
859 },
860 );
861
862 let dto = SchemaDefinitionDto::Object {
863 properties,
864 required: vec!["id".to_string()],
865 additional_properties: false,
866 };
867
868 let schema: Schema = dto.into();
869
870 assert!(matches!(schema, Schema::Object {
871 properties,
872 required,
873 additional_properties: false
874 } if properties.len() == 1 && required.len() == 1));
875 }
876
877 #[test]
878 fn test_schema_dto_to_domain_oneof() {
879 let dto = SchemaDefinitionDto::OneOf {
880 schemas: vec![
881 SchemaDefinitionDto::String {
882 min_length: None,
883 max_length: None,
884 pattern: None,
885 enum_values: None,
886 },
887 SchemaDefinitionDto::Integer {
888 minimum: None,
889 maximum: None,
890 },
891 ],
892 };
893
894 let schema: Schema = dto.into();
895
896 assert!(matches!(schema, Schema::OneOf { schemas } if schemas.len() == 2));
897 }
898
899 #[test]
900 fn test_schema_dto_to_domain_allof() {
901 let dto = SchemaDefinitionDto::AllOf {
902 schemas: vec![
903 SchemaDefinitionDto::String {
904 min_length: Some(1),
905 max_length: None,
906 pattern: None,
907 enum_values: None,
908 },
909 SchemaDefinitionDto::String {
910 min_length: None,
911 max_length: Some(100),
912 pattern: None,
913 enum_values: None,
914 },
915 ],
916 };
917
918 let schema: Schema = dto.into();
919
920 assert!(matches!(schema, Schema::AllOf { schemas } if schemas.len() == 2));
921 }
922
923 #[test]
924 fn test_schema_dto_to_domain_any() {
925 let dto = SchemaDefinitionDto::Any;
926 let schema: Schema = dto.into();
927
928 assert!(matches!(schema, Schema::Any));
929 }
930
931 #[test]
936 fn test_domain_schema_to_dto_string() {
937 let schema = Schema::String {
938 min_length: Some(10),
939 max_length: Some(100),
940 pattern: Some("[0-9]+".into()),
941 allowed_values: Some(smallvec::smallvec!["123".into(), "456".into()]),
942 };
943
944 let dto: SchemaDefinitionDto = (&schema).into();
945
946 assert!(matches!(
947 dto,
948 SchemaDefinitionDto::String {
949 min_length: Some(10),
950 max_length: Some(100),
951 pattern: Some(_),
952 enum_values: Some(_)
953 }
954 ));
955 }
956
957 #[test]
958 fn test_domain_schema_to_dto_integer() {
959 let schema = Schema::Integer {
960 minimum: Some(0),
961 maximum: Some(1000),
962 };
963
964 let dto: SchemaDefinitionDto = (&schema).into();
965
966 assert!(matches!(
967 dto,
968 SchemaDefinitionDto::Integer {
969 minimum: Some(0),
970 maximum: Some(1000)
971 }
972 ));
973 }
974
975 #[test]
976 fn test_domain_schema_to_dto_number() {
977 let schema = Schema::Number {
978 minimum: Some(0.0),
979 maximum: Some(99.99),
980 };
981
982 let dto: SchemaDefinitionDto = (&schema).into();
983
984 assert!(matches!(dto, SchemaDefinitionDto::Number { .. }));
985 }
986
987 #[test]
988 fn test_domain_schema_to_dto_boolean() {
989 let schema = Schema::Boolean;
990 let dto: SchemaDefinitionDto = (&schema).into();
991
992 assert!(matches!(dto, SchemaDefinitionDto::Boolean));
993 }
994
995 #[test]
996 fn test_domain_schema_to_dto_null() {
997 let schema = Schema::Null;
998 let dto: SchemaDefinitionDto = (&schema).into();
999
1000 assert!(matches!(dto, SchemaDefinitionDto::Null));
1001 }
1002
1003 #[test]
1004 fn test_domain_schema_to_dto_array() {
1005 let schema = Schema::Array {
1006 items: Some(Box::new(Schema::String {
1007 min_length: None,
1008 max_length: None,
1009 pattern: None,
1010 allowed_values: None,
1011 })),
1012 min_items: Some(1),
1013 max_items: Some(50),
1014 unique_items: true,
1015 };
1016
1017 let dto: SchemaDefinitionDto = (&schema).into();
1018
1019 assert!(matches!(
1020 dto,
1021 SchemaDefinitionDto::Array {
1022 items: Some(_),
1023 min_items: Some(1),
1024 max_items: Some(50),
1025 unique_items: true
1026 }
1027 ));
1028 }
1029
1030 #[test]
1031 fn test_domain_schema_to_dto_object() {
1032 let mut properties = HashMap::new();
1033 properties.insert(
1034 "email".to_string(),
1035 Schema::String {
1036 min_length: Some(5),
1037 max_length: Some(200),
1038 pattern: None,
1039 allowed_values: None,
1040 },
1041 );
1042
1043 let schema = Schema::Object {
1044 properties,
1045 required: vec!["email".to_string()],
1046 additional_properties: true,
1047 };
1048
1049 let dto: SchemaDefinitionDto = (&schema).into();
1050
1051 assert!(matches!(dto, SchemaDefinitionDto::Object {
1052 properties,
1053 required,
1054 additional_properties: true
1055 } if properties.len() == 1 && required.len() == 1));
1056 }
1057
1058 #[test]
1059 fn test_domain_schema_to_dto_oneof() {
1060 let schema = Schema::OneOf {
1061 schemas: smallvec::smallvec![
1062 Box::new(Schema::String {
1063 min_length: None,
1064 max_length: None,
1065 pattern: None,
1066 allowed_values: None,
1067 }),
1068 Box::new(Schema::Integer {
1069 minimum: None,
1070 maximum: None,
1071 }),
1072 ],
1073 };
1074
1075 let dto: SchemaDefinitionDto = (&schema).into();
1076
1077 assert!(matches!(dto, SchemaDefinitionDto::OneOf { schemas } if schemas.len() == 2));
1078 }
1079
1080 #[test]
1081 fn test_domain_schema_to_dto_allof() {
1082 let schema = Schema::AllOf {
1083 schemas: smallvec::smallvec![
1084 Box::new(Schema::String {
1085 min_length: Some(1),
1086 max_length: None,
1087 pattern: None,
1088 allowed_values: None,
1089 }),
1090 Box::new(Schema::String {
1091 min_length: None,
1092 max_length: Some(50),
1093 pattern: None,
1094 allowed_values: None,
1095 }),
1096 ],
1097 };
1098
1099 let dto: SchemaDefinitionDto = (&schema).into();
1100
1101 assert!(matches!(dto, SchemaDefinitionDto::AllOf { schemas } if schemas.len() == 2));
1102 }
1103
1104 #[test]
1105 fn test_domain_schema_to_dto_any() {
1106 let schema = Schema::Any;
1107 let dto: SchemaDefinitionDto = (&schema).into();
1108
1109 assert!(matches!(dto, SchemaDefinitionDto::Any));
1110 }
1111
1112 #[test]
1117 fn test_schema_registration_dto_serialization() {
1118 let dto = SchemaRegistrationDto {
1119 id: "user-schema".to_string(),
1120 schema: SchemaDefinitionDto::Object {
1121 properties: HashMap::new(),
1122 required: vec![],
1123 additional_properties: true,
1124 },
1125 metadata: Some(SchemaMetadataDto {
1126 version: "1.0".to_string(),
1127 description: Some("User schema".to_string()),
1128 author: Some("John Doe".to_string()),
1129 created_at: Some(1234567890),
1130 }),
1131 };
1132
1133 let json = serde_json::to_string(&dto).unwrap();
1134 let deserialized: SchemaRegistrationDto = serde_json::from_str(&json).unwrap();
1135
1136 assert_eq!(deserialized.id, "user-schema");
1137 assert!(matches!(
1138 deserialized.schema,
1139 SchemaDefinitionDto::Object { .. }
1140 ));
1141 assert!(deserialized.metadata.is_some());
1142 }
1143
1144 #[test]
1145 fn test_schema_registration_dto_without_metadata() {
1146 let dto = SchemaRegistrationDto {
1147 id: "simple-schema".to_string(),
1148 schema: SchemaDefinitionDto::String {
1149 min_length: None,
1150 max_length: None,
1151 pattern: None,
1152 enum_values: None,
1153 },
1154 metadata: None,
1155 };
1156
1157 let json = serde_json::to_string(&dto).unwrap();
1158 let deserialized: SchemaRegistrationDto = serde_json::from_str(&json).unwrap();
1159
1160 assert_eq!(deserialized.id, "simple-schema");
1161 assert!(deserialized.metadata.is_none());
1162 }
1163
1164 #[test]
1169 fn test_schema_metadata_dto_full() {
1170 let dto = SchemaMetadataDto {
1171 version: "2.5".to_string(),
1172 description: Some("Complete metadata".to_string()),
1173 author: Some("Jane Smith".to_string()),
1174 created_at: Some(9876543210),
1175 };
1176
1177 let json = serde_json::to_string(&dto).unwrap();
1178 let deserialized: SchemaMetadataDto = serde_json::from_str(&json).unwrap();
1179
1180 assert_eq!(deserialized.version, "2.5");
1181 assert_eq!(
1182 deserialized.description,
1183 Some("Complete metadata".to_string())
1184 );
1185 assert_eq!(deserialized.author, Some("Jane Smith".to_string()));
1186 assert_eq!(deserialized.created_at, Some(9876543210));
1187 }
1188
1189 #[test]
1190 fn test_schema_metadata_dto_minimal() {
1191 let dto = SchemaMetadataDto {
1192 version: "1.0".to_string(),
1193 description: None,
1194 author: None,
1195 created_at: None,
1196 };
1197
1198 let json = serde_json::to_string(&dto).unwrap();
1199 let deserialized: SchemaMetadataDto = serde_json::from_str(&json).unwrap();
1200
1201 assert_eq!(deserialized.version, "1.0");
1202 assert!(deserialized.description.is_none());
1203 assert!(deserialized.author.is_none());
1204 assert!(deserialized.created_at.is_none());
1205 }
1206
1207 #[test]
1212 fn test_validation_request_dto_serialization() {
1213 let dto = ValidationRequestDto {
1214 schema_id: "user-schema".to_string(),
1215 data: r#"{"name": "John", "age": 30}"#.to_string(),
1216 };
1217
1218 let json = serde_json::to_string(&dto).unwrap();
1219 let deserialized: ValidationRequestDto = serde_json::from_str(&json).unwrap();
1220
1221 assert_eq!(deserialized.schema_id, "user-schema");
1222 assert_eq!(deserialized.data, r#"{"name": "John", "age": 30}"#);
1223 }
1224
1225 #[test]
1226 fn test_validation_result_dto_valid() {
1227 let dto = ValidationResultDto {
1228 valid: true,
1229 errors: vec![],
1230 };
1231
1232 let json = serde_json::to_string(&dto).unwrap();
1233 let json_with_errors = if json.contains("errors") {
1236 json
1237 } else {
1238 json.replace("}", r#","errors":[]}"#)
1239 };
1240 let deserialized: ValidationResultDto = serde_json::from_str(&json_with_errors).unwrap();
1241
1242 assert!(deserialized.valid);
1243 assert!(deserialized.errors.is_empty());
1244 }
1245
1246 #[test]
1247 fn test_validation_result_dto_with_errors() {
1248 let dto = ValidationResultDto {
1249 valid: false,
1250 errors: vec![
1251 ValidationErrorDto {
1252 path: "$.name".to_string(),
1253 message: "Too short".to_string(),
1254 error_type: "string_length".to_string(),
1255 },
1256 ValidationErrorDto {
1257 path: "$.age".to_string(),
1258 message: "Out of range".to_string(),
1259 error_type: "out_of_range".to_string(),
1260 },
1261 ],
1262 };
1263
1264 let json = serde_json::to_string(&dto).unwrap();
1265 let deserialized: ValidationResultDto = serde_json::from_str(&json).unwrap();
1266
1267 assert!(!deserialized.valid);
1268 assert_eq!(deserialized.errors.len(), 2);
1269 }
1270
1271 #[test]
1276 fn test_validation_error_type_mismatch_conversion() {
1277 let domain_error = SchemaValidationError::TypeMismatch {
1278 path: "$.field".to_string(),
1279 expected: "string".to_string(),
1280 actual: "number".to_string(),
1281 };
1282
1283 let dto: ValidationErrorDto = (&domain_error).into();
1284
1285 assert_eq!(dto.path, "$.field");
1286 assert_eq!(dto.error_type, "type_mismatch");
1287 assert!(dto.message.contains("string"));
1288 assert!(dto.message.contains("number"));
1289 }
1290
1291 #[test]
1292 fn test_validation_error_missing_required_conversion() {
1293 let domain_error = SchemaValidationError::MissingRequired {
1294 path: "$.".to_string(),
1295 field: "email".to_string(),
1296 };
1297
1298 let dto: ValidationErrorDto = (&domain_error).into();
1299
1300 assert_eq!(dto.path, "$.");
1301 assert_eq!(dto.error_type, "missing_required");
1302 assert!(dto.message.contains("email"));
1303 }
1304
1305 #[test]
1306 fn test_validation_error_out_of_range_conversion() {
1307 let domain_error = SchemaValidationError::OutOfRange {
1308 path: "$.age".to_string(),
1309 value: "200".to_string(),
1310 min: "0".to_string(),
1311 max: "150".to_string(),
1312 };
1313
1314 let dto: ValidationErrorDto = (&domain_error).into();
1315
1316 assert_eq!(dto.path, "$.age");
1317 assert_eq!(dto.error_type, "out_of_range");
1318 assert!(dto.message.contains("200"));
1319 }
1320
1321 #[test]
1322 fn test_validation_error_string_length_conversion() {
1323 let domain_error = SchemaValidationError::StringLengthConstraint {
1324 path: "$.name".to_string(),
1325 actual: 150,
1326 min: 1,
1327 max: 100,
1328 };
1329
1330 let dto: ValidationErrorDto = (&domain_error).into();
1331
1332 assert_eq!(dto.path, "$.name");
1333 assert_eq!(dto.error_type, "string_length");
1334 assert!(dto.message.contains("150"));
1335 }
1336
1337 #[test]
1338 fn test_validation_error_pattern_mismatch_conversion() {
1339 let domain_error = SchemaValidationError::PatternMismatch {
1340 path: "$.email".to_string(),
1341 value: "invalid".to_string(),
1342 pattern: "[a-z]+@[a-z]+\\.[a-z]+".to_string(),
1343 };
1344
1345 let dto: ValidationErrorDto = (&domain_error).into();
1346
1347 assert_eq!(dto.path, "$.email");
1348 assert_eq!(dto.error_type, "pattern_mismatch");
1349 assert!(dto.message.contains("invalid"));
1350 }
1351
1352 #[test]
1353 fn test_validation_error_array_size_conversion() {
1354 let domain_error = SchemaValidationError::ArraySizeConstraint {
1355 path: "$.items".to_string(),
1356 actual: 20,
1357 min: 1,
1358 max: 10,
1359 };
1360
1361 let dto: ValidationErrorDto = (&domain_error).into();
1362
1363 assert_eq!(dto.path, "$.items");
1364 assert_eq!(dto.error_type, "array_size");
1365 assert!(dto.message.contains("20"));
1366 }
1367
1368 #[test]
1369 fn test_validation_error_duplicate_items_conversion() {
1370 let domain_error = SchemaValidationError::DuplicateItems {
1371 path: "$.values".to_string(),
1372 };
1373
1374 let dto: ValidationErrorDto = (&domain_error).into();
1375
1376 assert_eq!(dto.path, "$.values");
1377 assert_eq!(dto.error_type, "duplicate_items");
1378 assert!(dto.message.contains("duplicate"));
1379 }
1380
1381 #[test]
1382 fn test_validation_error_invalid_enum_conversion() {
1383 let domain_error = SchemaValidationError::InvalidEnumValue {
1384 path: "$.status".to_string(),
1385 value: "pending".to_string(),
1386 };
1387
1388 let dto: ValidationErrorDto = (&domain_error).into();
1389
1390 assert_eq!(dto.path, "$.status");
1391 assert_eq!(dto.error_type, "invalid_enum");
1392 assert!(dto.message.contains("pending"));
1393 }
1394
1395 #[test]
1396 fn test_validation_error_additional_property_conversion() {
1397 let domain_error = SchemaValidationError::AdditionalPropertyNotAllowed {
1398 path: "$.".to_string(),
1399 property: "extra_field".to_string(),
1400 };
1401
1402 let dto: ValidationErrorDto = (&domain_error).into();
1403
1404 assert_eq!(dto.path, "$.");
1405 assert_eq!(dto.error_type, "additional_property");
1406 assert!(dto.message.contains("extra_field"));
1407 }
1408
1409 #[test]
1410 fn test_validation_error_no_matching_oneof_conversion() {
1411 let domain_error = SchemaValidationError::NoMatchingOneOf {
1412 path: "$.value".to_string(),
1413 };
1414
1415 let dto: ValidationErrorDto = (&domain_error).into();
1416
1417 assert_eq!(dto.path, "$.value");
1418 assert_eq!(dto.error_type, "no_matching_one_of");
1419 }
1420
1421 #[test]
1422 fn test_validation_error_allof_failure_conversion() {
1423 let domain_error = SchemaValidationError::AllOfFailure {
1424 path: "$.item".to_string(),
1425 failures: "schema1, schema2".to_string(),
1426 };
1427
1428 let dto: ValidationErrorDto = (&domain_error).into();
1429
1430 assert_eq!(dto.path, "$.item");
1431 assert_eq!(dto.error_type, "all_of_failure");
1432 assert!(dto.message.contains("schema1"));
1433 }
1434
1435 #[test]
1440 fn test_nested_object_with_array_conversion() {
1441 let mut inner_properties = HashMap::new();
1442 inner_properties.insert(
1443 "id".to_string(),
1444 SchemaDefinitionDto::Integer {
1445 minimum: Some(1),
1446 maximum: None,
1447 },
1448 );
1449
1450 let dto = SchemaDefinitionDto::Object {
1451 properties: {
1452 let mut props = HashMap::new();
1453 props.insert(
1454 "items".to_string(),
1455 SchemaDefinitionDto::Array {
1456 items: Some(Box::new(SchemaDefinitionDto::Object {
1457 properties: inner_properties,
1458 required: vec!["id".to_string()],
1459 additional_properties: false,
1460 })),
1461 min_items: Some(1),
1462 max_items: None,
1463 unique_items: false,
1464 },
1465 );
1466 props
1467 },
1468 required: vec!["items".to_string()],
1469 additional_properties: true,
1470 };
1471
1472 let schema: Schema = dto.into();
1473 assert!(matches!(schema, Schema::Object { .. }));
1474 }
1475
1476 #[test]
1477 fn test_nested_object_roundtrip() {
1478 let mut inner_props = HashMap::new();
1479 inner_props.insert(
1480 "name".to_string(),
1481 Schema::String {
1482 min_length: Some(1),
1483 max_length: Some(100),
1484 pattern: None,
1485 allowed_values: None,
1486 },
1487 );
1488
1489 let original_schema = Schema::Object {
1490 properties: {
1491 let mut props = HashMap::new();
1492 props.insert(
1493 "user".to_string(),
1494 Schema::Object {
1495 properties: inner_props,
1496 required: vec!["name".to_string()],
1497 additional_properties: false,
1498 },
1499 );
1500 props
1501 },
1502 required: vec!["user".to_string()],
1503 additional_properties: true,
1504 };
1505
1506 let dto: SchemaDefinitionDto = (&original_schema).into();
1507 let schema: Schema = dto.into();
1508
1509 assert!(matches!(schema, Schema::Object { .. }));
1510 }
1511
1512 #[test]
1513 fn test_deeply_nested_array() {
1514 let innermost_dto = SchemaDefinitionDto::String {
1515 min_length: None,
1516 max_length: None,
1517 pattern: None,
1518 enum_values: None,
1519 };
1520
1521 let level1 = SchemaDefinitionDto::Array {
1522 items: Some(Box::new(innermost_dto)),
1523 min_items: None,
1524 max_items: None,
1525 unique_items: false,
1526 };
1527
1528 let level2 = SchemaDefinitionDto::Array {
1529 items: Some(Box::new(level1)),
1530 min_items: None,
1531 max_items: None,
1532 unique_items: false,
1533 };
1534
1535 let schema: Schema = level2.into();
1536
1537 assert!(matches!(schema, Schema::Array {
1538 items: Some(boxed),
1539 ..
1540 } if matches!(*boxed, Schema::Array { .. })));
1541 }
1542
1543 fn nested_array_dto(levels: usize) -> SchemaDefinitionDto {
1551 let mut dto = SchemaDefinitionDto::Boolean;
1552 for _ in 0..levels {
1553 dto = SchemaDefinitionDto::Array {
1554 items: Some(Box::new(dto)),
1555 min_items: None,
1556 max_items: None,
1557 unique_items: false,
1558 };
1559 }
1560 dto
1561 }
1562
1563 #[test]
1571 fn test_schema_dto_deserialize_at_max_depth_succeeds() {
1572 let dto = nested_array_dto(pjson_rs_domain::MAX_DESERIALIZE_DEPTH);
1573 let bytes = rmp_serde::to_vec_named(&dto).unwrap();
1574 let result: Result<SchemaDefinitionDto, _> = rmp_serde::from_slice(&bytes);
1575 assert!(result.is_ok(), "{:?}", result.err());
1576 }
1577
1578 #[test]
1579 fn test_schema_dto_deserialize_beyond_max_depth_rejected() {
1580 let dto = nested_array_dto(pjson_rs_domain::MAX_DESERIALIZE_DEPTH + 1);
1581 let bytes = rmp_serde::to_vec_named(&dto).unwrap();
1582 let err = rmp_serde::from_slice::<SchemaDefinitionDto>(&bytes)
1583 .unwrap_err()
1584 .to_string();
1585 assert!(err.contains("SchemaDefinitionDto nesting depth"), "{err}");
1588 }
1589
1590 #[test]
1591 fn test_schema_dto_deserialize_small_nesting_roundtrips_via_json() {
1592 let dto = nested_array_dto(3);
1593 let json = serde_json::to_string(&dto).unwrap();
1594 let back: SchemaDefinitionDto = serde_json::from_str(&json).unwrap();
1595 let original_schema: Schema = dto.into();
1596 let roundtrip_schema: Schema = back.into();
1597 assert_eq!(original_schema, roundtrip_schema);
1598 }
1599
1600 #[test]
1607 fn test_schema_dto_array_items_still_optional_when_absent() {
1608 let json = r#"{"Array":{"min_items":null,"max_items":null,"unique_items":false}}"#;
1609 let dto: SchemaDefinitionDto = serde_json::from_str(json).unwrap();
1610 assert!(matches!(
1611 dto,
1612 SchemaDefinitionDto::Array { items: None, .. }
1613 ));
1614 }
1615
1616 #[test]
1621 fn test_schema_dto_properties_and_schemas_still_required() {
1622 let json = r#"{"Object":{"required":[],"additional_properties":true}}"#;
1623 let result: Result<SchemaDefinitionDto, _> = serde_json::from_str(json);
1624 assert!(result.is_err(), "properties should still be required");
1625
1626 let json = r#"{"OneOf":{}}"#;
1627 let result: Result<SchemaDefinitionDto, _> = serde_json::from_str(json);
1628 assert!(result.is_err(), "schemas should still be required");
1629 }
1630}