Skip to main content

hedl_json/
validation.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! JSON Schema validation for hedl-json
19//!
20//! This module provides JSON Schema validation capabilities using the `jsonschema` crate.
21//! Validation is optional and enabled via the `validation` feature flag.
22//!
23//! # Features
24//!
25//! - Support for JSON Schema Draft-07, Draft 2019-09, and Draft 2020-12
26//! - Schema compilation and caching for efficient repeated validation
27//! - Comprehensive error reporting with JSON path locations
28//! - Optional format validation (email, uri, date-time, uuid, etc.)
29//!
30//! # Examples
31//!
32//! ```ignore
33//! use hedl_json::validation::{CompiledSchema, ValidationConfig, SchemaDraft};
34//! use serde_json::json;
35//!
36//! let schema = json!({
37//!     "$schema": "http://json-schema.org/draft-07/schema#",
38//!     "type": "object",
39//!     "properties": {
40//!         "name": {"type": "string", "minLength": 1},
41//!         "age": {"type": "integer", "minimum": 0}
42//!     },
43//!     "required": ["name", "age"]
44//! });
45//!
46//! let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default())?;
47//!
48//! let valid_json = json!({"name": "Alice", "age": 30});
49//! assert!(compiled.validate(&valid_json).is_valid);
50//!
51//! let invalid_json = json!({"name": "", "age": -5});
52//! let result = compiled.validate(&invalid_json);
53//! assert!(!result.is_valid);
54//! for error in &result.errors {
55//!     println!("Error at {}: {}", error.instance_path, error.message);
56//! }
57//! ```
58
59use jsonschema::Validator;
60use serde_json::Value as JsonValue;
61use std::sync::Arc;
62
63/// JSON Schema draft version
64///
65/// Specifies which JSON Schema draft specification to use for validation.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum SchemaDraft {
68    /// JSON Schema Draft 4
69    Draft4,
70    /// JSON Schema Draft 6
71    Draft6,
72    /// JSON Schema Draft 7 (recommended - most widely supported)
73    #[default]
74    Draft7,
75    /// JSON Schema Draft 2019-09
76    Draft201909,
77    /// JSON Schema Draft 2020-12 (latest)
78    Draft202012,
79}
80
81/// JSON Schema validation configuration
82///
83/// Controls how validation is performed, including which draft version to use
84/// and how errors are collected.
85///
86/// # Examples
87///
88/// ```ignore
89/// use hedl_json::validation::{ValidationConfig, SchemaDraft};
90///
91/// // Strict validation with all errors
92/// let strict = ValidationConfig {
93///     draft: SchemaDraft::Draft7,
94///     collect_all_errors: true,
95///     max_errors: None,
96///     validate_formats: true,
97/// };
98///
99/// // Lenient validation - stop at first error
100/// let lenient = ValidationConfig {
101///     draft: SchemaDraft::Draft7,
102///     collect_all_errors: false,
103///     max_errors: Some(1),
104///     validate_formats: false,
105/// };
106/// ```
107#[derive(Debug, Clone)]
108pub struct ValidationConfig {
109    /// JSON Schema draft version to use
110    ///
111    /// Supported drafts:
112    /// - `SchemaDraft::Draft4`: JSON Schema Draft 4
113    /// - `SchemaDraft::Draft6`: JSON Schema Draft 6
114    /// - `SchemaDraft::Draft7`: JSON Schema Draft 7 (recommended)
115    /// - `SchemaDraft::Draft201909`: JSON Schema Draft 2019-09
116    /// - `SchemaDraft::Draft202012`: JSON Schema Draft 2020-12
117    pub draft: SchemaDraft,
118
119    /// Continue validation after first error
120    ///
121    /// When `true`, collects all errors in the document.
122    /// When `false`, stops at the first error (faster for invalid docs).
123    pub collect_all_errors: bool,
124
125    /// Maximum errors to collect before stopping
126    ///
127    /// `None` means collect all errors (when `collect_all_errors` is true).
128    /// Useful for limiting memory usage with highly invalid documents.
129    pub max_errors: Option<usize>,
130
131    /// Enable format validation
132    ///
133    /// When `true`, validates format keywords like "email", "uri", "date-time".
134    /// When `false`, format keywords are ignored (faster, more permissive).
135    pub validate_formats: bool,
136}
137
138impl Default for ValidationConfig {
139    fn default() -> Self {
140        Self {
141            draft: SchemaDraft::Draft7,
142            collect_all_errors: true,
143            max_errors: None,
144            validate_formats: true,
145        }
146    }
147}
148
149/// Validation error with location information
150///
151/// Represents a single validation failure with details about where
152/// it occurred and what went wrong.
153#[derive(Debug, Clone)]
154pub struct ValidationError {
155    /// JSON path to the error location (e.g., "/users/0/email")
156    pub instance_path: String,
157
158    /// Human-readable error message
159    pub message: String,
160
161    /// Schema path that failed (e.g., "/properties/email/format")
162    pub schema_path: String,
163}
164
165impl std::fmt::Display for ValidationError {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        write!(
168            f,
169            "Validation error at {}: {} (schema: {})",
170            self.instance_path, self.message, self.schema_path
171        )
172    }
173}
174
175impl std::error::Error for ValidationError {}
176
177/// Result of validating a JSON document against a schema
178///
179/// Contains the validation outcome and any errors found.
180#[derive(Debug, Clone)]
181pub struct ValidationResult {
182    /// Whether the document is valid according to the schema
183    pub is_valid: bool,
184
185    /// List of validation errors (empty if valid)
186    pub errors: Vec<ValidationError>,
187}
188
189impl ValidationResult {
190    /// Create a successful validation result
191    #[must_use]
192    pub fn valid() -> Self {
193        Self {
194            is_valid: true,
195            errors: Vec::new(),
196        }
197    }
198
199    /// Create a failed validation result with errors
200    #[must_use]
201    pub fn invalid(errors: Vec<ValidationError>) -> Self {
202        Self {
203            is_valid: false,
204            errors,
205        }
206    }
207}
208
209/// Schema compilation error
210#[derive(Debug, Clone, thiserror::Error)]
211pub enum SchemaError {
212    /// The schema itself is invalid
213    #[error("Invalid JSON Schema: {0}")]
214    InvalidSchema(String),
215
216    /// Schema reference could not be resolved
217    #[error("Unresolved schema reference: {0}")]
218    UnresolvedReference(String),
219}
220
221/// Compiled JSON Schema for efficient repeated validation
222///
223/// Compiling a schema is relatively expensive, so this type caches
224/// the compiled schema for efficient reuse.
225///
226/// # Thread Safety
227///
228/// `CompiledSchema` uses `Arc` internally and is safe to share
229/// across threads.
230///
231/// # Examples
232///
233/// ```ignore
234/// use hedl_json::validation::{CompiledSchema, ValidationConfig};
235/// use serde_json::json;
236///
237/// let schema = json!({
238///     "type": "object",
239///     "properties": {
240///         "id": {"type": "integer"}
241///     }
242/// });
243///
244/// let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default())?;
245///
246/// // Validate multiple documents efficiently
247/// let doc1 = json!({"id": 1});
248/// let doc2 = json!({"id": 2});
249///
250/// assert!(compiled.validate(&doc1).is_valid);
251/// assert!(compiled.validate(&doc2).is_valid);
252/// ```
253#[derive(Clone)]
254pub struct CompiledSchema {
255    validator: Arc<Validator>,
256    config: ValidationConfig,
257}
258
259impl std::fmt::Debug for CompiledSchema {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        f.debug_struct("CompiledSchema")
262            .field("config", &self.config)
263            .finish_non_exhaustive()
264    }
265}
266
267impl CompiledSchema {
268    /// Compile a JSON Schema for validation
269    ///
270    /// # Arguments
271    ///
272    /// * `schema` - JSON Schema document as a `serde_json::Value`
273    /// * `config` - Validation configuration
274    ///
275    /// # Returns
276    ///
277    /// * `Ok(CompiledSchema)` - Successfully compiled schema
278    /// * `Err(SchemaError)` - Schema is invalid
279    ///
280    /// # Examples
281    ///
282    /// ```ignore
283    /// use hedl_json::validation::{CompiledSchema, ValidationConfig};
284    /// use serde_json::json;
285    ///
286    /// let schema = json!({
287    ///     "$schema": "http://json-schema.org/draft-07/schema#",
288    ///     "type": "string",
289    ///     "minLength": 1
290    /// });
291    ///
292    /// let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default())?;
293    /// ```
294    pub fn compile(schema: &JsonValue, config: &ValidationConfig) -> Result<Self, SchemaError> {
295        let validator = match config.draft {
296            SchemaDraft::Draft4 => jsonschema::draft4::options()
297                .should_validate_formats(config.validate_formats)
298                .build(schema),
299            SchemaDraft::Draft6 => jsonschema::draft6::options()
300                .should_validate_formats(config.validate_formats)
301                .build(schema),
302            SchemaDraft::Draft7 => jsonschema::draft7::options()
303                .should_validate_formats(config.validate_formats)
304                .build(schema),
305            SchemaDraft::Draft201909 => jsonschema::draft201909::options()
306                .should_validate_formats(config.validate_formats)
307                .build(schema),
308            SchemaDraft::Draft202012 => jsonschema::draft202012::options()
309                .should_validate_formats(config.validate_formats)
310                .build(schema),
311        }
312        .map_err(|e| SchemaError::InvalidSchema(e.to_string()))?;
313
314        Ok(Self {
315            validator: Arc::new(validator),
316            config: config.clone(),
317        })
318    }
319
320    /// Validate a JSON value against the schema
321    ///
322    /// # Arguments
323    ///
324    /// * `instance` - JSON document to validate
325    ///
326    /// # Returns
327    ///
328    /// `ValidationResult` with validation outcome and any errors
329    ///
330    /// # Examples
331    ///
332    /// ```ignore
333    /// use hedl_json::validation::{CompiledSchema, ValidationConfig};
334    /// use serde_json::json;
335    ///
336    /// let schema = json!({"type": "integer"});
337    /// let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default())?;
338    ///
339    /// let result = compiled.validate(&json!(42));
340    /// assert!(result.is_valid);
341    ///
342    /// let result = compiled.validate(&json!("not an integer"));
343    /// assert!(!result.is_valid);
344    /// ```
345    #[must_use]
346    pub fn validate(&self, instance: &JsonValue) -> ValidationResult {
347        if self.validator.is_valid(instance) {
348            return ValidationResult::valid();
349        }
350
351        let max = self.config.max_errors.unwrap_or(usize::MAX);
352        let limit = if self.config.collect_all_errors {
353            max
354        } else {
355            1
356        };
357
358        let collected: Vec<ValidationError> = self
359            .validator
360            .iter_errors(instance)
361            .take(limit)
362            .map(|e| ValidationError {
363                instance_path: e.instance_path.to_string(),
364                message: e.to_string(),
365                schema_path: e.schema_path.to_string(),
366            })
367            .collect();
368
369        ValidationResult::invalid(collected)
370    }
371
372    /// Check if a JSON value is valid without collecting errors
373    ///
374    /// This is faster than `validate()` when you only need to know
375    /// whether the document is valid.
376    ///
377    /// # Arguments
378    ///
379    /// * `instance` - JSON document to validate
380    ///
381    /// # Returns
382    ///
383    /// `true` if valid, `false` otherwise
384    #[must_use]
385    pub fn is_valid(&self, instance: &JsonValue) -> bool {
386        self.validator.is_valid(instance)
387    }
388}
389
390/// Validate JSON against a schema (convenience function)
391///
392/// Compiles the schema and validates the instance in one call.
393/// Use `CompiledSchema` for repeated validations to avoid recompilation.
394///
395/// # Arguments
396///
397/// * `schema` - JSON Schema document
398/// * `instance` - JSON document to validate
399/// * `config` - Validation configuration
400///
401/// # Returns
402///
403/// * `Ok(ValidationResult)` - Validation completed
404/// * `Err(SchemaError)` - Schema is invalid
405///
406/// # Examples
407///
408/// ```ignore
409/// use hedl_json::validation::{validate_json, ValidationConfig};
410/// use serde_json::json;
411///
412/// let schema = json!({"type": "string"});
413/// let instance = json!("hello");
414///
415/// let result = validate_json(&schema, &instance, &ValidationConfig::default())?;
416/// assert!(result.is_valid);
417/// ```
418pub fn validate_json(
419    schema: &JsonValue,
420    instance: &JsonValue,
421    config: &ValidationConfig,
422) -> Result<ValidationResult, SchemaError> {
423    let compiled = CompiledSchema::compile(schema, config)?;
424    Ok(compiled.validate(instance))
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use serde_json::json;
431
432    #[test]
433    fn test_validation_config_default() {
434        let config = ValidationConfig::default();
435        assert!(matches!(config.draft, SchemaDraft::Draft7));
436        assert!(config.collect_all_errors);
437        assert!(config.max_errors.is_none());
438        assert!(config.validate_formats);
439    }
440
441    #[test]
442    fn test_compile_valid_schema() {
443        let schema = json!({
444            "type": "object",
445            "properties": {
446                "name": {"type": "string"}
447            }
448        });
449
450        let result = CompiledSchema::compile(&schema, &ValidationConfig::default());
451        assert!(result.is_ok());
452    }
453
454    #[test]
455    fn test_compile_invalid_schema() {
456        let schema = json!({
457            "type": "invalid_type_that_does_not_exist"
458        });
459
460        let result = CompiledSchema::compile(&schema, &ValidationConfig::default());
461        assert!(result.is_err());
462    }
463
464    #[test]
465    fn test_validate_valid_document() {
466        let schema = json!({
467            "type": "object",
468            "properties": {
469                "name": {"type": "string"},
470                "age": {"type": "integer"}
471            },
472            "required": ["name"]
473        });
474
475        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
476
477        let valid = json!({"name": "Alice", "age": 30});
478        let result = compiled.validate(&valid);
479
480        assert!(result.is_valid);
481        assert!(result.errors.is_empty());
482    }
483
484    #[test]
485    fn test_validate_invalid_document() {
486        let schema = json!({
487            "type": "object",
488            "properties": {
489                "name": {"type": "string"},
490                "age": {"type": "integer"}
491            },
492            "required": ["name"]
493        });
494
495        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
496
497        let invalid = json!({"age": "not an integer"});
498        let result = compiled.validate(&invalid);
499
500        assert!(!result.is_valid);
501        assert!(!result.errors.is_empty());
502    }
503
504    #[test]
505    fn test_validate_type_mismatch() {
506        let schema = json!({"type": "string"});
507        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
508
509        assert!(compiled.validate(&json!("hello")).is_valid);
510        assert!(!compiled.validate(&json!(42)).is_valid);
511        assert!(!compiled.validate(&json!(true)).is_valid);
512        assert!(!compiled.validate(&json!(null)).is_valid);
513    }
514
515    #[test]
516    fn test_validate_string_constraints() {
517        let schema = json!({
518            "type": "string",
519            "minLength": 2,
520            "maxLength": 5
521        });
522
523        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
524
525        assert!(!compiled.validate(&json!("a")).is_valid); // too short
526        assert!(compiled.validate(&json!("ab")).is_valid);
527        assert!(compiled.validate(&json!("abcde")).is_valid);
528        assert!(!compiled.validate(&json!("abcdef")).is_valid); // too long
529    }
530
531    #[test]
532    fn test_validate_number_constraints() {
533        let schema = json!({
534            "type": "integer",
535            "minimum": 0,
536            "maximum": 100
537        });
538
539        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
540
541        assert!(!compiled.validate(&json!(-1)).is_valid); // below minimum
542        assert!(compiled.validate(&json!(0)).is_valid);
543        assert!(compiled.validate(&json!(50)).is_valid);
544        assert!(compiled.validate(&json!(100)).is_valid);
545        assert!(!compiled.validate(&json!(101)).is_valid); // above maximum
546    }
547
548    #[test]
549    fn test_validate_array_constraints() {
550        let schema = json!({
551            "type": "array",
552            "items": {"type": "integer"},
553            "minItems": 1,
554            "maxItems": 3
555        });
556
557        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
558
559        assert!(!compiled.validate(&json!([])).is_valid); // empty
560        assert!(compiled.validate(&json!([1])).is_valid);
561        assert!(compiled.validate(&json!([1, 2, 3])).is_valid);
562        assert!(!compiled.validate(&json!([1, 2, 3, 4])).is_valid); // too many
563        assert!(!compiled.validate(&json!([1, "string", 3])).is_valid); // wrong type
564    }
565
566    #[test]
567    fn test_validate_required_properties() {
568        let schema = json!({
569            "type": "object",
570            "properties": {
571                "id": {"type": "integer"},
572                "name": {"type": "string"}
573            },
574            "required": ["id", "name"]
575        });
576
577        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
578
579        assert!(
580            compiled
581                .validate(&json!({"id": 1, "name": "test"}))
582                .is_valid
583        );
584        assert!(!compiled.validate(&json!({"id": 1})).is_valid); // missing name
585        assert!(!compiled.validate(&json!({"name": "test"})).is_valid); // missing id
586        assert!(!compiled.validate(&json!({})).is_valid); // missing both
587    }
588
589    #[test]
590    fn test_validate_ref_resolution() {
591        let schema = json!({
592            "$defs": {
593                "User": {
594                    "type": "object",
595                    "properties": {
596                        "name": {"type": "string"}
597                    },
598                    "required": ["name"]
599                }
600            },
601            "type": "object",
602            "properties": {
603                "user": {"$ref": "#/$defs/User"}
604            }
605        });
606
607        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
608
609        assert!(
610            compiled
611                .validate(&json!({"user": {"name": "Alice"}}))
612                .is_valid
613        );
614        assert!(!compiled.validate(&json!({"user": {}})).is_valid); // missing name
615    }
616
617    #[test]
618    fn test_validate_any_of() {
619        let schema = json!({
620            "anyOf": [
621                {"type": "string"},
622                {"type": "integer"}
623            ]
624        });
625
626        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
627
628        assert!(compiled.validate(&json!("hello")).is_valid);
629        assert!(compiled.validate(&json!(42)).is_valid);
630        assert!(!compiled.validate(&json!(true)).is_valid);
631    }
632
633    #[test]
634    fn test_validate_all_of() {
635        let schema = json!({
636            "allOf": [
637                {"type": "object"},
638                {"required": ["name"]},
639                {"properties": {"name": {"type": "string"}}}
640            ]
641        });
642
643        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
644
645        assert!(compiled.validate(&json!({"name": "test"})).is_valid);
646        assert!(!compiled.validate(&json!({})).is_valid); // missing name
647        assert!(!compiled.validate(&json!({"name": 123})).is_valid); // wrong type
648    }
649
650    #[test]
651    fn test_collect_all_errors() {
652        let schema = json!({
653            "type": "object",
654            "properties": {
655                "a": {"type": "integer"},
656                "b": {"type": "integer"},
657                "c": {"type": "integer"}
658            }
659        });
660
661        let config = ValidationConfig {
662            collect_all_errors: true,
663            ..Default::default()
664        };
665
666        let compiled = CompiledSchema::compile(&schema, &config).unwrap();
667
668        let invalid = json!({
669            "a": "not int",
670            "b": "not int",
671            "c": "not int"
672        });
673
674        let result = compiled.validate(&invalid);
675        assert!(!result.is_valid);
676        assert_eq!(result.errors.len(), 3); // All three errors collected
677    }
678
679    #[test]
680    fn test_stop_at_first_error() {
681        let schema = json!({
682            "type": "object",
683            "properties": {
684                "a": {"type": "integer"},
685                "b": {"type": "integer"},
686                "c": {"type": "integer"}
687            }
688        });
689
690        let config = ValidationConfig {
691            collect_all_errors: false,
692            ..Default::default()
693        };
694
695        let compiled = CompiledSchema::compile(&schema, &config).unwrap();
696
697        let invalid = json!({
698            "a": "not int",
699            "b": "not int",
700            "c": "not int"
701        });
702
703        let result = compiled.validate(&invalid);
704        assert!(!result.is_valid);
705        assert_eq!(result.errors.len(), 1); // Only first error
706    }
707
708    #[test]
709    fn test_max_errors_limit() {
710        let schema = json!({
711            "type": "object",
712            "properties": {
713                "a": {"type": "integer"},
714                "b": {"type": "integer"},
715                "c": {"type": "integer"},
716                "d": {"type": "integer"},
717                "e": {"type": "integer"}
718            }
719        });
720
721        let config = ValidationConfig {
722            collect_all_errors: true,
723            max_errors: Some(2),
724            ..Default::default()
725        };
726
727        let compiled = CompiledSchema::compile(&schema, &config).unwrap();
728
729        let invalid = json!({
730            "a": "x",
731            "b": "x",
732            "c": "x",
733            "d": "x",
734            "e": "x"
735        });
736
737        let result = compiled.validate(&invalid);
738        assert!(!result.is_valid);
739        assert!(result.errors.len() <= 2); // Limited to max_errors
740    }
741
742    #[test]
743    fn test_is_valid_method() {
744        let schema = json!({"type": "string"});
745        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
746
747        assert!(compiled.is_valid(&json!("hello")));
748        assert!(!compiled.is_valid(&json!(42)));
749    }
750
751    #[test]
752    fn test_validation_error_display() {
753        let error = ValidationError {
754            instance_path: "/users/0/email".to_string(),
755            message: "Invalid email format".to_string(),
756            schema_path: "/properties/email/format".to_string(),
757        };
758
759        let display = error.to_string();
760        assert!(display.contains("/users/0/email"));
761        assert!(display.contains("Invalid email format"));
762    }
763
764    #[test]
765    fn test_validate_json_convenience() {
766        let schema = json!({"type": "string"});
767        let instance = json!("hello");
768
769        let result = validate_json(&schema, &instance, &ValidationConfig::default()).unwrap();
770        assert!(result.is_valid);
771    }
772
773    #[test]
774    fn test_compiled_schema_clone() {
775        let schema = json!({"type": "string"});
776        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
777
778        let cloned = compiled.clone();
779        assert!(cloned.is_valid(&json!("test")));
780    }
781
782    #[test]
783    fn test_nested_object_validation() {
784        let schema = json!({
785            "type": "object",
786            "properties": {
787                "user": {
788                    "type": "object",
789                    "properties": {
790                        "profile": {
791                            "type": "object",
792                            "properties": {
793                                "age": {"type": "integer", "minimum": 0}
794                            }
795                        }
796                    }
797                }
798            }
799        });
800
801        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
802
803        assert!(
804            compiled
805                .validate(&json!({"user": {"profile": {"age": 25}}}))
806                .is_valid
807        );
808        assert!(
809            !compiled
810                .validate(&json!({"user": {"profile": {"age": -5}}}))
811                .is_valid
812        );
813    }
814
815    #[test]
816    fn test_pattern_validation() {
817        let schema = json!({
818            "type": "string",
819            "pattern": "^[a-z]+$"
820        });
821
822        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
823
824        assert!(compiled.validate(&json!("abc")).is_valid);
825        assert!(!compiled.validate(&json!("ABC")).is_valid);
826        assert!(!compiled.validate(&json!("abc123")).is_valid);
827    }
828
829    #[test]
830    fn test_enum_validation() {
831        let schema = json!({
832            "enum": ["red", "green", "blue"]
833        });
834
835        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
836
837        assert!(compiled.validate(&json!("red")).is_valid);
838        assert!(compiled.validate(&json!("green")).is_valid);
839        assert!(compiled.validate(&json!("blue")).is_valid);
840        assert!(!compiled.validate(&json!("yellow")).is_valid);
841    }
842
843    #[test]
844    fn test_const_validation() {
845        let schema = json!({
846            "const": "fixed_value"
847        });
848
849        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
850
851        assert!(compiled.validate(&json!("fixed_value")).is_valid);
852        assert!(!compiled.validate(&json!("other")).is_valid);
853    }
854
855    #[test]
856    fn test_additional_properties_false() {
857        let schema = json!({
858            "type": "object",
859            "properties": {
860                "name": {"type": "string"}
861            },
862            "additionalProperties": false
863        });
864
865        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
866
867        assert!(compiled.validate(&json!({"name": "test"})).is_valid);
868        assert!(
869            !compiled
870                .validate(&json!({"name": "test", "extra": "field"}))
871                .is_valid
872        );
873    }
874
875    #[test]
876    fn test_error_paths() {
877        let schema = json!({
878            "type": "object",
879            "properties": {
880                "users": {
881                    "type": "array",
882                    "items": {
883                        "type": "object",
884                        "properties": {
885                            "age": {"type": "integer"}
886                        }
887                    }
888                }
889            }
890        });
891
892        let compiled = CompiledSchema::compile(&schema, &ValidationConfig::default()).unwrap();
893
894        let invalid = json!({
895            "users": [
896                {"age": 25},
897                {"age": "not an integer"}
898            ]
899        });
900
901        let result = compiled.validate(&invalid);
902        assert!(!result.is_valid);
903        assert!(!result.errors.is_empty());
904        // Error path should point to the nested location
905        assert!(result.errors[0].instance_path.contains("users"));
906    }
907
908    #[test]
909    fn test_draft_versions() {
910        let schema = json!({"type": "string"});
911
912        // Test Draft 4
913        let config_d4 = ValidationConfig {
914            draft: SchemaDraft::Draft4,
915            ..Default::default()
916        };
917        assert!(CompiledSchema::compile(&schema, &config_d4).is_ok());
918
919        // Test Draft 6
920        let config_d6 = ValidationConfig {
921            draft: SchemaDraft::Draft6,
922            ..Default::default()
923        };
924        assert!(CompiledSchema::compile(&schema, &config_d6).is_ok());
925
926        // Test Draft 7
927        let config_d7 = ValidationConfig {
928            draft: SchemaDraft::Draft7,
929            ..Default::default()
930        };
931        assert!(CompiledSchema::compile(&schema, &config_d7).is_ok());
932
933        // Test Draft 2019-09
934        let config_d201909 = ValidationConfig {
935            draft: SchemaDraft::Draft201909,
936            ..Default::default()
937        };
938        assert!(CompiledSchema::compile(&schema, &config_d201909).is_ok());
939
940        // Test Draft 2020-12
941        let config_d202012 = ValidationConfig {
942            draft: SchemaDraft::Draft202012,
943            ..Default::default()
944        };
945        assert!(CompiledSchema::compile(&schema, &config_d202012).is_ok());
946    }
947}