Skip to main content

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