feat: Support for custom 'format' validators

Signed-off-by: Dmitry Dygalo <dadygalo@gmail.com>
This commit is contained in:
Dmitry Dygalo 2021-07-09 12:32:36 +02:00 committed by Dmitry Dygalo
parent 76cfe5073f
commit 068e49427e
3 changed files with 113 additions and 2 deletions

View File

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Support for custom `format` validators. [#158](https://github.com/Stranger6667/jsonschema-rs/issues/158)
## [0.11.0] - 2021-06-19
### Added

View File

@ -64,6 +64,7 @@ pub struct CompilationOptions {
content_encoding_checks_and_converters:
AHashMap<&'static str, Option<(ContentEncodingCheckType, ContentEncodingConverterType)>>,
store: AHashMap<String, serde_json::Value>,
formats: AHashMap<&'static str, fn(&str) -> bool>,
validate_schema: bool,
}
@ -75,6 +76,7 @@ impl Default for CompilationOptions {
content_media_type_checks: AHashMap::default(),
content_encoding_checks_and_converters: AHashMap::default(),
store: AHashMap::default(),
formats: AHashMap::default(),
}
}
}
@ -347,7 +349,38 @@ impl CompilationOptions {
self.store.insert(id, document);
self
}
/// Register a custom "format" validator.
///
/// ## Example
///
/// ```rust
/// # use jsonschema::JSONSchema;
/// # use serde_json::json;
/// fn my_format(s: &str) -> bool {
/// // Your awesome format check!
/// s.ends_with("42!")
/// }
/// # fn foo() {
/// let schema = json!({"type": "string", "format": "custom"});
/// let compiled = JSONSchema::options()
/// .with_format("custom", my_format)
/// .compile(&schema)
/// .expect("Valid schema");
/// // Invalid string
/// assert!(!compiled.is_valid(&json!("foo")));
/// // Valid string
/// assert!(compiled.is_valid(&json!("foo42!")));
/// # }
/// ```
///
/// The format check function should receive `&str` and return `bool`.
pub fn with_format(&mut self, name: &'static str, format: fn(&str) -> bool) -> &mut Self {
self.formats.insert(name, format);
self
}
pub(crate) fn format(&self, format: &str) -> FormatKV<'_> {
self.formats.get_key_value(format)
}
/// Do not perform schema validation during compilation.
/// This method is only used to disable meta-schema validation for meta-schemas itself to avoid
/// infinite recursion.
@ -359,6 +392,8 @@ impl CompilationOptions {
self
}
}
// format name & a pointer to a check function
type FormatKV<'a> = Option<(&'a &'static str, &'a fn(&str) -> bool)>;
impl fmt::Debug for CompilationOptions {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -404,8 +439,23 @@ mod tests {
json!({"rule": {"minLength": 5}}),
)
.compile(&schema)
.unwrap();
.expect("Valid schema");
assert!(!compiled.is_valid(&json!("foo")));
assert!(compiled.is_valid(&json!("foobar")));
}
fn custom(s: &str) -> bool {
s.ends_with("42!")
}
#[test]
fn custom_format() {
let schema = json!({"type": "string", "format": "custom"});
let compiled = JSONSchema::options()
.with_format("custom", custom)
.compile(&schema)
.expect("Valid schema");
assert!(!compiled.is_valid(&json!("foo")));
assert!(compiled.is_valid(&json!("foo42!")));
}
}

View File

@ -328,6 +328,60 @@ impl Validate for URITemplateValidator {
}
}
struct CustomFormatValidator {
schema_path: JSONPointer,
format_name: &'static str,
check: fn(&str) -> bool,
}
impl CustomFormatValidator {
pub(crate) fn compile<'a>(
context: &CompilationContext,
format_name: &'static str,
check: fn(&str) -> bool,
) -> CompilationResult<'a> {
let schema_path = context.as_pointer_with("format");
Ok(Box::new(CustomFormatValidator {
schema_path,
format_name,
check,
}))
}
}
impl ToString for CustomFormatValidator {
fn to_string(&self) -> String {
format!("format: {}", self.format_name)
}
}
impl Validate for CustomFormatValidator {
fn validate<'a>(
&self,
schema: &'a JSONSchema,
instance: &'a Value,
instance_path: &InstancePath,
) -> ErrorIterator<'a> {
if let Value::String(_item) = instance {
if !self.is_valid(schema, instance) {
return error(ValidationError::format(
self.schema_path.clone(),
instance_path.into(),
instance,
self.format_name,
));
}
}
no_error()
}
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
(self.check)(item)
} else {
true
}
}
}
#[inline]
pub(crate) fn compile<'a>(
_: &'a Map<String, Value>,
@ -335,6 +389,9 @@ pub(crate) fn compile<'a>(
context: &CompilationContext,
) -> Option<CompilationResult<'a>> {
if let Value::String(format) = schema {
if let Some((format, func)) = context.config.format(format) {
return Some(CustomFormatValidator::compile(context, format, *func));
}
let draft_version = context.config.draft();
match format.as_str() {
"date-time" => Some(DateTimeValidator::compile(context)),