Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for look-around patterns using fancy-regex #220

Merged
merged 7 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jsonschema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ url = "2"
lazy_static = "1"
percent-encoding = "2"
regex = "1"
fancy-regex = "0.5"
base64 = ">= 0.2"
chrono = ">= 0.2"
reqwest = { version = ">= 0.10", features = ["blocking", "json"], optional = true}
Expand Down
22 changes: 18 additions & 4 deletions jsonschema/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ impl fmt::Display for CompilationError {
}
}

impl From<regex::Error> for CompilationError {
impl From<fancy_regex::Error> for CompilationError {
#[inline]
fn from(_: regex::Error) -> Self {
fn from(_: fancy_regex::Error) -> Self {
CompilationError::SchemaError
}
}
Expand Down Expand Up @@ -98,13 +98,15 @@ pub enum ValidationErrorKind {
AdditionalProperties { unexpected: Vec<String> },
/// The input value is not valid under any of the given schemas.
AnyOf,
/// Results from a [`fancy_regex::Error::BacktrackLimitExceeded`] variant when matching
BacktrackLimitExceeded { error: fancy_regex::Error },
/// The input value doesn't match expected constant.
Constant { expected_value: Value },
/// The input array doesn't contain items conforming to the specified schema.
Contains,
/// Ths input value does not respect the defined contentEncoding
/// The input value does not respect the defined contentEncoding
ContentEncoding { content_encoding: String },
/// Ths input value does not respect the defined contentMediaType
/// The input value does not respect the defined contentMediaType
ContentMediaType { content_media_type: String },
/// The input value doesn't match any of specified options.
Enum { options: Value },
Expand Down Expand Up @@ -219,6 +221,17 @@ impl<'a> ValidationError<'a> {
kind: ValidationErrorKind::AnyOf,
}
}
pub(crate) fn backtrack_limit(
instance_path: JSONPointer,
instance: &'a Value,
error: fancy_regex::Error,
) -> ValidationError<'a> {
ValidationError {
instance_path,
instance: Cow::Borrowed(instance),
kind: ValidationErrorKind::BacktrackLimitExceeded { error },
}
}
pub(crate) fn constant_array(
instance_path: JSONPointer,
instance: &'a Value,
Expand Down Expand Up @@ -709,6 +722,7 @@ impl fmt::Display for ValidationError<'_> {
ValidationErrorKind::Reqwest { error } => write!(f, "{}", error),
ValidationErrorKind::FileNotFound { error } => write!(f, "{}", error),
ValidationErrorKind::InvalidURL { error } => write!(f, "{}", error),
ValidationErrorKind::BacktrackLimitExceeded { error } => write!(f, "{}", error),
ValidationErrorKind::UnknownReferenceScheme { scheme } => {
write!(f, "Unknown scheme: {}", scheme)
}
Expand Down
44 changes: 13 additions & 31 deletions jsonschema/src/keywords/additional_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
validator::Validate,
};
use ahash::AHashMap;
use regex::Regex;
use fancy_regex::Regex;
use serde_json::{Map, Value};

pub(crate) type PatternedValidators = Vec<(Regex, Validators)>;
Expand Down Expand Up @@ -106,7 +106,7 @@ macro_rules! is_valid_patterns {
let mut has_match = false;
for (re, validators) in $patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match($property) {
if re.is_match($property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, $schema, $value)
}
Expand Down Expand Up @@ -516,7 +516,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
for (property, value) in item.iter() {
let mut has_match = false;
for (re, validators) in &self.patterns {
if re.is_match(property) {
if re.is_match(property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, schema, value)
}
Expand All @@ -542,7 +542,7 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
Expand Down Expand Up @@ -631,7 +631,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
Expand Down Expand Up @@ -735,7 +735,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
// Valid for `properties`, check `patternProperties`
for (re, validators) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property) {
if re.is_match(property).unwrap_or(false) {
is_valid_pattern_schema!(validators, schema, value)
}
}
Expand All @@ -747,7 +747,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
let mut has_match = false;
for (re, validators) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property) {
if re.is_match(property).unwrap_or(false) {
has_match = true;
is_valid_pattern_schema!(validators, schema, value)
}
Expand Down Expand Up @@ -777,7 +777,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
validate!(validators, schema, value, instance_path, name)
}),
Expand All @@ -787,7 +787,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
Expand Down Expand Up @@ -892,7 +892,7 @@ impl<M: PropertiesValidatorsMap> Validate
// Valid for `properties`, check `patternProperties`
for (re, validators) in &self.patterns {
// If there is a match, then the value should match the sub-schema
if re.is_match(property) {
if re.is_match(property).unwrap_or(false) {
is_valid_pattern_schema!(validators, schema, value)
}
}
Expand Down Expand Up @@ -924,7 +924,7 @@ impl<M: PropertiesValidatorsMap> Validate
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
validate!(validators, schema, value, instance_path, name)
}),
Expand All @@ -934,7 +934,7 @@ impl<M: PropertiesValidatorsMap> Validate
errors.extend(
self.patterns
.iter()
.filter(|(re, _)| re.is_match(property))
.filter(|(re, _)| re.is_match(property).unwrap_or(false))
.flat_map(|(_, validators)| {
has_match = true;
validate!(validators, schema, value, instance_path, property)
Expand Down Expand Up @@ -1070,7 +1070,7 @@ fn compile_patterns(

#[cfg(test)]
mod tests {
use crate::{tests_util, JSONSchema};
use crate::tests_util;
use serde_json::{json, Value};
use test_case::test_case;

Expand Down Expand Up @@ -1440,22 +1440,4 @@ mod tests {
tests_util::is_not_valid(&schema, instance);
tests_util::expect_errors(&schema, instance, expected)
}

#[test]
fn unsupported_regex_does_not_compile() {
// See GH-213
let schema = json!({
"type": "object",
"properties": {
"eo:cloud_cover": {
"type": "number",
},
},
"patternProperties": {
"^(?!eo:)": {}
},
"additionalProperties": false
});
assert!(JSONSchema::compile(&schema).is_err())
}
}
28 changes: 20 additions & 8 deletions jsonschema/src/keywords/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
Draft,
};
use chrono::{DateTime, NaiveDate};
use regex::Regex;
use fancy_regex::Regex;
use serde_json::{Map, Value};

use std::{net::IpAddr, str::FromStr};
Expand Down Expand Up @@ -82,7 +82,9 @@ impl Validate for DateValidator {
// Padding with zeroes is ignored by the underlying parser. The most efficient
// way to check it will be to use a custom parser that won't ignore zeroes,
// but this regex will do the trick and costs ~20% extra time in this validator.
DATE_RE.is_match(item.as_str())
DATE_RE
.is_match(item.as_str())
.expect("Simple DATE_RE pattern")
} else {
false
}
Expand Down Expand Up @@ -219,7 +221,9 @@ impl Validate for IRIReferenceValidator {
validate!("iri-reference");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
IRI_REFERENCE_RE.is_match(item)
IRI_REFERENCE_RE
.is_match(item)
.expect("Simple IRI_REFERENCE_RE pattern")
} else {
true
}
Expand All @@ -230,7 +234,9 @@ impl Validate for JSONPointerValidator {
validate!("json-pointer");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
JSON_POINTER_RE.is_match(item)
JSON_POINTER_RE
.is_match(item)
.expect("Simple JSON_POINTER_RE pattern")
} else {
true
}
Expand All @@ -252,7 +258,9 @@ impl Validate for RelativeJSONPointerValidator {
validate!("relative-json-pointer");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
RELATIVE_JSON_POINTER_RE.is_match(item)
RELATIVE_JSON_POINTER_RE
.is_match(item)
.expect("Simple RELATIVE_JSON_POINTER_RE pattern")
} else {
true
}
Expand All @@ -263,7 +271,7 @@ impl Validate for TimeValidator {
validate!("time");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
TIME_RE.is_match(item)
TIME_RE.is_match(item).expect("Simple TIME_RE pattern")
} else {
true
}
Expand All @@ -274,7 +282,9 @@ impl Validate for URIReferenceValidator {
validate!("uri-reference");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
URI_REFERENCE_RE.is_match(item)
URI_REFERENCE_RE
.is_match(item)
.expect("Simple URI_REFERENCE_RE pattern")
} else {
true
}
Expand All @@ -285,7 +295,9 @@ impl Validate for URITemplateValidator {
validate!("uri-template");
fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool {
if let Value::String(item) = instance {
URI_TEMPLATE_RE.is_match(item)
URI_TEMPLATE_RE
.is_match(item)
.expect("Simple URI_TEMPLATE_RE pattern")
} else {
true
}
Expand Down
Loading