Files
jspg/src/validator/rules/polymorphism.rs

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)
}
}