jspg progress
This commit is contained in:
621
old_code/validator.rs
Normal file
621
old_code/validator.rs
Normal file
@ -0,0 +1,621 @@
|
||||
use crate::registry::REGISTRY;
|
||||
use crate::util::{equals, is_integer};
|
||||
use serde_json::{Value, json, Map};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ValidationError {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub path: String,
|
||||
pub context: Value,
|
||||
pub cause: Value,
|
||||
pub schema_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct ValidationOptions {
|
||||
pub be_strict: bool,
|
||||
}
|
||||
|
||||
pub struct Validator<'a> {
|
||||
options: ValidationOptions,
|
||||
// The top-level root schema ID we started with
|
||||
root_schema_id: String,
|
||||
// Accumulated errors
|
||||
errors: Vec<ValidationError>,
|
||||
// Max depth to prevent stack overflow
|
||||
max_depth: usize,
|
||||
_phantom: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
/// Context passed down through the recursion
|
||||
#[derive(Clone)]
|
||||
struct ValidationContext {
|
||||
// Current JSON pointer path in the instance (e.g. "/users/0/name")
|
||||
current_path: String,
|
||||
// The properties overridden by parent schemas (for JSPG inheritance)
|
||||
overrides: HashSet<String>,
|
||||
// Current resolution scope for $ref (changes when following refs)
|
||||
resolution_scope: String,
|
||||
// Current recursion depth
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
impl ValidationContext {
|
||||
fn append_path(&self, extra: &str) -> ValidationContext {
|
||||
let mut new_ctx = self.clone();
|
||||
if new_ctx.current_path.ends_with('/') {
|
||||
new_ctx.current_path.push_str(extra);
|
||||
} else if new_ctx.current_path.is_empty() {
|
||||
new_ctx.current_path.push('/');
|
||||
new_ctx.current_path.push_str(extra);
|
||||
} else {
|
||||
new_ctx.current_path.push('/');
|
||||
new_ctx.current_path.push_str(extra);
|
||||
}
|
||||
new_ctx
|
||||
}
|
||||
|
||||
fn append_path_new_scope(&self, extra: &str) -> ValidationContext {
|
||||
let mut new_ctx = self.append_path(extra);
|
||||
// Structural recursion clears overrides
|
||||
new_ctx.overrides.clear();
|
||||
new_ctx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Validator<'a> {
|
||||
pub fn new(options: ValidationOptions, root_schema_id: &str) -> Self {
|
||||
Self {
|
||||
options,
|
||||
root_schema_id: root_schema_id.to_string(),
|
||||
errors: Vec::new(),
|
||||
max_depth: 100,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(&mut self, schema: &Value, instance: &Value) -> Result<(), Vec<ValidationError>> {
|
||||
let ctx = ValidationContext {
|
||||
current_path: String::new(),
|
||||
overrides: HashSet::new(),
|
||||
resolution_scope: self.root_schema_id.clone(),
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
// We treat the top-level validate as "not lax" by default, unless specific schema logic says otherwise.
|
||||
let is_lax = !self.options.be_strict;
|
||||
|
||||
self.validate_node(schema, instance, ctx, is_lax, false, false);
|
||||
|
||||
if self.errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(self.errors.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_node(
|
||||
&mut self,
|
||||
schema: &Value,
|
||||
instance: &Value,
|
||||
mut ctx: ValidationContext,
|
||||
is_lax: bool,
|
||||
skip_strict: bool,
|
||||
skip_id: bool,
|
||||
) -> HashSet<String> {
|
||||
let mut evaluated = HashSet::new();
|
||||
|
||||
// Recursion limit
|
||||
if ctx.depth > self.max_depth {
|
||||
self.add_error("MAX_DEPTH_REACHED", "Maximum recursion depth exceeded".to_string(), instance, json!({ "depth": ctx.depth }), &ctx);
|
||||
return evaluated;
|
||||
}
|
||||
|
||||
ctx.depth += 1;
|
||||
|
||||
// Handle Boolean Schemas
|
||||
if let Value::Bool(b) = schema {
|
||||
if !b {
|
||||
self.add_error("FALSE_SCHEMA", "Schema is always false".to_string(), instance, Value::Null, &ctx);
|
||||
}
|
||||
return evaluated;
|
||||
}
|
||||
|
||||
let schema_obj = match schema.as_object() {
|
||||
Some(o) => o,
|
||||
None => return evaluated, // Should be object or bool
|
||||
};
|
||||
|
||||
// 1. Update Resolution Scope ($id)
|
||||
if !skip_id {
|
||||
if let Some(Value::String(id)) = schema_obj.get("$id") {
|
||||
if id.contains("://") {
|
||||
ctx.resolution_scope = id.clone();
|
||||
} else {
|
||||
if let Some(pos) = ctx.resolution_scope.rfind('/') {
|
||||
let base = &ctx.resolution_scope[..pos + 1];
|
||||
ctx.resolution_scope = format!("{}{}", base, id);
|
||||
} else {
|
||||
ctx.resolution_scope = id.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Identify Overrides (JSPG Custom Logic)
|
||||
let mut inheritance_ctx = ctx.clone();
|
||||
if let Some(Value::Object(props)) = schema_obj.get("properties") {
|
||||
for (pname, pval) in props {
|
||||
if let Some(Value::Bool(true)) = pval.get("override") {
|
||||
inheritance_ctx.overrides.insert(pname.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Determine Laxness
|
||||
let mut current_lax = is_lax;
|
||||
if let Some(Value::Bool(true)) = schema_obj.get("unevaluatedProperties") { current_lax = true; }
|
||||
if let Some(Value::Bool(true)) = schema_obj.get("additionalProperties") { current_lax = true; }
|
||||
|
||||
// ======== VALIDATION KEYWORDS ========
|
||||
|
||||
// Type
|
||||
if let Some(type_val) = schema_obj.get("type") {
|
||||
if !self.check_type(type_val, instance) {
|
||||
let got = value_type_name(instance);
|
||||
let want_json = serde_json::to_value(type_val).unwrap_or(json!("unknown"));
|
||||
self.add_error("TYPE_MISMATCH", format!("Expected type {:?} but got {}", type_val, got), instance, json!({ "want": type_val, "got": got }), &ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Enum
|
||||
if let Some(Value::Array(vals)) = schema_obj.get("enum") {
|
||||
if !vals.iter().any(|v| equals(v, instance)) {
|
||||
self.add_error("ENUM_VIOLATED", "Value not in enum".to_string(), instance, json!({ "want": vals }), &ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Const
|
||||
if let Some(c) = schema_obj.get("const") {
|
||||
if !equals(c, instance) {
|
||||
self.add_error("CONST_VIOLATED", "Value does not match constant".to_string(), instance, json!({ "want": c }), &ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Object Validation
|
||||
if let Value::Object(obj) = instance {
|
||||
let obj_eval = self.validate_object(schema_obj, obj, instance, &ctx, current_lax);
|
||||
evaluated.extend(obj_eval);
|
||||
}
|
||||
|
||||
// Array Validation
|
||||
if let Value::Array(arr) = instance {
|
||||
self.validate_array(schema_obj, arr, &ctx, current_lax);
|
||||
}
|
||||
|
||||
// Primitive Validation
|
||||
self.validate_primitives(schema_obj, instance, &ctx);
|
||||
|
||||
// Combinators
|
||||
evaluated.extend(self.validate_combinators(schema_obj, instance, &inheritance_ctx, current_lax));
|
||||
|
||||
// Conditionals
|
||||
evaluated.extend(self.validate_conditionals(schema_obj, instance, &inheritance_ctx, current_lax));
|
||||
|
||||
// $ref
|
||||
if let Some(Value::String(ref_str)) = schema_obj.get("$ref") {
|
||||
if let Some((ref_schema, scope_uri)) = REGISTRY.resolve(ref_str, Some(&inheritance_ctx.resolution_scope)) {
|
||||
let mut new_ctx = inheritance_ctx.clone();
|
||||
new_ctx.resolution_scope = scope_uri;
|
||||
let ref_evaluated = self.validate_node(&ref_schema, instance, new_ctx, is_lax, true, true);
|
||||
evaluated.extend(ref_evaluated);
|
||||
} else {
|
||||
self.add_error("SCHEMA_NOT_FOUND", format!("Ref '{}' not found", ref_str), instance, json!({ "ref": ref_str }), &ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Unevaluated / Strictness Check
|
||||
self.check_unevaluated(schema_obj, instance, &evaluated, &ctx, current_lax, skip_strict);
|
||||
|
||||
evaluated
|
||||
}
|
||||
|
||||
fn validate_object(
|
||||
&mut self,
|
||||
schema: &Map<String, Value>,
|
||||
obj: &Map<String, Value>,
|
||||
instance: &Value,
|
||||
ctx: &ValidationContext,
|
||||
is_lax: bool,
|
||||
) -> HashSet<String> {
|
||||
let mut evaluated = HashSet::new();
|
||||
|
||||
// required
|
||||
if let Some(Value::Array(req)) = schema.get("required") {
|
||||
for field_val in req {
|
||||
if let Some(field) = field_val.as_str() {
|
||||
if !obj.contains_key(field) {
|
||||
self.add_error("REQUIRED_FIELD_MISSING", format!("Required field '{}' is missing", field), &Value::Null, json!({ "want": [field] }), &ctx.append_path(field));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// properties
|
||||
if let Some(Value::Object(props)) = schema.get("properties") {
|
||||
for (pname, psch) in props {
|
||||
if obj.contains_key(pname) {
|
||||
if ctx.overrides.contains(pname) {
|
||||
evaluated.insert(pname.clone());
|
||||
continue;
|
||||
}
|
||||
evaluated.insert(pname.clone());
|
||||
let sub_ctx = ctx.append_path_new_scope(pname);
|
||||
self.validate_node(psch, &obj[pname], sub_ctx, is_lax, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// patternProperties
|
||||
if let Some(Value::Object(pprops)) = schema.get("patternProperties") {
|
||||
for (pattern, psch) in pprops {
|
||||
if let Ok(re) = regex::Regex::new(pattern) {
|
||||
for (pname, pval) in obj {
|
||||
if re.is_match(pname) {
|
||||
if ctx.overrides.contains(pname) {
|
||||
evaluated.insert(pname.clone());
|
||||
continue;
|
||||
}
|
||||
evaluated.insert(pname.clone());
|
||||
let sub_ctx = ctx.append_path_new_scope(pname);
|
||||
self.validate_node(psch, pval, sub_ctx, is_lax, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// additionalProperties
|
||||
if let Some(apsch) = schema.get("additionalProperties") {
|
||||
if apsch.is_object() || apsch.is_boolean() {
|
||||
for (key, val) in obj {
|
||||
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
|
||||
let in_patterns = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| {
|
||||
pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false))
|
||||
});
|
||||
|
||||
if !in_props && !in_patterns {
|
||||
evaluated.insert(key.clone());
|
||||
let sub_ctx = ctx.append_path_new_scope(key);
|
||||
self.validate_node(apsch, val, sub_ctx, is_lax, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dependentRequired
|
||||
if let Some(Value::Object(dep_req)) = schema.get("dependentRequired") {
|
||||
for (prop, required_fields_val) in dep_req {
|
||||
if obj.contains_key(prop) {
|
||||
if let Value::Array(required_fields) = required_fields_val {
|
||||
for req_field_val in required_fields {
|
||||
if let Some(req_field) = req_field_val.as_str() {
|
||||
if !obj.contains_key(req_field) {
|
||||
self.add_error("DEPENDENCY_FAILED", format!("Field '{}' is required when '{}' is present", req_field, prop), &Value::Null, json!({ "prop": prop, "missing": [req_field] }), &ctx.append_path(req_field));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dependentSchemas
|
||||
if let Some(Value::Object(dep_sch)) = schema.get("dependentSchemas") {
|
||||
for (prop, psch) in dep_sch {
|
||||
if obj.contains_key(prop) {
|
||||
let sub_evaluated = self.validate_node(psch, instance, ctx.clone(), is_lax, false, false);
|
||||
evaluated.extend(sub_evaluated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// legacy dependencies (Draft 4-7 compat)
|
||||
if let Some(Value::Object(deps)) = schema.get("dependencies") {
|
||||
for (prop, dep_val) in deps {
|
||||
if obj.contains_key(prop) {
|
||||
match dep_val {
|
||||
Value::Array(arr) => {
|
||||
for req_val in arr {
|
||||
if let Some(req_field) = req_val.as_str() {
|
||||
if !obj.contains_key(req_field) {
|
||||
self.add_error(
|
||||
"DEPENDENCY_FAILED",
|
||||
format!("Field '{}' is required when '{}' is present", req_field, prop),
|
||||
&Value::Null,
|
||||
json!({ "prop": prop, "missing": [req_field] }),
|
||||
&ctx.append_path(req_field),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Object(_) => {
|
||||
// Schema dependency
|
||||
let sub_evaluated = self.validate_node(dep_val, instance, ctx.clone(), is_lax, false, false);
|
||||
evaluated.extend(sub_evaluated);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// minProperties / maxProperties
|
||||
if let Some(min) = schema.get("minProperties").and_then(|v| v.as_u64()) {
|
||||
if (obj.len() as u64) < min {
|
||||
self.add_error("MIN_PROPERTIES_VIOLATED", format!("Object must have at least {} properties", min), &json!(obj.len()), json!({ "want": min, "got": obj.len() }), ctx);
|
||||
}
|
||||
}
|
||||
if let Some(max) = schema.get("maxProperties").and_then(|v| v.as_u64()) {
|
||||
if (obj.len() as u64) > max {
|
||||
self.add_error("MAX_PROPERTIES_VIOLATED", format!("Object must have at most {} properties", max), &json!(obj.len()), json!({ "want": max, "got": obj.len() }), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
evaluated
|
||||
}
|
||||
|
||||
fn validate_array(
|
||||
&mut self,
|
||||
schema: &Map<String, Value>,
|
||||
arr: &Vec<Value>,
|
||||
ctx: &ValidationContext,
|
||||
is_lax: bool,
|
||||
) {
|
||||
if let Some(min) = schema.get("minItems").and_then(|v| v.as_u64()) {
|
||||
if (arr.len() as u64) < min {
|
||||
self.add_error("MIN_ITEMS_VIOLATED", format!("Array must have at least {} items", min), &json!(arr.len()), json!({ "want": min, "got": arr.len() }), ctx);
|
||||
}
|
||||
}
|
||||
if let Some(max) = schema.get("maxItems").and_then(|v| v.as_u64()) {
|
||||
if (arr.len() as u64) > max {
|
||||
self.add_error("MAX_ITEMS_VIOLATED", format!("Array must have at most {} items", max), &json!(arr.len()), json!({ "want": max, "got": arr.len() }), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
let mut evaluated_index = 0;
|
||||
if let Some(Value::Array(prefix)) = schema.get("prefixItems") {
|
||||
for (i, psch) in prefix.iter().enumerate() {
|
||||
if let Some(item) = arr.get(i) {
|
||||
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
|
||||
self.validate_node(psch, item, sub_ctx, is_lax, false, false);
|
||||
evaluated_index = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(items_val) = schema.get("items") {
|
||||
if let Value::Bool(false) = items_val {
|
||||
if arr.len() > evaluated_index {
|
||||
self.add_error("ADDITIONAL_ITEMS_NOT_ALLOWED", "Extra items not allowed".to_string(), &json!(arr.len()), json!({ "got": arr.len() - evaluated_index }), ctx);
|
||||
}
|
||||
} else {
|
||||
// Schema or true
|
||||
for i in evaluated_index..arr.len() {
|
||||
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
|
||||
self.validate_node(items_val, &arr[i], sub_ctx, is_lax, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(contains_sch) = schema.get("contains") {
|
||||
let mut matches = 0;
|
||||
for (i, item) in arr.iter().enumerate() {
|
||||
let mut sub = self.branch();
|
||||
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
|
||||
sub.validate_node(contains_sch, item, sub_ctx, is_lax, false, false);
|
||||
if sub.errors.is_empty() {
|
||||
matches += 1;
|
||||
}
|
||||
}
|
||||
if matches == 0 {
|
||||
self.add_error("CONTAINS_FAILED", "No items match 'contains' schema".to_string(), &json!(arr), json!({}), ctx);
|
||||
}
|
||||
if let Some(min) = schema.get("minContains").and_then(|v| v.as_u64()) {
|
||||
if (matches as u64) < min {
|
||||
self.add_error("MIN_CONTAINS_VIOLATED", format!("Expected at least {} items to match 'contains'", min), &json!(arr), json!({ "want": min, "got": matches }), ctx);
|
||||
}
|
||||
}
|
||||
if let Some(max) = schema.get("maxContains").and_then(|v| v.as_u64()) {
|
||||
if (matches as u64) > max {
|
||||
self.add_error("MAX_CONTAINS_VIOLATED", format!("Expected at most {} items to match 'contains'", max), &json!(arr), json!({ "want": max, "got": matches }), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// uniqueItems
|
||||
if let Some(Value::Bool(true)) = schema.get("uniqueItems") {
|
||||
for i in 0..arr.len() {
|
||||
for j in (i + 1)..arr.len() {
|
||||
if equals(&arr[i], &arr[j]) {
|
||||
self.add_error("UNIQUE_ITEMS_VIOLATED", format!("Array items at indices {} and {} are equal", i, j), &json!(arr), json!({ "got": [i, j] }), ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_primitives(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext) {
|
||||
if let Some(s) = instance.as_str() {
|
||||
if let Some(min) = schema.get("minLength").and_then(|v| v.as_u64()) {
|
||||
if (s.chars().count() as u64) < min { self.add_error("MIN_LENGTH_VIOLATED", format!("String too short (min {})", min), instance, json!({ "want": min, "got": s.len() }), ctx); }
|
||||
}
|
||||
if let Some(max) = schema.get("maxLength").and_then(|v| v.as_u64()) {
|
||||
if (s.chars().count() as u64) > max { self.add_error("MAX_LENGTH_VIOLATED", format!("String too long (max {})", max), instance, json!({ "want": max, "got": s.len() }), ctx); }
|
||||
}
|
||||
if let Some(Value::String(pat)) = schema.get("pattern") {
|
||||
if let Ok(re) = regex::Regex::new(pat) {
|
||||
if !re.is_match(s) { self.add_error("PATTERN_VIOLATED", format!("String does not match pattern '{}'", pat), instance, json!({ "want": pat, "got": s }), ctx); }
|
||||
}
|
||||
}
|
||||
if let Some(Value::String(fmt)) = schema.get("format") {
|
||||
if !s.is_empty() {
|
||||
match fmt.as_str() {
|
||||
"uuid" => { if uuid::Uuid::parse_str(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid UUID", s), instance, json!({ "format": "uuid" }), ctx); } }
|
||||
"date-time" => { if chrono::DateTime::parse_from_rfc3339(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid date-time", s), instance, json!({ "format": "date-time" }), ctx); } }
|
||||
"email" => { if !s.contains('@') { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid email", s), instance, json!({ "format": "email" }), ctx); } }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(n) = instance.as_f64() {
|
||||
if let Some(min) = schema.get("minimum").and_then(|v| v.as_f64()) {
|
||||
if n < min { self.add_error("MINIMUM_VIOLATED", format!("Value {} < minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
|
||||
}
|
||||
if let Some(max) = schema.get("maximum").and_then(|v| v.as_f64()) {
|
||||
if n > max { self.add_error("MAXIMUM_VIOLATED", format!("Value {} > maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
|
||||
}
|
||||
if let Some(min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
|
||||
if n <= min { self.add_error("EXCLUSIVE_MINIMUM_VIOLATED", format!("Value {} <= exclusive minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
|
||||
}
|
||||
if let Some(max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
|
||||
if n >= max { self.add_error("EXCLUSIVE_MAXIMUM_VIOLATED", format!("Value {} >= exclusive maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
|
||||
}
|
||||
if let Some(mult) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
|
||||
let rem = (n / mult).fract();
|
||||
if rem.abs() > f64::EPSILON && (1.0 - rem).abs() > f64::EPSILON {
|
||||
self.add_error("MULTIPLE_OF_VIOLATED", format!("Value {} not multiple of {}", n, mult), instance, json!({ "want": mult, "got": n }), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_combinators(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
|
||||
let mut evaluated = HashSet::new();
|
||||
if let Some(Value::Array(all_of)) = schema.get("allOf") {
|
||||
for sch in all_of { evaluated.extend(self.validate_node(sch, instance, ctx.clone(), is_lax, true, false)); }
|
||||
}
|
||||
if let Some(Value::Array(any_of)) = schema.get("anyOf") {
|
||||
let mut matched = false;
|
||||
let mut errors_acc = Vec::new();
|
||||
for sch in any_of {
|
||||
let mut sub = self.branch();
|
||||
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
|
||||
if sub.errors.is_empty() { matched = true; evaluated.extend(sub_eval); } else { errors_acc.extend(sub.errors); }
|
||||
}
|
||||
if !matched { self.add_error("ANY_OF_VIOLATED", "Value did not match any allowed schema".to_string(), instance, json!({ "causes": errors_acc }), ctx); }
|
||||
}
|
||||
if let Some(Value::Array(one_of)) = schema.get("oneOf") {
|
||||
let mut match_count = 0;
|
||||
let mut last_eval = HashSet::new();
|
||||
let mut error_causes = Vec::new();
|
||||
for sch in one_of {
|
||||
let mut sub = self.branch();
|
||||
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
|
||||
if sub.errors.is_empty() { match_count += 1; last_eval = sub_eval; } else { error_causes.extend(sub.errors); }
|
||||
}
|
||||
if match_count == 1 { evaluated.extend(last_eval); }
|
||||
else { self.add_error("ONE_OF_VIOLATED", format!("Value matched {} schemas, expected 1", match_count), instance, json!({ "matched": match_count, "causes": error_causes }), ctx); }
|
||||
}
|
||||
if let Some(not_sch) = schema.get("not") {
|
||||
let mut sub = self.branch();
|
||||
sub.validate_node(not_sch, instance, ctx.clone(), is_lax, false, false);
|
||||
if sub.errors.is_empty() { self.add_error("NOT_VIOLATED", "Value matched 'not' schema".to_string(), instance, Value::Null, ctx); }
|
||||
}
|
||||
evaluated
|
||||
}
|
||||
|
||||
fn validate_conditionals(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
|
||||
let mut evaluated = HashSet::new();
|
||||
if let Some(if_sch) = schema.get("if") {
|
||||
let mut sub = self.branch();
|
||||
let sub_eval = sub.validate_node(if_sch, instance, ctx.clone(), is_lax, true, false);
|
||||
if sub.errors.is_empty() {
|
||||
evaluated.extend(sub_eval);
|
||||
if let Some(then_sch) = schema.get("then") { evaluated.extend(self.validate_node(then_sch, instance, ctx.clone(), is_lax, false, false)); }
|
||||
} else if let Some(else_sch) = schema.get("else") {
|
||||
evaluated.extend(self.validate_node(else_sch, instance, ctx.clone(), is_lax, false, false));
|
||||
}
|
||||
}
|
||||
evaluated
|
||||
}
|
||||
|
||||
fn check_unevaluated(&mut self, schema: &Map<String, Value>, instance: &Value, evaluated: &HashSet<String>, ctx: &ValidationContext, is_lax: bool, skip_strict: bool) {
|
||||
if let Value::Object(obj) = instance {
|
||||
if let Some(Value::Bool(false)) = schema.get("additionalProperties") {
|
||||
for key in obj.keys() {
|
||||
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
|
||||
let in_pattern = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false)));
|
||||
if !in_props && !in_pattern {
|
||||
if ctx.overrides.contains(key) { continue; }
|
||||
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let explicit_opts = schema.contains_key("unevaluatedProperties") || schema.contains_key("additionalProperties");
|
||||
let should_check_strict = self.options.be_strict && !is_lax && !explicit_opts && !skip_strict;
|
||||
let check_unevaluated = matches!(schema.get("unevaluatedProperties"), Some(Value::Bool(false)));
|
||||
if should_check_strict || check_unevaluated {
|
||||
for key in obj.keys() {
|
||||
if !evaluated.contains(key) {
|
||||
if ctx.overrides.contains(key) { continue; }
|
||||
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed (strict/unevaluated)", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_type(&self, expected: &Value, instance: &Value) -> bool {
|
||||
match expected {
|
||||
Value::String(s) => self.is_primitive_type(s, instance),
|
||||
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).any(|pt| self.is_primitive_type(pt, instance)),
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_primitive_type(&self, pt: &str, instance: &Value) -> bool {
|
||||
match pt {
|
||||
"string" => instance.is_string(),
|
||||
"number" => instance.is_number(),
|
||||
"integer" => is_integer(instance),
|
||||
"boolean" => instance.is_boolean(),
|
||||
"array" => instance.is_array(),
|
||||
"object" => instance.is_object(),
|
||||
"null" => instance.is_null(),
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn branch(&self) -> Self {
|
||||
Self { options: self.options, root_schema_id: self.root_schema_id.clone(), errors: Vec::new(), max_depth: self.max_depth, _phantom: std::marker::PhantomData }
|
||||
}
|
||||
|
||||
fn add_error(&mut self, code: &str, message: String, context: &Value, cause: Value, ctx: &ValidationContext) {
|
||||
let path = ctx.current_path.clone();
|
||||
if self.errors.iter().any(|e| e.code == code && e.path == path) { return; }
|
||||
self.errors.push(ValidationError { code: code.to_string(), message, path, context: context.clone(), cause, schema_id: self.root_schema_id.clone() });
|
||||
}
|
||||
|
||||
fn extend_unique(&mut self, errors: Vec<ValidationError>) {
|
||||
for e in errors { if !self.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) { self.errors.push(e); } }
|
||||
}
|
||||
}
|
||||
|
||||
fn value_type_name(v: &Value) -> &'static str {
|
||||
match v {
|
||||
Value::Null => "null",
|
||||
Value::Bool(_) => "boolean",
|
||||
Value::Number(n) => if n.is_i64() { "integer" } else { "number" },
|
||||
Value::String(_) => "string",
|
||||
Value::Array(_) => "array",
|
||||
Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user