From e43f0fdc58662bc408bd5368a84dc01f4a5a7980 Mon Sep 17 00:00:00 2001 From: Toby Lawrence Date: Wed, 1 Mar 2023 16:10:10 -0500 Subject: [PATCH] feat: add support for unevaluatedProperties to draft 2019-09 and 2020-12 --- CHANGELOG.md | 3 + jsonschema/src/compilation/mod.rs | 38 +- jsonschema/src/error.rs | 34 + .../src/keywords/additional_properties.rs | 159 +-- jsonschema/src/keywords/mod.rs | 2 + jsonschema/src/keywords/ref_.rs | 33 +- .../src/keywords/unevaluated_properties.rs | 1108 +++++++++++++++++ jsonschema/src/lib.rs | 1 + jsonschema/src/paths.rs | 6 + jsonschema/src/properties.rs | 180 +++ jsonschema/src/schemas.rs | 8 + jsonschema/src/validator.rs | 9 + jsonschema/tests/test_suite.rs | 84 +- 13 files changed, 1463 insertions(+), 202 deletions(-) create mode 100644 jsonschema/src/keywords/unevaluated_properties.rs create mode 100644 jsonschema/src/properties.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3e904b..eca90e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Bump `fraction` to `0.13`. - Bump `iso8601` to `0.6`. - Replace `lazy_static` with `once_cell`. +- Add support for `unevaluatedProperties`. (gated by the `draft201909`/`draft202012` feature flags) +- When using the draft 2019-09 or draft 2020-12 specification, `$ref` is now evaluated alongside + other keywords. ## [0.16.1] - 2022-10-20 diff --git a/jsonschema/src/compilation/mod.rs b/jsonschema/src/compilation/mod.rs index 40b19a9a..71c1f563 100644 --- a/jsonschema/src/compilation/mod.rs +++ b/jsonschema/src/compilation/mod.rs @@ -154,7 +154,15 @@ pub(crate) fn compile_validators<'a>( )), }, Value::Object(object) => { - if let Some(reference) = object.get("$ref") { + // In Draft 2019-09 and later, `$ref` can be evaluated alongside other attribute aka + // adjacent validation. We check here to see if adjacent validation is supported, and if + // so, we use the normal keyword validator collection logic. + // + // Otherwise, we isolate `$ref` and generate a schema reference validator directly. + let maybe_reference = object + .get("$ref") + .filter(|_| !keywords::ref_::supports_adjacent_validation(context.config.draft())); + if let Some(reference) = maybe_reference { let unmatched_keywords = object .iter() .filter_map(|(k, v)| { @@ -165,24 +173,16 @@ pub(crate) fn compile_validators<'a>( } }) .collect(); - let mut validators = Vec::new(); - if let Value::String(reference) = reference { - let validator = keywords::ref_::compile(schema, reference, &context) - .expect("Should always return Some")?; - validators.push(("$ref".to_string(), validator)); - Ok(SchemaNode::new_from_keywords( - &context, - validators, - Some(unmatched_keywords), - )) - } else { - Err(ValidationError::single_type_error( - JSONPointer::default(), - relative_path, - reference, - PrimitiveType::String, - )) - } + + let validator = keywords::ref_::compile(object, reference, &context) + .expect("should always return Some")?; + + let validators = vec![("$ref".to_string(), validator)]; + Ok(SchemaNode::new_from_keywords( + &context, + validators, + Some(unmatched_keywords), + )) } else { let mut validators = Vec::with_capacity(object.len()); let mut unmatched_keywords = AHashMap::new(); diff --git a/jsonschema/src/error.rs b/jsonschema/src/error.rs index 39488d81..2b05f065 100644 --- a/jsonschema/src/error.rs +++ b/jsonschema/src/error.rs @@ -138,6 +138,8 @@ pub enum ValidationErrorKind { Schema, /// When the input value doesn't match one or multiple required types. Type { kind: TypeKind }, + /// Unexpected properties. + UnevaluatedProperties { unexpected: Vec }, /// When the input array has non-unique elements. UniqueItems, /// Reference contains unknown scheme. @@ -692,6 +694,19 @@ impl<'a> ValidationError<'a> { schema_path, } } + pub(crate) const fn unevaluated_properties( + schema_path: JSONPointer, + instance_path: JSONPointer, + instance: &'a Value, + unexpected: Vec, + ) -> ValidationError<'a> { + ValidationError { + instance_path, + instance: Cow::Borrowed(instance), + kind: ValidationErrorKind::UnevaluatedProperties { unexpected }, + schema_path, + } + } pub(crate) const fn unique_items( schema_path: JSONPointer, instance_path: JSONPointer, @@ -937,6 +952,25 @@ impl fmt::Display for ValidationError<'_> { ValidationErrorKind::MultipleOf { multiple_of } => { write!(f, "{} is not a multiple of {}", self.instance, multiple_of) } + ValidationErrorKind::UnevaluatedProperties { unexpected } => { + let verb = { + if unexpected.len() == 1 { + "was" + } else { + "were" + } + }; + write!( + f, + "Unevaluated properties are not allowed ({} {} unexpected)", + unexpected + .iter() + .map(|x| format!("'{}'", x)) + .collect::>() + .join(", "), + verb + ) + } ValidationErrorKind::UniqueItems => { write!(f, "{} has non-unique elements", self.instance) } diff --git a/jsonschema/src/keywords/additional_properties.rs b/jsonschema/src/keywords/additional_properties.rs index 448c35ac..d8b9bb0f 100644 --- a/jsonschema/src/keywords/additional_properties.rs +++ b/jsonschema/src/keywords/additional_properties.rs @@ -12,76 +12,12 @@ use crate::{ keywords::CompilationResult, output::{Annotations, BasicOutput, OutputUnit}, paths::{AbsolutePath, InstancePath, JSONPointer}, + properties::*, schema_node::SchemaNode, validator::{format_validators, PartialApplication, Validate}, }; -use ahash::AHashMap; -use fancy_regex::Regex; use serde_json::{Map, Value}; -pub(crate) type PatternedValidators = Vec<(Regex, SchemaNode)>; - -/// Provide mapping API to get validators associated with a property from the underlying storage. -pub(crate) trait PropertiesValidatorsMap: Send + Sync { - fn get_validator(&self, property: &str) -> Option<&SchemaNode>; - fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)>; -} - -// Iterating over a small vector and comparing strings is faster than a map lookup -const MAP_SIZE_THRESHOLD: usize = 40; -pub(crate) type SmallValidatorsMap = Vec<(String, SchemaNode)>; -pub(crate) type BigValidatorsMap = AHashMap; - -impl PropertiesValidatorsMap for SmallValidatorsMap { - #[inline] - fn get_validator(&self, property: &str) -> Option<&SchemaNode> { - for (prop, node) in self { - if prop == property { - return Some(node); - } - } - None - } - #[inline] - fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> { - for (prop, node) in self { - if prop == property { - return Some((prop, node)); - } - } - None - } -} - -impl PropertiesValidatorsMap for BigValidatorsMap { - #[inline] - fn get_validator(&self, property: &str) -> Option<&SchemaNode> { - self.get(property) - } - - #[inline] - fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> { - self.get_key_value(property) - } -} - -macro_rules! dynamic_map { - ($validator:tt, $properties:ident, $( $arg:expr ),* $(,)*) => {{ - if let Value::Object(map) = $properties { - if map.len() < MAP_SIZE_THRESHOLD { - Some($validator::::compile( - map, $($arg, )* - )) - } else { - Some($validator::::compile( - map, $($arg, )* - )) - } - } else { - Some(Err(ValidationError::null_schema())) - } - }}; -} macro_rules! is_valid { ($node:expr, $value:ident) => {{ $node.is_valid($value) @@ -124,37 +60,6 @@ macro_rules! validate { }}; } -fn compile_small_map<'a>( - map: &'a Map, - context: &CompilationContext, -) -> Result> { - let mut properties = Vec::with_capacity(map.len()); - let keyword_context = context.with_path("properties"); - for (key, subschema) in map { - let property_context = keyword_context.with_path(key.clone()); - properties.push(( - key.clone(), - compile_validators(subschema, &property_context)?, - )); - } - Ok(properties) -} -fn compile_big_map<'a>( - map: &'a Map, - context: &CompilationContext, -) -> Result> { - let mut properties = AHashMap::with_capacity(map.len()); - let keyword_context = context.with_path("properties"); - for (key, subschema) in map { - let property_context = keyword_context.with_path(key.clone()); - properties.insert( - key.clone(), - compile_validators(subschema, &property_context)?, - ); - } - Ok(properties) -} - /// # Schema example /// /// ```json @@ -345,16 +250,11 @@ impl AdditionalPropertiesNotEmptyFalseValidator { } impl Validate for AdditionalPropertiesNotEmptyFalseValidator { fn is_valid(&self, instance: &Value) -> bool { - if let Value::Object(item) = instance { - for (property, value) in item { - if let Some(node) = self.properties.get_validator(property) { - is_valid_pattern_schema!(node, value) - } - // No extra properties are allowed - return false; - } + if let Value::Object(props) = instance { + are_properties_valid(&self.properties, props, |_| false) + } else { + true } - true } fn validate<'instance>( @@ -484,17 +384,13 @@ impl AdditionalPropertiesNotEmptyValidator { } impl Validate for AdditionalPropertiesNotEmptyValidator { fn is_valid(&self, instance: &Value) -> bool { - if let Value::Object(map) = instance { - for (property, value) in map { - if let Some(property_validators) = self.properties.get_validator(property) { - is_valid_pattern_schema!(property_validators, value) - } - if !self.node.is_valid(value) { - return false; - } - } + if let Value::Object(props) = instance { + are_properties_valid(&self.properties, props, |instance| { + self.node.is_valid(instance) + }) + } else { + true } - true } fn validate<'instance>( @@ -1263,7 +1159,7 @@ pub(crate) fn compile<'a>( Value::Bool(true) => None, // "additionalProperties" are "true" by default Value::Bool(false) => { if let Some(properties) = properties { - dynamic_map!( + compile_dynamic_prop_map_validator!( AdditionalPropertiesWithPatternsNotEmptyFalseValidator, properties, compiled_patterns, @@ -1278,7 +1174,7 @@ pub(crate) fn compile<'a>( } _ => { if let Some(properties) = properties { - dynamic_map!( + compile_dynamic_prop_map_validator!( AdditionalPropertiesWithPatternsNotEmptyValidator, properties, schema, @@ -1305,7 +1201,7 @@ pub(crate) fn compile<'a>( Value::Bool(true) => None, // "additionalProperties" are "true" by default Value::Bool(false) => { if let Some(properties) = properties { - dynamic_map!( + compile_dynamic_prop_map_validator!( AdditionalPropertiesNotEmptyFalseValidator, properties, context @@ -1317,7 +1213,7 @@ pub(crate) fn compile<'a>( } _ => { if let Some(properties) = properties { - dynamic_map!( + compile_dynamic_prop_map_validator!( AdditionalPropertiesNotEmptyValidator, properties, schema, @@ -1331,31 +1227,6 @@ pub(crate) fn compile<'a>( } } -/// Create a vector of pattern-validators pairs. -#[inline] -fn compile_patterns<'a>( - obj: &'a Map, - context: &CompilationContext, -) -> Result> { - let keyword_context = context.with_path("patternProperties"); - let mut compiled_patterns = Vec::with_capacity(obj.len()); - for (pattern, subschema) in obj { - let pattern_context = keyword_context.with_path(pattern.to_string()); - if let Ok(compiled_pattern) = Regex::new(pattern) { - let node = compile_validators(subschema, &pattern_context)?; - compiled_patterns.push((compiled_pattern, node)); - } else { - return Err(ValidationError::format( - JSONPointer::default(), - keyword_context.clone().into_pointer(), - subschema, - "regex", - )); - } - } - Ok(compiled_patterns) -} - #[cfg(test)] mod tests { use crate::tests_util; diff --git a/jsonschema/src/keywords/mod.rs b/jsonschema/src/keywords/mod.rs index a8af996a..197a21d7 100644 --- a/jsonschema/src/keywords/mod.rs +++ b/jsonschema/src/keywords/mod.rs @@ -34,6 +34,8 @@ pub(crate) mod property_names; pub(crate) mod ref_; pub(crate) mod required; pub(crate) mod type_; +#[cfg(any(feature = "draft201909", feature = "draft202012"))] +pub(crate) mod unevaluated_properties; pub(crate) mod unique_items; use crate::{error, validator::Validate}; diff --git a/jsonschema/src/keywords/ref_.rs b/jsonschema/src/keywords/ref_.rs index db16b71f..12f8b399 100644 --- a/jsonschema/src/keywords/ref_.rs +++ b/jsonschema/src/keywords/ref_.rs @@ -3,13 +3,14 @@ use crate::{ error::{error, ErrorIterator}, keywords::CompilationResult, paths::{InstancePath, JSONPointer}, + primitive_type::PrimitiveType, resolver::Resolver, schema_node::SchemaNode, validator::Validate, - CompilationOptions, + CompilationOptions, Draft, ValidationError, }; use parking_lot::RwLock; -use serde_json::Value; +use serde_json::{Map, Value}; use std::sync::Arc; use url::Url; @@ -123,11 +124,33 @@ impl core::fmt::Display for RefValidator { #[inline] pub(crate) fn compile<'a>( - _: &'a Value, - reference: &'a str, + _: &'a Map, + schema: &'a Value, context: &CompilationContext, ) -> Option> { - Some(RefValidator::compile(reference, context)) + Some( + schema + .as_str() + .ok_or_else(|| { + ValidationError::single_type_error( + JSONPointer::default(), + context.clone().into_pointer(), + schema, + PrimitiveType::String, + ) + }) + .and_then(|reference| RefValidator::compile(reference, context)), + ) +} + +pub(crate) const fn supports_adjacent_validation(draft: Draft) -> bool { + match draft { + #[cfg(feature = "draft201909")] + Draft::Draft201909 => true, + #[cfg(feature = "draft202012")] + Draft::Draft202012 => true, + _ => false, + } } #[cfg(test)] diff --git a/jsonschema/src/keywords/unevaluated_properties.rs b/jsonschema/src/keywords/unevaluated_properties.rs new file mode 100644 index 00000000..fd513f13 --- /dev/null +++ b/jsonschema/src/keywords/unevaluated_properties.rs @@ -0,0 +1,1108 @@ +use std::sync::Arc; + +use crate::{ + compilation::{compile_validators, context::CompilationContext}, + error::{no_error, ErrorIterator, ValidationError}, + keywords::CompilationResult, + output::BasicOutput, + paths::{InstancePath, JSONPointer}, + primitive_type::PrimitiveType, + properties::*, + schema_node::SchemaNode, + validator::{PartialApplication, Validate}, +}; +use ahash::AHashMap; +use serde_json::{Map, Value}; + +/// A validator for unevaluated properties. +/// +/// In contrast to `additionalProperties`, which can only be used for properties defined in a schema +/// of type `object`, `unevaluatedProperties` can "see through" advanced validation features like +/// subschema validation (`allOf`, `oneOf`, `anyOf`), conditional validation (`if`/`then`/`else`), +/// dependent schemas (`dependentSchemas`), and schema references (`$ref`), which allows applying +/// `additionalProperties`-like behavior to schemas which use the aforementioned advanced validation +/// keywords. +#[derive(Debug)] +struct UnevaluatedPropertiesValidator { + schema_path: JSONPointer, + unevaluated: UnevaluatedSubvalidator, + properties: Option, + patterns: Option, + conditional: Option>, + dependent: Option, + reference: Option, + subschemas: Option>, +} + +impl UnevaluatedPropertiesValidator { + fn compile<'a>( + parent: &'a Map, + schema: &'a Value, + context: &CompilationContext, + ) -> Result> { + let unevaluated = UnevaluatedSubvalidator::from_value(parent, schema, context)?; + + let properties = parent + .get("properties") + .map(|properties| PropertySubvalidator::from_value(properties, context)) + .transpose()?; + let patterns = parent + .get("patternProperties") + .map(|pattern_properties| PatternSubvalidator::from_value(pattern_properties, context)) + .transpose()?; + + let conditional = parent + .get("if") + .map(|condition| { + let success = parent.get("then"); + let failure = parent.get("else"); + + ConditionalSubvalidator::from_values(condition, success, failure, context) + .map(Box::new) + }) + .transpose()?; + + let dependent = parent + .get("dependentSchemas") + .map(|dependent_schemas| { + DependentSchemaSubvalidator::from_value(dependent_schemas, context) + }) + .transpose()?; + + let reference = parent + .get("$ref") + .map(|reference| ReferenceSubvalidator::from_value(reference, context)) + .transpose()? + .flatten(); + + let mut subschema_validators = vec![]; + if let Some(Value::Array(subschemas)) = parent.get("allOf") { + let validator = SubschemaSubvalidator::from_values(subschemas, context)?; + subschema_validators.push(validator); + } + + if let Some(Value::Array(subschemas)) = parent.get("anyOf") { + let validator = SubschemaSubvalidator::from_values(subschemas, context)?; + subschema_validators.push(validator); + } + + if let Some(Value::Array(subschemas)) = parent.get("oneOf") { + let validator = SubschemaSubvalidator::from_values(subschemas, context)?; + subschema_validators.push(validator); + } + + let subschemas = if subschema_validators.is_empty() { + None + } else { + Some(subschema_validators) + }; + + Ok(Self { + schema_path: JSONPointer::from(&context.schema_path), + unevaluated, + properties, + patterns, + conditional, + dependent, + reference, + subschemas, + }) + } + + fn is_valid_property( + &self, + instance: &Value, + property_instance: &Value, + property_name: &str, + ) -> Option { + self.properties + .as_ref() + .and_then(|prop_map| prop_map.is_valid_property(property_instance, property_name)) + .or_else(|| { + self.patterns.as_ref().and_then(|patterns| { + patterns.is_valid_property(property_instance, property_name) + }) + }) + .or_else(|| { + self.conditional.as_ref().and_then(|conditional| { + conditional.is_valid_property(instance, property_instance, property_name) + }) + }) + .or_else(|| { + self.dependent.as_ref().and_then(|dependent| { + dependent.is_valid_property(instance, property_instance, property_name) + }) + }) + .or_else(|| { + self.reference.as_ref().and_then(|reference| { + reference.is_valid_property(instance, property_instance, property_name) + }) + }) + .or_else(|| { + self.subschemas.as_ref().and_then(|subschemas| { + subschemas.iter().find_map(|subschema| { + subschema.is_valid_property(instance, property_instance, property_name) + }) + }) + }) + .or_else(|| { + self.unevaluated + .is_valid_property(property_instance, property_name) + }) + } + + fn validate_property<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.properties + .as_ref() + .and_then(|prop_map| { + prop_map.validate_property(property_path, property_instance, property_name) + }) + .or_else(|| { + self.patterns.as_ref().and_then(|patterns| { + patterns.validate_property(property_path, property_instance, property_name) + }) + }) + .or_else(|| { + self.conditional.as_ref().and_then(|conditional| { + conditional.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + self.dependent.as_ref().and_then(|dependent| { + dependent.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + self.reference.as_ref().and_then(|reference| { + reference.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + let result = self.subschemas.as_ref().and_then(|subschemas| { + subschemas.iter().find_map(|subschema| { + subschema.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }); + + result + }) + .or_else(|| { + self.unevaluated + .validate_property(property_path, property_instance, property_name) + }) + } + + fn apply_property<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.properties + .as_ref() + .and_then(|prop_map| { + prop_map.apply_property(property_path, property_instance, property_name) + }) + .or_else(|| { + self.patterns.as_ref().and_then(|patterns| { + patterns.apply_property(property_path, property_instance, property_name) + }) + }) + .or_else(|| { + self.conditional.as_ref().and_then(|conditional| { + conditional.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + self.dependent.as_ref().and_then(|dependent| { + dependent.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + self.reference.as_ref().and_then(|reference| { + reference.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + .or_else(|| { + let result = self.subschemas.as_ref().and_then(|subschemas| { + subschemas.iter().find_map(|subschema| { + subschema.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }); + + result + }) + .or_else(|| { + self.unevaluated + .apply_property(property_path, property_instance, property_name) + }) + } +} + +impl Validate for UnevaluatedPropertiesValidator { + fn is_valid(&self, instance: &Value) -> bool { + if let Value::Object(props) = instance { + props.iter().all(|(property_name, property_instance)| { + self.is_valid_property(instance, property_instance, property_name) + .unwrap_or(false) + }) + } else { + true + } + } + + fn validate<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + ) -> ErrorIterator<'instance> { + if let Value::Object(props) = instance { + let mut errors = vec![]; + let mut unexpected = vec![]; + + for (property_name, property_instance) in props { + let property_path = instance_path.push(property_name.clone()); + let maybe_property_errors = self.validate_property( + instance, + instance_path, + &property_path, + property_instance, + property_name, + ); + + match maybe_property_errors { + Some(property_errors) => errors.extend(property_errors), + None => { + // If we can't validate, that means that "unevaluatedProperties" is + // "false", which means that this property was not expected. + unexpected.push(property_name.to_string()); + } + } + } + + if !unexpected.is_empty() { + errors.push(ValidationError::unevaluated_properties( + self.schema_path.clone(), + instance_path.into(), + instance, + unexpected, + )) + } + Box::new(errors.into_iter()) + } else { + no_error() + } + } + + fn apply<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + ) -> PartialApplication<'a> { + if let Value::Object(props) = instance { + let mut output = BasicOutput::default(); + let mut unexpected = vec![]; + + for (property_name, property_instance) in props { + let property_path = instance_path.push(property_name.clone()); + let maybe_property_output = self.apply_property( + instance, + instance_path, + &property_path, + property_instance, + property_name, + ); + + match maybe_property_output { + Some(property_output) => output += property_output, + None => { + // If we can't validate, that means that "unevaluatedProperties" is + // "false", which means that this property was not expected. + unexpected.push(property_name.to_string()); + } + } + } + + let mut result: PartialApplication = output.into(); + if !unexpected.is_empty() { + result.mark_errored( + ValidationError::unevaluated_properties( + self.schema_path.clone(), + instance_path.into(), + instance, + unexpected, + ) + .into(), + ) + } + result + } else { + PartialApplication::valid_empty() + } + } +} + +impl core::fmt::Display for UnevaluatedPropertiesValidator { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + "unevaluatedProperties: {}".fmt(f) + } +} + +/// A subvalidator for properties. +#[derive(Debug)] +struct PropertySubvalidator { + prop_map: SmallValidatorsMap, +} + +impl PropertySubvalidator { + fn from_value<'a>( + properties: &'a Value, + context: &CompilationContext, + ) -> Result> { + properties + .as_object() + .ok_or_else(ValidationError::null_schema) + .and_then(|props| SmallValidatorsMap::from_map(props, context)) + .map(|prop_map| Self { prop_map }) + } + + fn is_valid_property(&self, property_instance: &Value, property_name: &str) -> Option { + self.prop_map + .get_validator(property_name) + .map(|node| node.is_valid(property_instance)) + } + + fn validate_property<'instance>( + &self, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.prop_map + .get_key_validator(property_name) + .map(|(_, node)| node.validate(property_instance, property_path)) + } + + fn apply_property<'a>( + &'a self, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.prop_map + .get_key_validator(property_name) + .map(|(_, node)| node.apply_rooted(property_instance, property_path)) + } +} + +/// A subvalidator for pattern properties. +#[derive(Debug)] +struct PatternSubvalidator { + patterns: PatternedValidators, +} + +impl PatternSubvalidator { + fn from_value<'a>( + properties: &'a Value, + context: &CompilationContext, + ) -> Result> { + properties + .as_object() + .ok_or_else(ValidationError::null_schema) + .and_then(|props| compile_patterns(props, context)) + .map(|patterns| Self { patterns }) + } + + fn is_valid_property(&self, property_instance: &Value, property_name: &str) -> Option { + let mut had_match = false; + + for (pattern, node) in &self.patterns { + if pattern.is_match(property_name).unwrap_or(false) { + had_match = true; + + if !node.is_valid(property_instance) { + return Some(false); + } + } + } + + had_match.then(|| true) + } + + fn validate_property<'instance>( + &self, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + let mut had_match = false; + let mut errors = vec![]; + + for (pattern, node) in &self.patterns { + if pattern.is_match(property_name).unwrap_or(false) { + had_match = true; + + errors.extend(node.validate(property_instance, property_path)); + } + } + + let errors: ErrorIterator<'instance> = Box::new(errors.into_iter()); + had_match.then(|| errors) + } + + fn apply_property<'a>( + &'a self, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + let mut had_match = false; + let mut output = BasicOutput::default(); + + for (pattern, node) in &self.patterns { + if pattern.is_match(property_name).unwrap_or(false) { + had_match = true; + + let pattern_output = node.apply_rooted(property_instance, property_path); + output += pattern_output; + } + } + + had_match.then(|| output) + } +} + +/// A subvalidator for subschema validation such as `allOf`, `oneOf`, and `anyOf`. +/// +/// Unlike the validation logic for `allOf`/`oneOf`/`anyOf` themselves, this subvalidator searches +/// configured subvalidators in a first-match-wins process. For example, a property will be +/// considered evaluated against subschemas defined via `oneOf` so long as one subschema would evaluate +/// the property, even if, say, more than one subschema in `oneOf` is technically valid, which would +/// otherwise be a failure for validation of `oneOf` in and of itself. +#[derive(Debug)] +struct SubschemaSubvalidator { + subvalidators: Vec, +} + +impl SubschemaSubvalidator { + fn from_values<'a>( + values: &'a [Value], + context: &CompilationContext, + ) -> Result> { + let mut subvalidators = vec![]; + for value in values { + if let Value::Object(subschema) = value { + let subvalidator = UnevaluatedPropertiesValidator::compile( + subschema, + get_unevaluated_props_schema(subschema), + context, + )?; + subvalidators.push(subvalidator); + } + } + + Ok(Self { subvalidators }) + } + + fn is_valid_property( + &self, + instance: &Value, + property_instance: &Value, + property_name: &str, + ) -> Option { + self.subvalidators.iter().find_map(|subvalidator| { + subvalidator.is_valid_property(instance, property_instance, property_name) + }) + } + + fn validate_property<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.subvalidators.iter().find_map(|subvalidator| { + subvalidator.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } + + fn apply_property<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.subvalidators.iter().find_map(|subvalidator| { + subvalidator.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } +} + +/// Unevaluated properties behavior. +#[derive(Debug)] +enum UnevaluatedBehavior { + /// Unevaluated properties are allowed, regardless of instance value. + Allow, + + /// Unevaluated properties are not allowed, regardless of instance value. + Deny, + + /// Unevaluated properties are allowed, so long as the instance is valid against the given + /// schema. + IfValid(SchemaNode), +} + +/// A subvalidator for unevaluated properties. +#[derive(Debug)] +struct UnevaluatedSubvalidator { + behavior: UnevaluatedBehavior, +} + +impl UnevaluatedSubvalidator { + fn from_value<'a>( + parent: &'a Map, + value: &'a Value, + context: &CompilationContext, + ) -> Result> { + // We also examine the value of `additionalProperties` here, if present, because if it's + // specified as `true`, it can potentially override the behavior of the validator depending + // on the value of `unevaluatedProperties`. + // + // TODO: We probably need to think about this more because `unevaluatedProperties` affects + // subschema validation, when really all we want to have this do (based on the JSON Schema + // test suite cases) is disable the `unevaluatedProperties: false` bit _just_ for normal + // properties on the top-level instance. + let additional_properties = parent.get("additionalProperties"); + let behavior = match (value, additional_properties) { + (Value::Bool(false), None) | (Value::Bool(false), Some(Value::Bool(false))) => { + UnevaluatedBehavior::Deny + } + (Value::Bool(true), _) | (Value::Bool(false), Some(Value::Bool(true))) => { + UnevaluatedBehavior::Allow + } + _ => UnevaluatedBehavior::IfValid(compile_validators( + value, + &context.with_path("unevaluatedProperties"), + )?), + }; + + Ok(Self { behavior }) + } + + fn is_valid_property(&self, property_instance: &Value, _property_name: &str) -> Option { + match &self.behavior { + UnevaluatedBehavior::Allow => Some(true), + UnevaluatedBehavior::Deny => None, + UnevaluatedBehavior::IfValid(node) => Some(node.is_valid(property_instance)), + } + } + + fn validate_property<'instance>( + &self, + property_path: &InstancePath, + property_instance: &'instance Value, + _property_name: &str, + ) -> Option> { + match &self.behavior { + UnevaluatedBehavior::Allow => Some(no_error()), + UnevaluatedBehavior::Deny => None, + UnevaluatedBehavior::IfValid(node) => { + Some(node.validate(property_instance, property_path)) + } + } + } + + fn apply_property<'a>( + &'a self, + property_path: &InstancePath, + property_instance: &Value, + _property_name: &str, + ) -> Option> { + match &self.behavior { + UnevaluatedBehavior::Allow => Some(BasicOutput::default()), + UnevaluatedBehavior::Deny => None, + UnevaluatedBehavior::IfValid(node) => { + Some(node.apply_rooted(property_instance, property_path)) + } + } + } +} + +/// A subvalidator for any conditional subschemas. +/// +/// This subvalidator handles any subschemas specified via `if`, and handles both the `then` case +/// (`success`) and `else` case (`failure`). +#[derive(Debug)] +struct ConditionalSubvalidator { + // Validator created from the `if` schema to actually validate the given instance and + // determine whether or not to check the `then` or `else` schemas, if defined. + condition: SchemaNode, + + // Validator for checking if the `if` schema evaluates a particular property. + node: Option, + + success: Option, + failure: Option, +} + +impl ConditionalSubvalidator { + fn from_values<'a>( + schema: &'a Value, + success: Option<&'a Value>, + failure: Option<&'a Value>, + context: &CompilationContext, + ) -> Result> { + compile_validators(schema, context).and_then(|condition| { + let node = schema + .as_object() + .map(|parent| { + UnevaluatedPropertiesValidator::compile( + parent, + get_unevaluated_props_schema(parent), + context, + ) + }) + .transpose()?; + let success = success + .and_then(|value| value.as_object()) + .map(|parent| { + UnevaluatedPropertiesValidator::compile( + parent, + get_unevaluated_props_schema(parent), + context, + ) + }) + .transpose()?; + let failure = failure + .and_then(|value| value.as_object()) + .map(|parent| { + UnevaluatedPropertiesValidator::compile( + parent, + get_unevaluated_props_schema(parent), + context, + ) + }) + .transpose()?; + + Ok(Self { + condition, + node, + success, + failure, + }) + }) + } + + fn is_valid_property( + &self, + instance: &Value, + property_instance: &Value, + property_name: &str, + ) -> Option { + self.node + .as_ref() + .and_then(|node| node.is_valid_property(instance, property_instance, property_name)) + .or_else(|| { + if self.condition.is_valid(instance) { + self.success.as_ref().and_then(|success| { + success.is_valid_property(instance, property_instance, property_name) + }) + } else { + self.failure.as_ref().and_then(|failure| { + failure.is_valid_property(instance, property_instance, property_name) + }) + } + }) + } + + fn validate_property<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.node + .as_ref() + .and_then(|node| { + node.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + .or_else(|| { + if self.condition.validate(instance, instance_path).count() == 0 { + self.success.as_ref().and_then(|success| { + success.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } else { + self.failure.as_ref().and_then(|failure| { + failure.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } + }) + } + + fn apply_property<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.node + .as_ref() + .and_then(|node| { + node.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + .or_else(|| { + let partial = self.condition.apply(instance, instance_path); + if partial.is_valid() { + self.success.as_ref().and_then(|success| { + success.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } else { + self.failure.as_ref().and_then(|failure| { + failure.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + } + }) + } +} + +/// A subvalidator for dependent schemas. +#[derive(Debug)] +struct DependentSchemaSubvalidator { + nodes: AHashMap, +} + +impl DependentSchemaSubvalidator { + fn from_value<'a>( + value: &'a Value, + context: &CompilationContext, + ) -> Result> { + let schemas = value + .as_object() + .ok_or_else(|| unexpected_type(context, value, PrimitiveType::Object))?; + let mut nodes = AHashMap::new(); + for (dependent_property_name, dependent_schema) in schemas { + let parent = dependent_schema + .as_object() + .ok_or_else(ValidationError::null_schema)?; + + let node = UnevaluatedPropertiesValidator::compile( + parent, + get_unevaluated_props_schema(parent), + context, + )?; + nodes.insert(dependent_property_name.to_string(), node); + } + + Ok(Self { nodes }) + } + + fn is_valid_property( + &self, + instance: &Value, + property_instance: &Value, + property_name: &str, + ) -> Option { + self.nodes + .iter() + .find_map(|(dependent_property_name, node)| { + value_has_object_key(instance, dependent_property_name) + .then(|| node) + .and_then(|node| { + node.is_valid_property(instance, property_instance, property_name) + }) + }) + } + + fn validate_property<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.nodes + .iter() + .find_map(|(dependent_property_name, node)| { + value_has_object_key(instance, dependent_property_name) + .then(|| node) + .and_then(|node| { + node.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + } + + fn apply_property<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.nodes + .iter() + .find_map(|(dependent_property_name, node)| { + value_has_object_key(instance, dependent_property_name) + .then(|| node) + .and_then(|node| { + node.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + }) + }) + } +} + +/// A subvalidator for a top-level schema reference. (`$ref`) +#[derive(Debug)] +struct ReferenceSubvalidator { + node: Box, +} + +impl ReferenceSubvalidator { + fn from_value<'a>( + value: &'a Value, + context: &CompilationContext, + ) -> Result, ValidationError<'a>> { + let reference = value + .as_str() + .ok_or_else(|| unexpected_type(context, value, PrimitiveType::String))?; + + let reference_url = context.build_url(reference)?; + let (scope, resolved) = context + .resolver + .resolve_fragment(context.config.draft(), &reference_url, reference) + .map_err(|e| e.into_owned())?; + + let ref_context = CompilationContext::new( + scope.into(), + Arc::clone(&context.config), + Arc::clone(&context.resolver), + ); + + resolved + .as_object() + .map(|parent| { + UnevaluatedPropertiesValidator::compile( + parent, + get_unevaluated_props_schema(parent), + &ref_context, + ) + .map(|validator| ReferenceSubvalidator { + node: Box::new(validator), + }) + .map_err(|e| e.into_owned()) + }) + .transpose() + } + + fn is_valid_property( + &self, + instance: &Value, + property_instance: &Value, + property_name: &str, + ) -> Option { + self.node + .is_valid_property(instance, property_instance, property_name) + } + + fn validate_property<'instance>( + &self, + instance: &'instance Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &'instance Value, + property_name: &str, + ) -> Option> { + self.node.validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + } + + fn apply_property<'a>( + &'a self, + instance: &Value, + instance_path: &InstancePath, + property_path: &InstancePath, + property_instance: &Value, + property_name: &str, + ) -> Option> { + self.node.apply_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + } +} + +fn value_has_object_key(value: &Value, key: &str) -> bool { + match value { + Value::Object(map) => map.contains_key(key), + _ => false, + } +} + +fn get_unevaluated_props_schema(parent: &Map) -> &Value { + parent + .get("unevaluatedProperties") + .unwrap_or(&Value::Bool(false)) +} + +pub(crate) fn compile<'a>( + parent: &'a Map, + schema: &'a Value, + context: &CompilationContext, +) -> Option> { + // Nothing to validate if `unevaluatedProperties` is set to `true`, which is the default: + if let Value::Bool(true) = schema { + return None; + } + + match UnevaluatedPropertiesValidator::compile(parent, schema, context) { + Ok(compiled) => Some(Ok(Box::new(compiled))), + Err(e) => Some(Err(e)), + } +} + +fn unexpected_type<'a>( + context: &CompilationContext, + instance: &'a Value, + expected_type: PrimitiveType, +) -> ValidationError<'a> { + ValidationError::single_type_error( + JSONPointer::default(), + context.clone().into_pointer(), + instance, + expected_type, + ) +} diff --git a/jsonschema/src/lib.rs b/jsonschema/src/lib.rs index 0b247934..1e9be247 100644 --- a/jsonschema/src/lib.rs +++ b/jsonschema/src/lib.rs @@ -91,6 +91,7 @@ mod keywords; pub mod output; pub mod paths; pub mod primitive_type; +pub(crate) mod properties; mod resolver; mod schema_node; mod schemas; diff --git a/jsonschema/src/paths.rs b/jsonschema/src/paths.rs index 02a5f078..b8f6dd68 100644 --- a/jsonschema/src/paths.rs +++ b/jsonschema/src/paths.rs @@ -213,6 +213,12 @@ impl From<&[PathChunk]> for JSONPointer { } } +impl From<&str> for JSONPointer { + fn from(value: &str) -> Self { + JSONPointer(vec![value.to_string().into()]) + } +} + /// An absolute reference #[derive(Debug, Clone, PartialEq, Eq)] pub struct AbsolutePath(url::Url); diff --git a/jsonschema/src/properties.rs b/jsonschema/src/properties.rs new file mode 100644 index 00000000..c8217986 --- /dev/null +++ b/jsonschema/src/properties.rs @@ -0,0 +1,180 @@ +use ahash::AHashMap; +use fancy_regex::Regex; +use serde_json::{Map, Value}; + +use crate::{ + compilation::{compile_validators, context::CompilationContext}, + paths::JSONPointer, + schema_node::SchemaNode, + validator::Validate, + ValidationError, +}; + +pub(crate) type PatternedValidators = Vec<(Regex, SchemaNode)>; + +/// A value that can look up property validators by name. +pub(crate) trait PropertiesValidatorsMap: Send + Sync { + fn from_map<'a>( + map: &'a Map, + context: &CompilationContext, + ) -> Result> + where + Self: Sized; + + fn get_validator(&self, property: &str) -> Option<&SchemaNode>; + fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)>; +} + +// We're defining two different property validator map implementations, one for small map sizes and +// one for large map sizes, to optimize the performance depending on the number of properties +// present. +// +// Implementors should use `compile_dynamic_prop_map_validator!` for building their validator maps +// at runtime, as it wraps up all of the logic to choose the right map size and then build and +// compile the validator. +pub(crate) type SmallValidatorsMap = Vec<(String, SchemaNode)>; +pub(crate) type BigValidatorsMap = AHashMap; + +impl PropertiesValidatorsMap for SmallValidatorsMap { + fn from_map<'a>( + map: &'a Map, + context: &CompilationContext, + ) -> Result> + where + Self: Sized, + { + compile_small_map(map, context) + } + + #[inline] + fn get_validator(&self, property: &str) -> Option<&SchemaNode> { + for (prop, node) in self { + if prop == property { + return Some(node); + } + } + None + } + #[inline] + fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> { + for (prop, node) in self { + if prop == property { + return Some((prop, node)); + } + } + None + } +} + +impl PropertiesValidatorsMap for BigValidatorsMap { + fn from_map<'a>( + map: &'a Map, + context: &CompilationContext, + ) -> Result> + where + Self: Sized, + { + compile_big_map(map, context) + } + + #[inline] + fn get_validator(&self, property: &str) -> Option<&SchemaNode> { + self.get(property) + } + + #[inline] + fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> { + self.get_key_value(property) + } +} + +pub(crate) fn compile_small_map<'a>( + map: &'a Map, + context: &CompilationContext, +) -> Result> { + let mut properties = Vec::with_capacity(map.len()); + let keyword_context = context.with_path("properties"); + for (key, subschema) in map { + let property_context = keyword_context.with_path(key.clone()); + properties.push(( + key.clone(), + compile_validators(subschema, &property_context)?, + )); + } + Ok(properties) +} + +pub(crate) fn compile_big_map<'a>( + map: &'a Map, + context: &CompilationContext, +) -> Result> { + let mut properties = AHashMap::with_capacity(map.len()); + let keyword_context = context.with_path("properties"); + for (key, subschema) in map { + let property_context = keyword_context.with_path(key.clone()); + properties.insert( + key.clone(), + compile_validators(subschema, &property_context)?, + ); + } + Ok(properties) +} + +pub(crate) fn are_properties_valid(prop_map: &M, props: &Map, check: F) -> bool +where + M: PropertiesValidatorsMap, + F: Fn(&Value) -> bool, +{ + props.iter().all(|(property, instance)| { + if let Some(validator) = prop_map.get_validator(property) { + validator.is_valid(instance) + } else { + check(instance) + } + }) +} + +/// Create a vector of pattern-validators pairs. +#[inline] +pub(crate) fn compile_patterns<'a>( + obj: &'a Map, + context: &CompilationContext, +) -> Result> { + let keyword_context = context.with_path("patternProperties"); + let mut compiled_patterns = Vec::with_capacity(obj.len()); + for (pattern, subschema) in obj { + let pattern_context = keyword_context.with_path(pattern.to_string()); + if let Ok(compiled_pattern) = Regex::new(pattern) { + let node = compile_validators(subschema, &pattern_context)?; + compiled_patterns.push((compiled_pattern, node)); + } else { + return Err(ValidationError::format( + JSONPointer::default(), + keyword_context.clone().into_pointer(), + subschema, + "regex", + )); + } + } + Ok(compiled_patterns) +} + +macro_rules! compile_dynamic_prop_map_validator { + ($validator:tt, $properties:ident, $( $arg:expr ),* $(,)*) => {{ + if let Value::Object(map) = $properties { + if map.len() < 40 { + Some($validator::::compile( + map, $($arg, )* + )) + } else { + Some($validator::::compile( + map, $($arg, )* + )) + } + } else { + Some(Err(ValidationError::null_schema())) + } + }}; +} + +pub(crate) use compile_dynamic_prop_map_validator; diff --git a/jsonschema/src/schemas.rs b/jsonschema/src/schemas.rs index fb9c3685..e8f786ab 100644 --- a/jsonschema/src/schemas.rs +++ b/jsonschema/src/schemas.rs @@ -50,6 +50,7 @@ impl Draft { #[allow(clippy::match_same_arms)] pub(crate) fn get_validator(self, keyword: &str) -> Option { match keyword { + "$ref" => Some(keywords::ref_::compile), "additionalItems" => Some(keywords::additional_items::compile), "additionalProperties" => Some(keywords::additional_properties::compile), "allOf" => Some(keywords::all_of::compile), @@ -167,6 +168,13 @@ impl Draft { #[cfg(feature = "draft202012")] Draft::Draft202012 => Some(keywords::type_::compile), }, + "unevaluatedProperties" => match self { + #[cfg(feature = "draft201909")] + Draft::Draft201909 => Some(keywords::unevaluated_properties::compile), + #[cfg(feature = "draft202012")] + Draft::Draft202012 => Some(keywords::unevaluated_properties::compile), + _ => None, + }, "uniqueItems" => Some(keywords::unique_items::compile), _ => None, } diff --git a/jsonschema/src/validator.rs b/jsonschema/src/validator.rs index c2c36639..17eb3019 100644 --- a/jsonschema/src/validator.rs +++ b/jsonschema/src/validator.rs @@ -129,6 +129,15 @@ impl<'a> PartialApplication<'a> { } } + /// A shortcut to check whether the partial represents passed validation. + #[must_use] + pub(crate) const fn is_valid(&self) -> bool { + match self { + Self::Valid { .. } => true, + Self::Invalid { .. } => false, + } + } + /// Set the annotation that will be returned for the current validator. If this /// `PartialApplication` is invalid then this method does nothing pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) { diff --git a/jsonschema/tests/test_suite.rs b/jsonschema/tests/test_suite.rs index 56508e69..c17e7eca 100644 --- a/jsonschema/tests/test_suite.rs +++ b/jsonschema/tests/test_suite.rs @@ -13,8 +13,6 @@ use std::fs; // These depend on the new `$defs` keyword (which is renamed from `definitions`) r"id_0_[0-6]", // Various types of new behavior used in the `$ref` context - "ref_5_1", - "ref_13_0", "refRemote_4_0", "refRemote_4_1", "recursiveRef_0_3", @@ -35,7 +33,6 @@ use std::fs; r"optional_format_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265 r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266 r"unevaluatedItems_.+", - r"unevaluatedProperties_.+", }))] #[cfg_attr(feature = "draft202012", json_schema_test_suite("tests/suite", "draft2020-12", { r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101 @@ -43,8 +40,6 @@ use std::fs; // These depend on the new `$defs` keyword (which is renamed from `definitions`) r"id_0_[0-6]", // Various types of new behavior used in the `$ref` context - "ref_5_1", - "ref_13_0", "refRemote_4_0", "refRemote_4_1", "recursiveRef_0_3", @@ -72,7 +67,6 @@ use std::fs; r"optional_format_uri_reference_.+", r"optional_format_uri_template_.+", r"unevaluatedItems_.+", - r"unevaluatedProperties_.+", }))] fn test_draft(_server_address: &str, test_case: TestCase) { let draft_version = match test_case.draft_version.as_ref() { @@ -91,7 +85,7 @@ fn test_draft(_server_address: &str, test_case: TestCase) { .with_meta_schemas() .should_validate_formats(true) .compile(&test_case.schema) - .unwrap(); + .expect("should not fail to compile schema"); let result = compiled.validate(&test_case.instance); @@ -100,50 +94,72 @@ fn test_draft(_server_address: &str, test_case: TestCase) { let first_error = errors_iterator.next(); assert!( first_error.is_none(), - "Schema: {}\nInstance: {}\nError: {:?}", + "Test case should not have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}", + test_case.group_description, + test_case.test_case_description, test_case.schema, test_case.instance, first_error, ); } - if !compiled.is_valid(&test_case.instance) { - panic!( - "Schema: {}\nInstance: {}\nError: It is supposed to be VALID!", - test_case.schema, test_case.instance, - ); - } + assert!( + compiled.is_valid(&test_case.instance), + "Test case should be valid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", + test_case.group_description, + test_case.test_case_description, + test_case.schema, + test_case.instance, + ); let output = compiled.apply(&test_case.instance).basic(); - if !output.is_valid() { - panic!( - "Schema: {}\nInstance: {}\nError: {:?}", - test_case.schema, test_case.instance, output - ); - } + assert!( + output.is_valid(), + "Test case should be valid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}", + test_case.group_description, + test_case.test_case_description, + test_case.schema, + test_case.instance, + output + ); } else { assert!( result.is_err(), - "Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!", + "Test case should have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", + test_case.group_description, + test_case.test_case_description, test_case.schema, test_case.instance, ); - let errors: Vec<_> = result.expect_err("Errors").collect(); + let errors = result.unwrap_err(); for error in errors { let pointer = error.instance_path.to_string(); - assert_eq!(test_case.instance.pointer(&pointer), Some(&*error.instance)) - } - if compiled.is_valid(&test_case.instance) { - panic!( - "Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!", - test_case.schema, test_case.instance, + assert_eq!( + test_case.instance.pointer(&pointer), Some(&*error.instance), + "Expected error instance did not match actual error instance:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}", + test_case.group_description, + test_case.test_case_description, + test_case.schema, + test_case.instance, + &*error.instance, + &pointer, ); } + assert!( + !compiled.is_valid(&test_case.instance), + "Test case should be invalid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", + test_case.group_description, + test_case.test_case_description, + test_case.schema, + test_case.instance, + ); let output = compiled.apply(&test_case.instance).basic(); - if output.is_valid() { - panic!( - "Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!", - test_case.schema, test_case.instance, - ); - } + assert!( + !output.is_valid(), + "Test case should be invalid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", + test_case.group_description, + test_case.test_case_description, + test_case.schema, + test_case.instance, + ); } }