pjson_rs/application/dto/
schema_dto.rs

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