feat: add support for unevaluatedProperties to draft 2019-09 and 2020-12
This commit is contained in:
parent
848b7b1284
commit
63494600b8
|
@ -9,6 +9,9 @@
|
||||||
- Bump `fraction` to `0.13`.
|
- Bump `fraction` to `0.13`.
|
||||||
- Bump `iso8601` to `0.6`.
|
- Bump `iso8601` to `0.6`.
|
||||||
- Replace `lazy_static` with `once_cell`.
|
- 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
|
## [0.16.1] - 2022-10-20
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,15 @@ pub(crate) fn compile_validators<'a>(
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Value::Object(object) => {
|
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
|
let unmatched_keywords = object
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(k, v)| {
|
.filter_map(|(k, v)| {
|
||||||
|
@ -165,24 +173,16 @@ pub(crate) fn compile_validators<'a>(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut validators = Vec::new();
|
|
||||||
if let Value::String(reference) = reference {
|
let validator = keywords::ref_::compile(object, reference, &context)
|
||||||
let validator = keywords::ref_::compile(schema, reference, &context)
|
.expect("should always return Some")?;
|
||||||
.expect("Should always return Some")?;
|
|
||||||
validators.push(("$ref".to_string(), validator));
|
let validators = vec![("$ref".to_string(), validator)];
|
||||||
Ok(SchemaNode::new_from_keywords(
|
Ok(SchemaNode::new_from_keywords(
|
||||||
&context,
|
&context,
|
||||||
validators,
|
validators,
|
||||||
Some(unmatched_keywords),
|
Some(unmatched_keywords),
|
||||||
))
|
))
|
||||||
} else {
|
|
||||||
Err(ValidationError::single_type_error(
|
|
||||||
JSONPointer::default(),
|
|
||||||
relative_path,
|
|
||||||
reference,
|
|
||||||
PrimitiveType::String,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let mut validators = Vec::with_capacity(object.len());
|
let mut validators = Vec::with_capacity(object.len());
|
||||||
let mut unmatched_keywords = AHashMap::new();
|
let mut unmatched_keywords = AHashMap::new();
|
||||||
|
|
|
@ -138,6 +138,8 @@ pub enum ValidationErrorKind {
|
||||||
Schema,
|
Schema,
|
||||||
/// When the input value doesn't match one or multiple required types.
|
/// When the input value doesn't match one or multiple required types.
|
||||||
Type { kind: TypeKind },
|
Type { kind: TypeKind },
|
||||||
|
/// Unexpected properties.
|
||||||
|
UnevaluatedProperties { unexpected: Vec<String> },
|
||||||
/// When the input array has non-unique elements.
|
/// When the input array has non-unique elements.
|
||||||
UniqueItems,
|
UniqueItems,
|
||||||
/// Reference contains unknown scheme.
|
/// Reference contains unknown scheme.
|
||||||
|
@ -692,6 +694,19 @@ impl<'a> ValidationError<'a> {
|
||||||
schema_path,
|
schema_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub(crate) const fn unevaluated_properties(
|
||||||
|
schema_path: JSONPointer,
|
||||||
|
instance_path: JSONPointer,
|
||||||
|
instance: &'a Value,
|
||||||
|
unexpected: Vec<String>,
|
||||||
|
) -> ValidationError<'a> {
|
||||||
|
ValidationError {
|
||||||
|
instance_path,
|
||||||
|
instance: Cow::Borrowed(instance),
|
||||||
|
kind: ValidationErrorKind::UnevaluatedProperties { unexpected },
|
||||||
|
schema_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(crate) const fn unique_items(
|
pub(crate) const fn unique_items(
|
||||||
schema_path: JSONPointer,
|
schema_path: JSONPointer,
|
||||||
instance_path: JSONPointer,
|
instance_path: JSONPointer,
|
||||||
|
@ -937,6 +952,25 @@ impl fmt::Display for ValidationError<'_> {
|
||||||
ValidationErrorKind::MultipleOf { multiple_of } => {
|
ValidationErrorKind::MultipleOf { multiple_of } => {
|
||||||
write!(f, "{} is not a multiple of {}", self.instance, 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::<Vec<String>>()
|
||||||
|
.join(", "),
|
||||||
|
verb
|
||||||
|
)
|
||||||
|
}
|
||||||
ValidationErrorKind::UniqueItems => {
|
ValidationErrorKind::UniqueItems => {
|
||||||
write!(f, "{} has non-unique elements", self.instance)
|
write!(f, "{} has non-unique elements", self.instance)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,76 +12,12 @@ use crate::{
|
||||||
keywords::CompilationResult,
|
keywords::CompilationResult,
|
||||||
output::{Annotations, BasicOutput, OutputUnit},
|
output::{Annotations, BasicOutput, OutputUnit},
|
||||||
paths::{AbsolutePath, InstancePath, JSONPointer},
|
paths::{AbsolutePath, InstancePath, JSONPointer},
|
||||||
|
properties::*,
|
||||||
schema_node::SchemaNode,
|
schema_node::SchemaNode,
|
||||||
validator::{format_validators, PartialApplication, Validate},
|
validator::{format_validators, PartialApplication, Validate},
|
||||||
};
|
};
|
||||||
use ahash::AHashMap;
|
|
||||||
use fancy_regex::Regex;
|
|
||||||
use serde_json::{Map, Value};
|
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<String, SchemaNode>;
|
|
||||||
|
|
||||||
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::<SmallValidatorsMap>::compile(
|
|
||||||
map, $($arg, )*
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Some($validator::<BigValidatorsMap>::compile(
|
|
||||||
map, $($arg, )*
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(Err(ValidationError::null_schema()))
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
macro_rules! is_valid {
|
macro_rules! is_valid {
|
||||||
($node:expr, $value:ident) => {{
|
($node:expr, $value:ident) => {{
|
||||||
$node.is_valid($value)
|
$node.is_valid($value)
|
||||||
|
@ -124,37 +60,6 @@ macro_rules! validate {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_small_map<'a>(
|
|
||||||
map: &'a Map<String, Value>,
|
|
||||||
context: &CompilationContext,
|
|
||||||
) -> Result<SmallValidatorsMap, ValidationError<'a>> {
|
|
||||||
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<String, Value>,
|
|
||||||
context: &CompilationContext,
|
|
||||||
) -> Result<BigValidatorsMap, ValidationError<'a>> {
|
|
||||||
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
|
/// # Schema example
|
||||||
///
|
///
|
||||||
/// ```json
|
/// ```json
|
||||||
|
@ -345,16 +250,11 @@ impl AdditionalPropertiesNotEmptyFalseValidator<BigValidatorsMap> {
|
||||||
}
|
}
|
||||||
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseValidator<M> {
|
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseValidator<M> {
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
fn is_valid(&self, instance: &Value) -> bool {
|
||||||
if let Value::Object(item) = instance {
|
if let Value::Object(props) = instance {
|
||||||
for (property, value) in item {
|
are_properties_valid(&self.properties, props, |_| false)
|
||||||
if let Some(node) = self.properties.get_validator(property) {
|
} else {
|
||||||
is_valid_pattern_schema!(node, value)
|
true
|
||||||
}
|
|
||||||
// No extra properties are allowed
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate<'instance>(
|
fn validate<'instance>(
|
||||||
|
@ -484,17 +384,13 @@ impl AdditionalPropertiesNotEmptyValidator<BigValidatorsMap> {
|
||||||
}
|
}
|
||||||
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValidator<M> {
|
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValidator<M> {
|
||||||
fn is_valid(&self, instance: &Value) -> bool {
|
fn is_valid(&self, instance: &Value) -> bool {
|
||||||
if let Value::Object(map) = instance {
|
if let Value::Object(props) = instance {
|
||||||
for (property, value) in map {
|
are_properties_valid(&self.properties, props, |instance| {
|
||||||
if let Some(property_validators) = self.properties.get_validator(property) {
|
self.node.is_valid(instance)
|
||||||
is_valid_pattern_schema!(property_validators, value)
|
})
|
||||||
}
|
} else {
|
||||||
if !self.node.is_valid(value) {
|
true
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate<'instance>(
|
fn validate<'instance>(
|
||||||
|
@ -1263,7 +1159,7 @@ pub(crate) fn compile<'a>(
|
||||||
Value::Bool(true) => None, // "additionalProperties" are "true" by default
|
Value::Bool(true) => None, // "additionalProperties" are "true" by default
|
||||||
Value::Bool(false) => {
|
Value::Bool(false) => {
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
dynamic_map!(
|
compile_dynamic_prop_map_validator!(
|
||||||
AdditionalPropertiesWithPatternsNotEmptyFalseValidator,
|
AdditionalPropertiesWithPatternsNotEmptyFalseValidator,
|
||||||
properties,
|
properties,
|
||||||
compiled_patterns,
|
compiled_patterns,
|
||||||
|
@ -1278,7 +1174,7 @@ pub(crate) fn compile<'a>(
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
dynamic_map!(
|
compile_dynamic_prop_map_validator!(
|
||||||
AdditionalPropertiesWithPatternsNotEmptyValidator,
|
AdditionalPropertiesWithPatternsNotEmptyValidator,
|
||||||
properties,
|
properties,
|
||||||
schema,
|
schema,
|
||||||
|
@ -1305,7 +1201,7 @@ pub(crate) fn compile<'a>(
|
||||||
Value::Bool(true) => None, // "additionalProperties" are "true" by default
|
Value::Bool(true) => None, // "additionalProperties" are "true" by default
|
||||||
Value::Bool(false) => {
|
Value::Bool(false) => {
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
dynamic_map!(
|
compile_dynamic_prop_map_validator!(
|
||||||
AdditionalPropertiesNotEmptyFalseValidator,
|
AdditionalPropertiesNotEmptyFalseValidator,
|
||||||
properties,
|
properties,
|
||||||
context
|
context
|
||||||
|
@ -1317,7 +1213,7 @@ pub(crate) fn compile<'a>(
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
dynamic_map!(
|
compile_dynamic_prop_map_validator!(
|
||||||
AdditionalPropertiesNotEmptyValidator,
|
AdditionalPropertiesNotEmptyValidator,
|
||||||
properties,
|
properties,
|
||||||
schema,
|
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<String, Value>,
|
|
||||||
context: &CompilationContext,
|
|
||||||
) -> Result<PatternedValidators, ValidationError<'a>> {
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tests_util;
|
use crate::tests_util;
|
||||||
|
|
|
@ -34,6 +34,8 @@ pub(crate) mod property_names;
|
||||||
pub(crate) mod ref_;
|
pub(crate) mod ref_;
|
||||||
pub(crate) mod required;
|
pub(crate) mod required;
|
||||||
pub(crate) mod type_;
|
pub(crate) mod type_;
|
||||||
|
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
|
||||||
|
pub(crate) mod unevaluated_properties;
|
||||||
pub(crate) mod unique_items;
|
pub(crate) mod unique_items;
|
||||||
use crate::{error, validator::Validate};
|
use crate::{error, validator::Validate};
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ use crate::{
|
||||||
error::{error, ErrorIterator},
|
error::{error, ErrorIterator},
|
||||||
keywords::CompilationResult,
|
keywords::CompilationResult,
|
||||||
paths::{InstancePath, JSONPointer},
|
paths::{InstancePath, JSONPointer},
|
||||||
|
primitive_type::PrimitiveType,
|
||||||
resolver::Resolver,
|
resolver::Resolver,
|
||||||
schema_node::SchemaNode,
|
schema_node::SchemaNode,
|
||||||
validator::Validate,
|
validator::Validate,
|
||||||
CompilationOptions,
|
CompilationOptions, Draft, ValidationError,
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde_json::Value;
|
use serde_json::{Map, Value};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -123,11 +124,33 @@ impl core::fmt::Display for RefValidator {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn compile<'a>(
|
pub(crate) fn compile<'a>(
|
||||||
_: &'a Value,
|
_: &'a Map<String, Value>,
|
||||||
reference: &'a str,
|
schema: &'a Value,
|
||||||
context: &CompilationContext,
|
context: &CompilationContext,
|
||||||
) -> Option<CompilationResult<'a>> {
|
) -> Option<CompilationResult<'a>> {
|
||||||
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)]
|
#[cfg(test)]
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -91,6 +91,7 @@ mod keywords;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod primitive_type;
|
pub mod primitive_type;
|
||||||
|
pub(crate) mod properties;
|
||||||
mod resolver;
|
mod resolver;
|
||||||
mod schema_node;
|
mod schema_node;
|
||||||
mod schemas;
|
mod schemas;
|
||||||
|
|
|
@ -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
|
/// An absolute reference
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct AbsolutePath(url::Url);
|
pub struct AbsolutePath(url::Url);
|
||||||
|
|
|
@ -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<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<Self, ValidationError<'a>>
|
||||||
|
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<String, SchemaNode>;
|
||||||
|
|
||||||
|
impl PropertiesValidatorsMap for SmallValidatorsMap {
|
||||||
|
fn from_map<'a>(
|
||||||
|
map: &'a Map<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<Self, ValidationError<'a>>
|
||||||
|
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<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<Self, ValidationError<'a>>
|
||||||
|
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<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<SmallValidatorsMap, ValidationError<'a>> {
|
||||||
|
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<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<BigValidatorsMap, ValidationError<'a>> {
|
||||||
|
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<M, F>(prop_map: &M, props: &Map<String, Value>, 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<String, Value>,
|
||||||
|
context: &CompilationContext,
|
||||||
|
) -> Result<PatternedValidators, ValidationError<'a>> {
|
||||||
|
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::<SmallValidatorsMap>::compile(
|
||||||
|
map, $($arg, )*
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Some($validator::<BigValidatorsMap>::compile(
|
||||||
|
map, $($arg, )*
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(Err(ValidationError::null_schema()))
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use compile_dynamic_prop_map_validator;
|
|
@ -50,6 +50,7 @@ impl Draft {
|
||||||
#[allow(clippy::match_same_arms)]
|
#[allow(clippy::match_same_arms)]
|
||||||
pub(crate) fn get_validator(self, keyword: &str) -> Option<CompileFunc> {
|
pub(crate) fn get_validator(self, keyword: &str) -> Option<CompileFunc> {
|
||||||
match keyword {
|
match keyword {
|
||||||
|
"$ref" => Some(keywords::ref_::compile),
|
||||||
"additionalItems" => Some(keywords::additional_items::compile),
|
"additionalItems" => Some(keywords::additional_items::compile),
|
||||||
"additionalProperties" => Some(keywords::additional_properties::compile),
|
"additionalProperties" => Some(keywords::additional_properties::compile),
|
||||||
"allOf" => Some(keywords::all_of::compile),
|
"allOf" => Some(keywords::all_of::compile),
|
||||||
|
@ -167,6 +168,13 @@ impl Draft {
|
||||||
#[cfg(feature = "draft202012")]
|
#[cfg(feature = "draft202012")]
|
||||||
Draft::Draft202012 => Some(keywords::type_::compile),
|
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),
|
"uniqueItems" => Some(keywords::unique_items::compile),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
/// Set the annotation that will be returned for the current validator. If this
|
||||||
/// `PartialApplication` is invalid then this method does nothing
|
/// `PartialApplication` is invalid then this method does nothing
|
||||||
pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) {
|
pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) {
|
||||||
|
|
|
@ -13,8 +13,6 @@ use std::fs;
|
||||||
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
|
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
|
||||||
r"id_0_[0-6]",
|
r"id_0_[0-6]",
|
||||||
// Various types of new behavior used in the `$ref` context
|
// Various types of new behavior used in the `$ref` context
|
||||||
"ref_5_1",
|
|
||||||
"ref_13_0",
|
|
||||||
"refRemote_4_0",
|
"refRemote_4_0",
|
||||||
"refRemote_4_1",
|
"refRemote_4_1",
|
||||||
"recursiveRef_0_3",
|
"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_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265
|
||||||
r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266
|
r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266
|
||||||
r"unevaluatedItems_.+",
|
r"unevaluatedItems_.+",
|
||||||
r"unevaluatedProperties_.+",
|
|
||||||
}))]
|
}))]
|
||||||
#[cfg_attr(feature = "draft202012", json_schema_test_suite("tests/suite", "draft2020-12", {
|
#[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
|
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`)
|
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
|
||||||
r"id_0_[0-6]",
|
r"id_0_[0-6]",
|
||||||
// Various types of new behavior used in the `$ref` context
|
// Various types of new behavior used in the `$ref` context
|
||||||
"ref_5_1",
|
|
||||||
"ref_13_0",
|
|
||||||
"refRemote_4_0",
|
"refRemote_4_0",
|
||||||
"refRemote_4_1",
|
"refRemote_4_1",
|
||||||
"recursiveRef_0_3",
|
"recursiveRef_0_3",
|
||||||
|
@ -72,7 +67,6 @@ use std::fs;
|
||||||
r"optional_format_uri_reference_.+",
|
r"optional_format_uri_reference_.+",
|
||||||
r"optional_format_uri_template_.+",
|
r"optional_format_uri_template_.+",
|
||||||
r"unevaluatedItems_.+",
|
r"unevaluatedItems_.+",
|
||||||
r"unevaluatedProperties_.+",
|
|
||||||
}))]
|
}))]
|
||||||
fn test_draft(_server_address: &str, test_case: TestCase) {
|
fn test_draft(_server_address: &str, test_case: TestCase) {
|
||||||
let draft_version = match test_case.draft_version.as_ref() {
|
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()
|
.with_meta_schemas()
|
||||||
.should_validate_formats(true)
|
.should_validate_formats(true)
|
||||||
.compile(&test_case.schema)
|
.compile(&test_case.schema)
|
||||||
.unwrap();
|
.expect("should not fail to compile schema");
|
||||||
|
|
||||||
let result = compiled.validate(&test_case.instance);
|
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();
|
let first_error = errors_iterator.next();
|
||||||
assert!(
|
assert!(
|
||||||
first_error.is_none(),
|
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.schema,
|
||||||
test_case.instance,
|
test_case.instance,
|
||||||
first_error,
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let output = compiled.apply(&test_case.instance).basic();
|
|
||||||
if !output.is_valid() {
|
|
||||||
panic!(
|
|
||||||
"Schema: {}\nInstance: {}\nError: {:?}",
|
|
||||||
test_case.schema, test_case.instance, output
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert!(
|
assert!(
|
||||||
result.is_err(),
|
compiled.is_valid(&test_case.instance),
|
||||||
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
|
"Test case should be valid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
|
||||||
|
test_case.group_description,
|
||||||
|
test_case.test_case_description,
|
||||||
test_case.schema,
|
test_case.schema,
|
||||||
test_case.instance,
|
test_case.instance,
|
||||||
);
|
);
|
||||||
let errors: Vec<_> = result.expect_err("Errors").collect();
|
let output = compiled.apply(&test_case.instance).basic();
|
||||||
|
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(),
|
||||||
|
"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 = result.unwrap_err();
|
||||||
for error in errors {
|
for error in errors {
|
||||||
let pointer = error.instance_path.to_string();
|
let pointer = error.instance_path.to_string();
|
||||||
assert_eq!(test_case.instance.pointer(&pointer), Some(&*error.instance))
|
assert_eq!(
|
||||||
}
|
test_case.instance.pointer(&pointer), Some(&*error.instance),
|
||||||
if compiled.is_valid(&test_case.instance) {
|
"Expected error instance did not match actual error instance:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}",
|
||||||
panic!(
|
test_case.group_description,
|
||||||
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
|
test_case.test_case_description,
|
||||||
test_case.schema, test_case.instance,
|
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();
|
let output = compiled.apply(&test_case.instance).basic();
|
||||||
if output.is_valid() {
|
assert!(
|
||||||
panic!(
|
!output.is_valid(),
|
||||||
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
|
"Test case should be invalid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
|
||||||
test_case.schema, test_case.instance,
|
test_case.group_description,
|
||||||
);
|
test_case.test_case_description,
|
||||||
}
|
test_case.schema,
|
||||||
|
test_case.instance,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue