use std::sync::Arc;
use crate::{
compiler,
error::{no_error, ErrorIterator, ValidationError},
evaluation::Annotations,
keywords::CompilationResult,
node::SchemaNode,
options::PatternEngineOptions,
paths::{LazyEvaluationPath, LazyLocation, Location, RefTracker},
regex::{analyze_pattern, LiteralMatcher, PatternOptimization, RegexEngine},
types::JsonType,
validator::{EvaluationResult, Validate, ValidationContext},
};
use serde_json::{Map, Value};
pub(crate) struct PatternPropertiesValidator<R> {
patterns: Vec<(Arc<R>, SchemaNode)>,
}
impl<R: RegexEngine> Validate for PatternPropertiesValidator<R> {
fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool {
if let Value::Object(item) = instance {
for (re, node) in &self.patterns {
for (key, value) in item {
if re.is_match(key).unwrap_or(false) && !node.is_valid(value, ctx) {
return false;
}
}
}
true
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::Object(item) = instance {
for (key, value) in item {
for (re, node) in &self.patterns {
if re.is_match(key).unwrap_or(false) {
node.validate(value, &location.push(key), tracker, ctx)?;
}
}
}
}
Ok(())
}
fn iter_errors<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> ErrorIterator<'i> {
if let Value::Object(item) = instance {
let mut errors = Vec::new();
for (re, node) in &self.patterns {
for (key, value) in item {
if re.is_match(key).unwrap_or(false) {
errors.extend(node.iter_errors(
value,
&location.push(key.as_str()),
tracker,
ctx,
));
}
}
}
ErrorIterator::from_iterator(errors.into_iter())
} else {
no_error()
}
}
fn evaluate(
&self,
instance: &Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> EvaluationResult {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
let mut children = Vec::new();
for (pattern, node) in &self.patterns {
for (key, value) in item {
if pattern.is_match(key).unwrap_or(false) {
matched_propnames.push(key.clone());
children.push(node.evaluate_instance(
value,
&location.push(key.as_str()),
tracker,
ctx,
));
}
}
}
let mut result = EvaluationResult::from_children(children);
result.annotate(Annotations::new(Value::from(matched_propnames)));
result
} else {
EvaluationResult::valid_empty()
}
}
}
pub(crate) struct SingleValuePatternPropertiesValidator<R> {
regex: Arc<R>,
node: SchemaNode,
}
impl<R: RegexEngine> Validate for SingleValuePatternPropertiesValidator<R> {
fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool {
if let Value::Object(item) = instance {
for (key, value) in item {
if self.regex.is_match(key).unwrap_or(false) && !self.node.is_valid(value, ctx) {
return false;
}
}
true
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::Object(item) = instance {
for (key, value) in item {
if self.regex.is_match(key).unwrap_or(false) {
self.node
.validate(value, &location.push(key), tracker, ctx)?;
}
}
}
Ok(())
}
fn iter_errors<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> ErrorIterator<'i> {
if let Value::Object(item) = instance {
let mut errors = Vec::new();
for (key, value) in item {
if self.regex.is_match(key).unwrap_or(false) {
errors.extend(self.node.iter_errors(
value,
&location.push(key.as_str()),
tracker,
ctx,
));
}
}
ErrorIterator::from_iterator(errors.into_iter())
} else {
no_error()
}
}
fn evaluate(
&self,
instance: &Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> EvaluationResult {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
let mut children = Vec::new();
for (key, value) in item {
if self.regex.is_match(key).unwrap_or(false) {
matched_propnames.push(key.clone());
children.push(self.node.evaluate_instance(
value,
&location.push(key.as_str()),
tracker,
ctx,
));
}
}
let mut result = EvaluationResult::from_children(children);
result.annotate(Annotations::new(Value::from(matched_propnames)));
result
} else {
EvaluationResult::valid_empty()
}
}
}
#[inline]
pub(crate) fn compile<'a>(
ctx: &compiler::Context,
parent: &'a Map<String, Value>,
schema: &'a Value,
) -> Option<CompilationResult<'a>> {
if matches!(
parent.get("additionalProperties"),
Some(Value::Bool(false) | Value::Object(_))
) {
return None;
}
let Value::Object(map) = schema else {
let location = ctx.location().join("patternProperties");
return Some(Err(ValidationError::single_type_error(
location.clone(),
location,
Location::new(),
schema,
JsonType::Object,
)));
};
let ctx = ctx.new_at_location("patternProperties");
if let Some(validator) = try_compile_as_literals(&ctx, map) {
return Some(validator);
}
let result = match ctx.config().pattern_options() {
PatternEngineOptions::FancyRegex { .. } => {
compile_pattern_entries(&ctx, map, |pctx, pattern, subschema| {
pctx.get_or_compile_regex(pattern)
.map_err(|()| invalid_regex(pctx, subschema))
})
.map(|patterns| {
build_validator_from_entries(patterns, |regex, node| {
Box::new(SingleValuePatternPropertiesValidator { regex, node })
as Box<dyn Validate>
})
})
}
PatternEngineOptions::Regex { .. } => {
compile_pattern_entries(&ctx, map, |pctx, pattern, subschema| {
pctx.get_or_compile_standard_regex(pattern)
.map_err(|()| invalid_regex(pctx, subschema))
})
.map(|patterns| {
build_validator_from_entries(patterns, |regex, node| {
Box::new(SingleValuePatternPropertiesValidator { regex, node })
as Box<dyn Validate>
})
})
}
};
Some(result)
}
fn try_compile_as_literals<'a>(
ctx: &compiler::Context,
map: &'a Map<String, Value>,
) -> Option<CompilationResult<'a>> {
let mut entries = Vec::with_capacity(map.len());
for (pattern, subschema) in map {
let pctx = ctx.new_at_location(pattern.as_str());
let matcher = match analyze_pattern(pattern)? {
PatternOptimization::Prefix(literal) => LiteralMatcher::Prefix {
literal,
original: pattern.clone(),
},
PatternOptimization::Exact(exact) => LiteralMatcher::Exact {
exact,
original: pattern.clone(),
},
PatternOptimization::Alternation(alternatives) => LiteralMatcher::Alternation {
alternatives,
original: pattern.clone(),
},
PatternOptimization::NoWhitespace => LiteralMatcher::NoWhitespace {
original: pattern.clone(),
},
};
let node = match compiler::compile(&pctx, pctx.as_resource_ref(subschema)) {
Ok(node) => node,
Err(e) => return Some(Err(e)),
};
entries.push((Arc::new(matcher), node));
}
Some(Ok(build_validator_from_entries(entries, |regex, node| {
Box::new(SingleValuePatternPropertiesValidator { regex, node }) as Box<dyn Validate>
})))
}
fn invalid_regex<'a>(ctx: &compiler::Context, schema: &'a Value) -> ValidationError<'a> {
ValidationError::format(
ctx.location().clone(),
LazyEvaluationPath::SameAsSchemaPath,
Location::new(),
schema,
"regex",
)
}
fn compile_pattern_entries<'a, R, F>(
ctx: &compiler::Context,
map: &'a Map<String, Value>,
mut compile_regex: F,
) -> Result<Vec<(Arc<R>, SchemaNode)>, ValidationError<'a>>
where
F: FnMut(&compiler::Context, &str, &'a Value) -> Result<Arc<R>, ValidationError<'a>>,
{
let mut patterns = Vec::with_capacity(map.len());
for (pattern, subschema) in map {
let pctx = ctx.new_at_location(pattern.as_str());
let regex = compile_regex(&pctx, pattern, subschema)?;
let node = compiler::compile(&pctx, pctx.as_resource_ref(subschema))?;
patterns.push((regex, node));
}
Ok(patterns)
}
fn build_validator_from_entries<R>(
mut entries: Vec<(Arc<R>, SchemaNode)>,
single_factory: impl FnOnce(Arc<R>, SchemaNode) -> Box<dyn Validate>,
) -> Box<dyn Validate>
where
R: RegexEngine + 'static,
{
if entries.len() == 1 {
let (regex, node) = entries.pop().expect("len checked");
single_factory(regex, node)
} else {
Box::new(PatternPropertiesValidator { patterns: entries })
}
}
#[cfg(test)]
mod tests {
use crate::{
regex::{analyze_pattern, PatternOptimization},
tests_util,
};
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(&json!({"patternProperties": {"^f": {"type": "string"}}}), &json!({"f": 42}), "/patternProperties/^f/type")]
#[test_case(&json!({"patternProperties": {"^f": {"type": "string"}, "^x": {"type": "string"}}}), &json!({"f": 42}), "/patternProperties/^f/type")]
fn location(schema: &Value, instance: &Value, expected: &str) {
tests_util::assert_schema_location(schema, instance, expected);
}
#[test_case(&json!({"patternProperties": {"[invalid": {"type": "string"}}}))]
#[test_case(&json!({"additionalProperties": true, "patternProperties": {"[invalid": {"type": "string"}}}))]
fn invalid_regex_fancy_regex(schema: &Value) {
let error = crate::validator_for(schema).expect_err("Should fail to compile");
assert!(error.to_string().contains("regex"));
}
#[test_case(&json!({"patternProperties": {"[invalid": {"type": "string"}}}))]
#[test_case(&json!({"additionalProperties": true, "patternProperties": {"[invalid": {"type": "string"}}}))]
fn invalid_regex_standard_regex(schema: &Value) {
use crate::PatternOptions;
let error = crate::options()
.with_pattern_options(PatternOptions::regex())
.build(schema)
.expect_err("Should fail to compile");
assert!(error.to_string().contains("regex"));
}
#[test]
fn test_analyze_pattern() {
use PatternOptimization::{Exact, Prefix};
assert_eq!(analyze_pattern("^foo"), Some(Prefix("foo".into())));
assert_eq!(analyze_pattern("^x-"), Some(Prefix("x-".into())));
assert_eq!(analyze_pattern("^eo_band"), Some(Prefix("eo_band".into())));
assert_eq!(analyze_pattern("^path/to"), Some(Prefix("path/to".into())));
assert_eq!(analyze_pattern("^ABC123"), Some(Prefix("ABC123".into())));
assert_eq!(analyze_pattern("^\\/"), Some(Prefix("/".into())));
assert_eq!(analyze_pattern("^foo$"), Some(Exact("foo".into())));
assert_eq!(analyze_pattern("^\\$ref$"), Some(Exact("$ref".into())));
assert_eq!(analyze_pattern("foo"), None);
assert_eq!(analyze_pattern("^foo.*"), None);
assert_eq!(analyze_pattern("^foo+"), None);
assert_eq!(analyze_pattern("^foo?"), None);
assert_eq!(analyze_pattern("^[a-z]"), None);
assert_eq!(analyze_pattern("^foo|bar"), None);
assert_eq!(analyze_pattern("^foo(bar)"), None);
assert_eq!(analyze_pattern("^foo\\d"), None);
}
#[test_case("^x-", "x-custom", true)]
#[test_case("^x-", "custom", false)]
#[test_case("^eo_", "eo_bands", true)]
#[test_case("^eo_", "proj_epsg", false)]
fn test_prefix_pattern_validation(pattern: &str, key: &str, should_match: bool) {
let schema = json!({
"patternProperties": {
pattern: {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let valid_instance = json!({ key: "value" });
assert!(validator.is_valid(&valid_instance));
let invalid_instance = json!({ key: 42 });
assert_eq!(validator.is_valid(&invalid_instance), !should_match);
}
#[test]
fn test_multiple_prefix_patterns() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"},
"^y-": {"type": "number"}
}
});
let validator = crate::validator_for(&schema).unwrap();
assert!(validator.is_valid(&json!({"x-foo": "bar", "y-baz": 42})));
assert!(!validator.is_valid(&json!({"x-foo": 123}))); assert!(!validator.is_valid(&json!({"y-baz": "str"}))); }
#[test]
fn test_prefix_iter_errors_valid() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-foo": "bar"});
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert!(errors.is_empty());
let instance = json!({"other": 42});
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert!(errors.is_empty());
}
#[test]
fn test_prefix_iter_errors_invalid() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-foo": 42});
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert_eq!(errors.len(), 1);
assert!(errors[0].to_string().contains("not of type"));
}
#[test]
fn test_prefix_iter_errors_multiple_failures() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"},
"^y-": {"type": "number"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-a": 1, "y-b": "str"});
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert_eq!(errors.len(), 2);
}
#[test]
fn test_prefix_evaluate_valid() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-foo": "bar"});
let result = validator.evaluate(&instance);
assert!(result.flag().valid);
}
#[test]
fn test_prefix_evaluate_invalid() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-foo": 42});
let result = validator.evaluate(&instance);
assert!(!result.flag().valid);
}
#[test]
fn test_prefix_evaluate_annotations() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-foo": "bar", "x-baz": "qux", "other": 123});
let result = validator.evaluate(&instance);
assert!(result.flag().valid);
let annotations: Vec<_> = result.iter_annotations().collect();
assert!(!annotations.is_empty());
}
#[test]
fn test_prefix_multiple_patterns_evaluate() {
let schema = json!({
"patternProperties": {
"^x-": {"type": "string"},
"^y-": {"type": "number"},
"^z$": {"type": "boolean"},
}
});
let validator = crate::validator_for(&schema).unwrap();
let instance = json!({"x-a": "s", "y-b": 1, "z": true});
let result = validator.evaluate(&instance);
assert!(result.flag().valid);
let instance = json!({"x-a": 123});
let result = validator.evaluate(&instance);
assert!(!result.flag().valid);
}
}