156 lines
5.2 KiB
Rust
156 lines
5.2 KiB
Rust
use crate::validator::context::ValidationContext;
|
|
use crate::validator::error::ValidationError;
|
|
use crate::validator::result::ValidationResult;
|
|
|
|
impl<'a> ValidationContext<'a> {
|
|
pub(crate) fn validate_family(
|
|
&self,
|
|
result: &mut ValidationResult,
|
|
) -> Result<bool, ValidationError> {
|
|
if self.schema.family.is_some() {
|
|
let conflicts = self.schema.type_.is_some()
|
|
|| self.schema.properties.is_some()
|
|
|| self.schema.required.is_some()
|
|
|| self.schema.additional_properties.is_some()
|
|
|| self.schema.items.is_some()
|
|
|| self.schema.ref_string.is_some()
|
|
|| self.schema.one_of.is_some()
|
|
|| self.schema.all_of.is_some()
|
|
|| self.schema.enum_.is_some()
|
|
|| self.schema.const_.is_some();
|
|
|
|
if conflicts {
|
|
result.errors.push(ValidationError {
|
|
code: "INVALID_SCHEMA".to_string(),
|
|
message: "$family must be used exclusively without other constraints".to_string(),
|
|
path: self.path.to_string(),
|
|
});
|
|
// Short-circuit: the schema formulation is broken
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
pub(crate) fn validate_refs(
|
|
&self,
|
|
result: &mut ValidationResult,
|
|
) -> 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.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()));
|
|
}
|
|
|
|
let mut shadow = self.derive(
|
|
global_schema,
|
|
self.instance,
|
|
&self.path,
|
|
new_overrides,
|
|
self.extensible,
|
|
true,
|
|
);
|
|
shadow.root = global_schema;
|
|
result.merge(shadow.validate()?);
|
|
} else {
|
|
result.errors.push(ValidationError {
|
|
code: "REF_RESOLUTION_FAILED".to_string(),
|
|
message: format!(
|
|
"Reference pointer to '{}' was not found in schema registry",
|
|
ref_str
|
|
),
|
|
path: self.path.to_string(),
|
|
});
|
|
}
|
|
}
|
|
Ok(true)
|
|
}
|
|
}
|