diff --git a/README.md b/README.md index e898e77d..8968e3fc 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,10 @@ fn main() { } ``` -Fully supported drafts (with optional test cases included): +Supported drafts: - Draft 7 - Draft 6 +- Draft 4 (except optional `bignum.json` test case) ## Performance diff --git a/draft/src/lib.rs b/draft/src/lib.rs index 96913ed4..eeb461ed 100644 --- a/draft/src/lib.rs +++ b/draft/src/lib.rs @@ -1,4 +1,4 @@ -use heck::SnakeCase; +use heck::{SnakeCase, TitleCase}; use proc_macro::TokenStream; use serde_json::{from_str, Value}; use std::fs; @@ -6,6 +6,14 @@ use std::fs::File; use std::io::Read; use std::path::Path; +const TEST_TO_IGNORE: &[&str] = &["draft4_optional_bignum"]; + +fn should_ignore_test(prefix_test_name: &str) -> bool { + TEST_TO_IGNORE + .iter() + .any(|test_to_ignore| prefix_test_name.starts_with(test_to_ignore)) +} + #[proc_macro] pub fn test_draft(input: TokenStream) -> TokenStream { let dir_name = input.to_string(); @@ -28,9 +36,12 @@ pub fn test_draft(input: TokenStream) -> TokenStream { let description = test.get("description").unwrap().as_str().unwrap(); let data = test.get("data").unwrap(); let valid = test.get("valid").unwrap().as_bool().unwrap(); + if should_ignore_test(&file_name) { + output.push_str("\n#[ignore]\n"); + } output.push_str("\n#[test]\n"); output.push_str(&format!("fn {}_{}_{}()", file_name, i, j)); - output.push_str(&make_fn_body(schema, data, &description, valid)) + output.push_str(&make_fn_body(schema, data, &description, valid, &draft)) } } } @@ -68,7 +79,13 @@ fn load_tests(dir: &Path, prefix: String) -> Vec<(String, Value)> { tests } -fn make_fn_body(schema: &Value, data: &Value, description: &str, valid: bool) -> String { +fn make_fn_body( + schema: &Value, + data: &Value, + description: &str, + valid: bool, + draft: &str, +) -> String { let mut output = "{".to_string(); output.push_str(&format!( r###" @@ -78,13 +95,14 @@ fn make_fn_body(schema: &Value, data: &Value, description: &str, valid: bool) -> let data: serde_json::Value = serde_json::from_str(data_str).unwrap(); let description = r#"{}"#; println!("Description: {{}}", description); - let compiled = jsonschema::JSONSchema::compile(&schema, None).unwrap(); + let compiled = jsonschema::JSONSchema::compile(&schema, Some(jsonschema::Draft::{})).unwrap(); let result = compiled.validate(&data); assert_eq!(result.is_ok(), compiled.is_valid(&data)); "###, schema.to_string(), data.to_string(), - description + description, + draft.to_title_case() )); if valid { output.push_str( diff --git a/src/keywords/legacy/maximum_draft_4.rs b/src/keywords/legacy/maximum_draft_4.rs new file mode 100644 index 00000000..fb2d7ce8 --- /dev/null +++ b/src/keywords/legacy/maximum_draft_4.rs @@ -0,0 +1,15 @@ +use super::super::CompilationResult; +use super::super::{exclusive_maximum, maximum}; +use crate::compilation::CompilationContext; +use serde_json::{Map, Value}; + +pub(crate) fn compile( + parent: &Map, + schema: &Value, + context: &CompilationContext, +) -> Option { + match parent.get("exclusiveMaximum") { + Some(Value::Bool(true)) => exclusive_maximum::compile(parent, schema, context), + _ => maximum::compile(parent, schema, context), + } +} diff --git a/src/keywords/legacy/minimum_draft_4.rs b/src/keywords/legacy/minimum_draft_4.rs new file mode 100644 index 00000000..3b43152d --- /dev/null +++ b/src/keywords/legacy/minimum_draft_4.rs @@ -0,0 +1,15 @@ +use super::super::CompilationResult; +use super::super::{exclusive_minimum, minimum}; +use crate::compilation::CompilationContext; +use serde_json::{Map, Value}; + +pub(crate) fn compile( + parent: &Map, + schema: &Value, + context: &CompilationContext, +) -> Option { + match parent.get("exclusiveMinimum") { + Some(Value::Bool(true)) => exclusive_minimum::compile(parent, schema, context), + _ => minimum::compile(parent, schema, context), + } +} diff --git a/src/keywords/legacy/mod.rs b/src/keywords/legacy/mod.rs new file mode 100644 index 00000000..a2cd1f3d --- /dev/null +++ b/src/keywords/legacy/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod maximum_draft_4; +pub(crate) mod minimum_draft_4; +pub(crate) mod type_draft_4; diff --git a/src/keywords/legacy/type_draft_4.rs b/src/keywords/legacy/type_draft_4.rs new file mode 100644 index 00000000..1487795c --- /dev/null +++ b/src/keywords/legacy/type_draft_4.rs @@ -0,0 +1,125 @@ +use super::super::{type_, CompilationResult, Validate}; +use crate::compilation::{CompilationContext, JSONSchema}; +use crate::error::{no_error, CompilationError, ErrorIterator, PrimitiveType, ValidationError}; +use serde_json::{Map, Number, Value}; + +pub struct MultipleTypesValidator { + types: Vec, +} + +impl MultipleTypesValidator { + pub(crate) fn compile(items: &[Value]) -> CompilationResult { + let mut types = Vec::with_capacity(items.len()); + for item in items { + match item { + Value::String(string) => match string.as_str() { + "integer" => types.push(PrimitiveType::Integer), + "null" => types.push(PrimitiveType::Null), + "boolean" => types.push(PrimitiveType::Boolean), + "string" => types.push(PrimitiveType::String), + "array" => types.push(PrimitiveType::Array), + "object" => types.push(PrimitiveType::Object), + "number" => types.push(PrimitiveType::Number), + _ => return Err(CompilationError::SchemaError), + }, + _ => return Err(CompilationError::SchemaError), + } + } + Ok(Box::new(MultipleTypesValidator { types })) + } +} + +impl Validate for MultipleTypesValidator { + fn validate<'a>(&self, schema: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { + if !self.is_valid(schema, instance) { + return ValidationError::multiple_type_error(instance.clone(), self.types.clone()); + } + no_error() + } + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + for type_ in self.types.iter() { + match (type_, instance) { + (PrimitiveType::Integer, Value::Number(num)) if is_integer(num) => return true, + (PrimitiveType::Null, Value::Null) + | (PrimitiveType::Boolean, Value::Bool(_)) + | (PrimitiveType::String, Value::String(_)) + | (PrimitiveType::Array, Value::Array(_)) + | (PrimitiveType::Object, Value::Object(_)) + | (PrimitiveType::Number, Value::Number(_)) => return true, + (_, _) => continue, + }; + } + false + } + + fn name(&self) -> String { + format!("", self.types) + } +} + +pub struct IntegerTypeValidator {} + +impl IntegerTypeValidator { + pub(crate) fn compile() -> CompilationResult { + Ok(Box::new(IntegerTypeValidator {})) + } +} + +impl Validate for IntegerTypeValidator { + fn validate<'a>(&self, schema: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { + if !self.is_valid(schema, instance) { + return ValidationError::single_type_error(instance.clone(), PrimitiveType::Integer); + } + no_error() + } + + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + if let Value::Number(num) = instance { + return is_integer(num); + } + false + } + + fn name(&self) -> String { + "".to_string() + } +} + +fn is_integer(num: &Number) -> bool { + num.is_u64() || num.is_i64() +} + +pub(crate) fn compile( + _: &Map, + schema: &Value, + _: &CompilationContext, +) -> Option { + match schema { + Value::String(item) => compile_single_type(item.as_str()), + Value::Array(items) => { + if items.len() == 1 { + if let Some(Value::String(item)) = items.iter().next() { + compile_single_type(item.as_str()) + } else { + Some(Err(CompilationError::SchemaError)) + } + } else { + Some(MultipleTypesValidator::compile(items)) + } + } + _ => Some(Err(CompilationError::SchemaError)), + } +} + +fn compile_single_type(item: &str) -> Option { + match item { + "integer" => Some(IntegerTypeValidator::compile()), + "null" => Some(type_::NullTypeValidator::compile()), + "boolean" => Some(type_::BooleanTypeValidator::compile()), + "string" => Some(type_::StringTypeValidator::compile()), + "array" => Some(type_::ArrayTypeValidator::compile()), + "object" => Some(type_::ObjectTypeValidator::compile()), + "number" => Some(type_::NumberTypeValidator::compile()), + _ => Some(Err(CompilationError::SchemaError)), + } +} diff --git a/src/keywords/mod.rs b/src/keywords/mod.rs index 5fb9f8ac..2faaa6cb 100644 --- a/src/keywords/mod.rs +++ b/src/keywords/mod.rs @@ -14,6 +14,7 @@ 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; diff --git a/src/keywords/type_.rs b/src/keywords/type_.rs index d2b03dba..c0f8732f 100644 --- a/src/keywords/type_.rs +++ b/src/keywords/type_.rs @@ -1,6 +1,5 @@ use super::{CompilationResult, Validate}; -use crate::compilation::CompilationContext; -use crate::compilation::JSONSchema; +use crate::compilation::{CompilationContext, JSONSchema}; use crate::error::{no_error, CompilationError, ErrorIterator, PrimitiveType, ValidationError}; use serde_json::{Map, Number, Value}; diff --git a/src/schemas.rs b/src/schemas.rs index 5aaa077b..192c9fc6 100644 --- a/src/schemas.rs +++ b/src/schemas.rs @@ -87,6 +87,34 @@ impl Draft { "uniqueItems" => Some(keywords::unique_items::compile), _ => None, }, + Draft::Draft4 => match keyword { + "additionalItems" => Some(keywords::additional_items::compile), + "additionalProperties" => Some(keywords::additional_properties::compile), + "allOf" => Some(keywords::all_of::compile), + "anyOf" => Some(keywords::any_of::compile), + "dependencies" => Some(keywords::dependencies::compile), + "enum" => Some(keywords::enum_::compile), + "format" => Some(keywords::format::compile), + "items" => Some(keywords::items::compile), + "maximum" => Some(keywords::legacy::maximum_draft_4::compile), + "maxItems" => Some(keywords::max_items::compile), + "maxLength" => Some(keywords::max_length::compile), + "maxProperties" => Some(keywords::max_properties::compile), + "minimum" => Some(keywords::legacy::minimum_draft_4::compile), + "minItems" => Some(keywords::min_items::compile), + "minLength" => Some(keywords::min_length::compile), + "minProperties" => Some(keywords::min_properties::compile), + "multipleOf" => Some(keywords::multiple_of::compile), + "not" => Some(keywords::not::compile), + "oneOf" => Some(keywords::one_of::compile), + "pattern" => Some(keywords::pattern::compile), + "patternProperties" => Some(keywords::pattern_properties::compile), + "properties" => Some(keywords::properties::compile), + "required" => Some(keywords::required::compile), + "type" => Some(keywords::legacy::type_draft_4::compile), + "uniqueItems" => Some(keywords::unique_items::compile), + _ => None, + }, _ => None, } } diff --git a/tests/test_suite.rs b/tests/test_suite.rs index a097f44e..ad9ac280 100644 --- a/tests/test_suite.rs +++ b/tests/test_suite.rs @@ -1,4 +1,5 @@ use draft::test_draft; +test_draft!("tests/suite/tests/draft4/"); test_draft!("tests/suite/tests/draft6/"); test_draft!("tests/suite/tests/draft7/");