jsonschema-rs/jsonschema/src/keywords/contains.rs

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")
}
}