feat: implement basic output format

This commit is contained in:
Alex Good 2021-07-29 18:10:44 +01:00 committed by Dmitry Dygalo
parent e45ebe6c34
commit 9daae35d2d
26 changed files with 3023 additions and 493 deletions

View File

@ -22,6 +22,7 @@ draft201909 = []
[dependencies]
serde_json = "1"
serde = "1"
url = "2"
lazy_static = "1"
percent-encoding = "2"
@ -32,7 +33,7 @@ chrono = ">= 0.2"
reqwest = { version = ">= 0.10", features = ["blocking", "json"], optional = true}
parking_lot = ">= 0.1"
num-cmp = ">= 0.1"
ahash = "0.7"
ahash = { version = "0.7", features = ["serde"] }
structopt = { version = ">= 0.3", optional = true }
itoa = "0.4"
fraction = { version = "0.8", default-features = false, features = ["with-bigint"] }

View File

@ -1,5 +1,6 @@
use super::options::CompilationOptions;
use crate::{
compilation::DEFAULT_SCOPE,
paths::{InstancePath, JSONPointer, PathChunk},
schemas,
};
@ -7,18 +8,74 @@ use serde_json::Value;
use std::borrow::Cow;
use url::{ParseError, Url};
static DEFAULT_SCHEME: &str = "json-schema";
/// Context holds information about used draft and current scope.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct CompilationContext<'a> {
pub(crate) scope: Cow<'a, Url>,
base_uri: BaseUri<'a>,
pub(crate) config: &'a CompilationOptions,
pub(crate) schema_path: InstancePath<'a>,
}
#[derive(Debug, Clone)]
pub(crate) enum BaseUri<'a> {
/// A base URL was specified, either because we know a reasonable base URI where we retrieved the
/// schema or because a URI was specified via $id
Known(Cow<'a, Url>),
/// We don't know a base URI for this schema
Unknown,
}
impl<'a> BaseUri<'a> {
pub(crate) fn with_new_scope(&self, new_scope: &str) -> Result<Self, ParseError> {
let options = match self {
BaseUri::Known(u) => Url::options().base_url(Some(u)),
BaseUri::Unknown => Url::options().base_url(Some(&DEFAULT_SCOPE)),
};
Ok(options.parse(new_scope)?.into())
}
}
impl<'a> From<Option<Url>> for BaseUri<'a> {
fn from(u: Option<Url>) -> Self {
u.map_or(BaseUri::Unknown, |u| u.into())
}
}
impl<'a> From<&'a Url> for BaseUri<'a> {
fn from(u: &'a Url) -> Self {
if u.scheme() == DEFAULT_SCHEME {
BaseUri::Unknown
} else {
BaseUri::Known(Cow::Borrowed(u))
}
}
}
impl<'a> From<Url> for BaseUri<'a> {
fn from(u: Url) -> Self {
if u.scheme() == DEFAULT_SCHEME {
BaseUri::Unknown
} else {
BaseUri::Known(Cow::Owned(u))
}
}
}
impl<'a> From<&'a BaseUri<'a>> for Cow<'a, Url> {
fn from(uri: &BaseUri<'a>) -> Self {
match uri {
BaseUri::Unknown => Cow::Borrowed(&DEFAULT_SCOPE),
BaseUri::Known(u) => u.clone(),
}
}
}
impl<'a> CompilationContext<'a> {
pub(crate) const fn new(scope: Url, config: &'a CompilationOptions) -> Self {
pub(crate) const fn new(scope: BaseUri<'a>, config: &'a CompilationOptions) -> Self {
CompilationContext {
scope: Cow::Owned(scope),
base_uri: scope,
config,
schema_path: InstancePath::new(),
}
@ -37,15 +94,14 @@ impl<'a> CompilationContext<'a> {
#[inline]
pub(crate) fn push(&'a self, schema: &Value) -> Result<Self, ParseError> {
if let Some(id) = schemas::id_of(self.config.draft(), schema) {
let scope = Url::options().base_url(Some(&self.scope)).parse(id)?;
Ok(CompilationContext {
scope: Cow::Owned(scope),
base_uri: self.base_uri.with_new_scope(id)?,
config: self.config,
schema_path: self.schema_path.clone(),
})
} else {
Ok(CompilationContext {
scope: Cow::Borrowed(self.scope.as_ref()),
base_uri: self.base_uri.clone(),
config: self.config,
schema_path: self.schema_path.clone(),
})
@ -56,7 +112,7 @@ impl<'a> CompilationContext<'a> {
pub(crate) fn with_path(&'a self, chunk: impl Into<PathChunk>) -> Self {
let schema_path = self.schema_path.push(chunk);
CompilationContext {
scope: Cow::Borrowed(self.scope.as_ref()),
base_uri: self.base_uri.clone(),
config: self.config,
schema_path,
}
@ -76,6 +132,16 @@ impl<'a> CompilationContext<'a> {
/// Build a new URL. Used for `ref` compilation to keep their full paths.
pub(crate) fn build_url(&self, reference: &str) -> Result<Url, ParseError> {
Url::options().base_url(Some(&self.scope)).parse(reference)
let cowbase: Cow<Url> = (&self.base_uri).into();
Url::options()
.base_url(Some(cowbase.as_ref()))
.parse(reference)
}
pub(crate) fn base_uri(&self) -> Option<Url> {
match &self.base_uri {
BaseUri::Known(u) => Some(u.as_ref().clone()),
BaseUri::Unknown => None,
}
}
}

View File

@ -5,9 +5,10 @@ pub(crate) mod context;
pub(crate) mod options;
use crate::{
error::ErrorIterator, keywords, paths::InstancePath, resolver::Resolver, validator::Validators,
Draft, ValidationError,
error::ErrorIterator, keywords, paths::InstancePath, resolver::Resolver,
schema_node::SchemaNode, validator::Validate, Draft, Output, ValidationError,
};
use ahash::AHashMap;
use context::CompilationContext;
use options::CompilationOptions;
use serde_json::Value;
@ -20,7 +21,7 @@ pub(crate) const DEFAULT_ROOT_URL: &str = "json-schema:///";
#[derive(Debug)]
pub struct JSONSchema {
pub(crate) schema: Arc<Value>,
pub(crate) validators: Validators,
pub(crate) node: SchemaNode,
pub(crate) resolver: Resolver,
config: CompilationOptions,
}
@ -61,9 +62,8 @@ impl JSONSchema {
pub fn validate<'a>(&'a self, instance: &'a Value) -> Result<(), ErrorIterator<'a>> {
let instance_path = InstancePath::new();
let mut errors = self
.validators
.iter()
.flat_map(move |validator| validator.validate(self, instance, &instance_path))
.node
.validate(self, instance, &instance_path)
.peekable();
if errors.peek().is_none() {
Ok(())
@ -78,9 +78,42 @@ impl JSONSchema {
#[must_use]
#[inline]
pub fn is_valid(&self, instance: &Value) -> bool {
self.validators
.iter()
.all(|validator| validator.is_valid(self, instance))
self.node.is_valid(self, instance)
}
/// Apply the schema and return an `Output`. No actual work is done at this point, the
/// evaluation of the schema is deferred until a method is called on the `Output`. This is
/// because different output formats will have different performance characteristics.
///
/// # Examples
///
/// "basic" output format
///
/// ```rust
/// # use crate::jsonschema::{Draft, Output, BasicOutput, JSONSchema};
/// let schema_json = serde_json::json!({
/// "title": "string value",
/// "type": "string"
/// });
/// let instance = serde_json::json!{"some string"};
/// let schema = JSONSchema::options().compile(&schema_json).unwrap();
/// let output: BasicOutput = schema.apply(&instance).basic();
/// let output_json = serde_json::to_value(output).unwrap();
/// assert_eq!(output_json, serde_json::json!({
/// "valid": true,
/// "annotations": [
/// {
/// "keywordLocation": "",
/// "instanceLocation": "",
/// "annotations": {
/// "title": "string value"
/// }
/// }
/// ]
/// }));
/// ```
pub const fn apply<'a, 'b>(&'a self, instance: &'b Value) -> Output<'a, 'b> {
Output::new(self, &self.node, instance)
}
/// The [`Draft`] which this schema was compiled against
@ -99,34 +132,90 @@ impl JSONSchema {
pub(crate) fn compile_validators<'a, 'c>(
schema: &'a Value,
context: &'c CompilationContext,
) -> Result<Validators, ValidationError<'a>> {
) -> Result<SchemaNode, ValidationError<'a>> {
let context = context.push(schema)?;
let relative_path = context.clone().into_pointer();
match schema {
Value::Bool(value) => match value {
true => Ok(vec![]),
false => Ok(vec![keywords::boolean::FalseValidator::compile(
context.into_pointer(),
)
.expect("Should always compile")]),
true => Ok(SchemaNode::new_from_boolean(&context, None)),
false => Ok(SchemaNode::new_from_boolean(
&context,
Some(
keywords::boolean::FalseValidator::compile(relative_path)
.expect("Should always compile"),
),
)),
},
Value::Object(object) => {
if let Some(reference) = object.get("$ref") {
let unmatched_keywords = object
.iter()
.filter_map(|(k, v)| {
if k.as_str() != "$ref" {
Some((k.clone(), v.clone()))
} else {
None
}
})
.collect();
let mut validators = Vec::new();
if let Value::String(reference) = reference {
Ok(vec![keywords::ref_::compile(schema, reference, &context)
.expect("Should always return Some")?])
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::schema(schema))
}
} else {
let mut validators = Vec::with_capacity(object.len());
let mut unmatched_keywords = AHashMap::new();
let mut is_if = false;
let mut is_props = false;
for (keyword, subschema) in object {
if let Some(compilation_func) = context.config.draft().get_validator(keyword) {
if let Some(validator) = compilation_func(object, subschema, &context) {
validators.push(validator?)
}
if keyword == "if" {
is_if = true;
}
if keyword == "properties"
|| keyword == "additionalProperties"
|| keyword == "patternProperties"
{
is_props = true;
}
if let Some(validator) = context
.config
.draft()
.get_validator(keyword)
.and_then(|f| f(object, subschema, &context))
{
validators.push((keyword.clone(), validator?));
} else {
unmatched_keywords.insert(keyword.to_string(), subschema.clone());
}
}
Ok(validators)
if is_if {
unmatched_keywords.remove("then");
unmatched_keywords.remove("else");
}
if is_props {
unmatched_keywords.remove("additionalProperties");
unmatched_keywords.remove("patternProperties");
unmatched_keywords.remove("properties");
}
let unmatched_keywords = if unmatched_keywords.is_empty() {
None
} else {
Some(unmatched_keywords)
};
Ok(SchemaNode::new_from_keywords(
&context,
validators,
unmatched_keywords,
))
}
}
_ => Err(ValidationError::schema(schema)),
@ -159,7 +248,7 @@ mod tests {
let value1 = json!("AB");
let value2 = json!(1);
// And only this validator
assert_eq!(compiled.validators.len(), 1);
assert_eq!(compiled.node.validators().len(), 1);
assert!(compiled.validate(&value1).is_ok());
assert!(compiled.validate(&value2).is_err());
}

View File

@ -186,7 +186,7 @@ impl CompilationOptions {
};
let schema_json = Arc::new(schema.clone());
let resolver = Resolver::new(draft, &scope, schema_json.clone(), self.store.clone())?;
let context = CompilationContext::new(scope, &config);
let context = CompilationContext::new(scope.into(), &config);
if self.validate_schema {
if let Some(mut errors) = META_SCHEMA_VALIDATORS
@ -199,12 +199,11 @@ impl CompilationOptions {
}
}
let mut validators = compile_validators(schema, &context)?;
validators.shrink_to_fit();
let node = compile_validators(schema, &context)?;
Ok(JSONSchema {
schema: schema_json,
validators,
node,
resolver,
config,
})

View File

@ -3,12 +3,13 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{boolean::FalseValidator, CompilationResult},
paths::{InstancePath, JSONPointer},
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct AdditionalItemsObjectValidator {
validators: Validators,
node: SchemaNode,
items_count: usize,
}
impl AdditionalItemsObjectValidator {
@ -18,9 +19,9 @@ impl AdditionalItemsObjectValidator {
items_count: usize,
context: &CompilationContext,
) -> CompilationResult<'a> {
let validators = compile_validators(schema, context)?;
let node = compile_validators(schema, context)?;
Ok(Box::new(AdditionalItemsObjectValidator {
validators,
node,
items_count,
}))
}
@ -28,16 +29,16 @@ impl AdditionalItemsObjectValidator {
impl Validate for AdditionalItemsObjectValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Array(items) = instance {
items.iter().skip(self.items_count).all(|item| {
self.validators
.iter()
.all(move |validator| validator.is_valid(schema, item))
})
items
.iter()
.skip(self.items_count)
.all(|item| self.node.is_valid(schema, item))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -49,11 +50,7 @@ impl Validate for AdditionalItemsObjectValidator {
.iter()
.enumerate()
.skip(self.items_count)
.flat_map(|(idx, item)| {
self.validators.iter().flat_map(move |validator| {
validator.validate(schema, item, &instance_path.push(idx))
})
})
.flat_map(|(idx, item)| self.node.validate(schema, item, &instance_path.push(idx)))
.collect();
Box::new(errors.into_iter())
} else {
@ -67,7 +64,7 @@ impl core::fmt::Display for AdditionalItemsObjectValidator {
write!(
f,
"additionalItems: {}",
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}

View File

@ -10,41 +10,43 @@ use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
validator::{format_validators, Validate, Validators},
output::{Annotations, BasicOutput, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
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, Validators)>;
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<&Validators>;
fn get_key_validator(&self, property: &str) -> Option<(&String, &Validators)>;
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, Validators)>;
pub(crate) type BigValidatorsMap = AHashMap<String, Validators>;
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<&Validators> {
for (prop, validators) in self {
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
for (prop, node) in self {
if prop == property {
return Some(validators);
return Some(node);
}
}
None
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &Validators)> {
for (prop, validators) in self {
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
for (prop, node) in self {
if prop == property {
return Some((prop, validators));
return Some((prop, node));
}
}
None
@ -53,12 +55,12 @@ impl PropertiesValidatorsMap for SmallValidatorsMap {
impl PropertiesValidatorsMap for BigValidatorsMap {
#[inline]
fn get_validator(&self, property: &str) -> Option<&Validators> {
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
self.get(property)
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &Validators)> {
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
self.get_key_value(property)
}
}
@ -81,16 +83,14 @@ macro_rules! dynamic_map {
}};
}
macro_rules! is_valid {
($validators:expr, $schema:ident, $value:ident) => {{
$validators
.iter()
.all(|validator| validator.is_valid($schema, $value))
($node:expr, $schema:ident, $value:ident) => {{
$node.is_valid($schema, $value)
}};
}
macro_rules! is_valid_pattern_schema {
($validators:expr, $schema:ident, $value:ident) => {{
if is_valid!($validators, $schema, $value) {
($node:expr, $schema:ident, $value:ident) => {{
if $node.is_valid($schema, $value) {
// Matched & valid - check the next pattern
continue;
}
@ -103,11 +103,11 @@ macro_rules! is_valid_patterns {
($schema:ident, $patterns:expr, $property:ident, $value:ident) => {{
// One property may match multiple patterns, therefore we need to check them all
let mut has_match = false;
for (re, validators) in $patterns {
for (re, node) in $patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match($property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, $schema, $value)
is_valid_pattern_schema!(node, $schema, $value)
}
}
if !has_match {
@ -118,11 +118,9 @@ macro_rules! is_valid_patterns {
}
macro_rules! validate {
($validators:expr, $schema:ident, $value:ident, $instance_path:expr, $property_name:expr) => {{
($node:expr, $schema:ident, $value:ident, $instance_path:expr, $property_name:expr) => {{
let instance_path = $instance_path.push($property_name.clone());
$validators
.iter()
.flat_map(move |validator| validator.validate($schema, $value, &instance_path))
$node.validate($schema, $value, &instance_path)
}};
}
@ -173,7 +171,7 @@ fn compile_big_map<'a>(
/// }
/// ```
pub(crate) struct AdditionalPropertiesValidator {
validators: Validators,
node: SchemaNode,
}
impl AdditionalPropertiesValidator {
#[inline]
@ -183,22 +181,20 @@ impl AdditionalPropertiesValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("additionalProperties");
Ok(Box::new(AdditionalPropertiesValidator {
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
}))
}
}
impl Validate for AdditionalPropertiesValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for value in item.values() {
if !is_valid!(self.validators, schema, value) {
return false;
}
}
item.values().all(|i| self.node.is_valid(schema, i))
} else {
true
}
true
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -208,15 +204,35 @@ impl Validate for AdditionalPropertiesValidator {
if let Value::Object(item) = instance {
let errors: Vec<_> = item
.iter()
.flat_map(|(name, value)| {
validate!(self.validators, schema, value, instance_path, name)
})
.flat_map(|(name, value)| validate!(self.node, schema, value, instance_path, name))
.collect();
Box::new(errors.into_iter())
} else {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_props = Vec::with_capacity(item.len());
let mut output = BasicOutput::default();
for (name, value) in item.iter() {
let path = instance_path.push(name.to_string());
output += self.node.apply_rooted(schema, value, &path);
matched_props.push(name.clone());
}
let mut result: PartialApplication = output.into();
result.annotate(serde_json::Value::from(matched_props).into());
result
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for AdditionalPropertiesValidator {
@ -224,7 +240,7 @@ impl core::fmt::Display for AdditionalPropertiesValidator {
write!(
f,
"additionalProperties: {}",
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -334,8 +350,8 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseV
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item {
if let Some(validators) = self.properties.get_validator(property) {
is_valid_pattern_schema!(validators, schema, value)
if let Some(node) = self.properties.get_validator(property) {
is_valid_pattern_schema!(node, schema, value)
}
// No extra properties are allowed
return false;
@ -354,9 +370,9 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseV
let mut errors = vec![];
let mut unexpected = vec![];
for (property, value) in item {
if let Some((name, validators)) = self.properties.get_key_validator(property) {
if let Some((name, node)) = self.properties.get_key_validator(property) {
// When a property is in `properties`, then it should be VALID
errors.extend(validate!(validators, schema, value, instance_path, name));
errors.extend(validate!(node, schema, value, instance_path, name));
} else {
// No extra properties are allowed
unexpected.push(property.clone());
@ -375,6 +391,41 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseV
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut unexpected = Vec::with_capacity(item.len());
let mut output = BasicOutput::default();
for (property, value) in item {
if let Some((_name, node)) = self.properties.get_key_validator(property) {
let path = instance_path.push(property.clone());
output += node.apply_rooted(schema, value, &path);
} else {
unexpected.push(property.clone())
}
}
let mut result: PartialApplication = output.into();
if !unexpected.is_empty() {
result.mark_errored(
ValidationError::additional_properties(
self.schema_path.clone(),
instance_path.into(),
instance,
unexpected,
)
.into(),
);
}
result
} else {
PartialApplication::valid_empty()
}
}
}
impl<M: PropertiesValidatorsMap> core::fmt::Display
@ -405,7 +456,7 @@ impl<M: PropertiesValidatorsMap> core::fmt::Display
/// }
/// ```
pub(crate) struct AdditionalPropertiesNotEmptyValidator<M: PropertiesValidatorsMap> {
validators: Validators,
node: SchemaNode,
properties: M,
}
impl AdditionalPropertiesNotEmptyValidator<SmallValidatorsMap> {
@ -418,7 +469,7 @@ impl AdditionalPropertiesNotEmptyValidator<SmallValidatorsMap> {
let keyword_context = context.with_path("additionalProperties");
Ok(Box::new(AdditionalPropertiesNotEmptyValidator {
properties: compile_small_map(map, context)?,
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
}))
}
}
@ -432,7 +483,7 @@ impl AdditionalPropertiesNotEmptyValidator<BigValidatorsMap> {
let keyword_context = context.with_path("additionalProperties");
Ok(Box::new(AdditionalPropertiesNotEmptyValidator {
properties: compile_big_map(map, context)?,
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
}))
}
}
@ -443,10 +494,8 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
if let Some(property_validators) = self.properties.get_validator(property) {
is_valid_pattern_schema!(property_validators, schema, value)
}
for validator in &self.validators {
if !validator.is_valid(schema, value) {
return false;
}
if !self.node.is_valid(schema, value) {
return false;
}
}
}
@ -473,13 +522,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
name
))
} else {
errors.extend(validate!(
self.validators,
schema,
value,
instance_path,
property
))
errors.extend(validate!(self.node, schema, value, instance_path, property))
}
}
Box::new(errors.into_iter())
@ -487,6 +530,36 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(map) = instance {
let mut matched_propnames = Vec::with_capacity(map.len());
let mut output = BasicOutput::default();
for (property, value) in map {
let path = instance_path.push(property.clone());
if let Some((_name, property_validators)) =
self.properties.get_key_validator(property)
{
output += property_validators.apply_rooted(schema, value, &path);
} else {
output += self.node.apply_rooted(schema, value, &path);
matched_propnames.push(property.clone());
}
}
let mut result: PartialApplication = output.into();
if !matched_propnames.is_empty() {
result.annotate(serde_json::Value::from(matched_propnames).into());
}
result
} else {
PartialApplication::valid_empty()
}
}
}
impl<M: PropertiesValidatorsMap> core::fmt::Display for AdditionalPropertiesNotEmptyValidator<M> {
@ -494,7 +567,7 @@ impl<M: PropertiesValidatorsMap> core::fmt::Display for AdditionalPropertiesNotE
write!(
f,
"additionalProperties: {}",
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -521,8 +594,14 @@ impl<M: PropertiesValidatorsMap> core::fmt::Display for AdditionalPropertiesNotE
/// }
/// ```
pub(crate) struct AdditionalPropertiesWithPatternsValidator {
validators: Validators,
node: SchemaNode,
patterns: PatternedValidators,
/// We need this because `compile_validators` uses the additionalProperties keyword to compile
/// this validator. That means that the schema node which contains this validator has
/// "additionalProperties" as it's path. However, we need to produce annotations which have the
/// patternProperties keyword as their path so we store the paths here.
pattern_keyword_path: JSONPointer,
pattern_keyword_absolute_path: Option<AbsolutePath>,
}
impl AdditionalPropertiesWithPatternsValidator {
#[inline]
@ -532,8 +611,13 @@ impl AdditionalPropertiesWithPatternsValidator {
context: &CompilationContext,
) -> CompilationResult<'a> {
Ok(Box::new(AdditionalPropertiesWithPatternsValidator {
validators: compile_validators(schema, context)?,
node: compile_validators(schema, &context.with_path("additionalProperties"))?,
patterns,
pattern_keyword_path: context.as_pointer_with("patternProperties"),
pattern_keyword_absolute_path: context
.with_path("patternProperties")
.base_uri()
.map(|u| u.into()),
}))
}
}
@ -542,13 +626,13 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
if let Value::Object(item) = instance {
for (property, value) in item.iter() {
let mut has_match = false;
for (re, validators) in &self.patterns {
for (re, node) in &self.patterns {
if re.is_match(property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, schema, value)
is_valid_pattern_schema!(node, schema, value)
}
}
if !has_match && !is_valid!(self.validators, schema, value) {
if !has_match && !is_valid!(self.node, schema, value) {
return false;
}
}
@ -570,19 +654,13 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
.flat_map(|(_, node)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
validate!(node, schema, value, instance_path, property)
}),
);
if !has_match {
errors.extend(validate!(
self.validators,
schema,
value,
instance_path,
property
))
errors.extend(validate!(self.node, schema, value, instance_path, property))
}
}
Box::new(errors.into_iter())
@ -590,6 +668,50 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut pattern_matched_propnames = Vec::with_capacity(item.len());
let mut additional_matched_propnames = Vec::with_capacity(item.len());
for (property, value) in item {
let path = instance_path.push(property.clone());
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
has_match = true;
pattern_matched_propnames.push(property.clone());
output += node.apply_rooted(schema, value, &path)
}
}
if !has_match {
additional_matched_propnames.push(property.clone());
output += self.node.apply_rooted(schema, value, &path)
}
}
if !pattern_matched_propnames.is_empty() {
output += OutputUnit::<Annotations<'_>>::annotations(
self.pattern_keyword_path.clone(),
instance_path.into(),
self.pattern_keyword_absolute_path.clone(),
serde_json::Value::from(pattern_matched_propnames).into(),
)
.into();
}
let mut result: PartialApplication = output.into();
if !additional_matched_propnames.is_empty() {
result.annotate(serde_json::Value::from(additional_matched_propnames).into())
}
result
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for AdditionalPropertiesWithPatternsValidator {
@ -597,7 +719,7 @@ impl core::fmt::Display for AdditionalPropertiesWithPatternsValidator {
write!(
f,
"additionalProperties: {}",
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -626,16 +748,23 @@ impl core::fmt::Display for AdditionalPropertiesWithPatternsValidator {
pub(crate) struct AdditionalPropertiesWithPatternsFalseValidator {
patterns: PatternedValidators,
schema_path: JSONPointer,
pattern_keyword_path: JSONPointer,
pattern_keyword_absolute_path: Option<AbsolutePath>,
}
impl AdditionalPropertiesWithPatternsFalseValidator {
#[inline]
pub(crate) fn compile<'a>(
patterns: PatternedValidators,
schema_path: JSONPointer,
context: &CompilationContext<'_>,
) -> CompilationResult<'a> {
Ok(Box::new(AdditionalPropertiesWithPatternsFalseValidator {
patterns,
schema_path,
schema_path: context.as_pointer_with("additionalProperties"),
pattern_keyword_path: context.as_pointer_with("patternProperties"),
pattern_keyword_absolute_path: context
.with_path("patternProperties")
.base_uri()
.map(|u| u.into()),
}))
}
}
@ -665,9 +794,9 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
.flat_map(|(_, node)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
validate!(node, schema, value, instance_path, property)
}),
);
if !has_match {
@ -687,6 +816,57 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut unexpected = Vec::with_capacity(item.len());
let mut pattern_matched_props = Vec::with_capacity(item.len());
for (property, value) in item {
let path = instance_path.push(property.clone());
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
has_match = true;
pattern_matched_props.push(property.clone());
output += node.apply_rooted(schema, value, &path);
}
}
if !has_match {
unexpected.push(property.clone());
}
}
if !pattern_matched_props.is_empty() {
output += OutputUnit::<Annotations<'_>>::annotations(
self.pattern_keyword_path.clone(),
instance_path.into(),
self.pattern_keyword_absolute_path.clone(),
serde_json::Value::from(pattern_matched_props).into(),
)
.into();
}
let mut result: PartialApplication = output.into();
if !unexpected.is_empty() {
result.mark_errored(
ValidationError::additional_properties(
self.schema_path.clone(),
instance_path.into(),
instance,
unexpected,
)
.into(),
);
}
result
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for AdditionalPropertiesWithPatternsFalseValidator {
@ -722,7 +902,7 @@ impl core::fmt::Display for AdditionalPropertiesWithPatternsFalseValidator {
/// }
/// ```
pub(crate) struct AdditionalPropertiesWithPatternsNotEmptyValidator<M: PropertiesValidatorsMap> {
validators: Validators,
node: SchemaNode,
properties: M,
patterns: PatternedValidators,
}
@ -737,7 +917,7 @@ impl AdditionalPropertiesWithPatternsNotEmptyValidator<SmallValidatorsMap> {
let keyword_context = context.with_path("additionalProperties");
Ok(Box::new(
AdditionalPropertiesWithPatternsNotEmptyValidator {
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
properties: compile_small_map(map, context)?,
patterns,
},
@ -755,7 +935,7 @@ impl AdditionalPropertiesWithPatternsNotEmptyValidator<BigValidatorsMap> {
let keyword_context = context.with_path("additionalProperties");
Ok(Box::new(
AdditionalPropertiesWithPatternsNotEmptyValidator {
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
properties: compile_big_map(map, context)?,
patterns,
},
@ -766,13 +946,13 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item.iter() {
if let Some(validators) = self.properties.get_validator(property) {
if is_valid!(validators, schema, value) {
if let Some(node) = self.properties.get_validator(property) {
if is_valid!(node, schema, value) {
// Valid for `properties`, check `patternProperties`
for (re, validators) in &self.patterns {
for (re, node) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property).unwrap_or(false) {
is_valid_pattern_schema!(validators, schema, value)
is_valid_pattern_schema!(node, schema, value)
}
}
} else {
@ -781,14 +961,14 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
}
} else {
let mut has_match = false;
for (re, validators) in &self.patterns {
for (re, node) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, schema, value)
is_valid_pattern_schema!(node, schema, value)
}
}
if !has_match && !is_valid!(self.validators, schema, value) {
if !has_match && !is_valid!(self.node, schema, value) {
return false;
}
}
@ -808,8 +988,8 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
if let Value::Object(item) = instance {
let mut errors = vec![];
for (property, value) in item.iter() {
if let Some((name, validators)) = self.properties.get_key_validator(property) {
errors.extend(validate!(validators, schema, value, instance_path, name));
if let Some((name, node)) = self.properties.get_key_validator(property) {
errors.extend(validate!(node, schema, value, instance_path, name));
errors.extend(
self.patterns
.iter()
@ -824,19 +1004,13 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
.flat_map(|(_, node)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
validate!(node, schema, value, instance_path, property)
}),
);
if !has_match {
errors.extend(validate!(
self.validators,
schema,
value,
instance_path,
property
))
errors.extend(validate!(self.node, schema, value, instance_path, property))
}
}
}
@ -845,6 +1019,41 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut additional_matches = Vec::with_capacity(item.len());
for (property, value) in item.iter() {
let path = instance_path.push(property.clone());
if let Some((_name, node)) = self.properties.get_key_validator(property) {
output += node.apply_rooted(schema, value, &path);
} else {
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
has_match = true;
output += node.apply_rooted(schema, value, &path);
}
}
if !has_match {
additional_matches.push(property.clone());
output += self.node.apply_rooted(schema, value, &path);
}
}
}
let mut result: PartialApplication = output.into();
result.annotate(serde_json::Value::from(additional_matches).into());
result
} else {
PartialApplication::valid_empty()
}
}
}
impl<M: PropertiesValidatorsMap> core::fmt::Display
for AdditionalPropertiesWithPatternsNotEmptyValidator<M>
@ -853,7 +1062,7 @@ impl<M: PropertiesValidatorsMap> core::fmt::Display
write!(
f,
"additionalProperties: {}",
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -929,13 +1138,13 @@ impl<M: PropertiesValidatorsMap> Validate
if let Value::Object(item) = instance {
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
if let Some(validators) = self.properties.get_validator(property) {
if is_valid!(validators, schema, value) {
if let Some(node) = self.properties.get_validator(property) {
if is_valid!(node, schema, value) {
// Valid for `properties`, check `patternProperties`
for (re, validators) in &self.patterns {
for (re, node) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property).unwrap_or(false) {
is_valid_pattern_schema!(validators, schema, value)
is_valid_pattern_schema!(node, schema, value)
}
}
} else {
@ -961,14 +1170,14 @@ impl<M: PropertiesValidatorsMap> Validate
let mut unexpected = vec![];
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
if let Some((name, validators)) = self.properties.get_key_validator(property) {
errors.extend(validate!(validators, schema, value, instance_path, name));
if let Some((name, node)) = self.properties.get_key_validator(property) {
errors.extend(validate!(node, schema, value, instance_path, name));
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
validate!(validators, schema, value, instance_path, name)
.flat_map(|(_, node)| {
validate!(node, schema, value, instance_path, name)
}),
);
} else {
@ -977,9 +1186,9 @@ impl<M: PropertiesValidatorsMap> Validate
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
.flat_map(|(_, node)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
validate!(node, schema, value, instance_path, property)
}),
);
if !has_match {
@ -1000,6 +1209,51 @@ impl<M: PropertiesValidatorsMap> Validate
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut unexpected = vec![];
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
let path = instance_path.push(property.clone());
if let Some((_name, node)) = self.properties.get_key_validator(property) {
output += node.apply_rooted(schema, value, &path);
} else {
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
has_match = true;
output += node.apply_rooted(schema, value, &path);
}
}
if !has_match {
unexpected.push(property.clone());
}
}
}
let mut result: PartialApplication = output.into();
if !unexpected.is_empty() {
result.mark_errored(
ValidationError::additional_properties(
self.schema_path.clone(),
instance_path.into(),
instance,
unexpected,
)
.into(),
)
}
result
} else {
PartialApplication::valid_empty()
}
}
}
impl<M: PropertiesValidatorsMap> core::fmt::Display
@ -1033,12 +1287,11 @@ pub(crate) fn compile<'a>(
} else {
Some(AdditionalPropertiesWithPatternsFalseValidator::compile(
compiled_patterns,
context.as_pointer_with("additionalProperties"),
context,
))
}
}
_ => {
let keyword_context = context.with_path("additionalProperties");
if let Some(properties) = properties {
dynamic_map!(
AdditionalPropertiesWithPatternsNotEmptyValidator,
@ -1051,7 +1304,7 @@ pub(crate) fn compile<'a>(
Some(AdditionalPropertiesWithPatternsValidator::compile(
schema,
compiled_patterns,
&keyword_context,
context,
))
}
}
@ -1104,8 +1357,8 @@ fn compile_patterns<'a>(
for (pattern, subschema) in obj {
let pattern_context = keyword_context.with_path(pattern.to_string());
if let Ok(compiled_pattern) = Regex::new(pattern) {
if let Ok(validators) = compile_validators(subschema, &pattern_context) {
compiled_patterns.push((compiled_pattern, validators));
if let Ok(node) = compile_validators(subschema, &pattern_context) {
compiled_patterns.push((compiled_pattern, node));
} else {
return Err(ValidationError::schema(subschema));
}

View File

@ -1,15 +1,17 @@
use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{ErrorIterator, ValidationError},
output::BasicOutput,
paths::InstancePath,
validator::{format_validators, format_vec_of_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_iter_of_validators, format_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
use super::CompilationResult;
pub(crate) struct AllOfValidator {
schemas: Vec<Validators>,
schemas: Vec<SchemaNode>,
}
impl AllOfValidator {
@ -31,13 +33,10 @@ impl AllOfValidator {
impl Validate for AllOfValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
self.schemas.iter().all(move |validators| {
validators
.iter()
.all(move |validator| validator.is_valid(schema, instance))
})
self.schemas.iter().all(|n| n.is_valid(schema, instance))
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -47,23 +46,36 @@ impl Validate for AllOfValidator {
let errors: Vec<_> = self
.schemas
.iter()
.flat_map(move |validators| {
validators
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
})
.flat_map(move |node| node.validate(schema, instance, instance_path))
.collect();
Box::new(errors.into_iter())
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
self.schemas
.iter()
.map(move |node| node.apply_rooted(schema, instance, instance_path))
.sum::<BasicOutput<'_>>()
.into()
}
}
impl core::fmt::Display for AllOfValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "allOf: [{}]", format_vec_of_validators(&self.schemas))
write!(
f,
"allOf: [{}]",
format_iter_of_validators(self.schemas.iter().map(|s| s.validators()))
)
}
}
pub(crate) struct SingleValueAllOfValidator {
validators: Validators,
node: SchemaNode,
}
impl SingleValueAllOfValidator {
@ -74,16 +86,14 @@ impl SingleValueAllOfValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("allOf");
let item_context = keyword_context.with_path(0);
let validators = compile_validators(schema, &item_context)?;
Ok(Box::new(SingleValueAllOfValidator { validators }))
let node = compile_validators(schema, &item_context)?;
Ok(Box::new(SingleValueAllOfValidator { node }))
}
}
impl Validate for SingleValueAllOfValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
self.validators
.iter()
.all(move |validator| validator.is_valid(schema, instance))
self.node.is_valid(schema, instance)
}
fn validate<'a, 'b>(
@ -92,18 +102,24 @@ impl Validate for SingleValueAllOfValidator {
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
let errors: Vec<_> = self
.validators
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
.collect();
Box::new(errors.into_iter())
self.node.validate(schema, instance, instance_path)
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
self.node
.apply_rooted(schema, instance, instance_path)
.into()
}
}
impl core::fmt::Display for SingleValueAllOfValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "allOf: [{}]", format_validators(&self.validators))
write!(f, "allOf: [{}]", format_validators(self.node.validators()))
}
}
#[inline]

View File

@ -2,7 +2,8 @@ use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{error, no_error, ErrorIterator, ValidationError},
paths::InstancePath,
validator::{format_vec_of_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
@ -10,7 +11,7 @@ use super::CompilationResult;
use crate::paths::JSONPointer;
pub(crate) struct AnyOfValidator {
schemas: Vec<Validators>,
schemas: Vec<SchemaNode>,
schema_path: JSONPointer,
}
@ -25,8 +26,8 @@ impl AnyOfValidator {
let mut schemas = Vec::with_capacity(items.len());
for (idx, item) in items.iter().enumerate() {
let item_context = keyword_context.with_path(idx);
let validators = compile_validators(item, &item_context)?;
schemas.push(validators)
let node = compile_validators(item, &item_context)?;
schemas.push(node)
}
Ok(Box::new(AnyOfValidator {
schemas,
@ -40,15 +41,7 @@ impl AnyOfValidator {
impl Validate for AnyOfValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
for validators in &self.schemas {
if validators
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
return true;
}
}
false
self.schemas.iter().any(|s| s.is_valid(schema, instance))
}
fn validate<'a, 'b>(
@ -67,11 +60,38 @@ impl Validate for AnyOfValidator {
))
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let mut successes = Vec::new();
let mut failures = Vec::new();
for node in &self.schemas {
let result = node.apply_rooted(schema, instance, instance_path);
if result.is_valid() {
successes.push(result);
} else {
failures.push(result);
}
}
if successes.is_empty() {
failures.into_iter().collect()
} else {
successes.into_iter().collect()
}
}
}
impl core::fmt::Display for AnyOfValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "anyOf: [{}]", format_vec_of_validators(&self.schemas))
write!(
f,
"anyOf: [{}]",
format_iter_of_validators(self.schemas.iter().map(|s| s.validators()))
)
}
}
#[inline]

View File

@ -3,13 +3,14 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
Draft,
};
use serde_json::{Map, Value};
pub(crate) struct ContainsValidator {
validators: Validators,
node: SchemaNode,
schema_path: JSONPointer,
}
@ -21,7 +22,7 @@ impl ContainsValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("contains");
Ok(Box::new(ContainsValidator {
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
schema_path: keyword_context.into_pointer(),
}))
}
@ -30,16 +31,7 @@ impl ContainsValidator {
impl Validate for ContainsValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Array(items) = instance {
for item in items {
if self
.validators
.iter()
.all(|validator| validator.is_valid(schema, item))
{
return true;
}
}
false
items.iter().any(|i| self.node.is_valid(schema, i))
} else {
true
}
@ -52,14 +44,8 @@ impl Validate for ContainsValidator {
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Value::Array(items) = instance {
for item in items {
if self
.validators
.iter()
.all(|validator| validator.is_valid(schema, item))
{
return no_error();
}
if items.iter().any(|i| self.node.is_valid(schema, i)) {
return no_error();
}
error(ValidationError::contains(
self.schema_path.clone(),
@ -70,11 +56,49 @@ impl Validate for ContainsValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
let mut results = Vec::with_capacity(items.len());
let mut indices = Vec::new();
for (idx, item) in items.iter().enumerate() {
let path = instance_path.push(idx);
let result = self.node.apply_rooted(schema, item, &path);
if result.is_valid() {
indices.push(idx);
results.push(result);
}
}
let mut result: PartialApplication = results.into_iter().collect();
if indices.is_empty() {
result.mark_errored(
ValidationError::contains(
self.schema_path.clone(),
instance_path.into(),
instance,
)
.into(),
);
} else {
result.annotate(serde_json::Value::from(indices).into());
}
result
} else {
let mut result = PartialApplication::valid_empty();
result.annotate(serde_json::Value::Array(Vec::new()).into());
result
}
}
}
impl core::fmt::Display for ContainsValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "contains: {}", format_validators(&self.validators))
write!(f, "contains: {}", format_validators(self.node.validators()))
}
}
@ -82,7 +106,7 @@ impl core::fmt::Display for ContainsValidator {
///
/// Docs: <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.5>
pub(crate) struct MinContainsValidator {
validators: Validators,
node: SchemaNode,
min_contains: u64,
schema_path: JSONPointer,
}
@ -96,7 +120,7 @@ impl MinContainsValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("minContains");
Ok(Box::new(MinContainsValidator {
validators: compile_validators(schema, context)?,
node: compile_validators(schema, context)?,
min_contains,
schema_path: keyword_context.into_pointer(),
}))
@ -118,8 +142,8 @@ impl Validate for MinContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -149,8 +173,8 @@ impl Validate for MinContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -172,7 +196,7 @@ impl core::fmt::Display for MinContainsValidator {
f,
"minContains: {}, contains: {}",
self.min_contains,
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -181,7 +205,7 @@ impl core::fmt::Display for MinContainsValidator {
///
/// Docs: <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.4>
pub(crate) struct MaxContainsValidator {
validators: Validators,
node: SchemaNode,
max_contains: u64,
schema_path: JSONPointer,
}
@ -195,7 +219,7 @@ impl MaxContainsValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("maxContains");
Ok(Box::new(MaxContainsValidator {
validators: compile_validators(schema, context)?,
node: compile_validators(schema, context)?,
max_contains,
schema_path: keyword_context.into_pointer(),
}))
@ -217,8 +241,8 @@ impl Validate for MaxContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -254,8 +278,8 @@ impl Validate for MaxContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -277,7 +301,7 @@ impl core::fmt::Display for MaxContainsValidator {
f,
"maxContains: {}, contains: {}",
self.max_contains,
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}
@ -288,7 +312,7 @@ impl core::fmt::Display for MaxContainsValidator {
/// `maxContains` - <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.4>
/// `minContains` - <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.5>
pub(crate) struct MinMaxContainsValidator {
validators: Validators,
node: SchemaNode,
min_contains: u64,
max_contains: u64,
schema_path: JSONPointer,
@ -303,7 +327,7 @@ impl MinMaxContainsValidator {
max_contains: u64,
) -> CompilationResult<'a> {
Ok(Box::new(MinMaxContainsValidator {
validators: compile_validators(schema, context)?,
node: compile_validators(schema, context)?,
min_contains,
max_contains,
schema_path: context.schema_path.clone().into(),
@ -322,8 +346,8 @@ impl Validate for MinMaxContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -357,8 +381,8 @@ impl Validate for MinMaxContainsValidator {
let mut matches = 0;
for item in items {
if self
.validators
.iter()
.node
.validators()
.all(|validator| validator.is_valid(schema, item))
{
matches += 1;
@ -381,7 +405,7 @@ impl core::fmt::Display for MinMaxContainsValidator {
"minContains: {}, maxContains: {}, contains: {}",
self.min_contains,
self.max_contains,
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}

View File

@ -3,12 +3,13 @@ use crate::{
error::{no_error, ErrorIterator, ValidationError},
keywords::{required, CompilationResult},
paths::InstancePath,
validator::{format_key_value_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_key_value_validators, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct DependenciesValidator {
dependencies: Vec<(String, Validators)>,
dependencies: Vec<(String, SchemaNode)>,
}
impl DependenciesValidator {
@ -24,11 +25,12 @@ impl DependenciesValidator {
let item_context = keyword_context.with_path(key.to_string());
let s = match subschema {
Value::Array(_) => {
vec![required::compile_with_path(
let validators = vec![required::compile_with_path(
subschema,
(&keyword_context.schema_path).into(),
)
.expect("The required validator compilation does not return None")?]
.expect("The required validator compilation does not return None")?];
SchemaNode::new_from_array(&keyword_context, validators)
}
_ => compile_validators(subschema, &item_context)?,
};
@ -47,16 +49,13 @@ impl Validate for DependenciesValidator {
self.dependencies
.iter()
.filter(|(property, _)| item.contains_key(property))
.all(move |(_, validators)| {
validators
.iter()
.all(move |validator| validator.is_valid(schema, instance))
})
.all(move |(_, node)| node.is_valid(schema, instance))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -68,11 +67,7 @@ impl Validate for DependenciesValidator {
.dependencies
.iter()
.filter(|(property, _)| item.contains_key(property))
.flat_map(move |(_, validators)| {
validators.iter().flat_map(move |validator| {
validator.validate(schema, instance, instance_path)
})
})
.flat_map(move |(_, node)| node.validate(schema, instance, instance_path))
.collect();
// TODO. custom error message for "required" case
Box::new(errors.into_iter())

View File

@ -3,13 +3,14 @@ use crate::{
error::{no_error, ErrorIterator},
keywords::CompilationResult,
paths::InstancePath,
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct IfThenValidator {
schema: Validators,
then_schema: Validators,
schema: SchemaNode,
then_schema: SchemaNode,
}
impl IfThenValidator {
@ -34,40 +35,46 @@ impl IfThenValidator {
impl Validate for IfThenValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if self
.schema
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
self.then_schema
.iter()
.all(move |validator| validator.is_valid(schema, instance))
if self.schema.is_valid(schema, instance) {
self.then_schema.is_valid(schema, instance)
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if self
.schema
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
if self.schema.is_valid(schema, instance) {
let errors: Vec<_> = self
.then_schema
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
.validate(schema, instance, instance_path)
.collect();
Box::new(errors.into_iter())
} else {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let mut if_result = self.schema.apply_rooted(schema, instance, instance_path);
if if_result.is_valid() {
let then_result = self
.then_schema
.apply_rooted(schema, instance, instance_path);
if_result += then_result;
}
if_result.into()
}
}
impl core::fmt::Display for IfThenValidator {
@ -75,15 +82,15 @@ impl core::fmt::Display for IfThenValidator {
write!(
f,
"if: {}, then: {}",
format_validators(&self.schema),
format_validators(&self.then_schema)
format_validators(self.schema.validators()),
format_validators(self.then_schema.validators())
)
}
}
pub(crate) struct IfElseValidator {
schema: Validators,
else_schema: Validators,
schema: SchemaNode,
else_schema: SchemaNode,
}
impl IfElseValidator {
@ -108,40 +115,46 @@ impl IfElseValidator {
impl Validate for IfElseValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if self
.schema
.iter()
.any(|validator| !validator.is_valid(schema, instance))
{
self.else_schema
.iter()
.all(move |validator| validator.is_valid(schema, instance))
if !self.schema.is_valid(schema, instance) {
self.else_schema.is_valid(schema, instance)
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if self
.schema
.iter()
.any(|validator| !validator.is_valid(schema, instance))
{
if !self.schema.is_valid(schema, instance) {
let errors: Vec<_> = self
.else_schema
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
.validate(schema, instance, instance_path)
.collect();
Box::new(errors.into_iter())
} else {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let if_result = self.schema.apply_rooted(schema, instance, instance_path);
if if_result.is_valid() {
if_result.into()
} else {
self.else_schema
.apply_rooted(schema, instance, instance_path)
.into()
}
}
}
impl core::fmt::Display for IfElseValidator {
@ -149,16 +162,16 @@ impl core::fmt::Display for IfElseValidator {
write!(
f,
"if: {}, else: {}",
format_validators(&self.schema),
format_validators(&self.else_schema)
format_validators(self.schema.validators()),
format_validators(self.else_schema.validators())
)
}
}
pub(crate) struct IfThenElseValidator {
schema: Validators,
then_schema: Validators,
else_schema: Validators,
schema: SchemaNode,
then_schema: SchemaNode,
else_schema: SchemaNode,
}
impl IfThenElseValidator {
@ -188,47 +201,53 @@ impl IfThenElseValidator {
impl Validate for IfThenElseValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if self
.schema
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
self.then_schema
.iter()
.all(move |validator| validator.is_valid(schema, instance))
if self.schema.is_valid(schema, instance) {
self.then_schema.is_valid(schema, instance)
} else {
self.else_schema
.iter()
.all(move |validator| validator.is_valid(schema, instance))
self.else_schema.is_valid(schema, instance)
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if self
.schema
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
if self.schema.is_valid(schema, instance) {
let errors: Vec<_> = self
.then_schema
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
.validate(schema, instance, instance_path)
.collect();
Box::new(errors.into_iter())
} else {
let errors: Vec<_> = self
.else_schema
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
.validate(schema, instance, instance_path)
.collect();
Box::new(errors.into_iter())
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let mut if_result = self.schema.apply_rooted(schema, instance, instance_path);
if if_result.is_valid() {
if_result += self
.then_schema
.apply_rooted(schema, instance, instance_path);
if_result.into()
} else {
self.else_schema
.apply_rooted(schema, instance, instance_path)
.into()
}
}
}
impl core::fmt::Display for IfThenElseValidator {
@ -236,9 +255,9 @@ impl core::fmt::Display for IfThenElseValidator {
write!(
f,
"if: {}, then: {}, else: {}",
format_validators(&self.schema),
format_validators(&self.then_schema),
format_validators(&self.else_schema)
format_validators(self.schema.validators()),
format_validators(self.then_schema.validators()),
format_validators(self.else_schema.validators())
)
}
}

View File

@ -3,12 +3,13 @@ use crate::{
error::{no_error, ErrorIterator},
keywords::CompilationResult,
paths::InstancePath,
validator::{format_validators, format_vec_of_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_iter_of_validators, format_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct ItemsArrayValidator {
items: Vec<Validators>,
items: Vec<SchemaNode>,
}
impl ItemsArrayValidator {
#[inline]
@ -32,16 +33,13 @@ impl Validate for ItemsArrayValidator {
items
.iter()
.zip(self.items.iter())
.all(move |(item, validators)| {
validators
.iter()
.all(move |validator| validator.is_valid(schema, item))
})
.all(move |(item, node)| node.is_valid(schema, item))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -53,10 +51,8 @@ impl Validate for ItemsArrayValidator {
.iter()
.zip(self.items.iter())
.enumerate()
.flat_map(move |(idx, (item, validators))| {
validators.iter().flat_map(move |validator| {
validator.validate(schema, item, &instance_path.push(idx))
})
.flat_map(move |(idx, (item, node))| {
node.validate(schema, item, &instance_path.push(idx))
})
.collect();
Box::new(errors.into_iter())
@ -68,12 +64,16 @@ impl Validate for ItemsArrayValidator {
impl core::fmt::Display for ItemsArrayValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "items: [{}]", format_vec_of_validators(&self.items))
write!(
f,
"items: [{}]",
format_iter_of_validators(self.items.iter().map(|i| i.validators()))
)
}
}
pub(crate) struct ItemsObjectValidator {
validators: Validators,
node: SchemaNode,
}
impl ItemsObjectValidator {
#[inline]
@ -82,23 +82,20 @@ impl ItemsObjectValidator {
context: &CompilationContext,
) -> CompilationResult<'a> {
let keyword_context = context.with_path("items");
let validators = compile_validators(schema, &keyword_context)?;
Ok(Box::new(ItemsObjectValidator { validators }))
let node = compile_validators(schema, &keyword_context)?;
Ok(Box::new(ItemsObjectValidator { node }))
}
}
impl Validate for ItemsObjectValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Array(items) = instance {
self.validators.iter().all(move |validator| {
items
.iter()
.all(move |item| validator.is_valid(schema, item))
})
items.iter().all(|i| self.node.is_valid(schema, i))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -106,13 +103,11 @@ impl Validate for ItemsObjectValidator {
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Value::Array(items) = instance {
let errors: Vec<_> = self
.validators
let errors: Vec<_> = items
.iter()
.flat_map(move |validator| {
items.iter().enumerate().flat_map(move |(idx, item)| {
validator.validate(schema, item, &instance_path.push(idx))
})
.enumerate()
.flat_map(move |(idx, item)| {
self.node.validate(schema, item, &instance_path.push(idx))
})
.collect();
Box::new(errors.into_iter())
@ -120,11 +115,36 @@ impl Validate for ItemsObjectValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
let mut results = Vec::with_capacity(items.len());
for (idx, item) in items.iter().enumerate() {
let path = instance_path.push(idx);
results.push(self.node.apply_rooted(schema, item, &path));
}
let mut output: PartialApplication = results.into_iter().collect();
// Per draft 2020-12 section https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.3.1.2
// we must produce an annotation with a boolean value indicating whether the subschema
// was applied to any positions in the underlying array. As we have not yet implemented
// prefixItems this is true if there are any items in the instance.
let schema_was_applied = !items.is_empty();
output.annotate(serde_json::json! {schema_was_applied}.into());
output
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for ItemsObjectValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "items: {}", format_validators(&self.validators))
write!(f, "items: {}", format_validators(self.node.validators()))
}
}

View File

@ -122,7 +122,10 @@ mod tests {
#[test_case(&json!({"uniqueItems": true}), "uniqueItems: true")]
fn debug_representation(schema: &Value, expected: &str) {
let compiled = JSONSchema::compile(schema).unwrap();
assert_eq!(format!("{:?}", compiled.validators[0]), expected);
assert_eq!(
format!("{:?}", compiled.node.validators().next().unwrap()),
expected
);
}
#[test_case(&json!({"items": [{}], "additionalItems": {"type": "integer"}}), &json!([ null, 2, 3, "foo" ]), r#""foo" is not of type "integer""#)]

View File

@ -3,14 +3,15 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct NotValidator {
// needed only for error representation
original: Value,
validators: Validators,
node: SchemaNode,
schema_path: JSONPointer,
}
@ -23,7 +24,7 @@ impl NotValidator {
let keyword_context = context.with_path("not");
Ok(Box::new(NotValidator {
original: schema.clone(),
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
schema_path: keyword_context.into_pointer(),
}))
}
@ -31,10 +32,7 @@ impl NotValidator {
impl Validate for NotValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
!self
.validators
.iter()
.all(|validator| validator.is_valid(schema, instance))
!self.node.is_valid(schema, instance)
}
fn validate<'a, 'b>(
@ -58,7 +56,7 @@ impl Validate for NotValidator {
impl core::fmt::Display for NotValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not: {}", format_validators(&self.validators))
write!(f, "not: {}", format_validators(self.node.validators()))
}
}

View File

@ -2,13 +2,15 @@ use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::{InstancePath, JSONPointer},
validator::{format_vec_of_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct OneOfValidator {
schemas: Vec<Validators>,
schemas: Vec<SchemaNode>,
schema_path: JSONPointer,
}
@ -21,8 +23,10 @@ impl OneOfValidator {
if let Value::Array(items) = schema {
let keyword_context = context.with_path("oneOf");
let mut schemas = Vec::with_capacity(items.len());
for item in items {
schemas.push(compile_validators(item, &keyword_context)?)
for (idx, item) in items.iter().enumerate() {
let item_context = keyword_context.with_path(idx);
let node = compile_validators(item, &item_context)?;
schemas.push(node)
}
Ok(Box::new(OneOfValidator {
schemas,
@ -35,11 +39,8 @@ impl OneOfValidator {
fn get_first_valid(&self, schema: &JSONSchema, instance: &Value) -> Option<usize> {
let mut first_valid_idx = None;
for (idx, validators) in self.schemas.iter().enumerate() {
if validators
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
for (idx, node) in self.schemas.iter().enumerate() {
if node.is_valid(schema, instance) {
first_valid_idx = Some(idx);
break;
}
@ -52,15 +53,10 @@ impl OneOfValidator {
// `idx + 1` will not overflow, because the maximum possible value there is `usize::MAX - 1`
// For example we have `usize::MAX` schemas and only the last one is valid, then
// in `get_first_valid` we enumerate from `0`, and on the last index will be `usize::MAX - 1`
for validators in self.schemas.iter().skip(idx + 1) {
if validators
.iter()
.all(|validator| validator.is_valid(schema, instance))
{
return true;
}
}
false
self.schemas
.iter()
.skip(idx + 1)
.any(|n| n.is_valid(schema, instance))
}
}
@ -93,11 +89,40 @@ impl Validate for OneOfValidator {
))
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let mut failures = Vec::new();
let mut successes = Vec::new();
for node in &self.schemas {
match node.apply_rooted(schema, instance, instance_path) {
output @ BasicOutput::Valid(..) => successes.push(output),
output @ BasicOutput::Invalid(..) => failures.push(output),
};
}
if successes.len() == 1 {
let success = successes.remove(0);
success.into()
} else if successes.len() > 1 {
PartialApplication::invalid_empty(vec!["more than one subschema succeeded".into()])
} else if !failures.is_empty() {
failures.into_iter().sum::<BasicOutput<'_>>().into()
} else {
unreachable!("compilation should fail for oneOf with no subschemas")
}
}
}
impl core::fmt::Display for OneOfValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "oneOf: [{}]", format_vec_of_validators(&self.schemas))
write!(
f,
"oneOf: [{}]",
format_iter_of_validators(self.schemas.iter().map(|s| s.validators()))
)
}
}

View File

@ -159,7 +159,7 @@ pub(crate) fn compile<'a>(
#[cfg(test)]
mod tests {
use super::*;
use crate::{compilation::DEFAULT_SCOPE, tests_util};
use crate::{compilation::context::BaseUri, tests_util};
use serde_json::{json, Value};
use test_case::test_case;
@ -188,7 +188,7 @@ mod tests {
let text = Value::String(text.into());
let schema = json!({});
let schema = JSONSchema::compile(&schema).unwrap();
let context = CompilationContext::new(DEFAULT_SCOPE.clone(), schema.config());
let context = CompilationContext::new(BaseUri::Unknown, schema.config());
let compiled = PatternValidator::compile(&pattern, &context).unwrap();
assert_eq!(compiled.is_valid(&schema, &text), is_matching,)
}

View File

@ -2,14 +2,16 @@ use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::InstancePath,
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
use fancy_regex::Regex;
use serde_json::{Map, Value};
pub(crate) struct PatternPropertiesValidator {
patterns: Vec<(Regex, Validators)>,
patterns: Vec<(Regex, SchemaNode)>,
}
impl PatternPropertiesValidator {
@ -37,20 +39,17 @@ impl PatternPropertiesValidator {
impl Validate for PatternPropertiesValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = instance {
self.patterns.iter().all(move |(re, validators)| {
self.patterns.iter().all(move |(re, node)| {
item.iter()
.filter(move |(key, _)| re.is_match(key).unwrap_or(false))
.all(move |(_key, value)| {
validators
.iter()
.all(move |validator| validator.is_valid(schema, value))
})
.all(move |(_key, value)| node.is_valid(schema, value))
})
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -61,14 +60,12 @@ impl Validate for PatternPropertiesValidator {
let errors: Vec<_> = self
.patterns
.iter()
.flat_map(move |(re, validators)| {
.flat_map(move |(re, node)| {
item.iter()
.filter(move |(key, _)| re.is_match(key).unwrap_or(false))
.flat_map(move |(key, value)| {
let instance_path = instance_path.push(key.clone());
validators.iter().flat_map(move |validator| {
validator.validate(schema, value, &instance_path)
})
node.validate(schema, value, &instance_path)
})
})
.collect();
@ -77,6 +74,32 @@ impl Validate for PatternPropertiesValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
let mut sub_results = BasicOutput::default();
for (pattern, node) in &self.patterns {
for (key, value) in item {
if pattern.is_match(key).unwrap_or(false) {
let path = instance_path.push(key.clone());
matched_propnames.push(key.clone());
sub_results += node.apply_rooted(schema, value, &path);
}
}
}
let mut result: PartialApplication = sub_results.into();
result.annotate(serde_json::Value::from(matched_propnames).into());
result
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for PatternPropertiesValidator {
@ -86,7 +109,7 @@ impl core::fmt::Display for PatternPropertiesValidator {
"patternProperties: {{{}}}",
self.patterns
.iter()
.map(|(key, validators)| { format!("{}: {}", key, format_validators(validators)) })
.map(|(key, node)| { format!("{}: {}", key, format_validators(node.validators())) })
.collect::<Vec<String>>()
.join(", ")
)
@ -95,7 +118,7 @@ impl core::fmt::Display for PatternPropertiesValidator {
pub(crate) struct SingleValuePatternPropertiesValidator {
pattern: Regex,
validators: Validators,
node: SchemaNode,
}
impl SingleValuePatternPropertiesValidator {
@ -112,7 +135,7 @@ impl SingleValuePatternPropertiesValidator {
Ok(r) => r,
Err(_) => return Err(ValidationError::schema(schema)),
},
validators: compile_validators(schema, &pattern_context)?,
node: compile_validators(schema, &pattern_context)?,
}))
}
}
@ -122,16 +145,13 @@ impl Validate for SingleValuePatternPropertiesValidator {
if let Value::Object(item) = instance {
item.iter()
.filter(move |(key, _)| self.pattern.is_match(key).unwrap_or(false))
.all(move |(_key, value)| {
self.validators
.iter()
.all(move |validator| validator.is_valid(schema, value))
})
.all(move |(_key, value)| self.node.is_valid(schema, value))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -144,9 +164,7 @@ impl Validate for SingleValuePatternPropertiesValidator {
.filter(move |(key, _)| self.pattern.is_match(key).unwrap_or(false))
.flat_map(move |(key, value)| {
let instance_path = instance_path.push(key.clone());
self.validators.iter().flat_map(move |validator| {
validator.validate(schema, value, &instance_path)
})
self.node.validate(schema, value, &instance_path)
})
.collect();
Box::new(errors.into_iter())
@ -154,6 +172,30 @@ impl Validate for SingleValuePatternPropertiesValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
let mut outputs = BasicOutput::default();
for (key, value) in item {
if self.pattern.is_match(key).unwrap_or(false) {
let path = instance_path.push(key.clone());
matched_propnames.push(key.clone());
outputs += self.node.apply_rooted(schema, value, &path);
}
}
let mut result: PartialApplication = outputs.into();
result.annotate(serde_json::Value::from(matched_propnames).into());
result
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for SingleValuePatternPropertiesValidator {
@ -162,7 +204,7 @@ impl core::fmt::Display for SingleValuePatternPropertiesValidator {
f,
"patternProperties: {{{}: {}}}",
self.pattern,
format_validators(&self.validators)
format_validators(self.node.validators())
)
}
}

View File

@ -2,13 +2,15 @@ use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::InstancePath,
validator::{format_key_value_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_key_value_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct PropertiesValidator {
properties: Vec<(String, Validators)>,
properties: Vec<(String, SchemaNode)>,
}
impl PropertiesValidator {
@ -38,19 +40,18 @@ impl PropertiesValidator {
impl Validate for PropertiesValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = instance {
self.properties.iter().all(move |(name, validators)| {
self.properties.iter().all(move |(name, node)| {
let option = item.get(name);
option.into_iter().all(move |item| {
validators
.iter()
.all(move |validator| validator.is_valid(schema, item))
})
option
.into_iter()
.all(move |item| node.is_valid(schema, item))
})
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -61,13 +62,11 @@ impl Validate for PropertiesValidator {
let errors: Vec<_> = self
.properties
.iter()
.flat_map(move |(name, validators)| {
.flat_map(move |(name, node)| {
let option = item.get(name);
option.into_iter().flat_map(move |item| {
let instance_path = instance_path.push(name.clone());
validators.iter().flat_map(move |validator| {
validator.validate(schema, item, &instance_path)
})
node.validate(schema, item, &instance_path)
})
})
.collect();
@ -76,6 +75,30 @@ impl Validate for PropertiesValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(props) = instance {
let mut result = BasicOutput::default();
let mut matched_props = Vec::with_capacity(props.len());
for (prop_name, node) in &self.properties {
if let Some(prop) = props.get(prop_name) {
let path = instance_path.push(prop_name.clone());
matched_props.push(prop_name.clone());
result += node.apply_rooted(schema, prop, &path);
}
}
let mut application: PartialApplication = result.into();
application.annotate(serde_json::Value::from(matched_props).into());
application
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for PropertiesValidator {

View File

@ -3,12 +3,13 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
validator::{format_validators, Validate, Validators},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
pub(crate) struct PropertyNamesObjectValidator {
validators: Validators,
node: SchemaNode,
}
impl PropertyNamesObjectValidator {
@ -19,7 +20,7 @@ impl PropertyNamesObjectValidator {
) -> CompilationResult<'a> {
let keyword_context = context.with_path("propertyNames");
Ok(Box::new(PropertyNamesObjectValidator {
validators: compile_validators(schema, &keyword_context)?,
node: compile_validators(schema, &keyword_context)?,
}))
}
}
@ -27,17 +28,16 @@ impl PropertyNamesObjectValidator {
impl Validate for PropertyNamesObjectValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Object(item) = &instance {
self.validators.iter().all(|validator| {
item.keys().all(move |key| {
let wrapper = Value::String(key.to_string());
validator.is_valid(schema, &wrapper)
})
item.keys().all(move |key| {
let wrapper = Value::String(key.to_string());
self.node.is_valid(schema, &wrapper)
})
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
@ -45,25 +45,23 @@ impl Validate for PropertyNamesObjectValidator {
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Value::Object(item) = &instance {
let errors: Vec<_> = self
.validators
.iter()
.flat_map(|validator| {
item.keys().flat_map(move |key| {
let wrapper = Value::String(key.to_string());
let errors: Vec<_> = validator
.validate(schema, &wrapper, instance_path)
.map(|error| {
ValidationError::property_names(
error.schema_path.clone(),
instance_path.into(),
instance,
error.into_owned(),
)
})
.collect();
errors.into_iter()
})
let errors: Vec<_> = item
.keys()
.flat_map(move |key| {
let wrapper = Value::String(key.to_string());
let errors: Vec<_> = self
.node
.validate(schema, &wrapper, instance_path)
.map(|error| {
ValidationError::property_names(
error.schema_path.clone(),
instance_path.into(),
instance,
error.into_owned(),
)
})
.collect();
errors.into_iter()
})
.collect();
Box::new(errors.into_iter())
@ -71,10 +69,32 @@ impl Validate for PropertyNamesObjectValidator {
no_error()
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
item.keys()
.map(|key| {
let wrapper = Value::String(key.to_string());
self.node.apply_rooted(schema, &wrapper, instance_path)
})
.collect()
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for PropertyNamesObjectValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "propertyNames: {}", format_validators(&self.validators))
write!(
f,
"propertyNames: {}",
format_validators(self.node.validators())
)
}
}

View File

@ -3,7 +3,8 @@ use crate::{
error::{error, ErrorIterator},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
validator::{Validate, Validators},
schema_node::SchemaNode,
validator::Validate,
};
use parking_lot::RwLock;
use serde_json::Value;
@ -16,7 +17,7 @@ pub(crate) struct RefValidator {
/// at compile time without risking infinite loops of references
/// and at the same time during validation we iterate over shared
/// references (&self) and not owned references (&mut self).
validators: RwLock<Option<Validators>>,
sub_nodes: RwLock<Option<SchemaNode>>,
schema_path: JSONPointer,
}
@ -29,7 +30,7 @@ impl RefValidator {
let reference = context.build_url(reference)?;
Ok(Box::new(RefValidator {
reference,
validators: RwLock::new(None),
sub_nodes: RwLock::new(None),
schema_path: context.schema_path.clone().into(),
}))
}
@ -37,21 +38,17 @@ impl RefValidator {
impl Validate for RefValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Some(validators) = self.validators.read().as_ref() {
return validators
.iter()
.all(move |validator| validator.is_valid(schema, instance));
if let Some(sub_nodes) = self.sub_nodes.read().as_ref() {
return sub_nodes.is_valid(schema, instance);
}
if let Ok((scope, resolved)) = schema
.resolver
.resolve_fragment(schema.draft(), &self.reference)
{
let context = CompilationContext::new(scope, schema.config());
if let Ok(validators) = compile_validators(&resolved, &context) {
let result = validators
.iter()
.all(move |validator| validator.is_valid(schema, instance));
*self.validators.write() = Some(validators);
let context = CompilationContext::new(scope.into(), schema.config());
if let Ok(node) = compile_validators(&resolved, &context) {
let result = node.is_valid(schema, instance);
*self.sub_nodes.write() = Some(node);
return result;
}
};
@ -64,11 +61,9 @@ impl Validate for RefValidator {
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Some(validators) = self.validators.read().as_ref() {
if let Some(node) = self.sub_nodes.read().as_ref() {
return Box::new(
validators
.iter()
.flat_map(move |validator| validator.validate(schema, instance, instance_path))
node.validate(schema, instance, instance_path)
.collect::<Vec<_>>()
.into_iter(),
);
@ -78,27 +73,21 @@ impl Validate for RefValidator {
.resolve_fragment(schema.draft(), &self.reference)
{
Ok((scope, resolved)) => {
let context = CompilationContext::new(scope, schema.config());
let context = CompilationContext::new(scope.into(), schema.config());
match compile_validators(&resolved, &context) {
Ok(validators) => {
Ok(node) => {
let result = Box::new(
validators
.iter()
.flat_map(move |validator| {
node.err_iter(schema, instance, instance_path)
.map(move |mut error| {
let schema_path = self.schema_path.clone();
validator.validate(schema, instance, instance_path).map(
move |mut error| {
// Prepend $ref path to the actual error
error.schema_path = schema_path
.extend_with(error.schema_path.as_slice());
error
},
)
error.schema_path =
schema_path.extend_with(error.schema_path.as_slice());
error
})
.collect::<Vec<_>>()
.into_iter(),
);
*self.validators.write() = Some(validators);
*self.sub_nodes.write() = Some(node);
result
}
Err(err) => error(err.into_owned()),

View File

@ -88,9 +88,12 @@ mod content_encoding;
mod content_media_type;
pub mod error;
mod keywords;
mod output;
pub use output::{BasicOutput, Output};
pub mod paths;
pub mod primitive_type;
mod resolver;
mod schema_node;
mod schemas;
mod validator;

280
jsonschema/src/output.rs Normal file
View File

@ -0,0 +1,280 @@
use std::{
collections::VecDeque,
iter::{FromIterator, Sum},
ops::AddAssign,
};
use crate::{validator::PartialApplication, ValidationError};
use ahash::AHashMap;
use serde::ser::SerializeMap;
use crate::{
paths::{AbsolutePath, InstancePath, JSONPointer},
schema_node::SchemaNode,
JSONSchema,
};
/// The output format resulting from the application of a schema. This can be
/// converted into various representations based on the definitions in
/// <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.12.2>
///
/// Currently only the "flag" and "basic" output formats are supported
#[derive(Debug, Clone)]
pub struct Output<'a, 'b> {
schema: &'a JSONSchema,
root_node: &'a SchemaNode,
instance: &'b serde_json::Value,
}
impl<'a, 'b> Output<'a, 'b> {
pub(crate) const fn new<'c, 'd>(
schema: &'c JSONSchema,
root_node: &'c SchemaNode,
instance: &'d serde_json::Value,
) -> Output<'c, 'd> {
Output {
schema,
root_node,
instance,
}
}
/// Indicates whether the schema was valid, corresponds to the "flag" output
/// format
pub fn flag(&self) -> bool {
self.schema.is_valid(self.instance)
}
/// Output a list of errors and annotations for each element in the schema
/// according to the basic output format
pub fn basic(&self) -> BasicOutput<'a> {
self.root_node
.apply_rooted(self.schema, self.instance, &InstancePath::new())
}
}
/// The "basic" output format
#[derive(Debug, PartialEq)]
pub enum BasicOutput<'a> {
/// The schema was valid, collected annotations can be examined
Valid(VecDeque<OutputUnit<Annotations<'a>>>),
/// The schema was invalid
Invalid(VecDeque<OutputUnit<ErrorDescription>>),
}
impl<'a> BasicOutput<'a> {
pub(crate) const fn is_valid(&self) -> bool {
match self {
BasicOutput::Valid(..) => true,
BasicOutput::Invalid(..) => false,
}
}
}
impl<'a> From<OutputUnit<Annotations<'a>>> for BasicOutput<'a> {
fn from(unit: OutputUnit<Annotations<'a>>) -> Self {
let mut units = VecDeque::new();
units.push_front(unit);
BasicOutput::Valid(units)
}
}
impl<'a> AddAssign for BasicOutput<'a> {
fn add_assign(&mut self, rhs: Self) {
match (&mut *self, rhs) {
(BasicOutput::Valid(ref mut anns), BasicOutput::Valid(anns_rhs)) => {
anns.extend(anns_rhs);
}
(BasicOutput::Valid(..), BasicOutput::Invalid(errors)) => {
*self = BasicOutput::Invalid(errors)
}
(BasicOutput::Invalid(..), BasicOutput::Valid(..)) => {}
(BasicOutput::Invalid(errors), BasicOutput::Invalid(errors_rhs)) => {
errors.extend(errors_rhs)
}
}
}
}
impl<'a> Sum for BasicOutput<'a> {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
let result = BasicOutput::Valid(VecDeque::new());
iter.fold(result, |mut acc, elem| {
acc += elem;
acc
})
}
}
impl<'a> Default for BasicOutput<'a> {
fn default() -> Self {
BasicOutput::Valid(VecDeque::new())
}
}
impl<'a> From<BasicOutput<'a>> for PartialApplication<'a> {
fn from(output: BasicOutput<'a>) -> Self {
match output {
BasicOutput::Valid(anns) => PartialApplication::Valid {
annotations: None,
child_results: anns,
},
BasicOutput::Invalid(errors) => PartialApplication::Invalid {
errors: Vec::new(),
child_results: errors,
},
}
}
}
impl<'a> FromIterator<BasicOutput<'a>> for PartialApplication<'a> {
fn from_iter<T: IntoIterator<Item = BasicOutput<'a>>>(iter: T) -> Self {
iter.into_iter().sum::<BasicOutput<'_>>().into()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OutputUnit<T> {
keyword_location: JSONPointer,
instance_location: JSONPointer,
absolute_keyword_location: Option<AbsolutePath>,
value: T,
}
impl<T> OutputUnit<T> {
pub(crate) const fn annotations(
keyword_location: JSONPointer,
instance_location: JSONPointer,
absolute_keyword_location: Option<AbsolutePath>,
annotations: Annotations<'_>,
) -> OutputUnit<Annotations<'_>> {
OutputUnit {
keyword_location,
instance_location,
absolute_keyword_location,
value: annotations,
}
}
pub(crate) const fn error(
keyword_location: JSONPointer,
instance_location: JSONPointer,
absolute_keyword_location: Option<AbsolutePath>,
error: ErrorDescription,
) -> OutputUnit<ErrorDescription> {
OutputUnit {
keyword_location,
instance_location,
absolute_keyword_location,
value: error,
}
}
}
#[derive(serde::Serialize, Debug, Clone, PartialEq)]
pub struct Annotations<'a>(AnnotationsInner<'a>);
#[derive(Debug, Clone, PartialEq)]
enum AnnotationsInner<'a> {
UnmatchedKeywords(&'a AHashMap<String, serde_json::Value>),
ValueRef(&'a serde_json::Value),
Value(Box<serde_json::Value>),
}
impl<'a> From<&'a AHashMap<String, serde_json::Value>> for Annotations<'a> {
fn from(anns: &'a AHashMap<String, serde_json::Value>) -> Self {
Annotations(AnnotationsInner::UnmatchedKeywords(anns))
}
}
impl<'a> From<&'a serde_json::Value> for Annotations<'a> {
fn from(v: &'a serde_json::Value) -> Self {
Annotations(AnnotationsInner::ValueRef(v))
}
}
impl<'a> From<serde_json::Value> for Annotations<'a> {
fn from(v: serde_json::Value) -> Self {
Annotations(AnnotationsInner::Value(Box::new(v)))
}
}
#[derive(serde::Serialize, Debug, Clone, PartialEq)]
pub struct ErrorDescription(String);
impl From<ValidationError<'_>> for ErrorDescription {
fn from(e: ValidationError<'_>) -> Self {
ErrorDescription(e.to_string())
}
}
impl<'a> From<&'a str> for ErrorDescription {
fn from(s: &'a str) -> Self {
ErrorDescription(s.to_string())
}
}
impl<'a> serde::Serialize for BasicOutput<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map_ser = serializer.serialize_map(Some(2))?;
match self {
BasicOutput::Valid(outputs) => {
map_ser.serialize_entry("valid", &true)?;
map_ser.serialize_entry("annotations", outputs)?;
}
BasicOutput::Invalid(errors) => {
map_ser.serialize_entry("valid", &false)?;
map_ser.serialize_entry("errors", errors)?;
}
}
map_ser.end()
}
}
impl<'a> serde::Serialize for AnnotationsInner<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::UnmatchedKeywords(kvs) => kvs.serialize(serializer),
Self::Value(v) => v.serialize(serializer),
Self::ValueRef(v) => v.serialize(serializer),
}
}
}
impl<'a> serde::Serialize for OutputUnit<Annotations<'a>> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map_ser = serializer.serialize_map(Some(4))?;
map_ser.serialize_entry("keywordLocation", &self.keyword_location)?;
map_ser.serialize_entry("instanceLocation", &self.instance_location)?;
if let Some(absolute) = &self.absolute_keyword_location {
map_ser.serialize_entry("absoluteKeywordLocation", &absolute)?;
}
map_ser.serialize_entry("annotations", &self.value)?;
map_ser.end()
}
}
impl<'a> serde::Serialize for OutputUnit<ErrorDescription> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map_ser = serializer.serialize_map(Some(4))?;
map_ser.serialize_entry("keywordLocation", &self.keyword_location)?;
map_ser.serialize_entry("instanceLocation", &self.instance_location)?;
if let Some(absolute) = &self.absolute_keyword_location {
map_ser.serialize_entry("absoluteKeywordLocation", &absolute)?;
}
map_ser.serialize_entry("error", &self.value)?;
map_ser.end()
}
}

View File

@ -1,5 +1,5 @@
//! Facilities for working with paths within schemas or validated instances.
use std::{fmt, fmt::Write, slice::Iter};
use std::{fmt, fmt::Write, slice::Iter, str::FromStr};
#[derive(Clone, Debug, Eq, PartialEq)]
/// JSON Pointer as a wrapper around individual path components.
@ -41,11 +41,21 @@ impl JSONPointer {
new.0.extend_from_slice(chunks);
new
}
pub(crate) fn as_slice(&self) -> &[PathChunk] {
&self.0
}
}
impl serde::Serialize for JSONPointer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl Default for JSONPointer {
fn default() -> Self {
JSONPointer(Vec::new())
@ -209,6 +219,53 @@ impl From<&[PathChunk]> for JSONPointer {
}
}
/// An absolute reference
#[derive(Debug, Clone, PartialEq)]
pub struct AbsolutePath(url::Url);
impl AbsolutePath {
pub(crate) fn with_path(&self, path: &str) -> Self {
let mut result = self.0.clone();
result.set_path(path);
AbsolutePath(result)
}
}
impl serde::Serialize for AbsolutePath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.to_string().as_str())
}
}
impl FromStr for AbsolutePath {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
url::Url::parse(s).map(AbsolutePath::from)
}
}
impl fmt::Display for AbsolutePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<AbsolutePath> for url::Url {
fn from(p: AbsolutePath) -> Self {
p.0
}
}
impl From<url::Url> for AbsolutePath {
fn from(u: url::Url) -> Self {
AbsolutePath(u)
}
}
#[cfg(test)]
mod tests {
use super::JSONPointer;

View File

@ -0,0 +1,427 @@
use crate::{
compilation::context::CompilationContext,
error::ErrorIterator,
keywords::BoxedValidator,
output::{Annotations, BasicOutput, ErrorDescription, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
validator::{format_validators, PartialApplication, Validate},
JSONSchema,
};
use ahash::AHashMap;
use std::{collections::VecDeque, fmt};
/// A node in the schema tree, returned by [`compile_validators`]
#[derive(Debug)]
pub(crate) struct SchemaNode {
validators: NodeValidators,
relative_path: JSONPointer,
absolute_path: Option<AbsolutePath>,
}
#[derive(Debug)]
enum NodeValidators {
/// The result of compiling a boolean valued schema, e.g
///
/// ```json
/// {
/// "additionalProperties": false
/// }
/// ```
///
/// Here the result of `compile_validators` called with the `false` value will return a
/// `SchemaNode` with a single `BooleanValidator` as it's `validators`.
Boolean { validator: Option<BoxedValidator> },
/// The result of compiling a schema which is composed of keywords (almost all schemas)
Keyword(Box<KeywordValidators>),
/// The result of compiling a schema which is "array valued", e.g the "dependencies" keyword of
/// draft 7 which can take values which are an array of other property names
Array { validators: Vec<BoxedValidator> },
}
#[derive(Debug)]
struct KeywordValidators {
/// The keywords on this node which were not recognized by any vocabularies. These are
/// stored so we can later produce them as annotations
unmatched_keywords: Option<AHashMap<String, serde_json::Value>>,
// We should probably use AHashMap here but it breaks a bunch of test which assume
// validators are in a particular order
validators: Vec<(String, BoxedValidator)>,
}
impl SchemaNode {
pub(crate) fn new_from_boolean(
context: &CompilationContext<'_>,
validator: Option<BoxedValidator>,
) -> SchemaNode {
SchemaNode {
relative_path: context.clone().into_pointer(),
absolute_path: context.base_uri().map(AbsolutePath::from),
validators: NodeValidators::Boolean { validator },
}
}
pub(crate) fn new_from_keywords(
context: &CompilationContext<'_>,
mut validators: Vec<(String, BoxedValidator)>,
unmatched_keywords: Option<AHashMap<String, serde_json::Value>>,
) -> SchemaNode {
validators.shrink_to_fit();
SchemaNode {
relative_path: context.clone().into_pointer(),
absolute_path: context.base_uri().map(AbsolutePath::from),
validators: NodeValidators::Keyword(Box::new(KeywordValidators {
unmatched_keywords,
validators,
})),
}
}
pub(crate) fn new_from_array(
context: &CompilationContext<'_>,
mut validators: Vec<BoxedValidator>,
) -> SchemaNode {
validators.shrink_to_fit();
SchemaNode {
relative_path: context.clone().into_pointer(),
absolute_path: context.base_uri().map(AbsolutePath::from),
validators: NodeValidators::Array { validators },
}
}
pub(crate) fn validators(&self) -> impl Iterator<Item = &BoxedValidator> + ExactSizeIterator {
match &self.validators {
NodeValidators::Boolean { validator } => {
if let Some(v) = validator {
NodeValidatorsIter::BooleanValidators(std::iter::once(v))
} else {
NodeValidatorsIter::NoValidator
}
}
NodeValidators::Keyword(kvals) => {
NodeValidatorsIter::KeywordValidators(kvals.validators.iter())
}
NodeValidators::Array { validators } => {
NodeValidatorsIter::ArrayValidators(validators.iter())
}
}
}
fn format_validators(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", format_validators(self.validators()))
}
/// This is similar to `Validate::apply` except that `SchemaNode` knows where it is in the
/// validator tree and so rather than returning a `PartialApplication` it is able to return a
/// complete `BasicOutput`. This is the mechanism which compositional validators use to combine
/// results from sub-schemas
pub(crate) fn apply_rooted(
&self,
schema: &JSONSchema,
instance: &serde_json::Value,
instance_path: &InstancePath,
) -> BasicOutput {
match self.apply(schema, instance, instance_path) {
PartialApplication::Valid {
annotations,
mut child_results,
} => {
if let Some(annotations) = annotations {
child_results.insert(0, self.annotation_at(instance_path, annotations));
};
BasicOutput::Valid(child_results)
}
PartialApplication::Invalid {
errors,
mut child_results,
} => {
for error in errors {
child_results.insert(0, self.error_at(instance_path, error));
}
BasicOutput::Invalid(child_results)
}
}
}
/// Create an error output which is marked as occurring at this schema node
pub(crate) fn error_at(
&self,
instance_path: &InstancePath,
error: ErrorDescription,
) -> OutputUnit<ErrorDescription> {
OutputUnit::<ErrorDescription>::error(
self.relative_path.clone(),
instance_path.clone().into(),
self.absolute_path.clone(),
error,
)
}
/// Create an anontation output which is marked as occurring at this schema node
pub(crate) fn annotation_at<'a>(
&self,
instance_path: &InstancePath,
annotations: Annotations<'a>,
) -> OutputUnit<Annotations<'a>> {
OutputUnit::<Annotations<'_>>::annotations(
self.relative_path.clone(),
instance_path.clone().into(),
self.absolute_path.clone(),
annotations,
)
}
/// Here we return a `NodeValidatorsErrIter` to avoid allocating in some situations. This isn't
/// always possible but for a lot of common cases (e.g nodes with a single child) we can do it.
/// This is wrapped in a `Box` by `SchemaNode::validate`
pub(crate) fn err_iter<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b serde_json::Value,
instance_path: &InstancePath,
) -> NodeValidatorsErrIter<'b> {
match &self.validators {
NodeValidators::Keyword(kvs) if kvs.validators.len() == 1 => {
NodeValidatorsErrIter::Single(kvs.validators[0].1.validate(
schema,
instance,
instance_path,
))
}
NodeValidators::Keyword(kvs) => NodeValidatorsErrIter::Multiple(
kvs.validators
.iter()
.flat_map(|(_, v)| v.validate(schema, instance, instance_path))
.collect::<Vec<_>>()
.into_iter(),
),
NodeValidators::Boolean {
validator: Some(v), ..
} => NodeValidatorsErrIter::Single(v.validate(schema, instance, instance_path)),
NodeValidators::Boolean {
validator: None, ..
} => NodeValidatorsErrIter::NoErrs,
NodeValidators::Array { validators } => NodeValidatorsErrIter::Multiple(
validators
.iter()
.flat_map(move |v| v.validate(schema, instance, instance_path))
.collect::<Vec<_>>()
.into_iter(),
),
}
}
/// Helper function to apply an iterator of `(Into<PathChunk>, Validate)` to a value. This is
/// useful as a keyword schemanode has a set of validators keyed by their keywords, so the
/// `Into<Pathchunk>` is a `String` whereas an array schemanode has an array of validators so
/// the `Into<PathChunk>` is a `usize`
fn apply_subschemas<'a, I, P>(
&self,
schema: &JSONSchema,
instance: &serde_json::Value,
instance_path: &InstancePath,
path_and_validators: I,
annotations: Option<Annotations<'a>>,
) -> PartialApplication<'a>
where
I: Iterator<Item = (P, &'a Box<dyn Validate + Send + Sync + 'a>)> + 'a,
P: Into<crate::paths::PathChunk>,
P: std::fmt::Display,
{
let mut success_results: VecDeque<OutputUnit<Annotations>> = VecDeque::new();
let mut error_results = VecDeque::new();
for (path, validator) in path_and_validators {
let path = self.relative_path.extend_with(&[path.into()]);
let absolute_path = self
.absolute_path
.clone()
.map(|p| p.with_path(path.to_string().as_str()));
match validator.apply(schema, instance, instance_path) {
PartialApplication::Valid {
annotations,
child_results,
} => {
if let Some(annotations) = annotations {
success_results.push_front(OutputUnit::<Annotations<'a>>::annotations(
path,
instance_path.into(),
absolute_path,
annotations,
));
}
success_results.extend(child_results);
}
PartialApplication::Invalid {
errors: these_errors,
child_results,
} => {
error_results.extend(child_results);
error_results.extend(these_errors.into_iter().map(|error| {
OutputUnit::<ErrorDescription>::error(
path.clone(),
instance_path.into(),
absolute_path.clone(),
error,
)
}));
}
}
}
if !error_results.is_empty() {
PartialApplication::Invalid {
errors: Vec::new(),
child_results: error_results,
}
} else {
PartialApplication::Valid {
annotations,
child_results: success_results,
}
}
}
}
impl fmt::Display for SchemaNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.format_validators(f)
}
}
impl Validate for SchemaNode {
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b serde_json::Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
return Box::new(self.err_iter(schema, instance, instance_path));
}
fn is_valid(&self, schema: &JSONSchema, instance: &serde_json::Value) -> bool {
match &self.validators {
// If we only have one validator then calling it's `is_valid` directly does
// actually save the 20 or so instructions required to call the `slice::Iter::all`
// implementation. Validators at the leaf of a tree are all single node validators so
// this optimization can have significant cumulative benefits
NodeValidators::Keyword(kvs) if kvs.validators.len() == 1 => {
kvs.validators[0].1.is_valid(schema, instance)
}
NodeValidators::Keyword(kvs) => kvs
.validators
.iter()
.all(|(_, v)| v.is_valid(schema, instance)),
NodeValidators::Array { validators } => {
validators.iter().all(|v| v.is_valid(schema, instance))
}
NodeValidators::Boolean {
validator: Some(v), ..
} => v.is_valid(schema, instance),
NodeValidators::Boolean {
validator: None, ..
} => true,
}
}
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &serde_json::Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
match self.validators {
NodeValidators::Array { ref validators } => self.apply_subschemas(
schema,
instance,
instance_path,
validators.iter().enumerate(),
None,
),
NodeValidators::Boolean { ref validator } => {
if let Some(validator) = validator {
validator.apply(schema, instance, instance_path)
} else {
PartialApplication::Valid {
annotations: None,
child_results: VecDeque::new(),
}
}
}
NodeValidators::Keyword(ref kvals) => {
let KeywordValidators {
ref unmatched_keywords,
ref validators,
} = **kvals;
let annotations: Option<Annotations<'a>> =
unmatched_keywords.as_ref().map(Annotations::from);
self.apply_subschemas(
schema,
instance,
instance_path,
validators.iter().map(|(p, v)| (p.clone(), v)),
annotations,
)
}
}
}
}
enum NodeValidatorsIter<'a> {
NoValidator,
BooleanValidators(std::iter::Once<&'a BoxedValidator>),
KeywordValidators(std::slice::Iter<'a, (String, BoxedValidator)>),
ArrayValidators(std::slice::Iter<'a, BoxedValidator>),
}
impl<'a> Iterator for NodeValidatorsIter<'a> {
type Item = &'a BoxedValidator;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::NoValidator => None,
Self::BooleanValidators(i) => i.next(),
Self::KeywordValidators(v) => v.next().map(|(_, v)| v),
Self::ArrayValidators(v) => v.next(),
}
}
fn all<F>(&mut self, mut f: F) -> bool
where
Self: Sized,
F: FnMut(Self::Item) -> bool,
{
match self {
Self::NoValidator => true,
Self::BooleanValidators(i) => i.all(f),
Self::KeywordValidators(v) => v.all(|(_, v)| f(v)),
Self::ArrayValidators(v) => v.all(f),
}
}
}
impl<'a> ExactSizeIterator for NodeValidatorsIter<'a> {
fn len(&self) -> usize {
match self {
Self::NoValidator => 0,
Self::BooleanValidators(..) => 1,
Self::KeywordValidators(v) => v.len(),
Self::ArrayValidators(v) => v.len(),
}
}
}
pub(crate) enum NodeValidatorsErrIter<'a> {
NoErrs,
Single(ErrorIterator<'a>),
Multiple(std::vec::IntoIter<crate::error::ValidationError<'a>>),
}
impl<'a> Iterator for NodeValidatorsErrIter<'a> {
type Item = crate::error::ValidationError<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::NoErrs => None,
Self::Single(i) => i.next(),
Self::Multiple(ms) => ms.next(),
}
}
}

View File

@ -1,9 +1,28 @@
use crate::{
compilation::JSONSchema, error::ErrorIterator, keywords::BoxedValidator, paths::InstancePath,
compilation::JSONSchema,
error::ErrorIterator,
keywords::BoxedValidator,
output::{Annotations, ErrorDescription, OutputUnit},
paths::InstancePath,
schema_node::SchemaNode,
};
use serde_json::Value;
use std::fmt;
use std::{collections::VecDeque, fmt};
/// The Validate trait represents a predicate over some JSON value. Some validators are very simple
/// predicates such as "a value which is a string", whereas others may be much more complex,
/// consisting of several other validators composed together in various ways.
///
/// Much of the time all an application cares about is whether the predicate returns true or false,
/// in that case the `is_valid` function is sufficient. Sometimes applications will want more
/// detail about why a schema has failed, in which case the `validate` method can be used to
/// iterate over the errors produced by this validator. Finally, applications may be interested in
/// annotations produced by schemas over valid results, in this case the `apply` method can be used
/// to obtain this information.
///
/// If you are implementing `Validate` it is often sufficient to implement `validate` and
/// `is_valid`. `apply` is only necessary for validators which compose other validators. See the
/// documentation for `apply` for more information.
pub(crate) trait Validate: Send + Sync + core::fmt::Display {
fn validate<'a, 'b>(
&self,
@ -15,6 +34,127 @@ pub(crate) trait Validate: Send + Sync + core::fmt::Display {
// It is faster for cases when the result is not needed (like anyOf), since errors are
// not constructed
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool;
/// `apply` applies this validator and any sub-validators it is composed of to the value in
/// question and collects the resulting annotations or errors. Note that the result of `apply`
/// is a `PartialApplication`.
///
/// What does "partial" mean in this context? Each valdator can produce annotations or errors
/// in the case of successful or unsuccessful validation respectively. We're ultimately
/// producing these errors and annotations to produce the "basic" output format as specified in
/// the 2020-12 draft specification. In this format each annotation or error must include a
/// json pointer to the keyword in the schema and to the property in the instance. However,
/// most validators don't know where they are in the schema tree so we allow them to return the
/// errors or annotations they produce directly and leave it up to the parent validator to fill
/// in the path information. This means that only validators which are composed of other
/// validators must implement `apply`, for validators on the leaves of the validator tree the
/// default implementation which is defined in terms of `validate` will suffice.
///
/// If you are writing a validator which is composed of other validators then your validator will
/// need to store references to the `SchemaNode`s which contain those other validators.
/// `SchemaNode` stores information about where it is in the schema tree and therefore provides an
/// `apply_rooted` method which returns a full `BasicOutput`. `BasicOutput` implements `AddAssign`
/// so a typical pattern is to compose results from sub validators using `+=` and then use the
/// `From<BasicOutput> for PartialApplication` impl to convert the composed outputs into a
/// `PartialApplication` to return. For example, here is the implementation of
/// `IfThenValidator`
///
/// ```rust,ignore
/// // Note that self.schema is a `SchemaNode` and we use `apply_rooted` to return a `BasicOutput`
/// let mut if_result = self.schema.apply_rooted(schema, instance, instance_path);
/// if if_result.is_valid() {
/// // here we use the `AddAssign` implementation to combine the results of subschemas
/// if_result += self
/// .then_schema
/// .apply_rooted(schema, instance, instance_path);
/// // Here we use the `From<BasicOutput> for PartialApplication impl
/// if_result.into()
/// } else {
/// self.else_schema
/// .apply_rooted(schema, instance, instance_path)
/// .into()
/// }
/// ```
///
/// `BasicOutput` also implements `Sum<BasicOutput>` and `FromIterator<BasicOutput<'a>> for PartialApplication<'a>`
/// so you can use `sum()` and `collect()` in simple cases.
fn apply<'a>(
&'a self,
schema: &JSONSchema,
instance: &Value,
instance_path: &InstancePath,
) -> PartialApplication<'a> {
let errors: Vec<ErrorDescription> = self
.validate(schema, instance, instance_path)
.map(ErrorDescription::from)
.collect();
if errors.is_empty() {
PartialApplication::valid_empty()
} else {
PartialApplication::invalid_empty(errors)
}
}
}
/// The result of applying a validator to an instance. As explained in the documentation for
/// `Validate::apply` this is a "partial" result because it does not include information about
/// where the error or annotation occurred.
#[derive(Clone, PartialEq)]
pub(crate) enum PartialApplication<'a> {
Valid {
/// Annotations produced by this validator
annotations: Option<Annotations<'a>>,
/// Any outputs produced by validators which are children of this validator
child_results: VecDeque<OutputUnit<Annotations<'a>>>,
},
Invalid {
/// Errors which caused this schema to be invalid
errors: Vec<ErrorDescription>,
/// Any error outputs produced by child validators of this validator
child_results: VecDeque<OutputUnit<ErrorDescription>>,
},
}
impl<'a> PartialApplication<'a> {
/// Create an empty `PartialApplication` which is valid
pub(crate) fn valid_empty() -> PartialApplication<'static> {
PartialApplication::Valid {
annotations: None,
child_results: VecDeque::new(),
}
}
/// Create an empty `PartialApplication` which is invalid
pub(crate) fn invalid_empty(errors: Vec<ErrorDescription>) -> PartialApplication<'static> {
PartialApplication::Invalid {
errors,
child_results: VecDeque::new(),
}
}
/// Set the annotation that will be returned for the current validator. If this
/// `PartialApplication` is invalid then this method does nothing
pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) {
match self {
Self::Valid { annotations, .. } => *annotations = Some(new_annotations),
Self::Invalid { .. } => {}
}
}
/// Set the error that will be returned for the current validator. If this
/// `PartialApplication` is valid then this method converts this application into
/// `PartialApplication::Invalid`
pub(crate) fn mark_errored(&mut self, error: ErrorDescription) {
match self {
Self::Invalid { errors, .. } => errors.push(error),
Self::Valid { .. } => {
*self = Self::Invalid {
errors: vec![error],
child_results: VecDeque::new(),
}
}
}
}
}
impl fmt::Debug for dyn Validate + Send + Sync {
@ -23,13 +163,15 @@ impl fmt::Debug for dyn Validate + Send + Sync {
}
}
pub(crate) type Validators = Vec<BoxedValidator>;
pub(crate) fn format_validators(validators: &[BoxedValidator]) -> String {
pub(crate) fn format_validators<'a, I: ExactSizeIterator + Iterator<Item = &'a BoxedValidator>>(
mut validators: I,
) -> String {
match validators.len() {
0 => "{}".to_string(),
1 => {
let name = validators[0].to_string();
// Unwrap is okay due to the check on len
let validator = validators.next().unwrap();
let name = validator.to_string();
match name.as_str() {
// boolean validators are represented as is, without brackets because if they
// occur in a vector, then the schema is not a key/value mapping
@ -40,7 +182,6 @@ pub(crate) fn format_validators(validators: &[BoxedValidator]) -> String {
_ => format!(
"{{{}}}",
validators
.iter()
.map(|validator| format!("{:?}", validator))
.collect::<Vec<String>>()
.join(", ")
@ -48,18 +189,21 @@ pub(crate) fn format_validators(validators: &[BoxedValidator]) -> String {
}
}
pub(crate) fn format_vec_of_validators(validators: &[Validators]) -> String {
pub(crate) fn format_iter_of_validators<'a, G, I>(validators: I) -> String
where
I: Iterator<Item = G>,
G: ExactSizeIterator + Iterator<Item = &'a BoxedValidator>,
{
validators
.iter()
.map(|v| format_validators(v))
.map(format_validators)
.collect::<Vec<String>>()
.join(", ")
}
pub(crate) fn format_key_value_validators(validators: &[(String, Validators)]) -> String {
pub(crate) fn format_key_value_validators(validators: &[(String, SchemaNode)]) -> String {
validators
.iter()
.map(|(name, validators)| format!("{}: {}", name, format_validators(validators)))
.map(|(name, node)| format!("{}: {}", name, format_validators(node.validators())))
.collect::<Vec<String>>()
.join(", ")
}

1020
jsonschema/tests/output.rs Normal file

File diff suppressed because it is too large Load Diff