pjson_rs/domain/services/
validation_service.rs

1//! Schema validation domain service
2//!
3//! Provides core validation logic for JSON data against schemas.
4//! This is a domain service as it contains business logic that doesn't
5//! naturally fit into a value object or entity.
6
7use std::collections::HashSet;
8
9use crate::domain::value_objects::{
10    JsonData, Schema, SchemaValidationError, SchemaValidationResult,
11};
12
13/// Schema validation service
14///
15/// Validates JSON data against schema definitions following a subset of
16/// JSON Schema specification. Designed for high-performance validation
17/// in streaming scenarios.
18///
19/// # Design Philosophy
20/// - Zero allocation validation where possible
21/// - Early exit on validation failures for performance
22/// - Detailed error messages with full path context
23/// - Composable validators for complex schemas
24///
25/// # Examples
26/// ```
27/// # use pjson_rs::domain::services::ValidationService;
28/// # use pjson_rs::domain::value_objects::{Schema, JsonData};
29/// let validator = ValidationService::new();
30/// let schema = Schema::integer(Some(0), Some(100));
31/// let data = JsonData::Integer(50);
32///
33/// assert!(validator.validate(&data, &schema, "/value").is_ok());
34/// ```
35pub struct ValidationService {
36    /// Maximum validation depth to prevent stack overflow
37    max_depth: usize,
38}
39
40impl ValidationService {
41    /// Maximum default validation depth
42    const DEFAULT_MAX_DEPTH: usize = 32;
43
44    /// Create a new validation service with default configuration
45    pub fn new() -> Self {
46        Self {
47            max_depth: Self::DEFAULT_MAX_DEPTH,
48        }
49    }
50
51    /// Create validation service with custom maximum depth
52    ///
53    /// # Arguments
54    /// * `max_depth` - Maximum nested validation depth
55    pub fn with_max_depth(max_depth: usize) -> Self {
56        Self { max_depth }
57    }
58
59    /// Validate JSON data against a schema
60    ///
61    /// Performs comprehensive validation of JSON data against the provided schema,
62    /// including type checking, constraint validation, and nested structure validation.
63    ///
64    /// # Arguments
65    /// * `data` - JSON data to validate
66    /// * `schema` - Schema to validate against
67    /// * `path` - Current JSON path for error reporting
68    ///
69    /// # Returns
70    /// `Ok(())` if validation succeeds, error with details if validation fails
71    ///
72    /// # Errors
73    /// Returns `SchemaValidationError` with context when validation fails
74    pub fn validate(
75        &self,
76        data: &JsonData,
77        schema: &Schema,
78        path: &str,
79    ) -> SchemaValidationResult<()> {
80        self.validate_with_depth(data, schema, path, 0)
81    }
82
83    /// Internal validation with depth tracking
84    fn validate_with_depth(
85        &self,
86        data: &JsonData,
87        schema: &Schema,
88        path: &str,
89        depth: usize,
90    ) -> SchemaValidationResult<()> {
91        // Prevent stack overflow from deeply nested structures
92        if depth > self.max_depth {
93            return Err(SchemaValidationError::TypeMismatch {
94                path: path.to_string(),
95                expected: "maximum depth not exceeded".to_string(),
96                actual: format!("depth {depth} exceeds maximum {}", self.max_depth),
97            });
98        }
99
100        match schema {
101            Schema::Any => Ok(()),
102            Schema::Null => self.validate_null(data, path),
103            Schema::Boolean => self.validate_boolean(data, path),
104            Schema::Integer { minimum, maximum } => {
105                self.validate_integer(data, path, *minimum, *maximum)
106            }
107            Schema::Number { minimum, maximum } => {
108                self.validate_number(data, path, *minimum, *maximum)
109            }
110            Schema::String {
111                min_length,
112                max_length,
113                pattern,
114                allowed_values,
115            } => self.validate_string(
116                data,
117                path,
118                *min_length,
119                *max_length,
120                pattern,
121                allowed_values,
122            ),
123            Schema::Array {
124                items,
125                min_items,
126                max_items,
127                unique_items,
128            } => self.validate_array(
129                data,
130                path,
131                items,
132                *min_items,
133                *max_items,
134                *unique_items,
135                depth,
136            ),
137            Schema::Object {
138                properties,
139                required,
140                additional_properties,
141            } => self.validate_object(
142                data,
143                path,
144                properties,
145                required,
146                *additional_properties,
147                depth,
148            ),
149            Schema::OneOf { schemas } => self.validate_one_of(data, path, schemas, depth),
150            Schema::AllOf { schemas } => self.validate_all_of(data, path, schemas, depth),
151        }
152    }
153
154    fn validate_null(&self, data: &JsonData, path: &str) -> SchemaValidationResult<()> {
155        match data {
156            JsonData::Null => Ok(()),
157            _ => Err(SchemaValidationError::TypeMismatch {
158                path: path.to_string(),
159                expected: "null".to_string(),
160                actual: Self::get_type_name(data).to_string(),
161            }),
162        }
163    }
164
165    fn validate_boolean(&self, data: &JsonData, path: &str) -> SchemaValidationResult<()> {
166        match data {
167            JsonData::Bool(_) => Ok(()),
168            _ => Err(SchemaValidationError::TypeMismatch {
169                path: path.to_string(),
170                expected: "boolean".to_string(),
171                actual: Self::get_type_name(data).to_string(),
172            }),
173        }
174    }
175
176    fn get_type_name(data: &JsonData) -> &'static str {
177        match data {
178            JsonData::Null => "null",
179            JsonData::Bool(_) => "boolean",
180            JsonData::Integer(_) => "integer",
181            JsonData::Float(_) => "number",
182            JsonData::String(_) => "string",
183            JsonData::Array(_) => "array",
184            JsonData::Object(_) => "object",
185        }
186    }
187
188    fn validate_integer(
189        &self,
190        data: &JsonData,
191        path: &str,
192        minimum: Option<i64>,
193        maximum: Option<i64>,
194    ) -> SchemaValidationResult<()> {
195        let value = match data {
196            JsonData::Integer(v) => *v,
197            _ => {
198                return Err(SchemaValidationError::TypeMismatch {
199                    path: path.to_string(),
200                    expected: "integer".to_string(),
201                    actual: Self::get_type_name(data).to_string(),
202                });
203            }
204        };
205
206        if let Some(min) = minimum
207            && value < min
208        {
209            return Err(SchemaValidationError::OutOfRange {
210                path: path.to_string(),
211                value: value.to_string(),
212                min: min.to_string(),
213                max: maximum.map_or("∞".to_string(), |m| m.to_string()),
214            });
215        }
216
217        if let Some(max) = maximum
218            && value > max
219        {
220            return Err(SchemaValidationError::OutOfRange {
221                path: path.to_string(),
222                value: value.to_string(),
223                min: minimum.map_or("-∞".to_string(), |m| m.to_string()),
224                max: max.to_string(),
225            });
226        }
227
228        Ok(())
229    }
230
231    fn validate_number(
232        &self,
233        data: &JsonData,
234        path: &str,
235        minimum: Option<f64>,
236        maximum: Option<f64>,
237    ) -> SchemaValidationResult<()> {
238        let value = match data {
239            JsonData::Float(v) => *v,
240            JsonData::Integer(v) => *v as f64,
241            _ => {
242                return Err(SchemaValidationError::TypeMismatch {
243                    path: path.to_string(),
244                    expected: "number".to_string(),
245                    actual: Self::get_type_name(data).to_string(),
246                });
247            }
248        };
249
250        // Validate that the number is finite (not NaN or Infinity)
251        if value.is_nan() || value.is_infinite() {
252            return Err(SchemaValidationError::TypeMismatch {
253                path: path.to_string(),
254                expected: "finite number".to_string(),
255                actual: format!("{}", value),
256            });
257        }
258
259        if let Some(min) = minimum
260            && value < min
261        {
262            return Err(SchemaValidationError::OutOfRange {
263                path: path.to_string(),
264                value: value.to_string(),
265                min: min.to_string(),
266                max: maximum.map_or("∞".to_string(), |m| m.to_string()),
267            });
268        }
269
270        if let Some(max) = maximum
271            && value > max
272        {
273            return Err(SchemaValidationError::OutOfRange {
274                path: path.to_string(),
275                value: value.to_string(),
276                min: minimum.map_or("-∞".to_string(), |m| m.to_string()),
277                max: max.to_string(),
278            });
279        }
280
281        Ok(())
282    }
283
284    fn validate_string(
285        &self,
286        data: &JsonData,
287        path: &str,
288        min_length: Option<usize>,
289        max_length: Option<usize>,
290        _pattern: &Option<String>,
291        allowed_values: &Option<smallvec::SmallVec<[String; 8]>>,
292    ) -> SchemaValidationResult<()> {
293        let value = match data {
294            JsonData::String(s) => s,
295            _ => {
296                return Err(SchemaValidationError::TypeMismatch {
297                    path: path.to_string(),
298                    expected: "string".to_string(),
299                    actual: Self::get_type_name(data).to_string(),
300                });
301            }
302        };
303
304        let len = value.chars().count();
305
306        if let Some(min) = min_length
307            && len < min
308        {
309            return Err(SchemaValidationError::StringLengthConstraint {
310                path: path.to_string(),
311                actual: len,
312                min,
313                max: max_length.unwrap_or(usize::MAX),
314            });
315        }
316
317        if let Some(max) = max_length
318            && len > max
319        {
320            return Err(SchemaValidationError::StringLengthConstraint {
321                path: path.to_string(),
322                actual: len,
323                min: min_length.unwrap_or(0),
324                max,
325            });
326        }
327
328        if let Some(allowed) = allowed_values
329            && !allowed.iter().any(|v| v.as_str() == value)
330        {
331            return Err(SchemaValidationError::InvalidEnumValue {
332                path: path.to_string(),
333                value: value.clone(),
334            });
335        }
336
337        Ok(())
338    }
339
340    #[allow(clippy::too_many_arguments)]
341    fn validate_array(
342        &self,
343        data: &JsonData,
344        path: &str,
345        items: &Option<Box<Schema>>,
346        min_items: Option<usize>,
347        max_items: Option<usize>,
348        unique_items: bool,
349        depth: usize,
350    ) -> SchemaValidationResult<()> {
351        let arr = match data {
352            JsonData::Array(a) => a,
353            _ => {
354                return Err(SchemaValidationError::TypeMismatch {
355                    path: path.to_string(),
356                    expected: "array".to_string(),
357                    actual: Self::get_type_name(data).to_string(),
358                });
359            }
360        };
361
362        let len = arr.len();
363
364        if let Some(min) = min_items
365            && len < min
366        {
367            return Err(SchemaValidationError::ArraySizeConstraint {
368                path: path.to_string(),
369                actual: len,
370                min,
371                max: max_items.unwrap_or(usize::MAX),
372            });
373        }
374
375        if let Some(max) = max_items
376            && len > max
377        {
378            return Err(SchemaValidationError::ArraySizeConstraint {
379                path: path.to_string(),
380                actual: len,
381                min: min_items.unwrap_or(0),
382                max,
383            });
384        }
385
386        if unique_items {
387            let mut seen = HashSet::with_capacity(arr.len());
388            for item in arr {
389                // Use JsonData's Hash implementation directly for efficient uniqueness check
390                if !seen.insert(item) {
391                    return Err(SchemaValidationError::DuplicateItems {
392                        path: path.to_string(),
393                    });
394                }
395            }
396        }
397
398        if let Some(item_schema) = items {
399            // Pre-allocate path buffer to avoid repeated allocations
400            let mut path_buffer = String::with_capacity(path.len() + 16);
401            for (i, item) in arr.iter().enumerate() {
402                path_buffer.clear();
403                path_buffer.push_str(path);
404                path_buffer.push('[');
405                use std::fmt::Write;
406                write!(&mut path_buffer, "{}", i).unwrap();
407                path_buffer.push(']');
408
409                self.validate_with_depth(item, item_schema, &path_buffer, depth + 1)?;
410            }
411        }
412
413        Ok(())
414    }
415
416    fn validate_object(
417        &self,
418        data: &JsonData,
419        path: &str,
420        properties: &std::collections::HashMap<String, Schema>,
421        required: &[String],
422        additional_properties: bool,
423        depth: usize,
424    ) -> SchemaValidationResult<()> {
425        let obj = match data {
426            JsonData::Object(o) => o,
427            _ => {
428                return Err(SchemaValidationError::TypeMismatch {
429                    path: path.to_string(),
430                    expected: "object".to_string(),
431                    actual: Self::get_type_name(data).to_string(),
432                });
433            }
434        };
435
436        // Check required fields
437        for field in required {
438            if !obj.contains_key(field) {
439                return Err(SchemaValidationError::MissingRequired {
440                    path: path.to_string(),
441                    field: field.clone(),
442                });
443            }
444        }
445
446        // Validate defined properties
447        let mut path_buffer = String::with_capacity(path.len() + 32);
448        for (key, value) in obj {
449            if let Some(prop_schema) = properties.get(key) {
450                path_buffer.clear();
451                path_buffer.push_str(path);
452                path_buffer.push('/');
453                path_buffer.push_str(key);
454                self.validate_with_depth(value, prop_schema, &path_buffer, depth + 1)?;
455            } else if !additional_properties {
456                return Err(SchemaValidationError::AdditionalPropertyNotAllowed {
457                    path: path.to_string(),
458                    property: key.clone(),
459                });
460            }
461        }
462
463        Ok(())
464    }
465
466    fn validate_one_of(
467        &self,
468        data: &JsonData,
469        path: &str,
470        schemas: &[Box<Schema>],
471        depth: usize,
472    ) -> SchemaValidationResult<()> {
473        let mut match_count = 0;
474
475        for schema in schemas {
476            if self
477                .validate_with_depth(data, schema, path, depth + 1)
478                .is_ok()
479            {
480                match_count += 1;
481                // Early exit: if we found 2 matches, we know it's invalid
482                if match_count > 1 {
483                    return Err(SchemaValidationError::NoMatchingOneOf {
484                        path: path.to_string(),
485                    });
486                }
487            }
488        }
489
490        if match_count == 1 {
491            Ok(())
492        } else {
493            Err(SchemaValidationError::NoMatchingOneOf {
494                path: path.to_string(),
495            })
496        }
497    }
498
499    fn validate_all_of(
500        &self,
501        data: &JsonData,
502        path: &str,
503        schemas: &[Box<Schema>],
504        depth: usize,
505    ) -> SchemaValidationResult<()> {
506        let mut failures = Vec::new();
507
508        for (i, schema) in schemas.iter().enumerate() {
509            if self
510                .validate_with_depth(data, schema, path, depth + 1)
511                .is_err()
512            {
513                failures.push(i);
514            }
515        }
516
517        if failures.is_empty() {
518            Ok(())
519        } else {
520            Err(SchemaValidationError::AllOfFailure {
521                path: path.to_string(),
522                failures: failures
523                    .iter()
524                    .map(|i| i.to_string())
525                    .collect::<Vec<_>>()
526                    .join(", "),
527            })
528        }
529    }
530}
531
532impl Default for ValidationService {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use std::collections::HashMap;
542
543    #[test]
544    fn test_validate_null() {
545        let validator = ValidationService::new();
546        let schema = Schema::Null;
547        let data = JsonData::Null;
548
549        assert!(validator.validate(&data, &schema, "/").is_ok());
550
551        let invalid = JsonData::Integer(42);
552        assert!(validator.validate(&invalid, &schema, "/").is_err());
553    }
554
555    #[test]
556    fn test_validate_boolean() {
557        let validator = ValidationService::new();
558        let schema = Schema::Boolean;
559
560        assert!(
561            validator
562                .validate(&JsonData::Bool(true), &schema, "/flag")
563                .is_ok()
564        );
565        assert!(
566            validator
567                .validate(&JsonData::Bool(false), &schema, "/flag")
568                .is_ok()
569        );
570        assert!(
571            validator
572                .validate(&JsonData::Integer(1), &schema, "/flag")
573                .is_err()
574        );
575    }
576
577    #[test]
578    fn test_validate_integer_range() {
579        let validator = ValidationService::new();
580        let schema = Schema::integer(Some(0), Some(100));
581
582        assert!(
583            validator
584                .validate(&JsonData::Integer(50), &schema, "/value")
585                .is_ok()
586        );
587        assert!(
588            validator
589                .validate(&JsonData::Integer(0), &schema, "/value")
590                .is_ok()
591        );
592        assert!(
593            validator
594                .validate(&JsonData::Integer(100), &schema, "/value")
595                .is_ok()
596        );
597        assert!(
598            validator
599                .validate(&JsonData::Integer(-1), &schema, "/value")
600                .is_err()
601        );
602        assert!(
603            validator
604                .validate(&JsonData::Integer(101), &schema, "/value")
605                .is_err()
606        );
607    }
608
609    #[test]
610    fn test_validate_string_length() {
611        let validator = ValidationService::new();
612        let schema = Schema::string(Some(2), Some(10));
613
614        assert!(
615            validator
616                .validate(&JsonData::String("hello".to_string()), &schema, "/")
617                .is_ok()
618        );
619        assert!(
620            validator
621                .validate(&JsonData::String("hi".to_string()), &schema, "/")
622                .is_ok()
623        );
624        assert!(
625            validator
626                .validate(&JsonData::String("0123456789".to_string()), &schema, "/")
627                .is_ok()
628        );
629        assert!(
630            validator
631                .validate(&JsonData::String("a".to_string()), &schema, "/")
632                .is_err()
633        );
634        assert!(
635            validator
636                .validate(&JsonData::String("12345678901".to_string()), &schema, "/")
637                .is_err()
638        );
639    }
640
641    #[test]
642    fn test_validate_array() {
643        let validator = ValidationService::new();
644        let schema = Schema::Array {
645            items: Some(Box::new(Schema::integer(Some(0), None))),
646            min_items: Some(1),
647            max_items: Some(5),
648            unique_items: false,
649        };
650
651        let valid = JsonData::Array(vec![JsonData::Integer(1), JsonData::Integer(2)]);
652        assert!(validator.validate(&valid, &schema, "/items").is_ok());
653
654        let empty = JsonData::Array(vec![]);
655        assert!(validator.validate(&empty, &schema, "/items").is_err());
656
657        let invalid_item = JsonData::Array(vec![JsonData::Integer(-1)]);
658        assert!(
659            validator
660                .validate(&invalid_item, &schema, "/items")
661                .is_err()
662        );
663    }
664
665    #[test]
666    fn test_validate_object() {
667        let validator = ValidationService::new();
668        let mut properties = HashMap::new();
669        properties.insert("id".to_string(), Schema::integer(Some(1), None));
670        properties.insert("name".to_string(), Schema::string(Some(1), Some(100)));
671
672        let schema = Schema::object(properties, vec!["id".to_string()]);
673
674        let mut valid_obj = HashMap::new();
675        valid_obj.insert("id".to_string(), JsonData::Integer(42));
676        valid_obj.insert("name".to_string(), JsonData::String("test".to_string()));
677
678        let valid = JsonData::Object(valid_obj);
679        assert!(validator.validate(&valid, &schema, "/user").is_ok());
680
681        let mut missing_required = HashMap::new();
682        missing_required.insert("name".to_string(), JsonData::String("test".to_string()));
683        let invalid = JsonData::Object(missing_required);
684        assert!(validator.validate(&invalid, &schema, "/user").is_err());
685    }
686
687    #[test]
688    fn test_validate_number() {
689        let validator = ValidationService::new();
690        let schema = Schema::number(Some(0.0), Some(100.0));
691
692        assert!(
693            validator
694                .validate(&JsonData::Float(50.0), &schema, "/value")
695                .is_ok()
696        );
697        assert!(
698            validator
699                .validate(&JsonData::Integer(50), &schema, "/value")
700                .is_ok()
701        );
702        assert!(
703            validator
704                .validate(&JsonData::Float(0.0), &schema, "/value")
705                .is_ok()
706        );
707        assert!(
708            validator
709                .validate(&JsonData::Float(100.0), &schema, "/value")
710                .is_ok()
711        );
712        assert!(
713            validator
714                .validate(&JsonData::Float(-0.1), &schema, "/value")
715                .is_err()
716        );
717        assert!(
718            validator
719                .validate(&JsonData::Float(100.1), &schema, "/value")
720                .is_err()
721        );
722    }
723
724    #[test]
725    fn test_validate_number_nan_infinity() {
726        let validator = ValidationService::new();
727        let schema = Schema::number(Some(0.0), Some(100.0));
728
729        // NaN should be rejected
730        let result = validator.validate(&JsonData::Float(f64::NAN), &schema, "/value");
731        assert!(result.is_err());
732        let err = result.unwrap_err();
733        assert!(matches!(err, SchemaValidationError::TypeMismatch { .. }));
734
735        // Infinity should be rejected
736        let result = validator.validate(&JsonData::Float(f64::INFINITY), &schema, "/value");
737        assert!(result.is_err());
738
739        // Negative infinity should be rejected
740        let result = validator.validate(&JsonData::Float(f64::NEG_INFINITY), &schema, "/value");
741        assert!(result.is_err());
742    }
743
744    #[test]
745    fn test_validate_string_enum_values() {
746        let validator = ValidationService::new();
747        use smallvec::SmallVec;
748
749        let allowed_values = Some(SmallVec::from_vec(vec![
750            String::from("red"),
751            String::from("green"),
752            String::from("blue"),
753        ]));
754
755        let schema = Schema::String {
756            min_length: None,
757            max_length: None,
758            pattern: None,
759            allowed_values,
760        };
761
762        // Valid enum values
763        assert!(
764            validator
765                .validate(&JsonData::String("red".to_string()), &schema, "/color")
766                .is_ok()
767        );
768        assert!(
769            validator
770                .validate(&JsonData::String("green".to_string()), &schema, "/color")
771                .is_ok()
772        );
773        assert!(
774            validator
775                .validate(&JsonData::String("blue".to_string()), &schema, "/color")
776                .is_ok()
777        );
778
779        // Invalid enum value
780        let result = validator.validate(&JsonData::String("yellow".to_string()), &schema, "/color");
781        assert!(result.is_err());
782        let err = result.unwrap_err();
783        assert!(matches!(
784            err,
785            SchemaValidationError::InvalidEnumValue { .. }
786        ));
787    }
788
789    #[test]
790    fn test_validate_array_unique_items() {
791        let validator = ValidationService::new();
792        let schema = Schema::Array {
793            items: Some(Box::new(Schema::integer(None, None))),
794            min_items: None,
795            max_items: None,
796            unique_items: true,
797        };
798
799        // Valid: all unique items
800        let unique = JsonData::Array(vec![
801            JsonData::Integer(1),
802            JsonData::Integer(2),
803            JsonData::Integer(3),
804        ]);
805        assert!(validator.validate(&unique, &schema, "/items").is_ok());
806
807        // Invalid: duplicate items
808        let duplicates = JsonData::Array(vec![
809            JsonData::Integer(1),
810            JsonData::Integer(2),
811            JsonData::Integer(1),
812        ]);
813        let result = validator.validate(&duplicates, &schema, "/items");
814        assert!(result.is_err());
815        let err = result.unwrap_err();
816        assert!(matches!(err, SchemaValidationError::DuplicateItems { .. }));
817    }
818
819    #[test]
820    fn test_validate_array_min_max_items() {
821        let validator = ValidationService::new();
822        let schema = Schema::Array {
823            items: None,
824            min_items: Some(2),
825            max_items: Some(4),
826            unique_items: false,
827        };
828
829        // Valid: within range
830        let valid = JsonData::Array(vec![JsonData::Integer(1), JsonData::Integer(2)]);
831        assert!(validator.validate(&valid, &schema, "/items").is_ok());
832
833        // Invalid: too few items
834        let too_few = JsonData::Array(vec![JsonData::Integer(1)]);
835        let result = validator.validate(&too_few, &schema, "/items");
836        assert!(result.is_err());
837        let err = result.unwrap_err();
838        assert!(matches!(
839            err,
840            SchemaValidationError::ArraySizeConstraint { .. }
841        ));
842
843        // Invalid: too many items
844        let too_many = JsonData::Array(vec![
845            JsonData::Integer(1),
846            JsonData::Integer(2),
847            JsonData::Integer(3),
848            JsonData::Integer(4),
849            JsonData::Integer(5),
850        ]);
851        let result = validator.validate(&too_many, &schema, "/items");
852        assert!(result.is_err());
853        assert!(matches!(
854            result.unwrap_err(),
855            SchemaValidationError::ArraySizeConstraint { .. }
856        ));
857    }
858
859    #[test]
860    fn test_validate_object_additional_properties() {
861        let validator = ValidationService::new();
862        let mut properties = HashMap::new();
863        properties.insert("name".to_string(), Schema::string(Some(1), Some(100)));
864
865        // Schema disallows additional properties
866        let schema = Schema::Object {
867            properties: properties.clone(),
868            required: vec![],
869            additional_properties: false,
870        };
871
872        let mut valid_obj = HashMap::new();
873        valid_obj.insert("name".to_string(), JsonData::String("test".to_string()));
874
875        // Valid: no additional properties
876        let valid = JsonData::Object(valid_obj.clone());
877        assert!(validator.validate(&valid, &schema, "/obj").is_ok());
878
879        // Invalid: has additional property
880        let mut invalid_obj = valid_obj;
881        invalid_obj.insert("extra".to_string(), JsonData::Integer(42));
882        let invalid = JsonData::Object(invalid_obj);
883        let result = validator.validate(&invalid, &schema, "/obj");
884        assert!(result.is_err());
885        let err = result.unwrap_err();
886        assert!(matches!(
887            err,
888            SchemaValidationError::AdditionalPropertyNotAllowed { .. }
889        ));
890
891        // Schema allows additional properties
892        let schema_allow = Schema::Object {
893            properties,
894            required: vec![],
895            additional_properties: true,
896        };
897
898        let mut obj_with_extra = HashMap::new();
899        obj_with_extra.insert("name".to_string(), JsonData::String("test".to_string()));
900        obj_with_extra.insert("extra".to_string(), JsonData::Integer(42));
901        let with_extra = JsonData::Object(obj_with_extra);
902        assert!(
903            validator
904                .validate(&with_extra, &schema_allow, "/obj")
905                .is_ok()
906        );
907    }
908
909    #[test]
910    fn test_validate_one_of_single_match() {
911        let validator = ValidationService::new();
912        use smallvec::SmallVec;
913
914        let schema = Schema::OneOf {
915            schemas: SmallVec::from_vec(vec![
916                Box::new(Schema::string(Some(1), None)),
917                Box::new(Schema::integer(Some(0), None)),
918            ]),
919        };
920
921        // Valid: matches exactly one schema (string)
922        assert!(
923            validator
924                .validate(&JsonData::String("test".to_string()), &schema, "/value")
925                .is_ok()
926        );
927
928        // Valid: matches exactly one schema (integer)
929        assert!(
930            validator
931                .validate(&JsonData::Integer(42), &schema, "/value")
932                .is_ok()
933        );
934    }
935
936    #[test]
937    fn test_validate_one_of_no_match() {
938        let validator = ValidationService::new();
939        use smallvec::SmallVec;
940
941        let schema = Schema::OneOf {
942            schemas: SmallVec::from_vec(vec![
943                Box::new(Schema::string(Some(5), None)),    // min length 5
944                Box::new(Schema::integer(Some(100), None)), // min 100
945            ]),
946        };
947
948        // Invalid: matches no schemas (string too short, not an integer)
949        let result = validator.validate(&JsonData::String("hi".to_string()), &schema, "/value");
950        assert!(result.is_err());
951        assert!(matches!(
952            result.unwrap_err(),
953            SchemaValidationError::NoMatchingOneOf { .. }
954        ));
955
956        // Invalid: matches no schemas (integer too small, not a string)
957        let result = validator.validate(&JsonData::Integer(50), &schema, "/value");
958        assert!(result.is_err());
959    }
960
961    #[test]
962    fn test_validate_one_of_multiple_matches() {
963        let validator = ValidationService::new();
964        use smallvec::SmallVec;
965
966        let schema = Schema::OneOf {
967            schemas: SmallVec::from_vec(vec![
968                Box::new(Schema::integer(None, None)), // matches any integer
969                Box::new(Schema::integer(Some(0), Some(100))), // matches integers 0-100
970            ]),
971        };
972
973        // Invalid: matches both schemas (ambiguous)
974        let result = validator.validate(&JsonData::Integer(50), &schema, "/value");
975        assert!(result.is_err());
976        assert!(matches!(
977            result.unwrap_err(),
978            SchemaValidationError::NoMatchingOneOf { .. }
979        ));
980    }
981
982    #[test]
983    fn test_validate_all_of_success() {
984        let validator = ValidationService::new();
985        use smallvec::SmallVec;
986
987        let schema = Schema::AllOf {
988            schemas: SmallVec::from_vec(vec![
989                Box::new(Schema::integer(Some(0), None)),   // >= 0
990                Box::new(Schema::integer(None, Some(100))), // <= 100
991            ]),
992        };
993
994        // Valid: matches all schemas
995        assert!(
996            validator
997                .validate(&JsonData::Integer(50), &schema, "/value")
998                .is_ok()
999        );
1000        assert!(
1001            validator
1002                .validate(&JsonData::Integer(0), &schema, "/value")
1003                .is_ok()
1004        );
1005        assert!(
1006            validator
1007                .validate(&JsonData::Integer(100), &schema, "/value")
1008                .is_ok()
1009        );
1010    }
1011
1012    #[test]
1013    fn test_validate_all_of_failure() {
1014        let validator = ValidationService::new();
1015        use smallvec::SmallVec;
1016
1017        let schema = Schema::AllOf {
1018            schemas: SmallVec::from_vec(vec![
1019                Box::new(Schema::integer(Some(0), None)),   // >= 0
1020                Box::new(Schema::integer(None, Some(100))), // <= 100
1021            ]),
1022        };
1023
1024        // Invalid: fails first constraint
1025        let result = validator.validate(&JsonData::Integer(-1), &schema, "/value");
1026        assert!(result.is_err());
1027        let err = result.unwrap_err();
1028        assert!(matches!(err, SchemaValidationError::AllOfFailure { .. }));
1029
1030        // Invalid: fails second constraint
1031        let result = validator.validate(&JsonData::Integer(101), &schema, "/value");
1032        assert!(result.is_err());
1033        assert!(matches!(
1034            result.unwrap_err(),
1035            SchemaValidationError::AllOfFailure { .. }
1036        ));
1037    }
1038
1039    #[test]
1040    fn test_validate_max_depth_exceeded() {
1041        let validator = ValidationService::with_max_depth(5);
1042
1043        // Create nested structure that exceeds max depth
1044        fn create_nested(depth: usize) -> JsonData {
1045            if depth == 0 {
1046                JsonData::Integer(42)
1047            } else {
1048                let mut obj = HashMap::new();
1049                obj.insert("nested".to_string(), create_nested(depth - 1));
1050                JsonData::Object(obj)
1051            }
1052        }
1053
1054        fn create_nested_schema(depth: usize) -> Schema {
1055            if depth == 0 {
1056                Schema::integer(None, None)
1057            } else {
1058                Schema::Object {
1059                    properties: [("nested".to_string(), create_nested_schema(depth - 1))]
1060                        .into_iter()
1061                        .collect(),
1062                    required: vec![],
1063                    additional_properties: false,
1064                }
1065            }
1066        }
1067
1068        let data = create_nested(10);
1069        let schema = create_nested_schema(10);
1070
1071        // Should fail due to depth limit
1072        let result = validator.validate(&data, &schema, "/deep");
1073        assert!(result.is_err());
1074        let err = result.unwrap_err();
1075        assert!(matches!(err, SchemaValidationError::TypeMismatch { .. }));
1076    }
1077
1078    #[test]
1079    fn test_validate_any_schema() {
1080        let validator = ValidationService::new();
1081        let schema = Schema::Any;
1082
1083        // Any schema accepts all types
1084        assert!(validator.validate(&JsonData::Null, &schema, "/").is_ok());
1085        assert!(
1086            validator
1087                .validate(&JsonData::Bool(true), &schema, "/")
1088                .is_ok()
1089        );
1090        assert!(
1091            validator
1092                .validate(&JsonData::Integer(42), &schema, "/")
1093                .is_ok()
1094        );
1095        assert!(
1096            validator
1097                .validate(&JsonData::Float(std::f64::consts::PI), &schema, "/")
1098                .is_ok()
1099        );
1100        assert!(
1101            validator
1102                .validate(&JsonData::String("test".to_string()), &schema, "/")
1103                .is_ok()
1104        );
1105        assert!(
1106            validator
1107                .validate(&JsonData::Array(vec![]), &schema, "/")
1108                .is_ok()
1109        );
1110        assert!(
1111            validator
1112                .validate(&JsonData::Object(HashMap::new()), &schema, "/")
1113                .is_ok()
1114        );
1115    }
1116
1117    #[test]
1118    fn test_validate_type_mismatches() {
1119        let validator = ValidationService::new();
1120
1121        // Test all type mismatches
1122        let test_cases = vec![
1123            (Schema::Null, JsonData::Integer(42), "null"),
1124            (
1125                Schema::Boolean,
1126                JsonData::String("true".to_string()),
1127                "boolean",
1128            ),
1129            (
1130                Schema::integer(None, None),
1131                JsonData::String("42".to_string()),
1132                "integer",
1133            ),
1134            (
1135                Schema::number(None, None),
1136                JsonData::String("3.14".to_string()),
1137                "number",
1138            ),
1139            (Schema::string(None, None), JsonData::Integer(42), "string"),
1140            (
1141                Schema::Array {
1142                    items: None,
1143                    min_items: None,
1144                    max_items: None,
1145                    unique_items: false,
1146                },
1147                JsonData::Integer(42),
1148                "array",
1149            ),
1150            (
1151                Schema::Object {
1152                    properties: HashMap::new(),
1153                    required: vec![],
1154                    additional_properties: true,
1155                },
1156                JsonData::Integer(42),
1157                "object",
1158            ),
1159        ];
1160
1161        for (schema, data, expected_type) in test_cases {
1162            let result = validator.validate(&data, &schema, "/test");
1163            assert!(result.is_err(), "Expected error for {expected_type}");
1164            let err = result.unwrap_err();
1165            assert!(
1166                matches!(err, SchemaValidationError::TypeMismatch { .. }),
1167                "Expected TypeMismatch for {expected_type}"
1168            );
1169        }
1170    }
1171
1172    #[test]
1173    fn test_default_validation_service() {
1174        let default = ValidationService::default();
1175        let created = ValidationService::new();
1176
1177        // Both should have same max_depth
1178        let schema = Schema::integer(None, None);
1179        let data = JsonData::Integer(42);
1180
1181        assert!(default.validate(&data, &schema, "/").is_ok());
1182        assert!(created.validate(&data, &schema, "/").is_ok());
1183    }
1184}