feat: Implement `prefixItems` keyword

This commit is contained in:
Rafael Caricio 2021-10-24 16:17:20 +02:00 committed by Dmitry Dygalo
parent f8b8ec0d6c
commit 0e150641e1
18 changed files with 899 additions and 35 deletions

View File

@ -38,8 +38,9 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
draft: [draft201909, draft202012]
name: Test (stable) on ${{ matrix.os}}
name: Test ${{ matrix.draft }} (stable) on ${{ matrix.os}}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
@ -51,7 +52,7 @@ jobs:
profile: minimal
toolchain: stable
override: true
- run: cargo test --no-fail-fast --features draft201909
- run: cargo test --no-fail-fast --features ${{ matrix.draft }}
working-directory: ./jsonschema
coverage:

View File

@ -2,6 +2,8 @@
## [Unreleased]
- Support for `prefixItems` keyword. [#303](https://github.com/Stranger6667/jsonschema-rs/pull/303)
## [0.13.1] - 2021-10-28
### Fixed
@ -14,7 +16,7 @@
- `uuid` format validator. [#266](https://github.com/Stranger6667/jsonschema-rs/issues/266)
- `duration` format validator. [#265](https://github.com/Stranger6667/jsonschema-rs/issues/265)
- Collect annotations whilst evaulating schemas. [#262](https://github.com/Stranger6667/jsonschema-rs/issues/262)
- Collect annotations whilst evaluating schemas. [#262](https://github.com/Stranger6667/jsonschema-rs/issues/262)
- Option to turn off processing of the `format` keyword. [#261](https://github.com/Stranger6667/jsonschema-rs/issues/261)
- `basic` & `flag` output formatting styles. [#100](https://github.com/Stranger6667/jsonschema-rs/issues/100)
- Support for `dependentRequired` & `dependentSchemas` keywords. [#286](https://github.com/Stranger6667/jsonschema-rs/issues/286)

View File

@ -19,6 +19,7 @@ name = "jsonschema"
cli = ["structopt"]
default = ["reqwest", "cli"]
draft201909 = []
draft202012 = []
reqwest-native-tls = ["reqwest/native-tls"]
reqwest-native-tls-alpn = ["reqwest/native-tls-alpn"]
reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"]

View File

@ -0,0 +1,48 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/applicator": true
},
"$dynamicAnchor": "meta",
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"prefixItems": { "$ref": "#/$defs/schemaArray" },
"items": { "$dynamicRef": "#meta" },
"contains": { "$dynamicRef": "#meta" },
"additionalProperties": { "$dynamicRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"propertyNames": { "$dynamicRef": "#meta" },
"if": { "$dynamicRef": "#meta" },
"then": { "$dynamicRef": "#meta" },
"else": { "$dynamicRef": "#meta" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$dynamicRef": "#meta" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$dynamicRef": "#meta" }
}
}
}

View File

@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$dynamicRef": "#meta" }
}
}

View File

@ -0,0 +1,51 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true
},
"$dynamicAnchor": "meta",
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"$ref": "#/$defs/uriReferenceString",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": { "$ref": "#/$defs/uriString" },
"$ref": { "$ref": "#/$defs/uriReferenceString" },
"$anchor": { "$ref": "#/$defs/anchorString" },
"$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
"$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
"$vocabulary": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/uriString" },
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" }
}
},
"$defs": {
"anchorString": {
"type": "string",
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
},
"uriString": {
"type": "string",
"format": "uri"
},
"uriReferenceString": {
"type": "string",
"format": "uri-reference"
}
}
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for annotation results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}

View File

@ -0,0 +1,37 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/meta-data": true
},
"$dynamicAnchor": "meta",
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}

View File

@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true
},
"$dynamicAnchor": "meta",
"title": "Unevaluated applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"unevaluatedItems": { "$dynamicRef": "#meta" },
"unevaluatedProperties": { "$dynamicRef": "#meta" }
}
}

View File

@ -0,0 +1,98 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/validation": true
},
"$dynamicAnchor": "meta",
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}

View File

@ -0,0 +1,58 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/unevaluated"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format-annotation"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
"properties": {
"definitions": {
"$comment": "\"definitions\" has been replaced by \"$defs\".",
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"deprecated": true,
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$dynamicRef": "#meta" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
},
"deprecated": true,
"default": {}
},
"$recursiveAnchor": {
"$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
"$ref": "meta/core#/$defs/anchorString",
"deprecated": true
},
"$recursiveRef": {
"$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
"$ref": "meta/core#/$defs/uriReferenceString",
"deprecated": true
}
}
}

