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
This commit is contained in:
parent
066a0da459
commit
fde9c4405c
|
@ -26,6 +26,7 @@ use serde_json::{Map, Value};
|
|||
struct UnevaluatedPropertiesValidator {
|
||||
schema_path: JSONPointer,
|
||||
unevaluated: UnevaluatedSubvalidator,
|
||||
additional: Option<UnevaluatedSubvalidator>,
|
||||
properties: Option<PropertySubvalidator>,
|
||||
patterns: Option<PatternSubvalidator>,
|
||||
conditional: Option<Box<ConditionalSubvalidator>>,
|
||||
|
@ -40,7 +41,20 @@ impl UnevaluatedPropertiesValidator {
|
|||
schema: &'a Value,
|
||||
context: &CompilationContext,
|
||||
) -> Result<Self, ValidationError<'a>> {
|
||||
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<UnevaluatedPropertiesValidator>,
|
||||
behavior: SubschemaBehavior,
|
||||
subvalidators: Vec<(SchemaNode, UnevaluatedPropertiesValidator)>,
|
||||
}
|
||||
|
||||
impl SubschemaSubvalidator {
|
||||
fn from_values<'a>(
|
||||
parent: &'a Value,
|
||||
values: &'a [Value],
|
||||
behavior: SubschemaBehavior,
|
||||
context: &CompilationContext,
|
||||
) -> Result<Self, ValidationError<'a>> {
|
||||
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<bool> {
|
||||
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::<Vec<_>>();
|
||||
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<ErrorIterator<'instance>> {
|
||||
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::<Vec<_>>());
|
||||
|
||||
let instance_result = node.validate(instance, instance_path).collect::<Vec<_>>();
|
||||
|
||||
(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::<Vec<_>>();
|
||||
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<BasicOutput<'a>> {
|
||||
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::<Vec<_>>();
|
||||
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<String, Value>,
|
||||
value: &'a Value,
|
||||
context: &CompilationContext,
|
||||
) -> Result<Self, ValidationError<'a>> {
|
||||
// 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<Self, ValidationError<'a>> {
|
||||
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<Self, ValidationError<'a>> {
|
||||
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<Option<Self>, 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<String, Value>) -> &Value {
|
||||
parent
|
||||
.get("unevaluatedProperties")
|
||||
.unwrap_or(&Value::Bool(false))
|
||||
fn get_transitive_unevaluated_props_schema<'a>(
|
||||
leaf: &'a Map<String, Value>,
|
||||
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<ValidationError<'a>>) -> 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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue