massively improves the jspg validator by removing mathmatical functions like allOf, anyOf, ref, etc to effectively use discriminators and OOP with types to determine valid pathing an nno intersections, unions, or guesswork; added cases to replace the former conditionals
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
use crate::database::schema::Schema;
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -13,9 +14,8 @@ impl<'a> ValidationContext<'a> {
|
||||
|| self.schema.required.is_some()
|
||||
|| self.schema.additional_properties.is_some()
|
||||
|| self.schema.items.is_some()
|
||||
|| self.schema.r#ref.is_some()
|
||||
|| self.schema.cases.is_some()
|
||||
|| self.schema.one_of.is_some()
|
||||
|| self.schema.all_of.is_some()
|
||||
|| self.schema.enum_.is_some()
|
||||
|| self.schema.const_.is_some();
|
||||
|
||||
@ -25,102 +25,325 @@ impl<'a> ValidationContext<'a> {
|
||||
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 {
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
// Add the target base schema itself
|
||||
if let Some(base_schema) = self.db.schemas.get(family_target) {
|
||||
candidates.push(base_schema);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// Add all descendants
|
||||
for child_id in descendants {
|
||||
if let Some(child_schema) = self.db.schemas.get(child_id) {
|
||||
candidates.push(child_schema);
|
||||
}
|
||||
}
|
||||
|
||||
// Use prefix from family string (e.g. `light.`)
|
||||
let prefix = family_target
|
||||
.rsplit_once('.')
|
||||
.map(|(p, _)| format!("{}.", p))
|
||||
.unwrap_or_default();
|
||||
|
||||
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(),
|
||||
});
|
||||
if !self.validate_polymorph(&candidates, Some(&prefix), result)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_refs(
|
||||
|
||||
pub(crate) fn validate_one_of(
|
||||
&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.r#ref {
|
||||
if let Some(global_schema) = self.db.schemas.get(ref_str) {
|
||||
if let Some(ref one_of) = self.schema.one_of {
|
||||
let mut candidates = Vec::new();
|
||||
for schema in one_of {
|
||||
candidates.push(schema.as_ref());
|
||||
}
|
||||
if !self.validate_polymorph(&candidates, None, result)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_polymorph(
|
||||
&self,
|
||||
candidates: &[&Schema],
|
||||
family_prefix: Option<&str>,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
let mut passed_candidates: Vec<(Option<String>, ValidationResult)> = Vec::new();
|
||||
let mut failed_candidates: Vec<ValidationResult> = Vec::new();
|
||||
|
||||
// 1. O(1) Fast-Path Router & Extractor
|
||||
let instance_type = self.instance.as_object().and_then(|o| o.get("type")).and_then(|t| t.as_str());
|
||||
let instance_kind = self.instance.as_object().and_then(|o| o.get("kind")).and_then(|k| k.as_str());
|
||||
|
||||
let mut viable_candidates = Vec::new();
|
||||
|
||||
for sub in candidates {
|
||||
let _child_id = sub.identifier().unwrap_or_default();
|
||||
let mut can_match = true;
|
||||
|
||||
if let Some(t) = instance_type {
|
||||
// Fast Path 1: Pure Ad-Hoc Match (schema identifier == type)
|
||||
// If it matches exactly, it's our golden candidate. Make all others non-viable manually?
|
||||
// Wait, we loop through all and filter down. If exact match is found, we should ideally break and use ONLY that.
|
||||
// Let's implement the logic safely.
|
||||
|
||||
let mut exact_match_found = false;
|
||||
|
||||
if let Some(schema_id) = &sub.id {
|
||||
// Compute Vertical Exact Target (e.g. "person" or "light.person")
|
||||
let exact_target = if let Some(prefix) = family_prefix {
|
||||
format!("{}{}", prefix, t)
|
||||
} else {
|
||||
t.to_string()
|
||||
};
|
||||
|
||||
// Fast Path 1 & 2: Vertical Exact Match
|
||||
if schema_id == &exact_target {
|
||||
if instance_kind.is_none() {
|
||||
exact_match_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fast Path 3: Horizontal Sibling Match (kind + . + type)
|
||||
if let Some(k) = instance_kind {
|
||||
let sibling_target = format!("{}.{}", k, t);
|
||||
if schema_id == &sibling_target {
|
||||
exact_match_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if exact_match_found {
|
||||
// We found an exact literal structural identity match!
|
||||
// Wipe the existing viable_candidates and only yield this guy!
|
||||
viable_candidates.clear();
|
||||
viable_candidates.push(*sub);
|
||||
break;
|
||||
}
|
||||
|
||||
// Fast Path 4: Vertical Inheritance Fallback (Physical DB constraint)
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t_ptr)) = &sub.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t_ptr) {
|
||||
if let Some(base_type) = t_ptr.split('.').last() {
|
||||
if let Some(type_def) = self.db.types.get(base_type) {
|
||||
if !type_def.variations.contains(&t.to_string()) {
|
||||
can_match = false;
|
||||
}
|
||||
} else {
|
||||
if t_ptr != t {
|
||||
can_match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast Path 5: Explicit Schema JSON `const` values check
|
||||
if can_match {
|
||||
if let Some(props) = &sub.properties {
|
||||
if let Some(type_prop) = props.get("type") {
|
||||
if let Some(const_val) = &type_prop.const_ {
|
||||
if let Some(const_str) = const_val.as_str() {
|
||||
if const_str != t {
|
||||
can_match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_match {
|
||||
viable_candidates.push(*sub);
|
||||
}
|
||||
}
|
||||
|
||||
println!("DEBUG VIABLE: {:?}", viable_candidates.iter().map(|s| s.id.clone()).collect::<Vec<_>>());
|
||||
// 2. Evaluate Viable Candidates
|
||||
// 2. Evaluate Viable Candidates
|
||||
// Composition validation is natively handled directly via type compilation.
|
||||
// The deprecated allOf JSON structure is no longer supported nor traversed.
|
||||
for sub in viable_candidates.clone() {
|
||||
let derived = self.derive_for_schema(sub, false);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
passed_candidates.push((sub.id.clone(), sub_res));
|
||||
} else {
|
||||
failed_candidates.push(sub_res);
|
||||
}
|
||||
}
|
||||
for f in &failed_candidates {
|
||||
println!(" - Failed candidate errors: {:?}", f.errors.iter().map(|e| e.code.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().1);
|
||||
} else if passed_candidates.is_empty() {
|
||||
// 3. Discriminator Pathing (Failure Analytics)
|
||||
let type_path = self.join_path("type");
|
||||
|
||||
if instance_type.is_some() {
|
||||
// Filter to candidates that didn't explicitly throw a CONST violation on `type`
|
||||
let mut genuinely_failed = Vec::new();
|
||||
for res in &failed_candidates {
|
||||
let rejected_type = res.errors.iter().any(|e| {
|
||||
(e.code == "CONST_VIOLATED" || e.code == "ENUM_VIOLATED") && e.path == type_path
|
||||
});
|
||||
if !rejected_type {
|
||||
genuinely_failed.push(res.clone());
|
||||
}
|
||||
}
|
||||
|
||||
println!("DEBUG genuinely_failed len: {}", genuinely_failed.len());
|
||||
|
||||
if genuinely_failed.len() == 1 {
|
||||
// Golden Type Match (1 candidate was structurally possible but failed property validation)
|
||||
let sub_res = genuinely_failed.pop().unwrap();
|
||||
result.errors.extend(sub_res.errors);
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Pure Ad-Hoc Union
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
|
||||
message: "Payload matches none of the required candidate sub-schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
for sub_res in &failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys.clone());
|
||||
}
|
||||
println!("DEBUG ELSE NO_FAMILY_MATCH RUNNING. Genuinely Failed len: {}", genuinely_failed.len());
|
||||
if viable_candidates.is_empty() {
|
||||
if let Some(obj) = self.instance.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
}
|
||||
}
|
||||
for sub_res in genuinely_failed {
|
||||
for e in sub_res.errors {
|
||||
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
|
||||
result.errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
// Instance missing type
|
||||
// Instance missing type
|
||||
let expects_type = viable_candidates.iter().any(|c| {
|
||||
c.compiled_property_names.get().map_or(false, |props| props.contains(&"type".to_string()))
|
||||
});
|
||||
|
||||
if expects_type {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
message: "Missing type discriminator. Unable to resolve polymorphic boundaries".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
for sub_res in failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
}
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Pure Ad-Hoc Union
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
|
||||
message: "Payload matches none of the required candidate sub-schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
if let Some(first) = failed_candidates.first() {
|
||||
let mut shared_errors = first.errors.clone();
|
||||
for sub_res in failed_candidates.iter().skip(1) {
|
||||
shared_errors.retain(|e1| {
|
||||
sub_res.errors.iter().any(|e2| e1.code == e2.code && e1.path == e2.path)
|
||||
});
|
||||
}
|
||||
for e in shared_errors {
|
||||
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
|
||||
result.errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sub_res in failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
|
||||
message: "Matches multiple polymorphic candidates inextricably".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_type_inheritance(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
// Core inheritance logic replaces legacy routing
|
||||
let payload_primitive = match self.instance {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "boolean",
|
||||
serde_json::Value::Number(n) => {
|
||||
if n.is_i64() || n.is_u64() {
|
||||
"integer"
|
||||
} else {
|
||||
"number"
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
};
|
||||
|
||||
let mut custom_types = Vec::new();
|
||||
match &self.schema.type_ {
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Multiple(arr)) => {
|
||||
if arr.contains(&payload_primitive.to_string()) || (payload_primitive == "integer" && arr.contains(&"number".to_string())) {
|
||||
// It natively matched a primitive in the array options, skip forcing custom proxy fallback
|
||||
} else {
|
||||
for t in arr {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
for t in custom_types {
|
||||
if let Some(global_schema) = self.db.schemas.get(&t) {
|
||||
let mut new_overrides = self.overrides.clone();
|
||||
if let Some(props) = &self.schema.properties {
|
||||
new_overrides.extend(props.keys().map(|k| k.to_string()));
|
||||
@ -132,16 +355,16 @@ impl<'a> ValidationContext<'a> {
|
||||
&self.path,
|
||||
new_overrides,
|
||||
self.extensible,
|
||||
true,
|
||||
true, // Reporter mode
|
||||
);
|
||||
shadow.root = global_schema;
|
||||
result.merge(shadow.validate()?);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REF_RESOLUTION_FAILED".to_string(),
|
||||
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Reference pointer to '{}' was not found in schema registry",
|
||||
ref_str
|
||||
"Inherited entity pointer '{}' was not found in schema registry",
|
||||
t
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user