View File

@ -24,6 +24,14 @@ lazy_static::lazy_static! {
static ref DRAFT201909_FORMAT:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2019-09/meta/format.json")).expect("Valid schema!");
static ref DRAFT201909_META_DATA:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2019-09/meta/meta-data.json")).expect("Valid schema!");
static ref DRAFT201909_VALIDATION:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2019-09/meta/validation.json")).expect("Valid schema!");
static ref DRAFT202012:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/schema.json")).expect("Valid schema!");
static ref DRAFT202012_CORE:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/core.json")).expect("Valid schema!");
static ref DRAFT202012_APPLICATOR:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/applicator.json")).expect("Valid schema!");
static ref DRAFT202012_UNEVALUATED:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/unevaluated.json")).expect("Valid schema!");
static ref DRAFT202012_VALIDATION:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/validation.json")).expect("Valid schema!");
static ref DRAFT202012_META_DATA:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/meta-data.json")).expect("Valid schema!");
static ref DRAFT202012_FORMAT_ANNOTATION:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/format-annotation.json")).expect("Valid schema!");
static ref DRAFT202012_CONTENT:serde_json::Value = serde_json::from_str(include_str!("../../meta_schemas/draft2020-12/meta/content.json")).expect("Valid schema!");
static ref META_SCHEMAS: AHashMap<String, Arc<serde_json::Value>> = {
let mut store = AHashMap::with_capacity(3);
@ -70,6 +78,41 @@ lazy_static::lazy_static! {
Arc::new(DRAFT201909_VALIDATION.clone())
);
}
#[cfg(feature = "draft202012")]
{
store.insert(
"https://json-schema.org/draft/2020-12/schema".to_string(),
Arc::new(DRAFT202012.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/core".to_string(),
Arc::new(DRAFT202012_CORE.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/applicator".to_string(),
Arc::new(DRAFT202012_APPLICATOR.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/unevaluated".to_string(),
Arc::new(DRAFT202012_UNEVALUATED.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/validation".to_string(),
Arc::new(DRAFT202012_VALIDATION.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/meta-data".to_string(),
Arc::new(DRAFT202012_META_DATA.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/format-annotation".to_string(),
Arc::new(DRAFT202012_FORMAT_ANNOTATION.clone())
);
store.insert(
"https://json-schema.org/draft/2020-12/meta/content".to_string(),
Arc::new(DRAFT202012_CONTENT.clone())
);
}
store
};
@ -119,6 +162,46 @@ lazy_static::lazy_static! {
.compile(&DRAFT201909)
.expect(EXPECT_MESSAGE)
);
#[cfg(feature = "draft202012")]
store.insert(
schemas::Draft::Draft202012,
JSONSchema::options()
.without_schema_validation()
.with_document(
"https://json-schema.org/draft/2020-12/meta/applicator".to_string(),
DRAFT202012_APPLICATOR.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/core".to_string(),
DRAFT202012_CORE.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/applicator".to_string(),
DRAFT202012_APPLICATOR.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/unevaluated".to_string(),
DRAFT202012_UNEVALUATED.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/validation".to_string(),
DRAFT202012_VALIDATION.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/meta-data".to_string(),
DRAFT202012_META_DATA.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/format-annotation".to_string(),
DRAFT202012_FORMAT_ANNOTATION.clone()
)
.with_document(
"https://json-schema.org/draft/2020-12/meta/content".to_string(),
DRAFT202012_CONTENT.clone()
)
.compile(&DRAFT202012)
.expect(EXPECT_MESSAGE)
);
store
};
}

View File

@ -9,7 +9,7 @@ use crate::{
};
use serde_json::{Map, Value};
#[cfg(feature = "draft201909")]
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
use super::helpers::map_get_u64;
pub(crate) struct ContainsValidator {
@ -423,26 +423,36 @@ pub(crate) fn compile<'a>(
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 => {
Some(ContainsValidator::compile(schema, context))
}
#[cfg(feature = "draft201909")]
Draft::Draft201909 => {
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)),
};
#[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),
}
}
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(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)),
}
}

View File

