diff --git a/CHANGELOG.md b/CHANGELOG.md index ee19d465..4293f9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Implement `IntoIterator` for `Location` to iterate over `LocationSegment`. - Implement `FromIter` for `Location` to build a `Location` from an iterator of `LocationSegment`. - `ValidationError::to_owned` method for converting errors into owned versions. +- Meta-schema validation support. [#442](https://github.com/Stranger6667/jsonschema/issues/442) ## [0.27.1] - 2024-12-24 diff --git a/README.md b/README.md index b1eb2cfd..f60d566f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ See more usage examples in the [documentation](https://docs.rs/jsonschema). - 🔧 Custom keywords and format validators - 🌐 Remote reference fetching (network/file) - 🎨 `Basic` output style as per JSON Schema spec +- ✨ Meta-schema validation for schema documents - 🔗 Bindings for [Python](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py) - 🚀 WebAssembly support - 💻 Command Line Interface diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 94be47c9..1f5b00c4 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Meta-schema validation support. [#442](https://github.com/Stranger6667/jsonschema/issues/442) + ## [0.27.1] - 2024-12-24 ### Added diff --git a/crates/jsonschema-py/README.md b/crates/jsonschema-py/README.md index 229a2fd1..9b1b9719 100644 --- a/crates/jsonschema-py/README.md +++ b/crates/jsonschema-py/README.md @@ -44,6 +44,7 @@ assert validator.is_valid(instance) - 📚 Full support for popular JSON Schema drafts - 🌐 Remote reference fetching (network/file) - 🔧 Custom format validators +- ✨ Meta-schema validation for schema documents ### Supported drafts @@ -157,6 +158,38 @@ On instance: "unknown"''' ``` +## Meta-Schema Validation + +JSON Schema documents can be validated against their meta-schemas to ensure they are valid schemas. `jsonschema-rs` provides this functionality through the `meta` module: + +```python +import jsonschema_rs + +# Valid schema +schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer", "minimum": 0} + }, + "required": ["name"] +} + +# Validate schema (draft is auto-detected) +assert jsonschema_rs.meta.is_valid(schema) +jsonschema_rs.meta.validate(schema) # No error raised + +# Invalid schema +invalid_schema = { + "minimum": "not_a_number" # "minimum" must be a number +} + +try: + jsonschema_rs.meta.validate(invalid_schema) +except jsonschema_rs.ValidationError as exc: + assert 'is not of type "number"' in str(exc) +``` + ## External References By default, `jsonschema-rs` resolves HTTP references and file references from the local file system. You can implement a custom retriever to handle external references. Here's an example that uses a static map of schemas: diff --git a/crates/jsonschema-py/src/lib.rs b/crates/jsonschema-py/src/lib.rs index e24c9f72..5c4d1c0e 100644 --- a/crates/jsonschema-py/src/lib.rs +++ b/crates/jsonschema-py/src/lib.rs @@ -1083,6 +1083,45 @@ mod build { include!(concat!(env!("OUT_DIR"), "/built.rs")); } +/// Meta-schema validation +mod meta { + use pyo3::prelude::*; + /// is_valid(schema) + /// + /// Validate a JSON Schema document against its meta-schema. Draft version is detected automatically. + /// + /// >>> jsonschema_rs.meta.is_valid({"type": "string"}) + /// True + /// >>> jsonschema_rs.meta.is_valid({"type": "invalid_type"}) + /// False + /// + #[pyfunction] + #[pyo3(signature = (schema))] + pub(crate) fn is_valid(schema: &Bound<'_, PyAny>) -> PyResult { + let schema = crate::ser::to_value(schema)?; + Ok(jsonschema::meta::is_valid(&schema)) + } + + /// validate(schema) + /// + /// Validate a JSON Schema document against its meta-schema and raise ValidationError if invalid. + /// Draft version is detected automatically. + /// + /// >>> jsonschema_rs.meta.validate({"type": "string"}) + /// >>> jsonschema_rs.meta.validate({"type": "invalid_type"}) + /// ... + /// + #[pyfunction] + #[pyo3(signature = (schema))] + pub(crate) fn validate(py: Python<'_>, schema: &Bound<'_, PyAny>) -> PyResult<()> { + let schema = crate::ser::to_value(schema)?; + match jsonschema::meta::validate(&schema) { + Ok(()) => Ok(()), + Err(error) => Err(crate::into_py_err(py, error, None)?), + } + } +} + /// JSON Schema validation for Python written in Rust. #[pymodule] fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { @@ -1107,6 +1146,11 @@ fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add("Draft201909", DRAFT201909)?; module.add("Draft202012", DRAFT202012)?; + let meta = PyModule::new(py, "meta")?; + meta.add_function(wrap_pyfunction!(meta::is_valid, &meta)?)?; + meta.add_function(wrap_pyfunction!(meta::validate, &meta)?)?; + module.add_submodule(&meta)?; + // Add build metadata to ease triaging incoming issues #[allow(deprecated)] module.add("__build__", pyo3_built::pyo3_built!(py, build))?; diff --git a/crates/jsonschema-py/tests-py/test_meta.py b/crates/jsonschema-py/tests-py/test_meta.py new file mode 100644 index 00000000..68b20df4 --- /dev/null +++ b/crates/jsonschema-py/tests-py/test_meta.py @@ -0,0 +1,59 @@ +import pytest +from jsonschema_rs import meta, ValidationError + + +@pytest.mark.parametrize( + "schema", + [ + {"type": "string"}, + {"type": "number", "minimum": 0}, + {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}, + # Boolean schemas are valid + True, + False, + ], +) +def test_valid_schemas(schema): + assert meta.is_valid(schema) + meta.validate(schema) # Should not raise + + +@pytest.mark.parametrize( + ["schema", "expected"], + [ + ({"type": "invalid_type"}, "is not valid"), + ({"type": "number", "minimum": "0"}, 'is not of type "number"'), + ({"type": "object", "required": "name"}, 'is not of type "array"'), + ], +) +def test_invalid_schemas(schema, expected): + assert not meta.is_valid(schema) + with pytest.raises(ValidationError, match=expected): + meta.validate(schema) + + +def test_validation_error_details(): + schema = {"type": "invalid_type"} + + with pytest.raises(ValidationError) as exc_info: + meta.validate(schema) + + error = exc_info.value + assert hasattr(error, "message") + assert hasattr(error, "instance_path") + assert hasattr(error, "schema_path") + assert "invalid_type" in str(error) + + +@pytest.mark.parametrize( + "invalid_input", + [ + None, + lambda: None, + object(), + {1, 2, 3}, + ], +) +def test_type_errors(invalid_input): + with pytest.raises((ValueError, ValidationError)): + meta.validate(invalid_input) diff --git a/crates/jsonschema/src/compiler.rs b/crates/jsonschema/src/compiler.rs index caf2baa8..7f720242 100644 --- a/crates/jsonschema/src/compiler.rs +++ b/crates/jsonschema/src/compiler.rs @@ -14,7 +14,6 @@ use crate::{ ValidationError, Validator, }; use ahash::{AHashMap, AHashSet}; -use once_cell::sync::Lazy; use referencing::{ uri, Draft, List, Registry, Resolved, Resolver, Resource, ResourceRef, Uri, Vocabulary, VocabularySet, SPECIFICATIONS, @@ -243,45 +242,6 @@ impl<'a> Context<'a> { } } -const EXPECT_MESSAGE: &str = "Invalid meta-schema"; -static META_SCHEMA_VALIDATORS: Lazy> = Lazy::new(|| { - let mut validators = AHashMap::with_capacity(5); - let mut options = crate::options(); - options.without_schema_validation(); - validators.insert( - Draft::Draft4, - options - .build(&referencing::meta::DRAFT4) - .expect(EXPECT_MESSAGE), - ); - validators.insert( - Draft::Draft6, - options - .build(&referencing::meta::DRAFT6) - .expect(EXPECT_MESSAGE), - ); - validators.insert( - Draft::Draft7, - options - .build(&referencing::meta::DRAFT7) - .expect(EXPECT_MESSAGE), - ); - validators.insert( - Draft::Draft201909, - options - .build(&referencing::meta::DRAFT201909) - .expect(EXPECT_MESSAGE), - ); - validators.insert( - Draft::Draft202012, - options - .without_schema_validation() - .build(&referencing::meta::DRAFT202012) - .expect(EXPECT_MESSAGE), - ); - validators -}); - pub(crate) fn build_validator( mut config: ValidationOptions, schema: &Value, @@ -322,10 +282,17 @@ pub(crate) fn build_validator( // Validate the schema itself if config.validate_schema { - if let Err(error) = META_SCHEMA_VALIDATORS - .get(&draft) - .expect("Existing draft") - .validate(schema) + if let Err(error) = { + match draft { + Draft::Draft4 => &crate::draft4::meta::VALIDATOR, + Draft::Draft6 => &crate::draft6::meta::VALIDATOR, + Draft::Draft7 => &crate::draft7::meta::VALIDATOR, + Draft::Draft201909 => &crate::draft201909::meta::VALIDATOR, + Draft::Draft202012 => &crate::draft202012::meta::VALIDATOR, + _ => unreachable!("Unknown draft"), + } + } + .validate(schema) { return Err(error.to_owned()); } diff --git a/crates/jsonschema/src/lib.rs b/crates/jsonschema/src/lib.rs index 03baebc6..2b17f774 100644 --- a/crates/jsonschema/src/lib.rs +++ b/crates/jsonschema/src/lib.rs @@ -4,6 +4,7 @@ //! - 🔧 Custom keywords and format validators //! - 🌐 Remote reference fetching (network/file) //! - 🎨 `Basic` output style as per JSON Schema spec +//! - ✨ Meta-schema validation for schema documents //! - 🚀 WebAssembly support //! //! ## Supported drafts @@ -55,6 +56,35 @@ //! # } //! ``` //! +//! # Meta-Schema Validation +//! +//! The crate provides functionality to validate JSON Schema documents themselves against their meta-schemas. +//! This ensures your schema documents are valid according to the JSON Schema specification. +//! +//! ```rust +//! use serde_json::json; +//! +//! let schema = json!({ +//! "type": "object", +//! "properties": { +//! "name": {"type": "string"}, +//! "age": {"type": "integer", "minimum": 0} +//! } +//! }); +//! +//! // Validate schema with automatic draft detection +//! assert!(jsonschema::meta::is_valid(&schema)); +//! assert!(jsonschema::meta::validate(&schema).is_ok()); +//! +//! // Invalid schema example +//! let invalid_schema = json!({ +//! "type": "invalid_type", // must be one of the valid JSON Schema types +//! "minimum": "not_a_number" +//! }); +//! assert!(!jsonschema::meta::is_valid(&invalid_schema)); +//! assert!(jsonschema::meta::validate(&invalid_schema).is_err()); +//! ``` +//! //! # Configuration //! //! `jsonschema` provides several ways to configure and use JSON Schema validation. @@ -74,6 +104,7 @@ //! - An `is_valid` function for validation with a boolean result //! - An `validate` function for getting the first validation error //! - An `options` function to create a draft-specific configuration builder +//! - A `meta` module for draft-specific meta-schema validation //! //! Here's how you can explicitly use a specific draft version: //! @@ -82,10 +113,13 @@ //! use serde_json::json; //! //! let schema = json!({"type": "string"}); -//! let validator = jsonschema::draft7::new(&schema)?; //! +//! // Instance validation +//! let validator = jsonschema::draft7::new(&schema)?; //! assert!(validator.is_valid(&json!("Hello"))); -//! assert!(validator.validate(&json!("Hello")).is_ok()); +//! +//! // Meta-schema validation +//! assert!(jsonschema::draft7::meta::is_valid(&schema)); //! # Ok(()) //! # } //! ``` @@ -607,6 +641,117 @@ pub fn options() -> ValidationOptions { Validator::options() } +/// Functionality for validating JSON Schema documents against their meta-schemas. +pub mod meta { + use crate::{error::ValidationError, Draft}; + use serde_json::Value; + + use crate::Validator; + + pub(crate) mod validators { + use crate::Validator; + use once_cell::sync::Lazy; + + pub static DRAFT4_META_VALIDATOR: Lazy = Lazy::new(|| { + crate::options() + .without_schema_validation() + .build(&referencing::meta::DRAFT4) + .expect("Draft 4 meta-schema should be valid") + }); + + pub static DRAFT6_META_VALIDATOR: Lazy = Lazy::new(|| { + crate::options() + .without_schema_validation() + .build(&referencing::meta::DRAFT6) + .expect("Draft 6 meta-schema should be valid") + }); + + pub static DRAFT7_META_VALIDATOR: Lazy = Lazy::new(|| { + crate::options() + .without_schema_validation() + .build(&referencing::meta::DRAFT7) + .expect("Draft 7 meta-schema should be valid") + }); + + pub static DRAFT201909_META_VALIDATOR: Lazy = Lazy::new(|| { + crate::options() + .without_schema_validation() + .build(&referencing::meta::DRAFT201909) + .expect("Draft 2019-09 meta-schema should be valid") + }); + + pub static DRAFT202012_META_VALIDATOR: Lazy = Lazy::new(|| { + crate::options() + .without_schema_validation() + .build(&referencing::meta::DRAFT202012) + .expect("Draft 2020-12 meta-schema should be valid") + }); + } + + /// Validate a JSON Schema document against its meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. Draft version is detected automatically. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::meta::is_valid(&schema)); + /// ``` + /// + /// # Panics + /// + /// This function panics if the meta-schema can't be detected. + pub fn is_valid(schema: &Value) -> bool { + meta_validator_for(schema).is_valid(schema) + } + /// Validate a JSON Schema document against its meta-schema and return the first error if any. + /// Draft version is detected automatically. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::meta::validate(&invalid_schema).is_err()); + /// ``` + /// + /// # Panics + /// + /// This function panics if the meta-schema can't be detected. + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + meta_validator_for(schema).validate(schema) + } + + fn meta_validator_for(schema: &Value) -> &'static Validator { + match Draft::default() + .detect(schema) + .expect("Failed to detect meta schema") + { + Draft::Draft4 => &validators::DRAFT4_META_VALIDATOR, + Draft::Draft6 => &validators::DRAFT6_META_VALIDATOR, + Draft::Draft7 => &validators::DRAFT7_META_VALIDATOR, + Draft::Draft201909 => &validators::DRAFT201909_META_VALIDATOR, + Draft::Draft202012 => &validators::DRAFT202012_META_VALIDATOR, + _ => unreachable!("Unknown draft"), + } + } +} + /// Functionality specific to JSON Schema Draft 4. /// /// [![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json)](https://bowtie.report/#/implementations/rust-jsonschema) @@ -710,6 +855,58 @@ pub mod draft4 { options.with_draft(Draft::Draft4); options } + + /// Functionality for validating JSON Schema Draft 4 documents. + pub mod meta { + use crate::ValidationError; + use serde_json::Value; + + pub use crate::meta::validators::DRAFT4_META_VALIDATOR as VALIDATOR; + + /// Validate a JSON Schema document against Draft 4 meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft4::meta::is_valid(&schema)); + /// ``` + #[must_use] + #[inline] + pub fn is_valid(schema: &Value) -> bool { + VALIDATOR.is_valid(schema) + } + + /// Validate a JSON Schema document against Draft 4 meta-schema and return the first error if any. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft4::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::draft4::meta::validate(&invalid_schema).is_err()); + /// ``` + #[inline] + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + VALIDATOR.validate(schema) + } + } } /// Functionality specific to JSON Schema Draft 6. @@ -815,6 +1012,58 @@ pub mod draft6 { options.with_draft(Draft::Draft6); options } + + /// Functionality for validating JSON Schema Draft 6 documents. + pub mod meta { + use crate::ValidationError; + use serde_json::Value; + + pub use crate::meta::validators::DRAFT6_META_VALIDATOR as VALIDATOR; + + /// Validate a JSON Schema document against Draft 6 meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft6::meta::is_valid(&schema)); + /// ``` + #[must_use] + #[inline] + pub fn is_valid(schema: &Value) -> bool { + VALIDATOR.is_valid(schema) + } + + /// Validate a JSON Schema document against Draft 6 meta-schema and return the first error if any. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft6::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::draft6::meta::validate(&invalid_schema).is_err()); + /// ``` + #[inline] + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + VALIDATOR.validate(schema) + } + } } /// Functionality specific to JSON Schema Draft 7. @@ -920,6 +1169,58 @@ pub mod draft7 { options.with_draft(Draft::Draft7); options } + + /// Functionality for validating JSON Schema Draft 7 documents. + pub mod meta { + use crate::ValidationError; + use serde_json::Value; + + pub use crate::meta::validators::DRAFT7_META_VALIDATOR as VALIDATOR; + + /// Validate a JSON Schema document against Draft 7 meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft7::meta::is_valid(&schema)); + /// ``` + #[must_use] + #[inline] + pub fn is_valid(schema: &Value) -> bool { + VALIDATOR.is_valid(schema) + } + + /// Validate a JSON Schema document against Draft 7 meta-schema and return the first error if any. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft7::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::draft7::meta::validate(&invalid_schema).is_err()); + /// ``` + #[inline] + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + VALIDATOR.validate(schema) + } + } } /// Functionality specific to JSON Schema Draft 2019-09. @@ -1025,6 +1326,57 @@ pub mod draft201909 { options.with_draft(Draft::Draft201909); options } + + /// Functionality for validating JSON Schema Draft 2019-09 documents. + pub mod meta { + use crate::ValidationError; + use serde_json::Value; + + pub use crate::meta::validators::DRAFT201909_META_VALIDATOR as VALIDATOR; + /// Validate a JSON Schema document against Draft 2019-09 meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft201909::meta::is_valid(&schema)); + /// ``` + #[must_use] + #[inline] + pub fn is_valid(schema: &Value) -> bool { + VALIDATOR.is_valid(schema) + } + + /// Validate a JSON Schema document against Draft 2019-09 meta-schema and return the first error if any. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft201909::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::draft201909::meta::validate(&invalid_schema).is_err()); + /// ``` + #[inline] + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + VALIDATOR.validate(schema) + } + } } /// Functionality specific to JSON Schema Draft 2020-12. @@ -1133,6 +1485,58 @@ pub mod draft202012 { options.with_draft(Draft::Draft202012); options } + + /// Functionality for validating JSON Schema Draft 2020-12 documents. + pub mod meta { + use crate::ValidationError; + use serde_json::Value; + + pub use crate::meta::validators::DRAFT202012_META_VALIDATOR as VALIDATOR; + + /// Validate a JSON Schema document against Draft 2020-12 meta-schema and get a `true` if the schema is valid + /// and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft202012::meta::is_valid(&schema)); + /// ``` + #[must_use] + #[inline] + pub fn is_valid(schema: &Value) -> bool { + VALIDATOR.is_valid(schema) + } + + /// Validate a JSON Schema document against Draft 2020-12 meta-schema and return the first error if any. + /// + /// # Examples + /// + /// ```rust + /// use serde_json::json; + /// + /// let schema = json!({ + /// "type": "string", + /// "maxLength": 5 + /// }); + /// assert!(jsonschema::draft202012::meta::validate(&schema).is_ok()); + /// + /// // Invalid schema + /// let invalid_schema = json!({ + /// "type": "invalid_type" + /// }); + /// assert!(jsonschema::draft202012::meta::validate(&invalid_schema).is_err()); + /// ``` + #[inline] + pub fn validate(schema: &Value) -> Result<(), ValidationError> { + VALIDATOR.validate(schema) + } + } } #[cfg(test)] @@ -1335,6 +1739,96 @@ mod tests { assert!(validate_fn(&schema, &invalid_instance).is_err()); } + #[test_case(crate::meta::validate, crate::meta::is_valid ; "autodetect")] + #[test_case(crate::draft4::meta::validate, crate::draft4::meta::is_valid ; "draft4")] + #[test_case(crate::draft6::meta::validate, crate::draft6::meta::is_valid ; "draft6")] + #[test_case(crate::draft7::meta::validate, crate::draft7::meta::is_valid ; "draft7")] + #[test_case(crate::draft201909::meta::validate, crate::draft201909::meta::is_valid ; "draft201909")] + #[test_case(crate::draft202012::meta::validate, crate::draft202012::meta::is_valid ; "draft202012")] + fn test_meta_validation( + validate_fn: fn(&serde_json::Value) -> Result<(), ValidationError>, + is_valid_fn: fn(&serde_json::Value) -> bool, + ) { + let valid = json!({ + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer", "minimum": 0} + }, + "required": ["name"] + }); + + let invalid = json!({ + "type": "invalid_type", + "minimum": "not_a_number", + "required": true // should be an array + }); + + assert!(validate_fn(&valid).is_ok()); + assert!(validate_fn(&invalid).is_err()); + assert!(is_valid_fn(&valid)); + assert!(!is_valid_fn(&invalid)); + } + + #[test] + fn test_exclusive_minimum_across_drafts() { + // In Draft 4, exclusiveMinimum is a boolean modifier for minimum + let draft4_schema = json!({ + "$schema": "http://json-schema.org/draft-04/schema#", + "minimum": 5, + "exclusiveMinimum": true + }); + assert!(crate::meta::is_valid(&draft4_schema)); + assert!(crate::meta::validate(&draft4_schema).is_ok()); + + // This is invalid in Draft 4 (exclusiveMinimum must be boolean) + let invalid_draft4 = json!({ + "$schema": "http://json-schema.org/draft-04/schema#", + "exclusiveMinimum": 5 + }); + assert!(!crate::meta::is_valid(&invalid_draft4)); + assert!(crate::meta::validate(&invalid_draft4).is_err()); + + // In Draft 6 and later, exclusiveMinimum is a numeric value + let drafts = [ + "http://json-schema.org/draft-06/schema#", + "http://json-schema.org/draft-07/schema#", + "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2020-12/schema", + ]; + + for uri in drafts { + // Valid in Draft 6+ (numeric exclusiveMinimum) + let valid_schema = json!({ + "$schema": uri, + "exclusiveMinimum": 5 + }); + assert!( + crate::meta::is_valid(&valid_schema), + "Schema should be valid for {uri}" + ); + assert!( + crate::meta::validate(&valid_schema).is_ok(), + "Schema validation should succeed for {uri}", + ); + + // Invalid in Draft 6+ (can't use boolean with minimum) + let invalid_schema = json!({ + "$schema": uri, + "minimum": 5, + "exclusiveMinimum": true + }); + assert!( + !crate::meta::is_valid(&invalid_schema), + "Schema should be invalid for {uri}", + ); + assert!( + crate::meta::validate(&invalid_schema).is_err(), + "Schema validation should fail for {uri}", + ); + } + } + #[test] fn test_invalid_schema_keyword() { let schema = json!({