469 lines
15 KiB
Rust
469 lines
15 KiB
Rust
use crate::{
|
|
compilation::{compile_validators, context::CompilationContext, JSONSchema},
|
|
error::{error, no_error, ErrorIterator, ValidationError},
|
|
keywords::CompilationResult,
|
|
paths::{InstancePath, JSONPointer},
|
|
schema_node::SchemaNode,
|
|
validator::{format_validators, PartialApplication, Validate},
|
|
Draft,
|
|
};
|
|
use serde_json::{Map, Value};
|
|
|
|
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
|
|
use super::helpers::map_get_u64;
|
|
|
|
pub(crate) struct ContainsValidator {
|
|
node: SchemaNode,
|
|
schema_path: JSONPointer,
|
|
}
|
|
|
|
impl ContainsValidator {
|
|
#[inline]
|
|
pub(crate) fn compile<'a>(
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
) -> CompilationResult<'a> {
|
|
let keyword_context = context.with_path("contains");
|
|
Ok(Box::new(ContainsValidator {
|
|
node: compile_validators(schema, &keyword_context)?,
|
|
schema_path: keyword_context.into_pointer(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Validate for ContainsValidator {
|
|
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
|
|
if let Value::Array(items) = instance {
|
|
items.iter().any(|i| self.node.is_valid(schema, i))
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
fn validate<'a, 'b>(
|
|
&self,
|
|
schema: &'a JSONSchema,
|
|
instance: &'b Value,
|
|
instance_path: &InstancePath,
|
|
) -> ErrorIterator<'b> {
|
|
if let Value::Array(items) = instance {
|
|
if items.iter().any(|i| self.node.is_valid(schema, i)) {
|
|
return no_error();
|
|
}
|
|
error(ValidationError::contains(
|
|
self.schema_path.clone(),
|
|
instance_path.into(),
|
|
instance,
|
|
))
|
|
} else {
|
|
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.node.validators()))
|
|
}
|
|
}
|
|
|
|
/// `minContains` validation. Used only if there is no `maxContains` present.
|
|
///
|
|
/// Docs: <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.5>
|
|
pub(crate) struct MinContainsValidator {
|
|
node: SchemaNode,
|
|
min_contains: u64,
|
|
schema_path: JSONPointer,
|
|
}
|
|
|
|
impl MinContainsValidator {
|
|
#[inline]
|
|
pub(crate) fn compile<'a>(
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
min_contains: u64,
|
|
) -> CompilationResult<'a> {
|
|
let keyword_context = context.with_path("minContains");
|
|
Ok(Box::new(MinContainsValidator {
|
|
node: compile_validators(schema, context)?,
|
|
min_contains,
|
|
schema_path: keyword_context.into_pointer(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Validate for MinContainsValidator {
|
|
fn validate<'a, 'b>(
|
|
&self,
|
|
schema: &'a JSONSchema,
|
|
instance: &'b Value,
|
|
instance_path: &InstancePath,
|
|
) -> ErrorIterator<'b> {
|
|
if let Value::Array(items) = instance {
|
|
// From docs:
|
|
// An array instance is valid against "minContains" if the number of elements
|
|
// that are valid against the schema for "contains" is greater than, or equal to,
|
|
// the value of this keyword.
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
// Shortcircuit - there is enough matches to satisfy `minContains`
|
|
if matches >= self.min_contains {
|
|
return no_error();
|
|
}
|
|
}
|
|
}
|
|
if self.min_contains > 0 {
|
|
error(ValidationError::contains(
|
|
self.schema_path.clone(),
|
|
instance_path.into(),
|
|
instance,
|
|
))
|
|
} else {
|
|
// No matches needed
|
|
no_error()
|
|
}
|
|
} else {
|
|
no_error()
|
|
}
|
|
}
|
|
|
|
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
|
|
if let Value::Array(items) = instance {
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
if matches >= self.min_contains {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
self.min_contains == 0
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Display for MinContainsValidator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"minContains: {}, contains: {}",
|
|
self.min_contains,
|
|
format_validators(self.node.validators())
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `maxContains` validation. Used only if there is no `minContains` present.
|
|
///
|
|
/// Docs: <https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.4.4>
|
|
pub(crate) struct MaxContainsValidator {
|
|
node: SchemaNode,
|
|
max_contains: u64,
|
|
schema_path: JSONPointer,
|
|
}
|
|
|
|
impl MaxContainsValidator {
|
|
#[inline]
|
|
pub(crate) fn compile<'a>(
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
max_contains: u64,
|
|
) -> CompilationResult<'a> {
|
|
let keyword_context = context.with_path("maxContains");
|
|
Ok(Box::new(MaxContainsValidator {
|
|
node: compile_validators(schema, context)?,
|
|
max_contains,
|
|
schema_path: keyword_context.into_pointer(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Validate for MaxContainsValidator {
|
|
fn validate<'a, 'b>(
|
|
&self,
|
|
schema: &'a JSONSchema,
|
|
instance: &'b Value,
|
|
instance_path: &InstancePath,
|
|
) -> ErrorIterator<'b> {
|
|
if let Value::Array(items) = instance {
|
|
// From docs:
|
|
// An array instance is valid against "maxContains" if the number of elements
|
|
// that are valid against the schema for "contains" is less than, or equal to,
|
|
// the value of this keyword.
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
// Shortcircuit - there should be no more than `self.max_contains` matches
|
|
if matches > self.max_contains {
|
|
return error(ValidationError::contains(
|
|
self.schema_path.clone(),
|
|
instance_path.into(),
|
|
instance,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
if matches > 0 {
|
|
// It is also less or equal to `self.max_contains`
|
|
// otherwise the loop above would exit early
|
|
no_error()
|
|
} else {
|
|
// No matches - it should be at least one match to satisfy `contains`
|
|
return error(ValidationError::contains(
|
|
self.schema_path.clone(),
|
|
instance_path.into(),
|
|
instance,
|
|
));
|
|
}
|
|
} else {
|
|
no_error()
|
|
}
|
|
}
|
|
|
|
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
|
|
if let Value::Array(items) = instance {
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
if matches > self.max_contains {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
matches != 0
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Display for MaxContainsValidator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"maxContains: {}, contains: {}",
|
|
self.max_contains,
|
|
format_validators(self.node.validators())
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `maxContains` & `minContains` validation combined.
|
|
///
|
|
/// Docs:
|
|
/// `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 {
|
|
node: SchemaNode,
|
|
min_contains: u64,
|
|
max_contains: u64,
|
|
schema_path: JSONPointer,
|
|
}
|
|
|
|
impl MinMaxContainsValidator {
|
|
#[inline]
|
|
pub(crate) fn compile<'a>(
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
min_contains: u64,
|
|
max_contains: u64,
|
|
) -> CompilationResult<'a> {
|
|
Ok(Box::new(MinMaxContainsValidator {
|
|
node: compile_validators(schema, context)?,
|
|
min_contains,
|
|
max_contains,
|
|
schema_path: context.schema_path.clone().into(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Validate for MinMaxContainsValidator {
|
|
fn validate<'a, 'b>(
|
|
&self,
|
|
schema: &'a JSONSchema,
|
|
instance: &'b Value,
|
|
instance_path: &InstancePath,
|
|
) -> ErrorIterator<'b> {
|
|
if let Value::Array(items) = instance {
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
// Shortcircuit - there should be no more than `self.max_contains` matches
|
|
if matches > self.max_contains {
|
|
return error(ValidationError::contains(
|
|
self.schema_path.clone_with("maxContains"),
|
|
instance_path.into(),
|
|
instance,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
if matches < self.min_contains {
|
|
// Not enough matches
|
|
error(ValidationError::contains(
|
|
self.schema_path.clone_with("minContains"),
|
|
instance_path.into(),
|
|
instance,
|
|
))
|
|
} else {
|
|
no_error()
|
|
}
|
|
} else {
|
|
no_error()
|
|
}
|
|
}
|
|
|
|
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
|
|
if let Value::Array(items) = instance {
|
|
let mut matches = 0;
|
|
for item in items {
|
|
if self
|
|
.node
|
|
.validators()
|
|
.all(|validator| validator.is_valid(schema, item))
|
|
{
|
|
matches += 1;
|
|
if matches > self.max_contains {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
matches <= self.max_contains && matches >= self.min_contains
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Display for MinMaxContainsValidator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"minContains: {}, maxContains: {}, contains: {}",
|
|
self.min_contains,
|
|
self.max_contains,
|
|
format_validators(self.node.validators())
|
|
)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn compile<'a>(
|
|
parent: &'a Map<String, Value>,
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
) -> Option<CompilationResult<'a>> {
|
|
match context.config.draft() {
|
|
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 => {
|
|
Some(ContainsValidator::compile(schema, context))
|
|
}
|
|
#[cfg(all(feature = "draft201909", feature = "draft202012"))]
|
|
Draft::Draft201909 | Draft::Draft202012 => compile_contains(parent, schema, context),
|
|
#[cfg(all(feature = "draft201909", not(feature = "draft202012")))]
|
|
Draft::Draft201909 => compile_contains(parent, schema, context),
|
|
#[cfg(all(feature = "draft202012", not(feature = "draft201909")))]
|
|
Draft::Draft202012 => compile_contains(parent, schema, context),
|
|
}
|
|
}
|
|
|
|
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
|
|
#[inline]
|
|
fn compile_contains<'a>(
|
|
parent: &'a Map<String, Value>,
|
|
schema: &'a Value,
|
|
context: &CompilationContext,
|
|
) -> Option<CompilationResult<'a>> {
|
|
let min_contains = match map_get_u64(parent, context, "minContains").transpose() {
|
|
Ok(n) => n,
|
|
Err(err) => return Some(Err(err)),
|
|
};
|
|
let max_contains = match map_get_u64(parent, context, "maxContains").transpose() {
|
|
Ok(n) => n,
|
|
Err(err) => return Some(Err(err)),
|
|
};
|
|
|
|
match (min_contains, max_contains) {
|
|
(Some(min), Some(max)) => Some(MinMaxContainsValidator::compile(schema, context, min, max)),
|
|
(Some(min), None) => Some(MinContainsValidator::compile(schema, context, min)),
|
|
(None, Some(max)) => Some(MaxContainsValidator::compile(schema, context, max)),
|
|
(None, None) => Some(ContainsValidator::compile(schema, context)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::tests_util;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn schema_path() {
|
|
tests_util::assert_schema_path(&json!({"contains": {"const": 2}}), &json!([]), "/contains")
|
|
}
|
|
}
|