@ -75,6 +75,7 @@ impl core::fmt::Display for ItemsArrayValidator {
pub(crate) struct ItemsObjectValidator {
node: SchemaNode,
}
impl ItemsObjectValidator {
#[inline]
pub(crate) fn compile<'a>(
@ -131,8 +132,9 @@ impl Validate for ItemsObjectValidator {
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.
// was applied to any positions in the underlying array. Since the struct
// `ItemsObjectValidator` is not used when prefixItems is defined, 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
@ -148,21 +150,111 @@ impl core::fmt::Display for ItemsObjectValidator {
}
}
pub(crate) struct ItemsObjectSkipPrefixValidator {
node: SchemaNode,
skip_prefix: usize,
}
impl ItemsObjectSkipPrefixValidator {
#[inline]
pub(crate) fn compile<'a>(
schema: &'a Value,
skip_prefix: usize,
context: &CompilationContext,
) -> CompilationResult<'a> {
let keyword_context = context.with_path("items");
let node = compile_validators(schema, &keyword_context)?;
Ok(Box::new(ItemsObjectSkipPrefixValidator {
node,
skip_prefix,
}))
}
}
impl Validate for ItemsObjectSkipPrefixValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Array(items) = instance {
items
.iter()
.skip(self.skip_prefix)
.all(|i| self.node.is_valid(schema, i))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Value::Array(items) = instance {
let errors: Vec<_> = items
.iter()
.skip(self.skip_prefix)
.enumerate()
.flat_map(move |(idx, item)| {
self.node
.validate(schema, item, &instance_path.push(idx + self.skip_prefix))
})
.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::Array(items) = instance {
let mut results = Vec::with_capacity(items.len() - self.skip_prefix);
for (idx, item) in items.iter().skip(self.skip_prefix).enumerate() {
let path = instance_path.push(idx + self.skip_prefix);
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.
let schema_was_applied = items.len() > self.skip_prefix;
output.annotate(serde_json::json! {schema_was_applied}.into());
output
} else {
PartialApplication::valid_empty()
}
}
}
impl core::fmt::Display for ItemsObjectSkipPrefixValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "items: {}", format_validators(self.node.validators()))
}
}
#[inline]
pub(crate) fn compile<'a>(
_: &'a Map<String, Value>,
parent: &'a Map<String, Value>,
schema: &'a Value,
context: &CompilationContext,
) -> Option<CompilationResult<'a>> {
match schema {
Value::Array(items) => Some(ItemsArrayValidator::compile(items, context)),
Value::Object(_) => Some(ItemsObjectValidator::compile(schema, context)),
Value::Bool(value) => {
if *value {
None
} else {
Some(ItemsObjectValidator::compile(schema, context))
Value::Object(_) | Value::Bool(false) => {
#[cfg(feature = "draft202012")]
if let Some(Value::Array(prefix_items)) = parent.get("prefixItems") {
return Some(ItemsObjectSkipPrefixValidator::compile(
schema,
prefix_items.len(),
context,
));
}
Some(ItemsObjectValidator::compile(schema, context))
}
_ => None,
}

View File

@ -28,6 +28,7 @@ pub(crate) mod not;
pub(crate) mod one_of;
pub(crate) mod pattern;
pub(crate) mod pattern_properties;
pub(crate) mod prefix_items;
pub(crate) mod properties;
pub(crate) mod property_names;
pub(crate) mod ref_;

View File

@ -0,0 +1,262 @@
use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema},
error::{no_error, ErrorIterator, ValidationError},
paths::{InstancePath, JSONPointer},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
};
use serde_json::{Map, Value};
use super::CompilationResult;
pub(crate) struct PrefixItemsValidator {
schemas: Vec<SchemaNode>,
}
impl PrefixItemsValidator {
#[inline]
pub(crate) fn compile<'a>(
items: &'a [Value],
context: &CompilationContext,
) -> CompilationResult<'a> {
let keyword_context = context.with_path("prefixItems");
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)
}
Ok(Box::new(PrefixItemsValidator { schemas }))
}
}
impl Validate for PrefixItemsValidator {
fn is_valid(&self, schema: &JSONSchema, instance: &Value) -> bool {
if let Value::Array(items) = instance {
self.schemas
.iter()
.zip(items.iter())
.all(|(n, i)| n.is_valid(schema, i))
} else {
true
}
}
#[allow(clippy::needless_collect)]
fn validate<'a, 'b>(
&self,
schema: &'a JSONSchema,
instance: &'b Value,
instance_path: &InstancePath,
) -> ErrorIterator<'b> {
if let Value::Array(items) = instance {
let errors: Vec<_> = self
.schemas
.iter()
.zip(items.iter())
.enumerate()
.flat_map(|(idx, (n, i))| n.validate(schema, i, &instance_path.push(idx)))
.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::Array(items) = instance {
if !items.is_empty() {
let validate_total = self.schemas.len();
let mut results = Vec::with_capacity(validate_total);
let mut max_index_applied = 0;
for (idx, (schema_node, item)) in self.schemas.iter().zip(items.iter()).enumerate()
{
let path = instance_path.push(idx);
results.push(schema_node.apply_rooted(schema, item, &path));
max_index_applied = idx;
}
// Per draft 2020-12 section https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.3.1.1
// we must produce an annotation with the largest index of the underlying
// array which the subschema was applied. The value MAY be a boolean true if
// a subschema was applied to every index of the instance.
let schema_was_applied: Value = if results.len() == items.len() {
true.into()
} else {
max_index_applied.into()
};
let mut output: PartialApplication = results.into_iter().collect();
output.annotate(schema_was_applied.into());
return output;
}
}
PartialApplication::valid_empty()
}
}
impl core::fmt::Display for PrefixItemsValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"prefixItems: [{}]",
format_iter_of_validators(self.schemas.iter().map(SchemaNode::validators))
)
}
}
#[inline]
pub(crate) fn compile<'a>(
_: &'a Map<String, Value>,
schema: &'a Value,
context: &CompilationContext,
) -> Option<CompilationResult<'a>> {
if let Value::Array(items) = schema {
Some(PrefixItemsValidator::compile(items, context))
} else {
Some(Err(ValidationError::single_type_error(
JSONPointer::default(),
context.clone().into_pointer(),
schema,
PrimitiveType::Array,
)))
}
}
#[cfg(all(test, feature = "draft202012"))]
mod tests {
use crate::compilation::JSONSchema;
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(&json!({"prefixItems": [{"type": "integer"}, {"maximum": 5}]}), &json!(["string"]), "/prefixItems/0/type")]
#[test_case(&json!({"prefixItems": [{"type": "integer"}, {"maximum": 5}]}), &json!([42, 42]), "/prefixItems/1/maximum")]
#[test_case(&json!({"prefixItems": [{"type": "integer"}, {"maximum": 5}], "items": {"type": "boolean"}}), &json!([42, 1, 42]), "/items/type")]
#[test_case(&json!({"prefixItems": [{"type": "integer"}, {"maximum": 5}], "items": {"type": "boolean"}}), &json!([42, 42, true]), "/prefixItems/1/maximum")]
fn schema_path(schema: &Value, instance: &Value, expected: &str) {
tests_util::assert_schema_path(schema, instance, expected)
}
#[test_case(&json!({"prefixItems": [{"type": "integer"}, {"maximum": 5}]}), "prefixItems: [{type: integer}, {maximum: 5}]")]
fn debug_representation(schema: &Value, expected: &str) {
let compiled = JSONSchema::compile(schema).unwrap();
assert_eq!(
format!("{:?}", compiled.node.validators().next().unwrap()),
expected
);
}
#[test_case{
&json!({
"type": "array",
"prefixItems": [
{
"type": "string"
}
]
}),
&json!{[]},
&json!({
"valid": true,
"annotations": []
}); "valid prefixItems empty array"
}]
#[test_case{
&json!({
"type": "array",
"prefixItems": [
{
"type": "string"
},
{
"type": "number"
}
]
}),
&json!{["string", 1]},
&json!({
"valid": true,
"annotations": [
{
"keywordLocation": "/prefixItems",
"instanceLocation": "",
"annotations": true
},
]
}); "prefixItems valid items"
}]
#[test_case{
&json!({
"type": "array",
"prefixItems": [
{
"type": "string"
}
]
}),
&json!{["string", 1]},
&json!({
"valid": true,
"annotations": [
{
"keywordLocation": "/prefixItems",
"instanceLocation": "",
"annotations": 0
},
]
}); "prefixItems valid mixed items"
}]
#[test_case{
&json!({
"type": "array",
"items": {
"type": "number",
"annotation": "value"
},
"prefixItems": [
{
"type": "string"
},
{
"type": "boolean"
}
]
}),
&json!{["string", true, 2, 3]},
&json!({
"valid": true,
"annotations": [
{
"keywordLocation": "/prefixItems",
"instanceLocation": "",
"annotations": 1
},
{
"keywordLocation": "/items",
"instanceLocation": "",
"annotations": true
},
{
"annotations": {"annotation": "value" },
"instanceLocation": "/2",
"keywordLocation": "/items"
},
{
"annotations": {"annotation": "value" },
"instanceLocation": "/3",
"keywordLocation": "/items"
}
]
}); "valid prefixItems with mixed items"
}]
fn test_basic_output(schema_json: &Value, instance: &Value, expected_output: &Value) {
let schema = JSONSchema::options().compile(schema_json).unwrap();
let output_json = serde_json::to_value(schema.apply(instance).basic()).unwrap();
assert_eq!(&output_json, expected_output);
}
}

