use crate::{
error::{error, no_error, ErrorIterator},
node::SchemaNode,
output::{Annotations, ErrorDescription, Output, OutputUnit},
paths::LazyLocation,
Draft, ValidationError, ValidationOptions,
};
use serde_json::Value;
use std::{collections::VecDeque, sync::Arc};
pub(crate) trait Validate: Send + Sync {
fn iter_errors<'i>(&self, instance: &'i Value, location: &LazyLocation) -> ErrorIterator<'i> {
match self.validate(instance, location) {
Ok(()) => no_error(),
Err(err) => error(err),
}
}
fn is_valid(&self, instance: &Value) -> bool;
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
) -> Result<(), ValidationError<'i>>;
fn apply<'a>(&'a self, instance: &Value, location: &LazyLocation) -> PartialApplication<'a> {
let errors: Vec<ErrorDescription> = self
.iter_errors(instance, location)
.map(ErrorDescription::from)
.collect();
if errors.is_empty() {
PartialApplication::valid_empty()
} else {
PartialApplication::invalid_empty(errors)
}
}
}
#[derive(Clone, PartialEq)]
pub(crate) enum PartialApplication<'a> {
Valid {
annotations: Option<Annotations<'a>>,
child_results: VecDeque<OutputUnit<Annotations<'a>>>,
},
Invalid {
errors: Vec<ErrorDescription>,
child_results: VecDeque<OutputUnit<ErrorDescription>>,
},
}
impl<'a> PartialApplication<'a> {
pub(crate) fn valid_empty() -> PartialApplication<'static> {
PartialApplication::Valid {
annotations: None,
child_results: VecDeque::new(),
}
}
pub(crate) fn invalid_empty(errors: Vec<ErrorDescription>) -> PartialApplication<'static> {
PartialApplication::Invalid {
errors,
child_results: VecDeque::new(),
}
}
pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) {
match self {
Self::Valid { annotations, .. } => *annotations = Some(new_annotations),
Self::Invalid { .. } => {}
}
}
pub(crate) fn mark_errored(&mut self, error: ErrorDescription) {
match self {
Self::Invalid { errors, .. } => errors.push(error),
Self::Valid { .. } => {
*self = Self::Invalid {
errors: vec![error],
child_results: VecDeque::new(),
}
}
}
}
}
#[derive(Debug)]
pub struct Validator {
pub(crate) root: SchemaNode,
pub(crate) config: Arc<ValidationOptions>,
}
impl Validator {
#[must_use]
pub fn options() -> ValidationOptions {
ValidationOptions::default()
}
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
Self::options().build(schema)
}
#[inline]
pub fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
self.root.validate(instance, &LazyLocation::new())
}
#[inline]
pub fn iter_errors<'i>(&'i self, instance: &'i Value) -> ErrorIterator<'i> {
self.root.iter_errors(instance, &LazyLocation::new())
}
#[must_use]
#[inline]
pub fn is_valid(&self, instance: &Value) -> bool {
self.root.is_valid(instance)
}
#[must_use]
pub const fn apply<'a, 'b>(&'a self, instance: &'b Value) -> Output<'a, 'b> {
Output::new(self, &self.root, instance)
}
#[must_use]
pub fn draft(&self) -> Draft {
self.config.draft()
}
#[must_use]
pub fn config(&self) -> Arc<ValidationOptions> {
Arc::clone(&self.config)
}
}
#[cfg(test)]
mod tests {
use crate::{
error::ValidationError,
keywords::custom::Keyword,
paths::{LazyLocation, Location},
primitive_type::PrimitiveType,
Validator,
};
use fancy_regex::Regex;
use num_cmp::NumCmp;
use once_cell::sync::Lazy;
use serde_json::{json, Map, Value};
#[cfg(not(target_arch = "wasm32"))]
fn load(path: &str, idx: usize) -> Value {
use std::{fs::File, io::Read, path::Path};
let path = Path::new(path);
let mut file = File::open(path).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).ok().unwrap();
let data: Value = serde_json::from_str(&content).unwrap();
let case = &data.as_array().unwrap()[idx];
case.get("schema").unwrap().clone()
}
#[test]
fn only_keyword() {
let schema = json!({"type": "string"});
let validator = crate::validator_for(&schema).unwrap();
let value1 = json!("AB");
let value2 = json!(1);
assert_eq!(validator.root.validators().len(), 1);
assert!(validator.validate(&value1).is_ok());
assert!(validator.validate(&value2).is_err());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn validate_ref() {
let schema = load("tests/suite/tests/draft7/ref.json", 1);
let value = json!({"bar": 3});
let validator = crate::validator_for(&schema).unwrap();
assert!(validator.validate(&value).is_ok());
let value = json!({"bar": true});
assert!(validator.validate(&value).is_err());
}
#[test]
fn wrong_schema_type() {
let schema = json!([1]);
let validator = crate::validator_for(&schema);
assert!(validator.is_err());
}
#[test]
fn multiple_errors() {
let schema = json!({"minProperties": 2, "propertyNames": {"minLength": 3}});
let value = json!({"a": 3});
let validator = crate::validator_for(&schema).unwrap();
let errors: Vec<_> = validator.iter_errors(&value).collect();
assert_eq!(errors.len(), 2);
assert_eq!(
errors[0].to_string(),
r#"{"a":3} has less than 2 properties"#
);
assert_eq!(errors[1].to_string(), r#""a" is shorter than 3 characters"#);
}
#[test]
fn custom_keyword_definition() {
struct CustomObjectValidator;
impl Keyword for CustomObjectValidator {
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
) -> Result<(), ValidationError<'i>> {
for key in instance.as_object().unwrap().keys() {
if !key.is_ascii() {
return Err(ValidationError::custom(
Location::new(),
location.into(),
instance,
"Key is not ASCII",
));
}
}
Ok(())
}
fn is_valid(&self, instance: &Value) -> bool {
for (key, _value) in instance.as_object().unwrap() {
if !key.is_ascii() {
return false;
}
}
true
}
}
fn custom_object_type_factory<'a>(
_: &'a Map<String, Value>,
schema: &'a Value,
path: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
const EXPECTED: &str = "ascii-keys";
if schema.as_str().map_or(true, |key| key != EXPECTED) {
Err(ValidationError::constant_string(
Location::new(),
path,
schema,
EXPECTED,
))
} else {
Ok(Box::new(CustomObjectValidator))
}
}
let schema =
json!({ "custom-object-type": "ascii-keys", "type": "object", "minProperties": 1 });
let validator = crate::options()
.with_keyword("custom-object-type", custom_object_type_factory)
.build(&schema)
.unwrap();
let instance = json!({});
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!({ "a" : 1 });
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!({ "Ã¥" : 1 });
let error = validator.validate(&instance).expect_err("Should fail");
assert_eq!(error.to_string(), "Key is not ASCII");
assert!(!validator.is_valid(&instance));
}
#[test]
fn custom_format_and_override_keyword() {
fn currency_format_checker(s: &str) -> bool {
static CURRENCY_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new("^(0|([1-9]+[0-9]*))(\\.[0-9]{2})$").expect("Invalid regex")
});
CURRENCY_RE.is_match(s).expect("Invalid regex")
}
struct CustomMinimumValidator {
limit: f64,
limit_val: Value,
with_currency_format: bool,
location: Location,
}
impl Keyword for CustomMinimumValidator {
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
) -> Result<(), ValidationError<'i>> {
if self.is_valid(instance) {
Ok(())
} else {
Err(ValidationError::minimum(
self.location.clone(),
location.into(),
instance,
self.limit_val.clone(),
))
}
}
fn is_valid(&self, instance: &Value) -> bool {
match instance {
Value::Number(instance) => {
if let Some(item) = instance.as_u64() {
!NumCmp::num_lt(item, self.limit)
} else if let Some(item) = instance.as_i64() {
!NumCmp::num_lt(item, self.limit)
} else {
let item = instance.as_f64().expect("Always valid");
!NumCmp::num_lt(item, self.limit)
}
}
Value::String(instance) => {
if self.with_currency_format && currency_format_checker(instance) {
let value = instance
.parse::<f64>()
.expect("format validated by regex checker");
!NumCmp::num_lt(value, self.limit)
} else {
true
}
}
_ => true,
}
}
}
fn custom_minimum_factory<'a>(
parent: &'a Map<String, Value>,
schema: &'a Value,
location: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
let limit = if let Value::Number(limit) = schema {
limit.as_f64().expect("Always valid")
} else {
return Err(ValidationError::single_type_error(
Location::new(),
location,
schema,
PrimitiveType::Number,
));
};
let with_currency_format = parent
.get("format")
.map_or(false, |format| format == "currency");
Ok(Box::new(CustomMinimumValidator {
limit,
limit_val: schema.clone(),
with_currency_format,
location,
}))
}
let schema = json!({ "minimum": 2, "type": "string", "format": "currency" });
let validator = crate::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.with_keyword("minimum-2", custom_minimum_factory)
.should_validate_formats(true)
.build(&schema)
.expect("Invalid schema");
let instance = json!(15);
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!("not a currency");
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!("3.00");
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!("1.99");
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let schema = json!({ "minimum": 2, "type": "integer" });
let validator = crate::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.build(&schema)
.expect("Invalid schema");
let instance = json!(3);
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!(1);
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let schema = json!({ "minimum": "foo" });
let error = crate::options()
.with_keyword("minimum", custom_minimum_factory)
.build(&schema)
.expect_err("Should fail");
assert_eq!(error.to_string(), "\"foo\" is not of type \"number\"");
}
#[test]
fn test_validator_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Validator>();
}
}