Skip to content

Commit

Permalink
feat: add support for unevaluatedProperties to draft 2019-09 and 2020-12
Browse files Browse the repository at this point in the history
  • Loading branch information
tobz committed Mar 15, 2023
1 parent 848b7b1 commit e43f0fd
Show file tree
Hide file tree
Showing 13 changed files with 1,463 additions and 202 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
- Bump `fraction` to `0.13`.
- Bump `iso8601` to `0.6`.
- Replace `lazy_static` with `once_cell`.
- Add support for `unevaluatedProperties`. (gated by the `draft201909`/`draft202012` feature flags)
- When using the draft 2019-09 or draft 2020-12 specification, `$ref` is now evaluated alongside
other keywords.

## [0.16.1] - 2022-10-20

Expand Down
38 changes: 19 additions & 19 deletions jsonschema/src/compilation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,15 @@ pub(crate) fn compile_validators<'a>(
)),
},
Value::Object(object) => {
if let Some(reference) = object.get("$ref") {
// In Draft 2019-09 and later, `$ref` can be evaluated alongside other attribute aka
// adjacent validation. We check here to see if adjacent validation is supported, and if
// so, we use the normal keyword validator collection logic.
//
// Otherwise, we isolate `$ref` and generate a schema reference validator directly.
let maybe_reference = object
.get("$ref")
.filter(|_| !keywords::ref_::supports_adjacent_validation(context.config.draft()));
if let Some(reference) = maybe_reference {
let unmatched_keywords = object
.iter()
.filter_map(|(k, v)| {
Expand All @@ -165,24 +173,16 @@ pub(crate) fn compile_validators<'a>(
}
})
.collect();
let mut validators = Vec::new();
if let Value::String(reference) = reference {
let validator = keywords::ref_::compile(schema, reference, &context)
.expect("Should always return Some")?;
validators.push(("$ref".to_string(), validator));
Ok(SchemaNode::new_from_keywords(
&context,
validators,
Some(unmatched_keywords),
))
} else {
Err(ValidationError::single_type_error(
JSONPointer::default(),
relative_path,
reference,
PrimitiveType::String,
))
}

let validator = keywords::ref_::compile(object, reference, &context)
.expect("should always return Some")?;

let validators = vec![("$ref".to_string(), validator)];
Ok(SchemaNode::new_from_keywords(
&context,
validators,
Some(unmatched_keywords),
))
} else {
let mut validators = Vec::with_capacity(object.len());
let mut unmatched_keywords = AHashMap::new();
Expand Down
34 changes: 34 additions & 0 deletions jsonschema/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ pub enum ValidationErrorKind {
Schema,
/// When the input value doesn't match one or multiple required types.
Type { kind: TypeKind },
/// Unexpected properties.
UnevaluatedProperties { unexpected: Vec<String> },
/// When the input array has non-unique elements.
UniqueItems,
/// Reference contains unknown scheme.
Expand Down Expand Up @@ -692,6 +694,19 @@ impl<'a> ValidationError<'a> {
schema_path,
}
}
pub(crate) const fn unevaluated_properties(
schema_path: JSONPointer,
instance_path: JSONPointer,
instance: &'a Value,
unexpected: Vec<String>,
) -> ValidationError<'a> {
ValidationError {
instance_path,
instance: Cow::Borrowed(instance),
kind: ValidationErrorKind::UnevaluatedProperties { unexpected },
schema_path,
}
}
pub(crate) const fn unique_items(
schema_path: JSONPointer,
instance_path: JSONPointer,
Expand Down Expand Up @@ -937,6 +952,25 @@ impl fmt::Display for ValidationError<'_> {
ValidationErrorKind::MultipleOf { multiple_of } => {
write!(f, "{} is not a multiple of {}", self.instance, multiple_of)
}
ValidationErrorKind::UnevaluatedProperties { unexpected } => {
let verb = {
if unexpected.len() == 1 {
"was"
} else {
"were"
}
};
write!(
f,
"Unevaluated properties are not allowed ({} {} unexpected)",
unexpected
.iter()
.map(|x| format!("'{}'", x))
.collect::<Vec<String>>()
.join(", "),
verb
)
}
ValidationErrorKind::UniqueItems => {
write!(f, "{} has non-unique elements", self.instance)
}
Expand Down
159 changes: 15 additions & 144 deletions jsonschema/src/keywords/additional_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,76 +12,12 @@ use crate::{
keywords::CompilationResult,
output::{Annotations, BasicOutput, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
properties::*,
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
use ahash::AHashMap;
use fancy_regex::Regex;
use serde_json::{Map, Value};

pub(crate) type PatternedValidators = Vec<(Regex, SchemaNode)>;

/// Provide mapping API to get validators associated with a property from the underlying storage.
pub(crate) trait PropertiesValidatorsMap: Send + Sync {
fn get_validator(&self, property: &str) -> Option<&SchemaNode>;
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)>;
}

// Iterating over a small vector and comparing strings is faster than a map lookup
const MAP_SIZE_THRESHOLD: usize = 40;
pub(crate) type SmallValidatorsMap = Vec<(String, SchemaNode)>;
pub(crate) type BigValidatorsMap = AHashMap<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 {
($node:expr, $value:ident) => {{
$node.is_valid($value)
Expand Down Expand Up @@ -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
///
/// ```json
Expand Down Expand Up @@ -345,16 +250,11 @@ impl AdditionalPropertiesNotEmptyFalseValidator<BigValidatorsMap> {
}
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseValidator<M> {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item {
if let Some(node) = self.properties.get_validator(property) {
is_valid_pattern_schema!(node, value)
}
// No extra properties are allowed
return false;
}
if let Value::Object(props) = instance {
are_properties_valid(&self.properties, props, |_| false)
} else {
true
}
true
}

fn validate<'instance>(
Expand Down Expand Up @@ -484,17 +384,13 @@ impl AdditionalPropertiesNotEmptyValidator<BigValidatorsMap> {
}
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValidator<M> {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(map) = instance {
for (property, value) in map {
if let Some(property_validators) = self.properties.get_validator(property) {
is_valid_pattern_schema!(property_validators, value)
}
if !self.node.is_valid(value) {
return false;
}
}
if let Value::Object(props) = instance {
are_properties_valid(&self.properties, props, |instance| {
self.node.is_valid(instance)
})
} else {
true
}
true
}

fn validate<'instance>(
Expand Down Expand Up @@ -1263,7 +1159,7 @@ pub(crate) fn compile<'a>(
Value::Bool(true) => None, // "additionalProperties" are "true" by default
Value::Bool(false) => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesWithPatternsNotEmptyFalseValidator,
properties,
compiled_patterns,
Expand All @@ -1278,7 +1174,7 @@ pub(crate) fn compile<'a>(
}
_ => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesWithPatternsNotEmptyValidator,
properties,
schema,
Expand All @@ -1305,7 +1201,7 @@ pub(crate) fn compile<'a>(
Value::Bool(true) => None, // "additionalProperties" are "true" by default
Value::Bool(false) => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesNotEmptyFalseValidator,
properties,
context
Expand All @@ -1317,7 +1213,7 @@ pub(crate) fn compile<'a>(
}
_ => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesNotEmptyValidator,
properties,
schema,
Expand All @@ -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)]
mod tests {
use crate::tests_util;
Expand Down
2 changes: 2 additions & 0 deletions jsonschema/src/keywords/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub(crate) mod property_names;
pub(crate) mod ref_;
pub(crate) mod required;
pub(crate) mod type_;
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
pub(crate) mod unevaluated_properties;
pub(crate) mod unique_items;
use crate::{error, validator::Validate};

Expand Down
Loading

0 comments on commit e43f0fd

Please sign in to comment.