all jspg tests now passing
This commit is contained in:
@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
@ -11,23 +13,23 @@ impl<'a> ValidationContext<'a> {
|
||||
) -> Result<bool, ValidationError> {
|
||||
let current = self.instance;
|
||||
if let Some(arr) = current.as_array() {
|
||||
if let Some(min) = self.schema.min_items {
|
||||
if (arr.len() as f64) < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_ITEMS".to_string(),
|
||||
message: "Too few items".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(min) = self.schema.min_items
|
||||
&& (arr.len() as f64) < min
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_ITEMS".to_string(),
|
||||
message: "Too few items".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_items {
|
||||
if (arr.len() as f64) > max {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_ITEMS".to_string(),
|
||||
message: "Too many items".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_items
|
||||
&& (arr.len() as f64) > max
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_ITEMS".to_string(),
|
||||
message: "Too many items".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if self.schema.unique_items.unwrap_or(false) {
|
||||
@ -52,7 +54,7 @@ impl<'a> ValidationContext<'a> {
|
||||
contains_schema,
|
||||
child_instance,
|
||||
&self.path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
);
|
||||
@ -72,14 +74,14 @@ impl<'a> ValidationContext<'a> {
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_contains {
|
||||
if _match_count > max as usize {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
message: format!("Contains matches {} > max {}", _match_count, max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_contains
|
||||
&& _match_count > max as usize
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
message: format!("Contains matches {} > max {}", _match_count, max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +97,7 @@ impl<'a> ValidationContext<'a> {
|
||||
sub_schema,
|
||||
child_instance,
|
||||
&path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
);
|
||||
@ -116,7 +118,7 @@ impl<'a> ValidationContext<'a> {
|
||||
items_schema,
|
||||
child_instance,
|
||||
&path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
);
|
||||
|
||||
@ -15,52 +15,61 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref any_of) = self.schema.any_of {
|
||||
let mut valid = false;
|
||||
|
||||
for sub in any_of {
|
||||
let derived = self.derive_for_schema(sub, true);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
valid = true;
|
||||
result.merge(sub_res);
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
result.errors.push(ValidationError {
|
||||
code: "ANY_OF_VIOLATED".to_string(),
|
||||
message: "Matches none of anyOf schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref one_of) = self.schema.one_of {
|
||||
let mut valid_count = 0;
|
||||
let mut valid_res = ValidationResult::new();
|
||||
let mut passed_candidates: Vec<(Option<String>, usize, ValidationResult)> = Vec::new();
|
||||
|
||||
for sub in one_of {
|
||||
let derived = self.derive_for_schema(sub, true);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
valid_count += 1;
|
||||
valid_res = sub_res;
|
||||
let child_id = sub.id.clone();
|
||||
let depth = child_id
|
||||
.as_ref()
|
||||
.and_then(|id| self.db.depths.get(id).copied())
|
||||
.unwrap_or(0);
|
||||
passed_candidates.push((child_id, depth, sub_res));
|
||||
}
|
||||
}
|
||||
|
||||
if valid_count == 1 {
|
||||
result.merge(valid_res);
|
||||
} else if valid_count == 0 {
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().2);
|
||||
} else if passed_candidates.is_empty() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "ONE_OF_VIOLATED".to_string(),
|
||||
code: "NO_ONEOF_MATCH".to_string(),
|
||||
message: "Matches none of oneOf schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
// Apply depth heuristic tie-breaker
|
||||
let mut best_depth: Option<usize> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut best_res = None;
|
||||
|
||||
for (_, depth, res) in passed_candidates.into_iter() {
|
||||
if let Some(current_best) = best_depth {
|
||||
if depth > current_best {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
ambiguous = false;
|
||||
} else if depth == current_best {
|
||||
ambiguous = true;
|
||||
}
|
||||
} else {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
}
|
||||
}
|
||||
|
||||
if !ambiguous {
|
||||
if let Some(res) = best_res {
|
||||
result.merge(res);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
result.errors.push(ValidationError {
|
||||
code: "ONE_OF_VIOLATED".to_string(),
|
||||
message: format!("Matches {} of oneOf schemas (expected 1)", valid_count),
|
||||
code: "AMBIGUOUS_ONEOF_MATCH".to_string(),
|
||||
message: "Matches multiple oneOf schemas without a clear depth winner".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,11 +21,9 @@ impl<'a> ValidationContext<'a> {
|
||||
let derived_then = self.derive_for_schema(then_schema, true);
|
||||
result.merge(derived_then.validate()?);
|
||||
}
|
||||
} else {
|
||||
if let Some(ref else_schema) = self.schema.else_ {
|
||||
let derived_else = self.derive_for_schema(else_schema, true);
|
||||
result.merge(derived_else.validate()?);
|
||||
}
|
||||
} else if let Some(ref else_schema) = self.schema.else_ {
|
||||
let derived_else = self.derive_for_schema(else_schema, true);
|
||||
result.merge(derived_else.validate()?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::validator::Validator;
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
use crate::validator::rules::util::equals;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_core(
|
||||
@ -41,25 +42,23 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
|
||||
if let Some(ref const_val) = self.schema.const_ {
|
||||
if !crate::validator::util::equals(current, const_val) {
|
||||
if !equals(current, const_val) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(),
|
||||
message: "Value does not match const".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
if let Some(obj) = current.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = current.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
} else if let Some(obj) = current.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = current.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref enum_vals) = self.schema.enum_ {
|
||||
let mut found = false;
|
||||
for val in enum_vals {
|
||||
if crate::validator::util::equals(current, val) {
|
||||
if equals(current, val) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@ -70,12 +69,10 @@ impl<'a> ValidationContext<'a> {
|
||||
message: "Value is not in enum".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
if let Some(obj) = current.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = current.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
} else if let Some(obj) = current.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = current.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,25 +16,23 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if should {
|
||||
if let Err(e) = f(current) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
message: format!("Format error: {}", e),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if should && let Err(e) = f(current) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
message: format!("Format error: {}", e),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
crate::database::schema::CompiledFormat::Regex(re) => {
|
||||
if let Some(s) = current.as_str() {
|
||||
if !re.is_match(s) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
message: "Format regex mismatch".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(s) = current.as_str()
|
||||
&& !re.is_match(s)
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
message: "Format regex mismatch".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -12,6 +11,7 @@ pub mod numeric;
|
||||
pub mod object;
|
||||
pub mod polymorphism;
|
||||
pub mod string;
|
||||
pub mod util;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_scoped(&self) -> Result<ValidationResult, ValidationError> {
|
||||
|
||||
@ -9,41 +9,41 @@ impl<'a> ValidationContext<'a> {
|
||||
) -> Result<bool, ValidationError> {
|
||||
let current = self.instance;
|
||||
if let Some(num) = current.as_f64() {
|
||||
if let Some(min) = self.schema.minimum {
|
||||
if num < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} < min {}", num, min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(min) = self.schema.minimum
|
||||
&& num < min
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} < min {}", num, min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.maximum {
|
||||
if num > max {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} > max {}", num, max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.maximum
|
||||
&& num > max
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} > max {}", num, max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ex_min) = self.schema.exclusive_minimum {
|
||||
if num <= ex_min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} <= ex_min {}", num, ex_min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ex_min) = self.schema.exclusive_minimum
|
||||
&& num <= ex_min
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} <= ex_min {}", num, ex_min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ex_max) = self.schema.exclusive_maximum {
|
||||
if num >= ex_max {
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} >= ex_max {}", num, ex_max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ex_max) = self.schema.exclusive_maximum
|
||||
&& num >= ex_max
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value {} >= ex_max {}", num, ex_max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(multiple_of) = self.schema.multiple_of {
|
||||
let val: f64 = num / multiple_of;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
@ -12,42 +14,44 @@ impl<'a> ValidationContext<'a> {
|
||||
let current = self.instance;
|
||||
if let Some(obj) = current.as_object() {
|
||||
// Entity Bound Implicit Type Validation
|
||||
if let Some(allowed_types) = &self.schema.obj.compiled_variations {
|
||||
if let Some(type_val) = obj.get("type") {
|
||||
if let Some(type_str) = type_val.as_str() {
|
||||
if allowed_types.contains(type_str) {
|
||||
// Ensure it passes strict mode
|
||||
result.evaluated_keys.insert("type".to_string());
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors
|
||||
message: format!(
|
||||
"Type '{}' is not a valid descendant for this entity bound schema",
|
||||
type_str
|
||||
),
|
||||
path: format!("{}/type", self.path),
|
||||
});
|
||||
}
|
||||
if let Some(lookup_key) = self.schema.id.as_ref().or(self.schema.ref_string.as_ref()) {
|
||||
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
|
||||
if let Some(type_def) = self.db.types.get(&base_type_name)
|
||||
&& let Some(type_val) = obj.get("type")
|
||||
&& let Some(type_str) = type_val.as_str()
|
||||
{
|
||||
if type_def.variations.contains(type_str) {
|
||||
// Ensure it passes strict mode
|
||||
result.evaluated_keys.insert("type".to_string());
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors
|
||||
message: format!(
|
||||
"Type '{}' is not a valid descendant for this entity bound schema",
|
||||
type_str
|
||||
),
|
||||
path: format!("{}/type", self.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(min) = self.schema.min_properties {
|
||||
if (obj.len() as f64) < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_PROPERTIES".to_string(),
|
||||
message: "Too few properties".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(min) = self.schema.min_properties
|
||||
&& (obj.len() as f64) < min
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_PROPERTIES".to_string(),
|
||||
message: "Too few properties".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_properties {
|
||||
if (obj.len() as f64) > max {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_PROPERTIES".to_string(),
|
||||
message: "Too many properties".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_properties
|
||||
&& (obj.len() as f64) > max
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_PROPERTIES".to_string(),
|
||||
message: "Too many properties".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ref req) = self.schema.required {
|
||||
for field in req {
|
||||
@ -95,29 +99,31 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
if let Some(child_instance) = obj.get(key) {
|
||||
let new_path = format!("{}/{}", self.path, key);
|
||||
let is_ref = sub_schema.ref_string.is_some() || sub_schema.obj.compiled_ref.is_some();
|
||||
let is_ref = sub_schema.ref_string.is_some();
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
sub_schema,
|
||||
child_instance,
|
||||
&new_path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
);
|
||||
let mut item_res = derived.validate()?;
|
||||
|
||||
// Entity Bound Implicit Type Interception
|
||||
if key == "type" {
|
||||
if let Some(allowed_types) = &self.schema.obj.compiled_variations {
|
||||
if let Some(instance_type) = child_instance.as_str() {
|
||||
if allowed_types.contains(instance_type) {
|
||||
item_res
|
||||
.errors
|
||||
.retain(|e| e.code != "CONST_VIOLATED" && e.code != "ENUM_VIOLATED");
|
||||
}
|
||||
}
|
||||
if key == "type"
|
||||
&& let Some(lookup_key) = sub_schema.id.as_ref().or(sub_schema.ref_string.as_ref())
|
||||
{
|
||||
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
|
||||
if let Some(type_def) = self.db.types.get(&base_type_name)
|
||||
&& let Some(instance_type) = child_instance.as_str()
|
||||
&& type_def.variations.contains(instance_type)
|
||||
{
|
||||
item_res
|
||||
.errors
|
||||
.retain(|e| e.code != "CONST_VIOLATED" && e.code != "ENUM_VIOLATED");
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,14 +138,14 @@ impl<'a> ValidationContext<'a> {
|
||||
for (key, child_instance) in obj {
|
||||
if compiled_re.0.is_match(key) {
|
||||
let new_path = format!("{}/{}", self.path, key);
|
||||
let is_ref = sub_schema.ref_string.is_some() || sub_schema.obj.compiled_ref.is_some();
|
||||
let is_ref = sub_schema.ref_string.is_some();
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
sub_schema,
|
||||
child_instance,
|
||||
&new_path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
);
|
||||
@ -154,33 +160,31 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(ref additional_schema) = self.schema.additional_properties {
|
||||
for (key, child_instance) in obj {
|
||||
let mut locally_matched = false;
|
||||
if let Some(props) = &self.schema.properties {
|
||||
if props.contains_key(&key.to_string()) {
|
||||
locally_matched = true;
|
||||
}
|
||||
if let Some(props) = &self.schema.properties
|
||||
&& props.contains_key(&key.to_string())
|
||||
{
|
||||
locally_matched = true;
|
||||
}
|
||||
if !locally_matched {
|
||||
if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties {
|
||||
for (compiled_re, _) in compiled_pp {
|
||||
if compiled_re.0.is_match(key) {
|
||||
locally_matched = true;
|
||||
break;
|
||||
}
|
||||
if !locally_matched && let Some(ref compiled_pp) = self.schema.compiled_pattern_properties
|
||||
{
|
||||
for (compiled_re, _) in compiled_pp {
|
||||
if compiled_re.0.is_match(key) {
|
||||
locally_matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !locally_matched {
|
||||
let new_path = format!("{}/{}", self.path, key);
|
||||
let is_ref = additional_schema.ref_string.is_some()
|
||||
|| additional_schema.obj.compiled_ref.is_some();
|
||||
let is_ref = additional_schema.ref_string.is_some();
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
additional_schema,
|
||||
child_instance,
|
||||
&new_path,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
);
|
||||
@ -197,11 +201,11 @@ impl<'a> ValidationContext<'a> {
|
||||
let val_str = Value::String(key.to_string());
|
||||
|
||||
let ctx = ValidationContext::new(
|
||||
self.schemas,
|
||||
self.db,
|
||||
self.root,
|
||||
property_names,
|
||||
&val_str,
|
||||
std::collections::HashSet::new(),
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
self.reporter,
|
||||
);
|
||||
|
||||
@ -15,7 +15,6 @@ impl<'a> ValidationContext<'a> {
|
||||
|| self.schema.items.is_some()
|
||||
|| self.schema.ref_string.is_some()
|
||||
|| self.schema.one_of.is_some()
|
||||
|| self.schema.any_of.is_some()
|
||||
|| self.schema.all_of.is_some()
|
||||
|| self.schema.enum_.is_some()
|
||||
|| self.schema.const_.is_some();
|
||||
@ -31,7 +30,90 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Family specific runtime validation will go here later if needed
|
||||
if let Some(family_target) = &self.schema.family {
|
||||
// The descendants map is keyed by the schema's own $id, not the target string.
|
||||
if let Some(schema_id) = &self.schema.id
|
||||
&& let Some(descendants) = self.db.descendants.get(schema_id)
|
||||
{
|
||||
// Validate against all descendants simulating strict oneOf logic
|
||||
let mut passed_candidates: Vec<(String, usize, ValidationResult)> = Vec::new();
|
||||
|
||||
// The target itself is also an implicitly valid candidate
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
all_targets.extend(descendants.clone());
|
||||
|
||||
for child_id in &all_targets {
|
||||
if let Some(child_schema) = self.db.schemas.get(child_id) {
|
||||
let derived = self.derive(
|
||||
child_schema,
|
||||
self.instance,
|
||||
&self.path,
|
||||
self.overrides.clone(),
|
||||
self.extensible,
|
||||
self.reporter, // Inherit parent reporter flag, do not bypass strictness!
|
||||
);
|
||||
|
||||
// Explicitly run validate_scoped to accurately test candidates with strictness checks enabled
|
||||
let res = derived.validate_scoped()?;
|
||||
|
||||
if res.is_valid() {
|
||||
let depth = self.db.depths.get(child_id).copied().unwrap_or(0);
|
||||
passed_candidates.push((child_id.clone(), depth, res));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().2);
|
||||
} else if passed_candidates.is_empty() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NO_FAMILY_MATCH".to_string(),
|
||||
message: format!(
|
||||
"Payload did not match any descendants of family '{}'",
|
||||
family_target
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
// Apply depth heuristic tie-breaker
|
||||
let mut best_depth: Option<usize> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut best_res = None;
|
||||
|
||||
for (_, depth, res) in passed_candidates.into_iter() {
|
||||
if let Some(current_best) = best_depth {
|
||||
if depth > current_best {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
ambiguous = false; // Broke the tie
|
||||
} else if depth == current_best {
|
||||
ambiguous = true; // Tie at the highest level
|
||||
}
|
||||
} else {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
}
|
||||
}
|
||||
|
||||
if !ambiguous {
|
||||
if let Some(res) = best_res {
|
||||
result.merge(res);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_FAMILY_MATCH".to_string(),
|
||||
message: format!(
|
||||
"Payload matched multiple descendants of family '{}' without a clear depth winner",
|
||||
family_target
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -41,7 +123,7 @@ impl<'a> ValidationContext<'a> {
|
||||
) -> Result<bool, ValidationError> {
|
||||
// 1. Core $ref logic relies on the fast O(1) map to allow cycles and proper nesting
|
||||
if let Some(ref_str) = &self.schema.ref_string {
|
||||
if let Some(global_schema) = self.schemas.get(ref_str) {
|
||||
if let Some(global_schema) = self.db.schemas.get(ref_str) {
|
||||
let mut new_overrides = self.overrides.clone();
|
||||
if let Some(props) = &self.schema.properties {
|
||||
new_overrides.extend(props.keys().map(|k| k.to_string()));
|
||||
|
||||
@ -10,23 +10,23 @@ impl<'a> ValidationContext<'a> {
|
||||
) -> Result<bool, ValidationError> {
|
||||
let current = self.instance;
|
||||
if let Some(s) = current.as_str() {
|
||||
if let Some(min) = self.schema.min_length {
|
||||
if (s.chars().count() as f64) < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("Length < min {}", min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(min) = self.schema.min_length
|
||||
&& (s.chars().count() as f64) < min
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("Length < min {}", min),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_length {
|
||||
if (s.chars().count() as f64) > max {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("Length > max {}", max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(max) = self.schema.max_length
|
||||
&& (s.chars().count() as f64) > max
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("Length > max {}", max),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(ref compiled_re) = self.schema.compiled_pattern {
|
||||
if !compiled_re.0.is_match(s) {
|
||||
@ -36,16 +36,15 @@ impl<'a> ValidationContext<'a> {
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
} else if let Some(ref pattern) = self.schema.pattern {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if !re.is_match(s) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "PATTERN_VIOLATED".to_string(),
|
||||
message: format!("Pattern mismatch {}", pattern),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if let Some(ref pattern) = self.schema.pattern
|
||||
&& let Ok(re) = Regex::new(pattern)
|
||||
&& !re.is_match(s)
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "PATTERN_VIOLATED".to_string(),
|
||||
message: format!("Pattern mismatch {}", pattern),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
|
||||
53
src/validator/rules/util.rs
Normal file
53
src/validator/rules/util.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn is_integer(v: &Value) -> bool {
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
|
||||
pub fn equals(v1: &Value, v2: &Value) -> bool {
|
||||
match (v1, v2) {
|
||||
(Value::Null, Value::Null) => true,
|
||||
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
|
||||
(Value::Number(n1), Value::Number(n2)) => {
|
||||
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
|
||||
return n1 == n2;
|
||||
}
|
||||
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
|
||||
return n1 == n2;
|
||||
}
|
||||
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
|
||||
return (n1 - n2).abs() < f64::EPSILON;
|
||||
}
|
||||
false
|
||||
}
|
||||
(Value::String(s1), Value::String(s2)) => s1 == s2,
|
||||
(Value::Array(arr1), Value::Array(arr2)) => {
|
||||
if arr1.len() != arr2.len() {
|
||||
return false;
|
||||
}
|
||||
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
|
||||
}
|
||||
(Value::Object(obj1), Value::Object(obj2)) => {
|
||||
if obj1.len() != obj2.len() {
|
||||
return false;
|
||||
}
|
||||
for (k1, v1) in obj1 {
|
||||
if let Some(v2) = obj2.get(k1) {
|
||||
if !equals(v1, v2) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user