From fde9c4405c6962d24bf7c030eb806adf5a424a4e Mon Sep 17 00:00:00 2001 From: Toby Lawrence Date: Wed, 5 Jul 2023 04:53:33 -0400 Subject: [PATCH] fix: `unevaluatedProperties` doesn't correctly handle subschema validation (#423) * fix: unevaluatedProperties doesn't correctly handle subschema validation * fix clippy lints and feature flag stuff * trying to optimize slightly * tweak * make additionalProperties a first-class subvalidator --- .../src/keywords/unevaluated_properties.rs | 548 ++++++++++++++---- jsonschema/src/lib.rs | 34 +- 2 files changed, 474 insertions(+), 108 deletions(-) diff --git a/jsonschema/src/keywords/unevaluated_properties.rs b/jsonschema/src/keywords/unevaluated_properties.rs index fd513f1..4183d68 100644 --- a/jsonschema/src/keywords/unevaluated_properties.rs +++ b/jsonschema/src/keywords/unevaluated_properties.rs @@ -26,6 +26,7 @@ use serde_json::{Map, Value}; struct UnevaluatedPropertiesValidator { schema_path: JSONPointer, unevaluated: UnevaluatedSubvalidator, + additional: Option, properties: Option, patterns: Option, conditional: Option>, @@ -40,7 +41,20 @@ impl UnevaluatedPropertiesValidator { schema: &'a Value, context: &CompilationContext, ) -> Result> { - let unevaluated = UnevaluatedSubvalidator::from_value(parent, schema, context)?; + let unevaluated = UnevaluatedSubvalidator::from_value( + schema, + &context.with_path("unevaluatedProperties"), + )?; + + let additional = parent + .get("additionalProperties") + .map(|additional_properties| { + UnevaluatedSubvalidator::from_value( + additional_properties, + &context.with_path("additionalProperties"), + ) + }) + .transpose()?; let properties = parent .get("properties") @@ -57,7 +71,7 @@ impl UnevaluatedPropertiesValidator { let success = parent.get("then"); let failure = parent.get("else"); - ConditionalSubvalidator::from_values(condition, success, failure, context) + ConditionalSubvalidator::from_values(schema, condition, success, failure, context) .map(Box::new) }) .transpose()?; @@ -65,29 +79,44 @@ impl UnevaluatedPropertiesValidator { let dependent = parent .get("dependentSchemas") .map(|dependent_schemas| { - DependentSchemaSubvalidator::from_value(dependent_schemas, context) + DependentSchemaSubvalidator::from_value(schema, dependent_schemas, context) }) .transpose()?; let reference = parent .get("$ref") - .map(|reference| ReferenceSubvalidator::from_value(reference, context)) + .map(|reference| ReferenceSubvalidator::from_value(schema, 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)?; + let validator = SubschemaSubvalidator::from_values( + schema, + subschemas, + SubschemaBehavior::All, + context, + )?; subschema_validators.push(validator); } if let Some(Value::Array(subschemas)) = parent.get("anyOf") { - let validator = SubschemaSubvalidator::from_values(subschemas, context)?; + let validator = SubschemaSubvalidator::from_values( + schema, + subschemas, + SubschemaBehavior::Any, + context, + )?; subschema_validators.push(validator); } if let Some(Value::Array(subschemas)) = parent.get("oneOf") { - let validator = SubschemaSubvalidator::from_values(subschemas, context)?; + let validator = SubschemaSubvalidator::from_values( + schema, + subschemas, + SubschemaBehavior::One, + context, + )?; subschema_validators.push(validator); } @@ -100,6 +129,7 @@ impl UnevaluatedPropertiesValidator { Ok(Self { schema_path: JSONPointer::from(&context.schema_path), unevaluated, + additional, properties, patterns, conditional, @@ -145,6 +175,11 @@ impl UnevaluatedPropertiesValidator { }) }) }) + .or_else(|| { + self.additional.as_ref().and_then(|additional| { + additional.is_valid_property(property_instance, property_name) + }) + }) .or_else(|| { self.unevaluated .is_valid_property(property_instance, property_name) @@ -203,7 +238,7 @@ impl UnevaluatedPropertiesValidator { }) }) .or_else(|| { - let result = self.subschemas.as_ref().and_then(|subschemas| { + self.subschemas.as_ref().and_then(|subschemas| { subschemas.iter().find_map(|subschema| { subschema.validate_property( instance, @@ -213,9 +248,12 @@ impl UnevaluatedPropertiesValidator { property_name, ) }) - }); - - result + }) + }) + .or_else(|| { + self.additional.as_ref().and_then(|additional| { + additional.validate_property(property_path, property_instance, property_name) + }) }) .or_else(|| { self.unevaluated @@ -289,6 +327,11 @@ impl UnevaluatedPropertiesValidator { result }) + .or_else(|| { + self.additional.as_ref().and_then(|additional| { + additional.apply_property(property_path, property_instance, property_name) + }) + }) .or_else(|| { self.unevaluated .apply_property(property_path, property_instance, property_name) @@ -315,7 +358,7 @@ impl Validate for UnevaluatedPropertiesValidator { ) -> ErrorIterator<'instance> { if let Value::Object(props) = instance { let mut errors = vec![]; - let mut unexpected = vec![]; + let mut unevaluated = vec![]; for (property_name, property_instance) in props { let property_path = instance_path.push(property_name.clone()); @@ -330,21 +373,20 @@ impl Validate for UnevaluatedPropertiesValidator { 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()); + unevaluated.push(property_name.to_string()); } } } - if !unexpected.is_empty() { + if !unevaluated.is_empty() { errors.push(ValidationError::unevaluated_properties( self.schema_path.clone(), instance_path.into(), instance, - unexpected, - )) + unevaluated, + )); } + Box::new(errors.into_iter()) } else { no_error() @@ -358,7 +400,7 @@ impl Validate for UnevaluatedPropertiesValidator { ) -> PartialApplication<'a> { if let Value::Object(props) = instance { let mut output = BasicOutput::default(); - let mut unexpected = vec![]; + let mut unevaluated = vec![]; for (property_name, property_instance) in props { let property_path = instance_path.push(property_name.clone()); @@ -373,21 +415,19 @@ impl Validate for UnevaluatedPropertiesValidator { 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()); + unevaluated.push(property_name.to_string()); } } } let mut result: PartialApplication = output.into(); - if !unexpected.is_empty() { + if !unevaluated.is_empty() { result.mark_errored( ValidationError::unevaluated_properties( self.schema_path.clone(), instance_path.into(), instance, - unexpected, + unevaluated, ) .into(), ) @@ -503,8 +543,7 @@ impl PatternSubvalidator { } } - let errors: ErrorIterator<'instance> = Box::new(errors.into_iter()); - had_match.then(|| errors) + had_match.then(|| boxed_errors(errors)) } fn apply_property<'a>( @@ -529,36 +568,65 @@ impl PatternSubvalidator { } } +/// Subschema validator behavior. +#[derive(Debug)] +enum SubschemaBehavior { + /// Properties must be valid for all subschemas that would evaluate them. + All, + + /// Properties must be valid for exactly one subschema that would evaluate them. + One, + + /// Properties must be valid for at least one subschema that would evaluate them. + Any, +} + +impl SubschemaBehavior { + const fn as_str(&self) -> &'static str { + match self { + SubschemaBehavior::All => "allOf", + SubschemaBehavior::One => "oneOf", + SubschemaBehavior::Any => "anyOf", + } + } +} + /// 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, + behavior: SubschemaBehavior, + subvalidators: Vec<(SchemaNode, UnevaluatedPropertiesValidator)>, } impl SubschemaSubvalidator { fn from_values<'a>( + parent: &'a Value, values: &'a [Value], + behavior: SubschemaBehavior, context: &CompilationContext, ) -> Result> { let mut subvalidators = vec![]; - for value in values { + let keyword_context = context.with_path(behavior.as_str()); + + for (i, value) in values.iter().enumerate() { if let Value::Object(subschema) = value { + let subschema_context = keyword_context.with_path(i.to_string()); + + let node = compile_validators(value, &subschema_context)?; let subvalidator = UnevaluatedPropertiesValidator::compile( subschema, - get_unevaluated_props_schema(subschema), - context, + get_transitive_unevaluated_props_schema(subschema, parent), + &subschema_context, )?; - subvalidators.push(subvalidator); + + subvalidators.push((node, subvalidator)); } } - Ok(Self { subvalidators }) + Ok(Self { + behavior, + subvalidators, + }) } fn is_valid_property( @@ -567,9 +635,58 @@ impl SubschemaSubvalidator { property_instance: &Value, property_name: &str, ) -> Option { - self.subvalidators.iter().find_map(|subvalidator| { - subvalidator.is_valid_property(instance, property_instance, property_name) - }) + let mapped = self.subvalidators.iter().map(|(node, subvalidator)| { + ( + subvalidator.is_valid_property(instance, property_instance, property_name), + node.is_valid(instance), + ) + }); + + match self.behavior { + // The instance must be valid against _all_ subschemas, and the property must be + // evaluated by at least one subschema. + SubschemaBehavior::All => { + let results = mapped.collect::>(); + let all_subschemas_valid = + results.iter().all(|(_, instance_valid)| *instance_valid); + all_subschemas_valid.then(|| { + // We only need to find the first valid evaluation because we know if that + // all subschemas were valid against the instance that there can't actually + // be any subschemas where the property was evaluated but invalid. + results + .iter() + .any(|(property_result, _)| matches!(property_result, Some(true))) + }) + } + + // The instance must be valid against only _one_ subschema, and for that subschema, the + // property must be evaluated by it. + SubschemaBehavior::One => { + let mut evaluated_property = None; + for (property_result, instance_valid) in mapped { + if instance_valid { + if evaluated_property.is_some() { + // We already found a subschema that the instance was valid against, and + // had evaluated the property, which means this `oneOf` is not valid + // overall, and so the property is not considered evaluated. + return None; + } + + evaluated_property = property_result; + } + } + + evaluated_property + } + + // The instance must be valid against _at least_ one subschema, and for that subschema, + // the property must be evaluated by it. + SubschemaBehavior::Any => mapped + .filter_map(|(property_result, instance_valid)| { + instance_valid.then(|| property_result).flatten() + }) + .find(|x| *x), + } } fn validate_property<'instance>( @@ -580,15 +697,78 @@ impl SubschemaSubvalidator { 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, - ) - }) + let mapped = self.subvalidators.iter().map(|(node, subvalidator)| { + let property_result = subvalidator + .validate_property( + instance, + instance_path, + property_path, + property_instance, + property_name, + ) + .map(|errs| errs.collect::>()); + + let instance_result = node.validate(instance, instance_path).collect::>(); + + (property_result, instance_result) + }); + + match self.behavior { + // The instance must be valid against _all_ subschemas, and the property must be + // evaluated by at least one subschema. We group the errors for the property itself + // across all subschemas, though. + SubschemaBehavior::All => { + let results = mapped.collect::>(); + let all_subschemas_valid = results + .iter() + .all(|(_, instance_errors)| instance_errors.is_empty()); + all_subschemas_valid + .then(|| { + results + .into_iter() + .filter_map(|(property_errors, _)| property_errors) + .reduce(|mut previous, current| { + previous.extend(current); + previous + }) + .map(boxed_errors) + }) + .flatten() + } + + // The instance must be valid against only _one_ subschema, and for that subschema, the + // property must be evaluated by it. + SubschemaBehavior::One => { + let mut evaluated_property_errors = None; + for (property_errors, instance_errors) in mapped { + if instance_errors.is_empty() { + if evaluated_property_errors.is_some() { + // We already found a subschema that the instance was valid against, and + // had evaluated the property, which means this `oneOf` is not valid + // overall, and so the property is not considered evaluated. + return None; + } + + evaluated_property_errors = property_errors.map(boxed_errors); + } + } + + evaluated_property_errors + } + + // The instance must be valid against _at least_ one subschema, and for that subschema, + // the property must be evaluated by it. + SubschemaBehavior::Any => mapped + .filter_map(|(property_errors, instance_errors)| { + instance_errors + .is_empty() + .then(|| property_errors) + .flatten() + }) + .filter(|x| x.is_empty()) + .map(boxed_errors) + .next(), + } } fn apply_property<'a>( @@ -599,15 +779,73 @@ impl SubschemaSubvalidator { property_instance: &Value, property_name: &str, ) -> Option> { - self.subvalidators.iter().find_map(|subvalidator| { - subvalidator.apply_property( + let mapped = self.subvalidators.iter().map(|(node, subvalidator)| { + let property_result = subvalidator.apply_property( instance, instance_path, property_path, property_instance, property_name, - ) - }) + ); + + let instance_result = node.apply(instance, instance_path); + + (property_result, instance_result) + }); + + match self.behavior { + // The instance must be valid against _all_ subschemas, and the property must be + // evaluated by at least one subschema. We group the errors for the property itself + // across all subschemas, though. + SubschemaBehavior::All => { + let results = mapped.collect::>(); + let all_subschemas_valid = results + .iter() + .all(|(_, instance_output)| instance_output.is_valid()); + all_subschemas_valid + .then(|| { + results + .into_iter() + .filter_map(|(property_output, _)| property_output) + .reduce(|mut previous, current| { + previous += current; + previous + }) + }) + .flatten() + } + + // The instance must be valid against only _one_ subschema, and for that subschema, the + // property must be evaluated by it. + SubschemaBehavior::One => { + let mut evaluated_property_output = None; + for (property_output, instance_output) in mapped { + if instance_output.is_valid() { + if evaluated_property_output.is_some() { + // We already found a subschema that the instance was valid against, and + // had evaluated the property, which means this `oneOf` is not valid + // overall, and so the property is not considered evaluated. + return None; + } + + evaluated_property_output = property_output; + } + } + + evaluated_property_output + } + + // The instance must be valid against _at least_ one subschema, and for that subschema, + // the property must be evaluated by it. + SubschemaBehavior::Any => mapped + .filter_map(|(property_output, instance_output)| { + instance_output + .is_valid() + .then(|| property_output) + .flatten() + }) + .find(|x| x.is_valid()), + } } } @@ -633,30 +871,13 @@ struct UnevaluatedSubvalidator { 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"), - )?), + let behavior = match value { + Value::Bool(false) => UnevaluatedBehavior::Deny, + Value::Bool(true) => UnevaluatedBehavior::Allow, + _ => UnevaluatedBehavior::IfValid(compile_validators(value, context)?), }; Ok(Self { behavior }) @@ -720,39 +941,41 @@ struct ConditionalSubvalidator { impl ConditionalSubvalidator { fn from_values<'a>( + parent: &'a Value, schema: &'a Value, success: Option<&'a Value>, failure: Option<&'a Value>, context: &CompilationContext, ) -> Result> { - compile_validators(schema, context).and_then(|condition| { + let if_context = context.with_path("if"); + compile_validators(schema, &if_context).and_then(|condition| { let node = schema .as_object() - .map(|parent| { + .map(|node_schema| { UnevaluatedPropertiesValidator::compile( - parent, - get_unevaluated_props_schema(parent), - context, + node_schema, + get_transitive_unevaluated_props_schema(node_schema, parent), + &if_context, ) }) .transpose()?; let success = success .and_then(|value| value.as_object()) - .map(|parent| { + .map(|success_schema| { UnevaluatedPropertiesValidator::compile( - parent, - get_unevaluated_props_schema(parent), - context, + success_schema, + get_transitive_unevaluated_props_schema(success_schema, parent), + &context.with_path("then"), ) }) .transpose()?; let failure = failure .and_then(|value| value.as_object()) - .map(|parent| { + .map(|failure_schema| { UnevaluatedPropertiesValidator::compile( - parent, - get_unevaluated_props_schema(parent), - context, + failure_schema, + get_transitive_unevaluated_props_schema(failure_schema, parent), + &context.with_path("else"), ) }) .transpose()?; @@ -886,22 +1109,26 @@ struct DependentSchemaSubvalidator { impl DependentSchemaSubvalidator { fn from_value<'a>( + parent: &'a Value, value: &'a Value, context: &CompilationContext, ) -> Result> { + let keyword_context = context.with_path("dependentSchemas"); let schemas = value .as_object() - .ok_or_else(|| unexpected_type(context, value, PrimitiveType::Object))?; + .ok_or_else(|| unexpected_type(&keyword_context, value, PrimitiveType::Object))?; let mut nodes = AHashMap::new(); + for (dependent_property_name, dependent_schema) in schemas { - let parent = dependent_schema + let dependent_schema = dependent_schema .as_object() .ok_or_else(ValidationError::null_schema)?; + let schema_context = keyword_context.with_path(dependent_property_name.to_string()); let node = UnevaluatedPropertiesValidator::compile( - parent, - get_unevaluated_props_schema(parent), - context, + dependent_schema, + get_transitive_unevaluated_props_schema(dependent_schema, parent), + &schema_context, )?; nodes.insert(dependent_property_name.to_string(), node); } @@ -985,31 +1212,34 @@ struct ReferenceSubvalidator { impl ReferenceSubvalidator { fn from_value<'a>( + parent: &'a Value, value: &'a Value, context: &CompilationContext, ) -> Result, ValidationError<'a>> { + let keyword_context = context.with_path("$ref"); let reference = value .as_str() - .ok_or_else(|| unexpected_type(context, value, PrimitiveType::String))?; + .ok_or_else(|| unexpected_type(&keyword_context, value, PrimitiveType::String))?; let reference_url = context.build_url(reference)?; - let (scope, resolved) = context + let (scope, resolved) = keyword_context .resolver - .resolve_fragment(context.config.draft(), &reference_url, reference) + .resolve_fragment(keyword_context.config.draft(), &reference_url, reference) .map_err(|e| e.into_owned())?; - let ref_context = CompilationContext::new( + let mut ref_context = CompilationContext::new( scope.into(), Arc::clone(&context.config), Arc::clone(&context.resolver), ); + ref_context.schema_path = keyword_context.schema_path.clone(); resolved .as_object() - .map(|parent| { + .map(|ref_schema| { UnevaluatedPropertiesValidator::compile( - parent, - get_unevaluated_props_schema(parent), + ref_schema, + get_transitive_unevaluated_props_schema(ref_schema, parent), &ref_context, ) .map(|validator| ReferenceSubvalidator { @@ -1072,10 +1302,15 @@ fn value_has_object_key(value: &Value, key: &str) -> bool { } } -fn get_unevaluated_props_schema(parent: &Map) -> &Value { - parent - .get("unevaluatedProperties") - .unwrap_or(&Value::Bool(false)) +fn get_transitive_unevaluated_props_schema<'a>( + leaf: &'a Map, + parent: &'a Value, +) -> &'a Value { + // We first try and if the leaf schema has `unevaluatedProperties` defined, and if so, we use + // that. Otherwise, we fallback to the parent schema value, which is the value of + // `unevaluatedProperties` as defined at the level of the schema right where `leaf_schema` + // lives. + leaf.get("unevaluatedProperties").unwrap_or(parent) } pub(crate) fn compile<'a>( @@ -1094,6 +1329,11 @@ pub(crate) fn compile<'a>( } } +fn boxed_errors<'a>(errors: Vec>) -> ErrorIterator<'a> { + let boxed_errors: ErrorIterator<'a> = Box::new(errors.into_iter()); + boxed_errors +} + fn unexpected_type<'a>( context: &CompilationContext, instance: &'a Value, @@ -1106,3 +1346,103 @@ fn unexpected_type<'a>( expected_type, ) } + +#[cfg(test)] +mod tests { + use crate::{tests_util, Draft}; + use serde_json::json; + + #[cfg(all(feature = "draft201909", not(feature = "draft202012")))] + const fn get_draft_version() -> Draft { + Draft::Draft201909 + } + + #[cfg(all(feature = "draft202012", not(feature = "draft201909")))] + const fn get_draft_version() -> Draft { + Draft::Draft202012 + } + + #[cfg(all(feature = "draft201909", feature = "draft202012"))] + const fn get_draft_version() -> Draft { + Draft::Draft202012 + } + + #[test] + fn one_of() { + tests_util::is_valid_with_draft( + get_draft_version(), + &json!({ + "oneOf": [ + { "properties": { "foo": { "const": "bar" } } }, + { "properties": { "foo": { "const": "quux" } } } + ], + "unevaluatedProperties": false + }), + &json!({ "foo": "quux" }), + ) + } + + #[test] + fn any_of() { + tests_util::is_valid_with_draft( + get_draft_version(), + &json!({ + "anyOf": [ + { "properties": { "foo": { "minLength": 10 } } }, + { "properties": { "foo": { "type": "string" } } } + ], + "unevaluatedProperties": false + }), + &json!({ "foo": "rut roh" }), + ) + } + + #[test] + fn all_of() { + tests_util::is_not_valid_with_draft( + get_draft_version(), + &json!({ + "allOf": [ + { "properties": { "foo": { "type": "string" } } }, + { "properties": { "foo": { "minLength": 10 } } } + ], + "unevaluatedProperties": false + }), + &json!({ "foo": "rut roh" }), + ) + } + + #[test] + fn all_of_with_additional_props_subschema() { + let schema = json!({ + "allOf": [ + { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { "type": "string" } + } + }, + { + "type": "object", + "additionalProperties": { "type": "string" } + } + ], + "unevaluatedProperties": false + }); + + tests_util::is_valid_with_draft( + get_draft_version(), + &schema, + &json!({ "foo": "wee", "another": "thing" }), + ); + + tests_util::is_not_valid_with_draft( + get_draft_version(), + &schema, + &json!({ "foo": "wee", "another": false }), + ); + } +} diff --git a/jsonschema/src/lib.rs b/jsonschema/src/lib.rs index ef65e8e..e43b98b 100644 --- a/jsonschema/src/lib.rs +++ b/jsonschema/src/lib.rs @@ -128,8 +128,7 @@ pub(crate) mod tests_util { use crate::ValidationError; use serde_json::Value; - pub(crate) fn is_not_valid(schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).unwrap(); + fn is_not_valid_inner(compiled: &JSONSchema, instance: &Value) { assert!( !compiled.is_valid(instance), "{} should not be valid (via is_valid)", @@ -147,6 +146,20 @@ pub(crate) mod tests_util { ); } + pub(crate) fn is_not_valid(schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).unwrap(); + is_not_valid_inner(&compiled, instance) + } + + #[cfg(any(feature = "draft201909", feature = "draft202012"))] + pub(crate) fn is_not_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { + let compiled = JSONSchema::options() + .with_draft(draft) + .compile(schema) + .unwrap(); + is_not_valid_inner(&compiled, instance) + } + pub(crate) fn expect_errors(schema: &Value, instance: &Value, errors: &[&str]) { assert_eq!( JSONSchema::compile(schema) @@ -159,8 +172,7 @@ pub(crate) mod tests_util { ) } - pub(crate) fn is_valid(schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).unwrap(); + fn is_valid_inner(compiled: &JSONSchema, instance: &Value) { assert!( compiled.is_valid(instance), "{} should be valid (via is_valid)", @@ -178,6 +190,20 @@ pub(crate) mod tests_util { ); } + pub(crate) fn is_valid(schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).unwrap(); + is_valid_inner(&compiled, instance); + } + + #[cfg(any(feature = "draft201909", feature = "draft202012"))] + pub(crate) fn is_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { + let compiled = JSONSchema::options() + .with_draft(draft) + .compile(schema) + .unwrap(); + is_valid_inner(&compiled, instance) + } + pub(crate) fn validate(schema: &Value, instance: &Value) -> ValidationError<'static> { let compiled = JSONSchema::compile(schema).unwrap(); let err = compiled