jsonschema-rs/jsonschema/src/keywords/mod.rs

283 lines
17 KiB
Rust

pub(crate) mod additional_items;
pub(crate) mod additional_properties;
pub(crate) mod all_of;
pub(crate) mod any_of;
pub(crate) mod boolean;
pub(crate) mod const_;
pub(crate) mod contains;
pub(crate) mod content;
pub(crate) mod dependencies;
pub(crate) mod enum_;
pub(crate) mod exclusive_maximum;
pub(crate) mod exclusive_minimum;
pub(crate) mod format;
pub(crate) mod helpers;
pub(crate) mod if_;
pub(crate) mod items;
pub(crate) mod legacy;
pub(crate) mod max_items;
pub(crate) mod max_length;
pub(crate) mod max_properties;
pub(crate) mod maximum;
pub(crate) mod min_items;
pub(crate) mod min_length;
pub(crate) mod min_properties;
pub(crate) mod minimum;
pub(crate) mod multiple_of;
pub(crate) mod not;
pub(crate) mod one_of;
pub(crate) mod pattern;
pub(crate) mod pattern_properties;
pub(crate) mod properties;
pub(crate) mod property_names;
pub(crate) mod ref_;
pub(crate) mod required;
pub(crate) mod type_;
pub(crate) mod unique_items;
use crate::{error, validator::Validate};
pub(crate) type CompilationResult = Result<BoxedValidator, error::CompilationError>;
pub(crate) type BoxedValidator = Box<dyn Validate + Send + Sync>;
pub(crate) type Validators = Vec<BoxedValidator>;
fn format_validators(validators: &[BoxedValidator]) -> String {
match validators.len() {
0 => "{}".to_string(),
1 => {
let name = validators[0].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
"true" | "false" => name,
_ => format!("{{{}}}", name),
}
}
_ => format!(
"{{{}}}",
validators
.iter()
.map(|validator| format!("{:?}", validator))
.collect::<Vec<String>>()
.join(", ")
),
}
}
fn format_vec_of_validators(validators: &[Validators]) -> String {
validators
.iter()
.map(|v| format_validators(v))
.collect::<Vec<String>>()
.join(", ")
}
fn format_key_value_validators(validators: &[(String, Validators)]) -> String {
validators
.iter()
.map(|(name, validators)| format!("{}: {}", name, format_validators(validators)))
.collect::<Vec<String>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use crate::compilation::JSONSchema;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(&json!({"additionalItems": false, "items": [{"type": "string"}]}), "additionalItems: false")]
#[test_case(&json!({"additionalItems": {"type": "integer"}, "items": [{"type": "string"}]}), "additionalItems: {type: integer}")]
#[test_case(&json!({"additionalProperties": {"type": "string"}}), "additionalProperties: {type: string}")]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "properties": {"foo": {}}}), "additionalProperties: {type: string}")]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "patternProperties": {"f.*o": {"type": "integer"}}}), "additionalProperties: {type: string}")]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "properties": {"foo": {}}, "patternProperties": {"f.*o": {"type": "integer"}}}), "additionalProperties: {type: string}")]
#[test_case(&json!({"additionalProperties": false}), "additionalProperties: false")]
#[test_case(&json!({"additionalProperties": false, "properties": {"foo": {}}}), "additionalProperties: false")]
#[test_case(&json!({"additionalProperties": false, "patternProperties": {"f.*o": {"type": "integer"}}}), "additionalProperties: false")]
#[test_case(&json!({"additionalProperties": false, "properties": {"foo": {}}, "patternProperties": {"f.*o": {"type": "integer"}}}), "additionalProperties: false")]
#[test_case(&json!({"allOf": [{"type": "integer"}, {"minimum": 2}]}), "allOf: [{type: integer}, {minimum: 2}]")]
#[test_case(&json!({"anyOf": [{"type": "integer"}, {"minimum": 2}]}), "anyOf: [{type: integer}, {minimum: 2}]")]
#[test_case(&json!(true), "true")]
#[test_case(&json!(false), "false")]
#[test_case(&json!({"const": 1}), "const: 1")]
#[test_case(&json!({"contains": {"minimum": 5}}), "contains: {minimum: 5}")]
#[test_case(&json!({"contentMediaType": "application/json"}), "contentMediaType: application/json")]
#[test_case(&json!({"contentEncoding": "base64"}), "contentEncoding: base64")]
#[test_case(&json!({"contentEncoding": "base64", "contentMediaType": "application/json"}), "{contentMediaType: application/json, contentEncoding: base64}")]
#[test_case(&json!({"dependencies": {"bar": ["foo"]}}), "dependencies: {bar: {required: [foo]}}")]
#[test_case(&json!({"enum": [1]}), "enum: [1]")]
#[test_case(&json!({"exclusiveMaximum": 1}), "exclusiveMaximum: 1")]
#[test_case(&json!({"exclusiveMinimum": 1}), "exclusiveMinimum: 1")]
#[test_case(&json!({"format": "date"}), "format: date")]
#[test_case(&json!({"format": "date-time"}), "format: date-time")]
#[test_case(&json!({"format": "email"}), "format: email")]
#[test_case(&json!({"format": "hostname"}), "format: hostname")]
#[test_case(&json!({"format": "idn-email"}), "format: idn-email")]
#[test_case(&json!({"format": "idn-hostname"}), "format: idn-hostname")]
#[test_case(&json!({"format": "ipv4"}), "format: ipv4")]
#[test_case(&json!({"format": "ipv6"}), "format: ipv6")]
#[test_case(&json!({"format": "iri"}), "format: iri")]
#[test_case(&json!({"format": "iri-reference"}), "format: iri-reference")]
#[test_case(&json!({"format": "json-pointer"}), "format: json-pointer")]
#[test_case(&json!({"format": "regex"}), "format: regex")]
#[test_case(&json!({"format": "relative-json-pointer"}), "format: relative-json-pointer")]
#[test_case(&json!({"format": "time"}), "format: time")]
#[test_case(&json!({"format": "uri"}), "format: uri")]
#[test_case(&json!({"format": "uri-reference"}), "format: uri-reference")]
#[test_case(&json!({"format": "uri-template"}), "format: uri-template")]
#[test_case(&json!({"if": {"exclusiveMaximum": 0}, "then": {"minimum": -10}}), "if: {exclusiveMaximum: 0}, then: {minimum: -10}")]
#[test_case(&json!({"if": {"exclusiveMaximum": 0}, "else": {"minimum": -10}}), "if: {exclusiveMaximum: 0}, else: {minimum: -10}")]
#[test_case(&json!({"if": {"exclusiveMaximum": 0}, "then": {"minimum": -10}, "else": {"multipleOf": 2}}), "if: {exclusiveMaximum: 0}, then: {minimum: -10}, else: {multipleOf: 2}")]
#[test_case(&json!({"items": [{"type": "string"}]}), "items: [{type: string}]")]
#[test_case(&json!({"items": {"type": "integer"}}), "items: {type: integer}")]
#[test_case(&json!({"items": {"type": "integer", "minimum": 4}}), "items: {minimum: 4, type: integer}")]
#[test_case(&json!({"maxItems": 1}), "maxItems: 1")]
#[test_case(&json!({"maxLength": 1}), "maxLength: 1")]
#[test_case(&json!({"maxProperties": 1}), "maxProperties: 1")]
#[test_case(&json!({"maximum": 1}), "maximum: 1")]
#[test_case(&json!({"minItems": 1}), "minItems: 1")]
#[test_case(&json!({"minLength": 1}), "minLength: 1")]
#[test_case(&json!({"minProperties": 1}), "minProperties: 1")]
#[test_case(&json!({"minimum": 1}), "minimum: 1")]
#[test_case(&json!({"multipleOf": 1}), "multipleOf: 1")]
#[test_case(&json!({"multipleOf": 1.5}), "multipleOf: 1.5")]
#[test_case(&json!({"not": true}), "not: true")]
#[test_case(&json!({"oneOf": [{"type": "integer"}, {"minimum": 2}]}), "oneOf: [{type: integer}, {minimum: 2}]")]
#[test_case(&json!({"pattern": "^a*$"}), "pattern: ^a*$")]
#[test_case(&json!({"patternProperties": {"f.*o": {"type": "integer"}}}), "patternProperties: {f.*o: {type: integer}}")]
#[test_case(&json!({"properties": {"foo": {}}}), "properties: {foo: {}}")]
#[test_case(&json!({"propertyNames": {"maxLength": 3}}), "propertyNames: {maxLength: 3}")]
#[test_case(&json!({"propertyNames": false}), "propertyNames: false")]
#[test_case(&json!({"$ref": "#/properties/foo"}), "$ref: json-schema:///#/properties/foo")]
#[test_case(&json!({"required": ["foo"]}), "required: [foo]")]
#[test_case(&json!({"type": "null"}), "type: null")]
#[test_case(&json!({"type": "boolean"}), "type: boolean")]
#[test_case(&json!({"type": "string"}), "type: string")]
#[test_case(&json!({"type": "array"}), "type: array")]
#[test_case(&json!({"type": "object"}), "type: object")]
#[test_case(&json!({"type": "number"}), "type: number")]
#[test_case(&json!({"type": "integer"}), "type: integer")]
#[test_case(&json!({"type": "integer", "$schema": "http://json-schema.org/draft-04/schema#"}), "type: integer")]
#[test_case(&json!({"type": ["integer", "null"]}), "type: [integer, null]")]
#[test_case(&json!({"type": ["integer", "null"], "$schema": "http://json-schema.org/draft-04/schema#"}), "type: [integer, null]")]
#[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);
}
#[test_case(&json!({"items": [{}], "additionalItems": {"type": "integer"}}), &json!([ null, 2, 3, "foo" ]), r#"'"foo"' is not of type 'integer'"#)]
#[test_case(&json!({"items": [{}, {}, {}], "additionalItems": false}), &json!([ 1, 2, 3, 4 ]), r#"Additional items are not allowed (4 was unexpected)"#)]
#[test_case(&json!({"items": [{}, {}, {}], "additionalItems": false}), &json!([ 1, 2, 3, 4, 5 ]), r#"Additional items are not allowed (4, 5 were unexpected)"#)]
#[test_case(&json!({"properties": {"foo": {}, "bar": {}}, "patternProperties": { "^v": {} }, "additionalProperties": false}), &json!({"foo" : 1, "bar" : 2, "quux" : "boom"}), r#"False schema does not allow '"quux"'"#)]
#[test_case(&json!({"anyOf": [{"type": "integer"}, {"minimum": 2}]}), &json!(1.5), r#"'1.5' is not valid under any of the given schemas"#)]
#[test_case(&json!({"const": 2}), &json!(5), r#"'2' was expected"#)]
#[test_case(&json!({"contains": {"minimum": 5}}), &json!([2, 3, 4]), r#"None of '[2,3,4]' are valid under the given schema"#)]
#[test_case(&json!({"enum": [1, 2, 3]}), &json!(4), r#"'4' is not one of '[1,2,3]'"#)]
#[test_case(&json!({"exclusiveMaximum": 3.0}), &json!(3.0), r#"3.0 is greater than or equal to the maximum of 3"#)]
#[test_case(&json!({"exclusiveMinimum": 1.1}), &json!(1.1), r#"1.1 is less than or equal to the minimum of 1.1"#)]
#[test_case(&json!({"format": "ipv4"}), &json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), r#"'"2001:0db8:85a3:0000:0000:8a2e:0370:7334"' is not a 'ipv4'"#)]
#[test_case(&json!({"maximum": 3.0}), &json!(3.5), r#"3.5 is greater than the maximum of 3"#)]
#[test_case(&json!({"maxItems": 2}), &json!([1, 2, 3]), r#"[1,2,3] has more than 2 items"#)]
#[test_case(&json!({"maxLength": 2}), &json!("foo"), r#"'"foo"' is longer than 2 characters"#)]
#[test_case(&json!({"maxProperties": 2}), &json!({"foo": 1, "bar": 2, "baz": 3}), r#"{"bar":2,"baz":3,"foo":1} has more than 2 properties"#)]
#[test_case(&json!({"minimum": 1.1}), &json!(0.6), r#"0.6 is less than the minimum of 1.1"#)]
#[test_case(&json!({"minItems": 1}), &json!([]), r#"[] has less than 1 item"#)]
#[test_case(&json!({"minLength": 2}), &json!("f"), r#"'"f"' is shorter than 2 characters"#)]
#[test_case(&json!({"minProperties": 1}), &json!({}), r#"{} has less than 1 property"#)]
#[test_case(&json!({"multipleOf": 2}), &json!(7), r#"7 is not a multiple of 2"#)]
#[test_case(&json!({"not": {"type": "integer"}}), &json!(1), r#"{"type":"integer"} is not allowed for 1"#)]
#[test_case(&json!({"oneOf": [{"type": "integer"}, {"minimum": 2}]}), &json!(1.1), r#"'1.1' is not valid under any of the given schemas"#)]
#[test_case(&json!({"oneOf": [{"type": "integer"}, {"minimum": 2}]}), &json!(3), r#"'3' is valid under more than one of the given schemas"#)]
#[test_case(&json!({"pattern": "^a*$"}), &json!("abc"), r#"'"abc"' does not match '^a*$'"#)]
#[test_case(&json!({"properties": {"foo": {}, "bar": {}}, "required": ["foo"]}), &json!({"bar": 1}), r#"'foo' is a required property"#)]
#[test_case(&json!({"type": "integer"}), &json!(1.1), r#"'1.1' is not of type 'integer'"#)]
#[test_case(&json!({"type": ["integer", "string"]}), &json!(null), r#"'null' is not of types 'integer', 'string'"#)]
#[test_case(&json!({"uniqueItems": true}), &json!([1, 1]), r#"'[1,1]' has non-unique elements"#)]
fn error_message(schema: &Value, instance: &Value, expected: &str) {
let compiled = JSONSchema::compile(schema).unwrap();
let errors: Vec<_> = compiled
.validate(instance)
.expect_err(&format!(
"Validation error is expected. Schema=`{:?}` Instance=`{:?}`",
schema, instance
))
.collect();
assert_eq!(errors[0].to_string(), expected);
}
// Extra cases not covered by JSON test suite
#[test_case(&json!({"additionalProperties": {"type": "string"}}))]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "properties": {"foo": {}}}))]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "patternProperties": {"f.*o": {"type": "integer"}}}))]
#[test_case(&json!({"additionalProperties": {"type": "string"}, "properties": {"foo": {}}, "patternProperties": {"f.*o": {"type": "integer"}}}))]
#[test_case(&json!({"additionalProperties": false}))]
#[test_case(&json!({"additionalProperties": false, "properties": {"foo": {}}}))]
#[test_case(&json!({"additionalProperties": false, "patternProperties": {"f.*o": {"type": "integer"}}}))]
#[test_case(&json!({"additionalProperties": false, "properties": {"foo": {}}, "patternProperties": {"f.*o": {"type": "integer"}}}))]
#[test_case(&json!({"additionalItems": false, "items": [{"type": "string"}]}))]
#[test_case(&json!({"additionalItems": {"type": "integer"}, "items": [{"type": "string"}]}))]
#[test_case(&json!({"contains": {"minimum": 5}}))]
#[test_case(&json!({"contentMediaType": "application/json"}))]
#[test_case(&json!({"contentEncoding": "base64"}))]
#[test_case(&json!({"contentEncoding": "base64", "contentMediaType": "application/json"}))]
#[test_case(&json!({"dependencies": {"bar": ["foo"]}}))]
#[test_case(&json!({"exclusiveMaximum": 5}))]
#[test_case(&json!({"exclusiveMinimum": 5}))]
#[test_case(&json!({"format": "ipv4"}))]
#[test_case(&json!({"maximum": 2}))]
#[test_case(&json!({"maxItems": 2}))]
#[test_case(&json!({"maxProperties": 2}))]
#[test_case(&json!({"minProperties": 2}))]
#[test_case(&json!({"multipleOf": 2.5}))]
#[test_case(&json!({"multipleOf": 2}))]
#[test_case(&json!({"required": ["a"]}))]
#[test_case(&json!({"pattern": "^a"}))]
#[test_case(&json!({"patternProperties": {"f.*o": {"type": "integer"}}}))]
#[test_case(&json!({"propertyNames": {"maxLength": 3}}))]
fn is_valid_another_type(schema: &Value) {
let instance = json!(null);
let compiled = JSONSchema::compile(schema).unwrap();
assert!(compiled.is_valid(&instance))
}
#[test_case(&json!({"additionalProperties": false}), &json!({}))]
#[test_case(&json!({"additionalItems": false, "items": true}), &json!([]))]
fn is_valid(schema: &Value, instance: &Value) {
let compiled = JSONSchema::compile(schema).unwrap();
assert!(compiled.is_valid(instance))
}
#[test_case(&json!({"type": "number"}), &json!(42))]
#[test_case(&json!({"type": ["number", "null"]}), &json!(42))]
fn integer_is_valid_number_multi_type(schema: &Value, instance: &Value) {
// Regression: https://github.com/Stranger6667/jsonschema-rs/issues/147
let compiled = JSONSchema::compile(schema).unwrap();
assert!(compiled.is_valid(instance))
}
// enum: Number
#[test_case(&json!({"enum": [0.0]}), &json!(0))]
// enum: Array
#[test_case(&json!({"enum": [[1.0]]}), &json!([1]))]
// enum: Object
#[test_case(&json!({"enum": [{"a": 1.0}]}), &json!({"a": 1}))]
// enum:: Object in Array
#[test_case(&json!({"enum": [[{"b": 1.0}]]}), &json!([{"b": 1}]))]
// enum:: Array in Object
#[test_case(&json!({"enum": [{"c": [1.0]}]}), &json!({"c": [1]}))]
// const: Number
#[test_case(&json!({"const": 0.0}), &json!(0))]
// const: Array
#[test_case(&json!({"const": [1.0]}), &json!([1]))]
// const: Object
#[test_case(&json!({"const": {"a": 1.0}}), &json!({"a": 1}))]
// const:: Object in Array
#[test_case(&json!({"const": [{"b": 1.0}]}), &json!([{"b": 1}]))]
// const:: Array in Object
#[test_case(&json!({"const": {"c": [1.0]}}), &json!({"c": [1]}))]
fn numeric_equivalence(schema: &Value, instance: &Value) {
// Regression: https://github.com/Stranger6667/jsonschema-rs/issues/149
let compiled = JSONSchema::compile(schema).unwrap();
assert!(compiled.is_valid(instance))
}
}