diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fde173..0c1e8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `ToString` trait implementation for validators. +- Define `JSONSchema::options` to customise `JSONSchema` compilation [#131](https://github.com/Stranger6667/jsonschema-rs/issues/131) ### Fixed diff --git a/benches/jsonschema.rs b/benches/jsonschema.rs index 2d52a7d..11715af 100644 --- a/benches/jsonschema.rs +++ b/benches/jsonschema.rs @@ -1,5 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use jsonschema::*; +use jsonschema::JSONSchema; use jsonschema_valid::schemas; use serde_json::{from_str, json, Value}; use std::{fs::File, io::Read, path::Path}; @@ -32,9 +32,9 @@ macro_rules! bench { #[allow(dead_code)] fn [](c: &mut Criterion) { let schema = json!($schema); - let validator = JSONSchema::compile(&schema, None).unwrap(); + let validator = JSONSchema::compile(&schema).unwrap(); let suffix = strip_characters(stringify!($schema)); - c.bench_function(format!("jsonschema-rs {} compile {}", $name, suffix).as_str(), |b| b.iter(|| JSONSchema::compile(&schema, None).unwrap())); + c.bench_function(format!("jsonschema-rs {} compile {}", $name, suffix).as_str(), |b| b.iter(|| JSONSchema::compile(&schema).unwrap())); $( let instance = black_box(json!($valid)); assert!(validator.is_valid(&instance)); @@ -62,9 +62,9 @@ macro_rules! bench { paste::item! { fn [](c: &mut Criterion) { let schema = json!($schema); - let validator = JSONSchema::compile(&schema, None).unwrap(); + let validator = JSONSchema::compile(&schema).unwrap(); let suffix = strip_characters(stringify!($schema)); - c.bench_function(format!("jsonschema-rs {} compile {}", $name, suffix).as_str(), |b| b.iter(|| JSONSchema::compile(&schema, None).unwrap())); + c.bench_function(format!("jsonschema-rs {} compile {}", $name, suffix).as_str(), |b| b.iter(|| JSONSchema::compile(&schema).unwrap())); $( let instance = black_box(json!($invalid)); assert!(!validator.is_valid(&instance)); @@ -85,9 +85,9 @@ fn big_schema(c: &mut Criterion) { let instance = black_box(read_json("benches/canada.json")); // jsonschema - let validator = JSONSchema::compile(&schema, None).unwrap(); + let validator = JSONSchema::compile(&schema).unwrap(); c.bench_function("compare jsonschema-rs big schema compile", |b| { - b.iter(|| JSONSchema::compile(&schema, None).unwrap()) + b.iter(|| JSONSchema::compile(&schema).unwrap()) }); c.bench_function("compare jsonschema-rs big schema is_valid", |b| { b.iter(|| validator.is_valid(&instance)) @@ -130,9 +130,9 @@ fn small_schema(c: &mut Criterion) { black_box(json!([10, "world", [1, "a", true], {"a": "a", "b": "b", "c": "xy"}, "str", 5])); // jsonschema - let validator = JSONSchema::compile(&schema, None).unwrap(); + let validator = JSONSchema::compile(&schema).unwrap(); c.bench_function("compare jsonschema-rs small schema compile", |b| { - b.iter(|| JSONSchema::compile(&schema, None).unwrap()) + b.iter(|| JSONSchema::compile(&schema).unwrap()) }); c.bench_function("compare jsonschema-rs small schema is_valid valid", |b| { b.iter(|| validator.is_valid(&valid)) diff --git a/perf-helpers/src/main.rs b/perf-helpers/src/main.rs index ee6960e..c61b59a 100644 --- a/perf-helpers/src/main.rs +++ b/perf-helpers/src/main.rs @@ -15,7 +15,7 @@ fn main() -> Result<(), Error> { eprintln!("Instance {}", instance); eprintln!("Number of Iterations {}", number_of_iterations); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); for _ in 0..number_of_iterations { compiled.is_valid(&instance); } diff --git a/python/src/lib.rs b/python/src/lib.rs index 25fe063..ac1c5be 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -44,19 +44,15 @@ impl From for PyErr { } } -fn get_draft(draft: Option) -> PyResult { - if let Some(value) = draft { - match value { - DRAFT4 => Ok(jsonschema::Draft::Draft4), - DRAFT6 => Ok(jsonschema::Draft::Draft6), - DRAFT7 => Ok(jsonschema::Draft::Draft7), - _ => Err(exceptions::ValueError::py_err(format!( - "Unknown draft: {}", - value - ))), - } - } else { - Ok(jsonschema::Draft::default()) +fn get_draft(draft: u8) -> PyResult { + match draft { + DRAFT4 => Ok(jsonschema::Draft::Draft4), + DRAFT6 => Ok(jsonschema::Draft::Draft6), + DRAFT7 => Ok(jsonschema::Draft::Draft7), + _ => Err(exceptions::ValueError::py_err(format!( + "Unknown draft: {}", + draft + ))), } } @@ -72,11 +68,15 @@ fn get_draft(draft: Option) -> PyResult { #[pyfunction] #[text_signature = "(schema, instance, draft=None)"] fn is_valid(schema: &PyAny, instance: &PyAny, draft: Option) -> PyResult { - let draft = get_draft(draft).map(Some)?; let schema = ser::to_value(schema)?; let instance = ser::to_value(instance)?; - let compiled = - jsonschema::JSONSchema::compile(&schema, draft).map_err(JSONSchemaError::Compilation)?; + let mut options = jsonschema::JSONSchema::options(); + if let Some(raw_draft_version) = draft { + options.with_draft(get_draft(raw_draft_version)?); + } + let compiled = options + .compile(&schema) + .map_err(JSONSchemaError::Compilation)?; Ok(compiled.is_valid(&instance)) } @@ -100,13 +100,17 @@ struct JSONSchema { impl JSONSchema { #[new] fn new(schema: &PyAny, draft: Option) -> PyResult { - let draft = get_draft(draft).map(Some)?; let raw_schema = ser::to_value(schema)?; + let mut options = jsonschema::JSONSchema::options(); + if let Some(raw_draft_version) = draft { + options.with_draft(get_draft(raw_draft_version)?); + } // Currently, it is the simplest way to pass a reference to `JSONSchema` // It is cleaned up in the `Drop` implementation let schema: &'static Value = Box::leak(Box::new(raw_schema)); Ok(JSONSchema { - schema: jsonschema::JSONSchema::compile(schema, draft) + schema: options + .compile(schema) .map_err(JSONSchemaError::Compilation)?, raw_schema: schema, }) diff --git a/src/compilation/context.rs b/src/compilation/context.rs new file mode 100644 index 0000000..e921d68 --- /dev/null +++ b/src/compilation/context.rs @@ -0,0 +1,52 @@ +use super::options::CompilationOptions; +use crate::schemas; +use serde_json::Value; +use std::borrow::Cow; +use url::{ParseError, Url}; + +/// Context holds information about used draft and current scope. +#[derive(Debug)] +pub(crate) struct CompilationContext<'a> { + pub(crate) scope: Cow<'a, Url>, + pub(crate) config: Cow<'a, CompilationOptions>, +} + +impl<'a> CompilationContext<'a> { + pub(crate) fn new(scope: Url, config: Cow<'a, CompilationOptions>) -> Self { + CompilationContext { + scope: Cow::Owned(scope), + config, + } + } + + #[allow(clippy::doc_markdown)] + /// Push a new scope. All URLs built from the new context will have this scope in them. + /// Before push: + /// scope = http://example.com/ + /// build_url("#/definitions/foo") -> "http://example.com/#/definitions/foo" + /// After push this schema - {"$id": "folder/", ...} + /// scope = http://example.com/folder/ + /// build_url("#/definitions/foo") -> "http://example.com/folder/#/definitions/foo" + /// + /// In other words it keeps track of sub-folders during compilation. + #[inline] + pub(crate) fn push(&'a self, schema: &Value) -> Result { + if let Some(id) = schemas::id_of(self.config.draft(), schema) { + let scope = Url::options().base_url(Some(&self.scope)).parse(id)?; + Ok(CompilationContext { + scope: Cow::Owned(scope), + config: Cow::Borrowed(&self.config), + }) + } else { + Ok(CompilationContext { + scope: Cow::Borrowed(self.scope.as_ref()), + config: Cow::Borrowed(&self.config), + }) + } + } + + /// Build a new URL. Used for `ref` compilation to keep their full paths. + pub(crate) fn build_url(&self, reference: &str) -> Result { + Url::options().base_url(Some(&self.scope)).parse(reference) + } +} diff --git a/src/compilation.rs b/src/compilation/mod.rs similarity index 61% rename from src/compilation.rs rename to src/compilation/mod.rs index 9c3f0d2..94b2dda 100644 --- a/src/compilation.rs +++ b/src/compilation/mod.rs @@ -1,26 +1,30 @@ //! Schema compilation. //! The main idea is to compile the input JSON Schema to a validators tree that will contain //! everything needed to perform such validation in runtime. +pub(crate) mod context; +pub(crate) mod options; + use crate::{ error::{CompilationError, ErrorIterator}, keywords, keywords::Validators, resolver::Resolver, - schemas, }; +use context::CompilationContext; +use options::CompilationOptions; use serde_json::Value; -use std::borrow::Cow; -use url::{ParseError, Url}; + +use url::Url; pub(crate) const DEFAULT_ROOT_URL: &str = "json-schema:///"; /// The structure that holds a JSON Schema compiled into a validation tree #[derive(Debug)] pub struct JSONSchema<'a> { - pub(crate) draft: schemas::Draft, pub(crate) schema: &'a Value, pub(crate) validators: Validators, pub(crate) resolver: Resolver<'a>, + pub(crate) context: CompilationContext<'a>, } lazy_static::lazy_static! { @@ -28,34 +32,29 @@ lazy_static::lazy_static! { } impl<'a> JSONSchema<'a> { - /// Compile the input schema into a validation tree - pub fn compile( - schema: &'a Value, - draft: Option, - ) -> Result, CompilationError> { - // Draft is detected in the following precedence order: - // - Explicitly specified; - // - $schema field in the document; - // - Draft7; - let draft = draft.unwrap_or_else(|| { - schemas::draft_from_schema(schema).unwrap_or(schemas::Draft::Draft7) - }); - let scope = match schemas::id_of(draft, schema) { - Some(url) => url::Url::parse(url)?, - None => DEFAULT_SCOPE.clone(), - }; - let resolver = Resolver::new(draft, &scope, schema)?; - let context = CompilationContext::new(scope, draft); + /// Return a default `CompilationOptions` that can configure + /// `JSONSchema` compilaton flow. + /// + /// Using options you will be able to configure the draft version + /// to use during `JSONSchema` compilation + /// + /// Example of usage: + /// ```rust + /// # use crate::jsonschema::{Draft, JSONSchema}; + /// # let schema = serde_json::json!({}); + /// let maybe_jsonschema: Result = JSONSchema::options() + /// .with_draft(Draft::Draft7) + /// .compile(&schema); + /// ``` + pub fn options() -> CompilationOptions { + CompilationOptions::default() + } - let mut validators = compile_validators(schema, &context)?; - validators.shrink_to_fit(); - - Ok(JSONSchema { - draft, - schema, - resolver, - validators, - }) + /// Compile the input schema into a validation tree. + /// + /// The method is equivalent to `JSONSchema::options().compile(schema)` + pub fn compile(schema: &'a Value) -> Result, CompilationError> { + Self::options().compile(schema) } /// Run validation against `instance` and return an iterator over `ValidationError` in the error case. @@ -85,53 +84,6 @@ impl<'a> JSONSchema<'a> { } } -/// Context holds information about used draft and current scope. -#[derive(Debug)] -pub(crate) struct CompilationContext<'a> { - pub(crate) scope: Cow<'a, Url>, - pub(crate) draft: schemas::Draft, -} - -impl<'a> CompilationContext<'a> { - pub(crate) fn new(scope: Url, draft: schemas::Draft) -> Self { - CompilationContext { - scope: Cow::Owned(scope), - draft, - } - } - - #[allow(clippy::doc_markdown)] - /// Push a new scope. All URLs built from the new context will have this scope in them. - /// Before push: - /// scope = http://example.com/ - /// build_url("#/definitions/foo") -> "http://example.com/#/definitions/foo" - /// After push this schema - {"$id": "folder/", ...} - /// scope = http://example.com/folder/ - /// build_url("#/definitions/foo") -> "http://example.com/folder/#/definitions/foo" - /// - /// In other words it keeps track of sub-folders during compilation. - #[inline] - pub(crate) fn push(&'a self, schema: &Value) -> Result { - if let Some(id) = schemas::id_of(self.draft, schema) { - let scope = Url::options().base_url(Some(&self.scope)).parse(id)?; - Ok(CompilationContext { - scope: Cow::Owned(scope), - draft: self.draft, - }) - } else { - Ok(CompilationContext { - scope: Cow::Borrowed(self.scope.as_ref()), - draft: self.draft, - }) - } - } - - /// Build a new URL. Used for `ref` compilation to keep their full paths. - pub(crate) fn build_url(&self, reference: &str) -> Result { - Url::options().base_url(Some(&self.scope)).parse(reference) - } -} - /// Compile JSON schema into a tree of validators. #[inline] pub(crate) fn compile_validators( @@ -154,7 +106,7 @@ pub(crate) fn compile_validators( } else { let mut validators = Vec::with_capacity(object.len()); for (keyword, subschema) in object { - if let Some(compilation_func) = context.draft.get_validator(keyword) { + if let Some(compilation_func) = context.config.draft().get_validator(keyword) { if let Some(validator) = compilation_func(object, subschema, &context) { validators.push(validator?) } @@ -169,9 +121,9 @@ pub(crate) fn compile_validators( #[cfg(test)] mod tests { - use super::*; - use crate::error::ValidationError; - use serde_json::*; + use super::JSONSchema; + use crate::{error::ValidationError, schemas}; + use serde_json::{from_str, json, Value}; use std::{borrow::Cow, fs::File, io::Read, path::Path}; use url::Url; @@ -189,7 +141,7 @@ mod tests { fn only_keyword() { // When only one keyword is specified let schema = json!({"type": "string"}); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); let value1 = json!("AB"); let value2 = json!(1); // And only this validator @@ -201,7 +153,7 @@ mod tests { #[test] fn resolve_ref() { let schema = load("tests/suite/tests/draft7/ref.json", 4); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); let url = Url::parse("json-schema:///#/definitions/a").unwrap(); if let (resource, Cow::Borrowed(resolved)) = compiled .resolver @@ -217,7 +169,7 @@ mod tests { fn validate_ref() { let schema = load("tests/suite/tests/draft7/ref.json", 1); let value = json!({"bar": 3}); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert!(compiled.validate(&value).is_ok()); let value = json!({"bar": true}); assert!(compiled.validate(&value).is_err()); @@ -226,7 +178,7 @@ mod tests { #[test] fn wrong_schema_type() { let schema = json!([1]); - let compiled = JSONSchema::compile(&schema, None); + let compiled = JSONSchema::compile(&schema); assert!(compiled.is_err()); } @@ -234,7 +186,7 @@ mod tests { fn multiple_errors() { let schema = json!({"minProperties": 2, "propertyNames": {"minLength": 3}}); let value = json!({"a": 3}); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); let result = compiled.validate(&value); let errors: Vec = result.unwrap_err().collect(); assert_eq!(errors.len(), 2); diff --git a/src/compilation/options.rs b/src/compilation/options.rs new file mode 100644 index 0000000..a0c80f3 --- /dev/null +++ b/src/compilation/options.rs @@ -0,0 +1,107 @@ +use crate::{ + compilation::{compile_validators, context::CompilationContext, JSONSchema, DEFAULT_SCOPE}, + error::CompilationError, + resolver::Resolver, + schemas, +}; +use serde_json::Value; +use std::{borrow::Cow, fmt}; + +/// Full configuration to guide the `JSONSchema` compilation. +/// +/// Using a `CompilationOptions` instance you can configure the supported draft, +/// content media types and more (check the exposed methods) +#[derive(Clone, Default)] +pub struct CompilationOptions { + draft: Option, +} + +impl CompilationOptions { + pub(crate) fn draft(&self) -> schemas::Draft { + self.draft.unwrap_or_default() + } + + /// Compile `schema` into `JSONSchema` using the currently defined options. + pub fn compile<'a>(&self, schema: &'a Value) -> Result, CompilationError> { + // Draft is detected in the following precedence order: + // - Explicitly specified; + // - $schema field in the document; + // - Draft::default() + + // Clone needed because we are going to store a Copy-on-Write (Cow) instance + // into the final JSONSchema as well as passing `self` (the instance and not + // the reference) would require Copy trait implementation from + // `CompilationOptions` which is something that we would like to avoid as + // options might contain heap-related objects (ie. an HashMap) and we want the + // memory-related operations to be explicit + let mut config = self.clone(); + if self.draft.is_none() { + if let Some(draft) = schemas::draft_from_schema(schema) { + config.with_draft(draft); + } + } + let processed_config: Cow<'_, CompilationOptions> = Cow::Owned(config); + let draft = processed_config.draft(); + + let scope = match schemas::id_of(draft, schema) { + Some(url) => url::Url::parse(url)?, + None => DEFAULT_SCOPE.clone(), + }; + let resolver = Resolver::new(draft, &scope, schema)?; + let context = CompilationContext::new(scope, processed_config); + + let mut validators = compile_validators(schema, &context)?; + validators.shrink_to_fit(); + + Ok(JSONSchema { + schema, + resolver, + validators, + context, + }) + } + + /// Ensure that the schema is going to be compiled using the defined Draft. + /// + /// ```rust + /// # use jsonschema::{Draft, CompilationOptions}; + /// # let mut options = CompilationOptions::default(); + /// options.with_draft(Draft::Draft4); + /// ``` + #[inline] + pub fn with_draft(&mut self, draft: schemas::Draft) -> &mut Self { + self.draft = Some(draft); + self + } +} + +impl fmt::Debug for CompilationOptions { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("CompilationConfig") + .field("draft", &self.draft) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::CompilationOptions; + use crate::schemas::Draft; + use serde_json::{json, Value}; + use test_case::test_case; + + #[test_case(Some(Draft::Draft4), &json!({}) => Draft::Draft4)] + #[test_case(None, &json!({"$schema": "http://json-schema.org/draft-06/schema#"}) => Draft::Draft6)] + #[test_case(None, &json!({}) => Draft::default())] + fn test_ensure_that_draft_detection_is_honored( + draft_version_in_options: Option, + schema: &Value, + ) -> Draft { + let mut options = CompilationOptions::default(); + if let Some(draft_version) = draft_version_in_options { + options.with_draft(draft_version); + } + let compiled = options.compile(schema).unwrap(); + compiled.context.config.draft() + } +} diff --git a/src/error.rs b/src/error.rs index efc1b49..cf81f4b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,7 +60,7 @@ pub struct ValidationError<'a> { /// /// let schema = json!({"maxLength": 5}); /// let instance = json!("foo"); -/// if let Ok(compiled) = JSONSchema::compile(&schema, None) { +/// if let Ok(compiled) = JSONSchema::compile(&schema) { /// let result = compiled.validate(&instance); /// if let Err(errors) = result { /// for error in errors { diff --git a/src/keywords/additional_items.rs b/src/keywords/additional_items.rs index 03765fe..ac759d5 100644 --- a/src/keywords/additional_items.rs +++ b/src/keywords/additional_items.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::{ boolean::{FalseValidator, TrueValidator}, diff --git a/src/keywords/additional_properties.rs b/src/keywords/additional_properties.rs index 48bf48f..5384cf7 100644 --- a/src/keywords/additional_properties.rs +++ b/src/keywords/additional_properties.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/all_of.rs b/src/keywords/all_of.rs index 522d729..6349cfd 100644 --- a/src/keywords/all_of.rs +++ b/src/keywords/all_of.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{CompilationError, ErrorIterator}, keywords::{format_vec_of_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/any_of.rs b/src/keywords/any_of.rs index aa83736..d3bbc11 100644 --- a/src/keywords/any_of.rs +++ b/src/keywords/any_of.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{CompilationError, ValidationError}, keywords::{format_vec_of_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/const_.rs b/src/keywords/const_.rs index 5633408..e5815ee 100644 --- a/src/keywords/const_.rs +++ b/src/keywords/const_.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{error, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/contains.rs b/src/keywords/contains.rs index fa05f93..1dea381 100644 --- a/src/keywords/contains.rs +++ b/src/keywords/contains.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, ErrorIterator, ValidationError}, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/content.rs b/src/keywords/content.rs index ee7eff2..603adff 100644 --- a/src/keywords/content.rs +++ b/src/keywords/content.rs @@ -1,6 +1,6 @@ //! Validators for `contentMediaType` and `contentEncoding` keywords. use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/dependencies.rs b/src/keywords/dependencies.rs index d42cd4a..cc6f9d0 100644 --- a/src/keywords/dependencies.rs +++ b/src/keywords/dependencies.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator}, keywords::{ format_key_value_validators, required::RequiredValidator, CompilationResult, Validators, diff --git a/src/keywords/enum_.rs b/src/keywords/enum_.rs index 07422ef..3134465 100644 --- a/src/keywords/enum_.rs +++ b/src/keywords/enum_.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{CompilationError, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/exclusive_maximum.rs b/src/keywords/exclusive_maximum.rs index 7ae9b3d..3e5765b 100644 --- a/src/keywords/exclusive_maximum.rs +++ b/src/keywords/exclusive_maximum.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/exclusive_minimum.rs b/src/keywords/exclusive_minimum.rs index 8d900a0..0e39e92 100644 --- a/src/keywords/exclusive_minimum.rs +++ b/src/keywords/exclusive_minimum.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/format.rs b/src/keywords/format.rs index 61386da..88d78a8 100644 --- a/src/keywords/format.rs +++ b/src/keywords/format.rs @@ -1,6 +1,6 @@ //! Validator for `format` keyword. use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, @@ -163,33 +163,34 @@ pub(crate) fn compile( context: &CompilationContext, ) -> Option { if let Value::String(format) = schema { + let draft_version = context.config.draft(); match format.as_str() { "date-time" => Some(DateTimeValidator::compile()), "date" => Some(DateValidator::compile()), "email" => Some(EmailValidator::compile()), "hostname" => Some(HostnameValidator::compile()), "idn-email" => Some(IDNEmailValidator::compile()), - "idn-hostname" if context.draft == Draft::Draft7 => { + "idn-hostname" if draft_version == Draft::Draft7 => { Some(IDNHostnameValidator::compile()) } "ipv4" => Some(IpV4Validator::compile()), "ipv6" => Some(IpV6Validator::compile()), - "iri-reference" if context.draft == Draft::Draft7 => { + "iri-reference" if draft_version == Draft::Draft7 => { Some(IRIReferenceValidator::compile()) } - "iri" if context.draft == Draft::Draft7 => Some(IRIValidator::compile()), - "json-pointer" if context.draft == Draft::Draft6 || context.draft == Draft::Draft7 => { + "iri" if draft_version == Draft::Draft7 => Some(IRIValidator::compile()), + "json-pointer" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(JSONPointerValidator::compile()) } "regex" => Some(RegexValidator::compile()), - "relative-json-pointer" if context.draft == Draft::Draft7 => { + "relative-json-pointer" if draft_version == Draft::Draft7 => { Some(RelativeJSONPointerValidator::compile()) } "time" => Some(TimeValidator::compile()), - "uri-reference" if context.draft == Draft::Draft6 || context.draft == Draft::Draft7 => { + "uri-reference" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(URIReferenceValidator::compile()) } - "uri-template" if context.draft == Draft::Draft6 || context.draft == Draft::Draft7 => { + "uri-template" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(URITemplateValidator::compile()) } "uri" => Some(URIValidator::compile()), @@ -209,7 +210,7 @@ mod tests { fn ignored_format() { let schema = json!({"format": "custom", "type": "string"}); let instance = json!("foo"); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert!(compiled.is_valid(&instance)) } } diff --git a/src/keywords/if_.rs b/src/keywords/if_.rs index a764e7b..159db97 100644 --- a/src/keywords/if_.rs +++ b/src/keywords/if_.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, ErrorIterator}, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/items.rs b/src/keywords/items.rs index d3e778f..8c64fe3 100644 --- a/src/keywords/items.rs +++ b/src/keywords/items.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, ErrorIterator}, keywords::{ boolean::TrueValidator, format_validators, format_vec_of_validators, CompilationResult, diff --git a/src/keywords/legacy/maximum_draft_4.rs b/src/keywords/legacy/maximum_draft_4.rs index cabf031..97bdc50 100644 --- a/src/keywords/legacy/maximum_draft_4.rs +++ b/src/keywords/legacy/maximum_draft_4.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::CompilationContext, + compilation::context::CompilationContext, keywords::{exclusive_maximum, maximum, CompilationResult}, }; use serde_json::{Map, Value}; diff --git a/src/keywords/legacy/minimum_draft_4.rs b/src/keywords/legacy/minimum_draft_4.rs index c3ddbf7..33eb6b2 100644 --- a/src/keywords/legacy/minimum_draft_4.rs +++ b/src/keywords/legacy/minimum_draft_4.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::CompilationContext, + compilation::context::CompilationContext, keywords::{exclusive_minimum, minimum, CompilationResult}, }; use serde_json::{Map, Value}; diff --git a/src/keywords/legacy/type_draft_4.rs b/src/keywords/legacy/type_draft_4.rs index d2eb55d..c248d92 100644 --- a/src/keywords/legacy/type_draft_4.rs +++ b/src/keywords/legacy/type_draft_4.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::{type_, CompilationResult}, primitive_type::{PrimitiveType, PrimitiveTypesBitMap}, diff --git a/src/keywords/max_items.rs b/src/keywords/max_items.rs index 516cd07..bc8889a 100644 --- a/src/keywords/max_items.rs +++ b/src/keywords/max_items.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/max_length.rs b/src/keywords/max_length.rs index a703fef..54c88dd 100644 --- a/src/keywords/max_length.rs +++ b/src/keywords/max_length.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/max_properties.rs b/src/keywords/max_properties.rs index e971ffb..362457f 100644 --- a/src/keywords/max_properties.rs +++ b/src/keywords/max_properties.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/maximum.rs b/src/keywords/maximum.rs index 2152767..5c4a843 100644 --- a/src/keywords/maximum.rs +++ b/src/keywords/maximum.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/min_items.rs b/src/keywords/min_items.rs index a6ced50..a0f4ab4 100644 --- a/src/keywords/min_items.rs +++ b/src/keywords/min_items.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/min_length.rs b/src/keywords/min_length.rs index 14584e5..9cb88a5 100644 --- a/src/keywords/min_length.rs +++ b/src/keywords/min_length.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/min_properties.rs b/src/keywords/min_properties.rs index ea1670e..1f50370 100644 --- a/src/keywords/min_properties.rs +++ b/src/keywords/min_properties.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/minimum.rs b/src/keywords/minimum.rs index 64c15b3..134d3fe 100644 --- a/src/keywords/minimum.rs +++ b/src/keywords/minimum.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/mod.rs b/src/keywords/mod.rs index 2e72509..f5a09ed 100644 --- a/src/keywords/mod.rs +++ b/src/keywords/mod.rs @@ -161,7 +161,7 @@ mod tests { #[test_case(json!({"type": ["integer", "null"], "$schema": "http://json-schema.org/draft-04/schema#"}), "type: [integer, null]")] #[test_case(json!({"uniqueItems": true}), "uniqueItems: true")] fn debug_representation(schema: Value, expected: &str) { - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert_eq!(format!("{:?}", compiled.validators[0]), expected); } @@ -194,7 +194,7 @@ mod tests { #[test_case(json!({"type": ["integer", "string"]}), json!(null), r#"'null' is not of types 'integer', 'string'"#)] #[test_case(json!({"uniqueItems": true}), json!([1, 1]), r#"'[1,1]' has non-unique elements"#)] fn error_message(schema: Value, instance: Value, expected: &str) { - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); let errors: Vec<_> = compiled .validate(&instance) .expect_err(&format!( @@ -236,7 +236,7 @@ mod tests { #[test_case(json!({"propertyNames": {"maxLength": 3}}))] fn is_valid_another_type(schema: Value) { let instance = json!(null); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert!(compiled.is_valid(&instance)) } @@ -244,7 +244,7 @@ mod tests { #[test_case(json!({"additionalItems": false, "items": true}), json!([]))] fn is_valid(schema: Value, instance: Value) { let data = json!(instance); - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert!(compiled.is_valid(&data)) } } diff --git a/src/keywords/multiple_of.rs b/src/keywords/multiple_of.rs index 099559b..93725ec 100644 --- a/src/keywords/multiple_of.rs +++ b/src/keywords/multiple_of.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/not.rs b/src/keywords/not.rs index 5d857a1..e424c6f 100644 --- a/src/keywords/not.rs +++ b/src/keywords/not.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::ValidationError, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/one_of.rs b/src/keywords/one_of.rs index 1bf208e..f9729bc 100644 --- a/src/keywords/one_of.rs +++ b/src/keywords/one_of.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::{format_vec_of_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/pattern.rs b/src/keywords/pattern.rs index e5caf04..68e9365 100644 --- a/src/keywords/pattern.rs +++ b/src/keywords/pattern.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/pattern_properties.rs b/src/keywords/pattern_properties.rs index 49e245b..32e8f09 100644 --- a/src/keywords/pattern_properties.rs +++ b/src/keywords/pattern_properties.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator}, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/properties.rs b/src/keywords/properties.rs index 375044c..d333e05 100644 --- a/src/keywords/properties.rs +++ b/src/keywords/properties.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, CompilationError, ErrorIterator}, keywords::{format_key_value_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/property_names.rs b/src/keywords/property_names.rs index 90c4c76..d3e9744 100644 --- a/src/keywords/property_names.rs +++ b/src/keywords/property_names.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{no_error, ErrorIterator, ValidationError}, keywords::{format_validators, CompilationResult, Validators}, validator::Validate, diff --git a/src/keywords/ref_.rs b/src/keywords/ref_.rs index c5a2f3f..b756965 100644 --- a/src/keywords/ref_.rs +++ b/src/keywords/ref_.rs @@ -1,11 +1,12 @@ use crate::{ - compilation::{compile_validators, CompilationContext, JSONSchema}, + compilation::{compile_validators, context::CompilationContext, JSONSchema}, error::{error, ErrorIterator, ValidationError}, keywords::{CompilationResult, Validators}, validator::Validate, }; use parking_lot::RwLock; use serde_json::{Map, Value}; +use std::borrow::Cow; use url::Url; pub(crate) struct RefValidator { @@ -32,11 +33,12 @@ impl RefValidator { #[inline] fn ensure_validators<'a>(&self, schema: &'a JSONSchema) -> Result<(), ValidationError<'a>> { if self.validators.read().is_none() { - let (scope, resolved) = - schema - .resolver - .resolve_fragment(schema.draft, &self.reference, schema.schema)?; - let context = CompilationContext::new(scope, schema.draft); + let (scope, resolved) = schema.resolver.resolve_fragment( + schema.context.config.draft(), + &self.reference, + schema.schema, + )?; + let context = CompilationContext::new(scope, Cow::Borrowed(&schema.context.config)); let validators = compile_validators(&resolved, &context)?; // Inject the validators into self.validators diff --git a/src/keywords/required.rs b/src/keywords/required.rs index c365e83..3f8b741 100644 --- a/src/keywords/required.rs +++ b/src/keywords/required.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/keywords/type_.rs b/src/keywords/type_.rs index f025c40..c48ba38 100644 --- a/src/keywords/type_.rs +++ b/src/keywords/type_.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, keywords::CompilationResult, primitive_type::{PrimitiveType, PrimitiveTypesBitMap}, diff --git a/src/keywords/unique_items.rs b/src/keywords/unique_items.rs index 7d17360..174bbd6 100644 --- a/src/keywords/unique_items.rs +++ b/src/keywords/unique_items.rs @@ -1,5 +1,5 @@ use crate::{ - compilation::{CompilationContext, JSONSchema}, + compilation::{context::CompilationContext, JSONSchema}, error::{no_error, ErrorIterator, ValidationError}, keywords::CompilationResult, validator::Validate, diff --git a/src/lib.rs b/src/lib.rs index 6bf21c1..fd0933c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,25 +7,50 @@ //! - JSON Schema drafts 6, 7 (all test cases); //! - Loading remote documents via HTTP(S); //! -//! ## Example: -//! +//! ## Usage Examples: +//! A schema can be compiled with two main flavours: +//! * using default configurations //! ```rust -//! use jsonschema::{JSONSchema, Draft, CompilationError}; +//! # use jsonschema::{CompilationError, Draft, JSONSchema}; +//! # use serde_json::json; +//! # fn foo() -> Result<(), CompilationError> { +//! # let schema = json!({"maxLength": 5}); +//! let compiled_schema = JSONSchema::compile(&schema)?; +//! # Ok(()) +//! # } +//! ``` +//! * using custom configurations (such as define a Draft version) +//! ```rust +//! # use jsonschema::{CompilationError, Draft, JSONSchema}; +//! # use serde_json::json; +//! # fn foo() -> Result<(), CompilationError> { +//! # let schema = json!({"maxLength": 5}); +//! let compiled_schema = JSONSchema::options() +//! .with_draft(Draft::Draft7) +//! .compile(&schema)?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Example (CLI tool to highlight print errors) +//! ```rust +//! use jsonschema::{CompilationError, Draft, JSONSchema}; //! use serde_json::json; //! -//!fn main() -> Result<(), CompilationError> { -//! let schema = json!({"maxLength": 5}); -//! let instance = json!("foo"); -//! let compiled = JSONSchema::compile(&schema, Some(Draft::Draft7))?; -//! let result = compiled.validate(&instance); -//! if let Err(errors) = result { -//! for error in errors { -//! println!("Validation error: {}", error) -//! } -//! } -//! Ok(()) +//! fn main() -> Result<(), CompilationError> { +//! let schema = json!({"maxLength": 5}); +//! let instance = json!("foo"); +//! let compiled = JSONSchema::options() +//! .with_draft(Draft::Draft7) +//! .compile(&schema)?; +//! let result = compiled.validate(&instance); +//! if let Err(errors) = result { +//! for error in errors { +//! println!("Validation error: {}", error) +//! } +//! } +//! Ok(()) //! } -//! //! ``` #![warn( clippy::cast_possible_truncation, @@ -57,7 +82,7 @@ mod primitive_type; mod resolver; mod schemas; mod validator; -pub use compilation::JSONSchema; +pub use compilation::{options::CompilationOptions, JSONSchema}; pub use error::{CompilationError, ErrorIterator, ValidationError}; pub use schemas::Draft; use serde_json::Value; @@ -76,7 +101,7 @@ use serde_json::Value; #[must_use] #[inline] pub fn is_valid(schema: &Value, instance: &Value) -> bool { - let compiled = JSONSchema::compile(schema, None).expect("Invalid schema"); + let compiled = JSONSchema::compile(schema).expect("Invalid schema"); compiled.is_valid(instance) } @@ -86,7 +111,7 @@ mod tests_util { use serde_json::Value; pub(crate) fn is_not_valid(schema: Value, instance: Value) { - let compiled = JSONSchema::compile(&schema, None).unwrap(); + let compiled = JSONSchema::compile(&schema).unwrap(); assert!(!compiled.is_valid(&instance), "{} should not be valid"); assert!( compiled.validate(&instance).is_err(), @@ -97,7 +122,7 @@ mod tests_util { #[cfg(test)] mod tests { - use super::*; + use super::is_valid; use serde_json::json; #[test] diff --git a/src/schemas.rs b/src/schemas.rs index ddfbb9e..405f69b 100644 --- a/src/schemas.rs +++ b/src/schemas.rs @@ -1,4 +1,4 @@ -use crate::{compilation::CompilationContext, keywords}; +use crate::{compilation::context::CompilationContext, keywords}; use serde_json::{Map, Value}; /// JSON Schema Draft version diff --git a/tests/test_suite.rs b/tests/test_suite.rs index f78dfc7..dc39d8a 100644 --- a/tests/test_suite.rs +++ b/tests/test_suite.rs @@ -14,7 +14,10 @@ fn test_draft(_server_address: &str, test_case: TestCase) { _ => panic!("Unsupported draft"), }; - let compiled = JSONSchema::compile(&test_case.schema, Some(draft_version)).unwrap(); + let compiled = JSONSchema::options() + .with_draft(draft_version) + .compile(&test_case.schema) + .unwrap(); let result = compiled.validate(&test_case.instance);