chore: update

Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
This commit is contained in:
Dmitry Dygalo 2024-04-23 22:20:56 +02:00
parent a240bd5aae
commit 44cc6529fa
No known key found for this signature in database
GPG Key ID: 26834366E8FDCFEF
5 changed files with 340 additions and 235 deletions

View File

@ -6,7 +6,7 @@ pub(crate) mod options;
use crate::{
error::ErrorIterator,
keywords::{self, custom_keyword::compile_custom_keyword_validator},
keywords,
output::Output,
paths::{InstancePath, JSONPointer},
primitive_type::{PrimitiveType, PrimitiveTypesBitMap},
@ -200,15 +200,16 @@ pub(crate) fn compile_validators<'a>(
}
// first check if this keyword was added as a custom keyword
// it may override existing keyword behavior
if let Some(f) = context.config.get_custom_keyword_constructor(keyword) {
let validator = compile_custom_keyword_validator(
&context,
keyword.clone(),
f(),
subschema.clone(),
schema.clone(),
)?;
validators.push((keyword.clone(), validator));
if let Some(factory) = context.config.get_keyword_factory(keyword) {
// let validator = factory.init(schema);
// let validator = compile(
// &context,
// keyword.clone(),
// f(),
// subschema.clone(),
// schema.clone(),
// )?;
// validators.push((keyword.clone(), validator.into()));
} else if let Some(validator) = context
.config
.draft()
@ -256,12 +257,12 @@ pub(crate) fn compile_validators<'a>(
mod tests {
use super::JSONSchema;
use crate::{
compilation::options::CustomKeywordValidator, error::ValidationError, paths::JSONPointer,
ErrorIterator,
compilation::context::CompilationContext, error::ValidationError,
keywords::custom::Keyword, paths::JSONPointer, ErrorIterator,
};
use num_cmp::NumCmp;
use regex::Regex;
use serde_json::{from_str, json, Value};
use serde_json::{from_str, json, Map, Value};
use std::{
borrow::Cow,
fs::File,
@ -330,43 +331,50 @@ mod tests {
// Define a custom validator that verifies the object's keys consist of
// only ASCII representable characters.
struct CustomObjectValidator;
impl CustomKeywordValidator for CustomObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: JSONPointer,
subschema: Arc<Value>,
subschema_path: JSONPointer,
_schema: Arc<Value>,
) -> ErrorIterator<'instance> {
if subschema.as_str().map_or(true, |str| str != "ascii-keys") {
let error = ValidationError {
instance: Cow::Borrowed(instance),
kind: crate::error::ValidationErrorKind::Schema,
instance_path,
schema_path: subschema_path,
};
return Box::new(Some(error).into_iter()); // Invalid schema
}
let mut errors = vec![];
for (key, _value) in instance.as_object().unwrap() {
if !key.is_ascii() {
let error = ValidationError {
instance: Cow::Borrowed(instance),
kind: crate::error::ValidationErrorKind::Format { format: "ASCII" },
instance_path: instance_path.clone(),
schema_path: subschema_path.clone(),
};
errors.push(error);
}
}
Box::new(errors.into_iter())
}
impl Keyword for CustomObjectValidator {
// fn compile<'a>(
// _: &'a Map<String, Value>,
// _: &'a Value,
// _: &CompilationContext,
// ) -> Box<dyn CustomKeywordValidator> {
// Box::new(CustomObjectValidator)
// }
// fn validate<'instance>(
// &self,
// instance: &'instance Value,
// instance_path: JSONPointer,
// subschema: Arc<Value>,
// subschema_path: JSONPointer,
// _schema: Arc<Value>,
// ) -> ErrorIterator<'instance> {
// if subschema.as_str().map_or(true, |str| str != "ascii-keys") {
// let error = ValidationError {
// instance: Cow::Borrowed(instance),
// kind: crate::error::ValidationErrorKind::Schema,
// instance_path,
// schema_path: subschema_path,
// };
// return Box::new(Some(error).into_iter()); // Invalid schema
// }
// let mut errors = vec![];
// for (key, _value) in instance.as_object().unwrap() {
// if !key.is_ascii() {
// let error = ValidationError {
// instance: Cow::Borrowed(instance),
// kind: crate::error::ValidationErrorKind::Format { format: "ASCII" },
// instance_path: instance_path.clone(),
// schema_path: subschema_path.clone(),
// };
// errors.push(error);
// }
// }
// Box::new(errors.into_iter())
// }
fn is_valid(&self, instance: &Value, subschema: &Value, _schema: &Value) -> bool {
if subschema.as_str().map_or(true, |str| str != "ascii-keys") {
return false; // Invalid schema
}
fn is_valid(&self, instance: &Value) -> bool {
// if subschema.as_str().map_or(true, |str| str != "ascii-keys") {
// return false; // Invalid schema
// }
for (key, _value) in instance.as_object().unwrap() {
if !key.is_ascii() {
return false;
@ -380,7 +388,9 @@ mod tests {
let schema =
json!({ "custom-object-type": "ascii-keys", "type": "object", "minProperties": 1 });
let json_schema = JSONSchema::options()
.with_custom_keyword("custom-object-type", || Box::new(CustomObjectValidator))
.with_keyword("custom-object-type", |_: &Value| -> Box<dyn Keyword> {
Box::new(CustomObjectValidator)
})
.compile(&schema)
.unwrap();
@ -413,115 +423,124 @@ mod tests {
// Define a custom keyword validator that overrides "minimum"
// so that "minimum" may apply to "currency"-formatted strings as well
struct CustomMinimumValidator;
impl CustomKeywordValidator for CustomMinimumValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: JSONPointer,
subschema: Arc<Value>,
subschema_path: JSONPointer,
schema: Arc<Value>,
) -> ErrorIterator<'instance> {
let subschema: &Value = &subschema;
let limit = match subschema {
Value::Number(limit) => limit,
_ => {
let error = ValidationError {
instance: Cow::Borrowed(instance),
kind: crate::error::ValidationErrorKind::Schema,
instance_path,
schema_path: subschema_path,
};
return Box::new(Some(error).into_iter()); // Invalid schema
}
};
let mut errors = vec![];
let valid = match instance {
// numeric comparison should happen just like original behavior
Value::Number(instance) => {
if let Some(item) = instance.as_u64() {
!NumCmp::num_lt(item, limit.as_f64().unwrap())
} else if let Some(item) = limit.as_i64() {
!NumCmp::num_lt(item, limit.as_f64().unwrap())
} else {
let item = instance.as_f64().expect("Always valid");
!NumCmp::num_lt(item, limit.as_f64().unwrap())
}
}
// string comparison should cast currency-formatted
Value::String(instance) => {
let mut valid = true;
if let Some(schema) = schema.as_object() {
if let Some(format) = schema.get("format") {
if format == "currency" && currency_format_checker(instance) {
// all preconditions for minimum applying are met
let as_f64 = instance
.parse::<f64>()
.expect("format validated by regex checker");
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
println!("valid {:#?}", valid);
}
}
}
valid
}
// in all other cases, the "minimum" keyword should not apply
_ => true,
};
if !valid {
let error = ValidationError {
instance: Cow::Borrowed(instance),
kind: crate::error::ValidationErrorKind::Minimum {
limit: subschema.clone(),
},
instance_path: instance_path.clone(),
schema_path: subschema_path.clone(),
};
errors.push(error);
}
Box::new(errors.into_iter())
}
impl Keyword for CustomMinimumValidator {
// fn compile<'a>(
// _: &'a Map<String, Value>,
// _: &'a Value,
// _: &CompilationContext,
// ) -> Box<dyn Keyword> {
// Box::new(CustomMinimumValidator)
// }
// fn validate<'instance>(
// &self,
// instance: &'instance Value,
// instance_path: JSONPointer,
// subschema: Arc<Value>,
// subschema_path: JSONPointer,
// schema: Arc<Value>,
// ) -> ErrorIterator<'instance> {
// let subschema: &Value = &subschema;
// let limit = match subschema {
// Value::Number(limit) => limit,
// _ => {
// let error = ValidationError {
// instance: Cow::Borrowed(instance),
// kind: crate::error::ValidationErrorKind::Schema,
// instance_path,
// schema_path: subschema_path,
// };
// return Box::new(Some(error).into_iter()); // Invalid schema
// }
// };
// let mut errors = vec![];
// let valid = match instance {
// // numeric comparison should happen just like original behavior
// Value::Number(instance) => {
// if let Some(item) = instance.as_u64() {
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// } else if let Some(item) = limit.as_i64() {
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// } else {
// let item = instance.as_f64().expect("Always valid");
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// }
// }
// // string comparison should cast currency-formatted
// Value::String(instance) => {
// let mut valid = true;
// if let Some(schema) = schema.as_object() {
// if let Some(format) = schema.get("format") {
// if format == "currency" && currency_format_checker(instance) {
// // all preconditions for minimum applying are met
// let as_f64 = instance
// .parse::<f64>()
// .expect("format validated by regex checker");
// println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
// valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
// println!("valid {:#?}", valid);
// }
// }
// }
// valid
// }
// // in all other cases, the "minimum" keyword should not apply
// _ => true,
// };
// if !valid {
// let error = ValidationError {
// instance: Cow::Borrowed(instance),
// kind: crate::error::ValidationErrorKind::Minimum {
// limit: subschema.clone(),
// },
// instance_path: instance_path.clone(),
// schema_path: subschema_path.clone(),
// };
// errors.push(error);
// }
// Box::new(errors.into_iter())
// }
fn is_valid(&self, instance: &Value, subschema: &Value, schema: &Value) -> bool {
let limit = match subschema {
Value::Number(limit) => limit,
_ => return false,
};
let valid = match instance {
// numeric comparison should happen just like original behavior
Value::Number(instance) => {
if let Some(item) = instance.as_u64() {
!NumCmp::num_lt(item, limit.as_f64().unwrap())
} else if let Some(item) = limit.as_i64() {
!NumCmp::num_lt(item, limit.as_f64().unwrap())
} else {
let item = instance.as_f64().expect("Always valid");
!NumCmp::num_lt(item, limit.as_f64().unwrap())
}
}
// string comparison should cast currency-formatted
Value::String(instance) => {
let mut valid = true;
if let Some(schema) = schema.as_object() {
if let Some(format) = schema.get("format") {
if format == "currency" && currency_format_checker(instance) {
// all preconditions for minimum applying are met
let as_f64 = instance
.parse::<f64>()
.expect("format validated by regex checker");
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
println!("valid {:#?}", valid);
}
}
}
valid
}
// in all other cases, the "minimum" keyword should not apply
_ => true,
};
valid
fn is_valid(&self, instance: &Value) -> bool {
true
// let limit = match subschema {
// Value::Number(limit) => limit,
// _ => return false,
// };
// let valid = match instance {
// // numeric comparison should happen just like original behavior
// Value::Number(instance) => {
// if let Some(item) = instance.as_u64() {
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// } else if let Some(item) = limit.as_i64() {
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// } else {
// let item = instance.as_f64().expect("Always valid");
// !NumCmp::num_lt(item, limit.as_f64().unwrap())
// }
// }
// // string comparison should cast currency-formatted
// Value::String(instance) => {
// let mut valid = true;
// if let Some(schema) = schema.as_object() {
// if let Some(format) = schema.get("format") {
// if format == "currency" && currency_format_checker(instance) {
// // all preconditions for minimum applying are met
// let as_f64 = instance
// .parse::<f64>()
// .expect("format validated by regex checker");
// println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
// valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
// println!("valid {:#?}", valid);
// }
// }
// }
// valid
// }
// // in all other cases, the "minimum" keyword should not apply
// _ => true,
// };
// valid
}
}
@ -529,7 +548,9 @@ mod tests {
let mut options = JSONSchema::options();
let options = options
.with_format("currency", currency_format_checker)
.with_custom_keyword("minimum", || Box::new(CustomMinimumValidator));
.with_keyword("minimum", |_: &Value| -> Box<dyn Keyword> {
Box::new(CustomMinimumValidator)
});
// Define a schema that includes both the custom format and the overridden keyword
let schema = json!({ "minimum": 2, "type": "string", "format": "currency" });
@ -585,29 +606,38 @@ mod tests {
}
}
impl CustomKeywordValidator for CountingValidator {
fn validate<'instance>(
&self,
_: &'instance Value,
_: JSONPointer,
subschema: Arc<Value>,
_: JSONPointer,
_: Arc<Value>,
) -> ErrorIterator<'instance> {
let amount = match &*subschema {
Value::Number(x) => x.as_i64().expect("countme value must be integer"),
_ => panic!("Validator requires numeric values"),
};
self.increment(amount);
Box::new(None.into_iter())
}
impl Keyword for CountingValidator {
// fn compile<'a>(
// _: &'a Map<String, Value>,
// _: &'a Value,
// _: &CompilationContext,
// ) -> Box<dyn Keyword> {
// Box::new(CountingValidator {
// count: Mutex::new(0),
// })
// }
// fn validate<'instance>(
// &self,
// _: &'instance Value,
// _: JSONPointer,
// subschema: Arc<Value>,
// _: JSONPointer,
// _: Arc<Value>,
// ) -> ErrorIterator<'instance> {
// let amount = match &*subschema {
// Value::Number(x) => x.as_i64().expect("countme value must be integer"),
// _ => panic!("Validator requires numeric values"),
// };
// self.increment(amount);
// Box::new(None.into_iter())
// }
fn is_valid(&self, _: &Value, subschema: &Value, _: &Value) -> bool {
let amount = match subschema {
Value::Number(x) => x.as_i64().expect("countme value must be integer"),
_ => return false,
};
self.increment(amount);
fn is_valid(&self, _: &Value) -> bool {
// let amount = match subschema {
// Value::Number(x) => x.as_i64().expect("countme value must be integer"),
// _ => return false,
// };
// self.increment(amount);
true
}
}
@ -615,7 +645,7 @@ mod tests {
// define compilation options that include the custom format and the overridden keyword
let count = Mutex::new(0);
let mut options = JSONSchema::options();
let options = options.with_custom_keyword("countme", || {
let options = options.with_keyword("countme", |_: &Value| -> Box<dyn Keyword> {
Box::new(CountingValidator {
count: Mutex::new(0),
})

View File

@ -1,3 +1,4 @@
use crate::keywords::custom::KeywordFactory;
use crate::{
compilation::{compile_validators, context::CompilationContext, JSONSchema, DEFAULT_SCOPE},
content_encoding::{
@ -276,10 +277,7 @@ pub struct CompilationOptions {
validate_formats: Option<bool>,
validate_schema: bool,
ignore_unknown_formats: bool,
custom_keywords: AHashMap<
String, // TODO<samgqroberts> 2024-04-13 should this also be a &'static str
CustomKeywordConstructor,
>,
keywords: AHashMap<String, Arc<dyn KeywordFactory>>,
}
impl Default for CompilationOptions {
@ -294,7 +292,7 @@ impl Default for CompilationOptions {
formats: AHashMap::default(),
validate_formats: None,
ignore_unknown_formats: true,
custom_keywords: AHashMap::default(),
keywords: AHashMap::default(),
}
}
}
@ -678,29 +676,22 @@ impl CompilationOptions {
/// }
///
/// assert!(JSONSchema::options()
/// .with_custom_keyword("my-type", MyCustomValidator)
/// .with_keyword("my-type", MyCustomValidator)
/// .compile(&json!({ "my-type": "my-schema"}))
/// .expect("A valid schema")
/// .is_valid(&json!({ "a": "b"})));
/// ```
pub fn with_custom_keyword<T>(
&mut self,
keyword: T,
definition: impl Fn() -> Box<dyn CustomKeywordValidator> + Send + Sync + 'static,
) -> &mut Self
pub fn with_keyword<N, F>(&mut self, name: N, factory: F) -> &mut Self
where
T: Into<String>,
N: Into<String>,
F: KeywordFactory + 'static,
{
self.custom_keywords
.insert(keyword.into(), Arc::new(definition));
self.keywords.insert(name.into(), Arc::new(factory));
self
}
pub(crate) fn get_custom_keyword_constructor(
&self,
keyword: &str,
) -> Option<&CustomKeywordConstructor> {
self.custom_keywords.get(keyword)
pub(crate) fn get_keyword_factory(&self, name: &str) -> Option<&Arc<dyn KeywordFactory>> {
self.keywords.get(name)
}
}
// format name & a pointer to a check function
@ -719,35 +710,6 @@ impl fmt::Debug for CompilationOptions {
}
}
pub(crate) type CustomKeywordConstructor =
Arc<dyn Fn() -> Box<dyn CustomKeywordValidator> + Send + Sync>;
/// Trait that allows implementing custom validation for keywords.
pub trait CustomKeywordValidator: Send + Sync {
/// Validate [instance](serde_json::Value) according to a custom specification
///
/// A custom keyword validator may be used when a validation that cannot, or
/// cannot be be easily or efficiently expressed in JSON schema.
///
/// The custom validation is applied in addition to the JSON schema validation.
/// Validate an instance returning any and all detected validation errors
fn validate<'instance>(
&self,
instance: &'instance serde_json::Value,
instance_path: JSONPointer,
subschema: Arc<serde_json::Value>,
subschema_path: JSONPointer,
schema: Arc<serde_json::Value>,
) -> ErrorIterator<'instance>;
/// Determine if an instance is valid
fn is_valid<'schema>(
&self,
instance: &serde_json::Value,
subschema: &'schema serde_json::Value,
schema: &'schema serde_json::Value,
) -> bool;
}
#[cfg(test)]
mod tests {
use super::CompilationOptions;

View File

@ -0,0 +1,115 @@
use crate::compilation::context::CompilationContext;
use crate::keywords::CompilationResult;
use crate::paths::{InstancePath, JSONPointer, PathChunk};
use crate::validator::Validate;
use crate::ErrorIterator;
use serde_json::Value;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
/// Custom keyword validation implemented by user provided validation functions.
pub(crate) struct CompiledCustomKeywordValidator {
schema: Arc<Value>,
subschema: Arc<Value>,
subschema_path: JSONPointer,
validator: Box<dyn CustomKeywordValidator>,
}
impl Display for CompiledCustomKeywordValidator {
fn fmt(&self, _: &mut Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl Validate for CompiledCustomKeywordValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
) -> ErrorIterator<'instance> {
self.validator.validate(
instance,
instance_path.into(),
self.subschema.clone(),
self.subschema_path.clone(),
self.schema.clone(),
)
}
fn is_valid(&self, instance: &Value) -> bool {
self.validator
.is_valid(instance, &self.subschema, &self.schema)
}
}
pub(crate) fn compile<'a>(
context: &CompilationContext,
keyword: impl Into<PathChunk>,
validator: Box<dyn CustomKeywordValidator>,
subschema: Value,
schema: Value,
) -> CompilationResult<'a> {
let subschema_path = context.as_pointer_with(keyword);
Ok(Box::new(CompiledCustomKeywordValidator {
schema: Arc::new(schema),
subschema: Arc::new(subschema),
subschema_path,
validator,
}))
}
mod sealed {
pub trait Sealed {}
}
/// Trait that allows implementing custom validation for keywords.
pub trait Keyword {
fn is_valid(&self, instance: &Value) -> bool;
}
pub trait CustomKeywordValidator: Send + Sync {
fn compile<'a>(
parent: &'a serde_json::Map<String, Value>,
schema: &'a Value,
context: &CompilationContext,
) -> Box<dyn CustomKeywordValidator>
where
Self: Sized;
/// Validate [instance](Value) according to a custom specification
///
/// A custom keyword validator may be used when a validation that cannot, or
/// cannot be be easily or efficiently expressed in JSON schema.
///
/// The custom validation is applied in addition to the JSON schema validation.
/// Validate an instance returning any and all detected validation errors
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: JSONPointer,
subschema: Arc<Value>,
subschema_path: JSONPointer,
schema: Arc<Value>,
) -> ErrorIterator<'instance>;
/// Determine if an instance is valid
fn is_valid<'schema>(
&self,
instance: &Value,
subschema: &'schema Value,
schema: &'schema Value,
) -> bool;
}
pub trait KeywordFactory: Send + Sync + sealed::Sealed {
fn init(&self, schema: &Value) -> Box<dyn Keyword>;
}
impl<F> sealed::Sealed for F where F: Fn(&Value) -> Box<dyn Keyword> + Send + Sync {}
impl<F> KeywordFactory for F
where
F: Fn(&Value) -> Box<dyn Keyword> + Send + Sync,
{
fn init(&self, schema: &Value) -> Box<dyn Keyword> {
self(schema)
}
}

View File

@ -6,7 +6,7 @@ pub(crate) mod boolean;
pub(crate) mod const_;
pub(crate) mod contains;
pub(crate) mod content;
pub(crate) mod custom_keyword;
pub(crate) mod custom;
pub(crate) mod dependencies;
pub(crate) mod enum_;
pub(crate) mod exclusive_maximum;

View File

@ -97,11 +97,9 @@ mod schema_node;
mod schemas;
mod validator;
pub use compilation::{
options::{CompilationOptions, CustomKeywordValidator},
JSONSchema,
};
pub use compilation::{options::CompilationOptions, JSONSchema};
pub use error::{ErrorIterator, ValidationError};
pub use keywords::custom::Keyword;
pub use resolver::{SchemaResolver, SchemaResolverError};
pub use schemas::Draft;