feat: Support for custom 'format' validators
Signed-off-by: Dmitry Dygalo <dadygalo@gmail.com>
This commit is contained in:
parent
76cfe5073f
commit
068e49427e
|
@ -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
|
||||
|
|
|
@ -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!")));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
Loading…
Reference in New Issue