View File

@ -14,6 +14,10 @@ pub enum Draft {
#[cfg(feature = "draft201909")]
/// JSON Schema Draft 2019-09
Draft201909,
#[cfg(feature = "draft202012")]
/// JSON Schema Draft 2020-12
Draft202012,
}
impl Default for Draft {
@ -26,8 +30,12 @@ impl Draft {
pub(crate) const fn validate_formats_by_default(self) -> bool {
match self {
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 => true,
#[cfg(feature = "draft201909")]
#[cfg(all(feature = "draft201909", feature = "draft202012"))]
Draft::Draft201909 | Draft::Draft202012 => false,
#[cfg(all(feature = "draft201909", not(feature = "draft202012")))]
Draft::Draft201909 => false,
#[cfg(all(feature = "draft202012", not(feature = "draft201909")))]
Draft::Draft202012 => false,
}
}
}
@ -51,12 +59,16 @@ impl Draft {
Draft::Draft6 | Draft::Draft7 => Some(keywords::const_::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::const_::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::const_::compile),
},
"contains" => match self {
Draft::Draft4 => None,
Draft::Draft6 | Draft::Draft7 => Some(keywords::contains::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::contains::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::contains::compile),
},
"contentMediaType" => match self {
Draft::Draft7 | Draft::Draft6 => Some(keywords::content::compile_media_type),
@ -64,6 +76,8 @@ impl Draft {
#[cfg(feature = "draft201909")]
// Should be collected as an annotation
Draft::Draft201909 => None,
#[cfg(feature = "draft202012")]
Draft::Draft202012 => None,
},
"contentEncoding" => match self {
Draft::Draft7 | Draft::Draft6 => Some(keywords::content::compile_content_encoding),
@ -71,11 +85,13 @@ impl Draft {
#[cfg(feature = "draft201909")]
// Should be collected as an annotation
Draft::Draft201909 => None,
#[cfg(feature = "draft202012")]
Draft::Draft202012 => None,
},
"dependencies" => Some(keywords::dependencies::compile),
#[cfg(feature = "draft201909")]
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
"dependentRequired" => Some(keywords::dependencies::compile_dependent_required),
#[cfg(feature = "draft201909")]
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
"dependentSchemas" => Some(keywords::dependencies::compile_dependent_schemas),
"enum" => Some(keywords::enum_::compile),
"exclusiveMaximum" => match self {
@ -83,12 +99,16 @@ impl Draft {
Draft::Draft4 => None,
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::exclusive_maximum::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::exclusive_maximum::compile),
},
"exclusiveMinimum" => match self {
Draft::Draft7 | Draft::Draft6 => Some(keywords::exclusive_minimum::compile),
Draft::Draft4 => None,
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::exclusive_minimum::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::exclusive_minimum::compile),
},
"format" => Some(keywords::format::compile),
"if" => match self {
@ -96,6 +116,8 @@ impl Draft {
Draft::Draft6 | Draft::Draft4 => None,
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::if_::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::if_::compile),
},
"items" => Some(keywords::items::compile),
"maximum" => match self {
@ -103,6 +125,8 @@ impl Draft {
Draft::Draft6 | Draft::Draft7 => Some(keywords::maximum::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::maximum::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::maximum::compile),
},
"maxItems" => Some(keywords::max_items::compile),
"maxLength" => Some(keywords::max_length::compile),
@ -112,6 +136,8 @@ impl Draft {
Draft::Draft6 | Draft::Draft7 => Some(keywords::minimum::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::minimum::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::minimum::compile),
},
"minItems" => Some(keywords::min_items::compile),
"minLength" => Some(keywords::min_length::compile),
@ -121,12 +147,16 @@ impl Draft {
"oneOf" => Some(keywords::one_of::compile),
"pattern" => Some(keywords::pattern::compile),
"patternProperties" => Some(keywords::pattern_properties::compile),
#[cfg(feature = "draft202012")]
"prefixItems" => Some(keywords::prefix_items::compile),
"properties" => Some(keywords::properties::compile),
"propertyNames" => match self {
Draft::Draft4 => None,
Draft::Draft6 | Draft::Draft7 => Some(keywords::property_names::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::property_names::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::property_names::compile),
},
"required" => Some(keywords::required::compile),
"type" => match self {
@ -134,6 +164,8 @@ impl Draft {
Draft::Draft6 | Draft::Draft7 => Some(keywords::type_::compile),
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::type_::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::type_::compile),
},
"uniqueItems" => Some(keywords::unique_items::compile),
_ => None,
@ -145,6 +177,8 @@ impl Draft {
#[inline]
pub(crate) fn draft_from_url(url: &str) -> Option<Draft> {
match url {
#[cfg(feature = "draft202012")]
"https://json-schema.org/draft/2020-12/schema#" => Some(Draft::Draft202012),
#[cfg(feature = "draft201909")]
"https://json-schema.org/draft/2019-09/schema#" => Some(Draft::Draft201909),
"http://json-schema.org/draft-07/schema#" => Some(Draft::Draft7),
@ -184,6 +218,7 @@ mod tests {
use test_case::test_case;
#[cfg_attr(feature = "draft201909", test_case(&json!({"$schema": "https://json-schema.org/draft/2019-09/schema#"}), Some(Draft::Draft201909)))]
#[cfg_attr(feature = "draft202012", test_case(&json!({"$schema": "https://json-schema.org/draft/2020-12/schema#"}), Some(Draft::Draft202012)))]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-07/schema#"}), Some(Draft::Draft7))]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-06/schema#"}), Some(Draft::Draft6))]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#"}), Some(Draft::Draft4))]

View File

@ -37,6 +37,43 @@ use std::fs;
r"unevaluatedItems_.+",
r"unevaluatedProperties_.+",
}))]
#[cfg_attr(feature = "draft202012", json_schema_test_suite("tests/suite", "draft2020-12", {
r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101
r"format_\d+_6", // https://github.com/Stranger6667/jsonschema-rs/issues/261
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
r"id_0_[0-6]",
// Various types of new behavior used in the `$ref` context
"ref_5_1",
"ref_13_0",
"refRemote_4_0",
"refRemote_4_1",
"recursiveRef_0_3",
"recursiveRef_1_2",
"recursiveRef_1_4",
"recursiveRef_3_2",
"recursiveRef_3_4",
"recursiveRef_4_2",
"recursiveRef_4_4",
"recursiveRef_5_2",
"recursiveRef_6_2",
"recursiveRef_7_0",
"recursiveRef_7_1",
// New keywords & formats.
// https://github.com/Stranger6667/jsonschema-rs/issues/100
r"anchor_.+",
r"defs_.+",
r"dynamicRef_.+",
r"uniqueItems_.+",
r"optional_format_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265
r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266
r"optional_format_iri_.+",
r"optional_format_json_pointer_.+",
r"optional_format_relative_json_pointer_.+",
r"optional_format_uri_reference_.+",
r"optional_format_uri_template_.+",
r"unevaluatedItems_.+",
r"unevaluatedProperties_.+",
}))]
fn test_draft(_server_address: &str, test_case: TestCase) {
let draft_version = match test_case.draft_version.as_ref() {
"draft4" => Draft::Draft4,
@ -44,6 +81,8 @@ fn test_draft(_server_address: &str, test_case: TestCase) {
"draft7" => Draft::Draft7,
#[cfg(feature = "draft201909")]
"draft2019-09" => Draft::Draft201909,
#[cfg(feature = "draft202012")]
"draft2020-12" => Draft::Draft202012,
_ => panic!("Unsupported draft"),
};