Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for unevaluatedProperties + a few other changes. #419

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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