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:
2026-04-08 13:08:24 -04:00
parent e4286ac6a9
commit 7c8df22709
30 changed files with 2526 additions and 4816 deletions

View File

@ -242,12 +242,14 @@ impl Database {
if !visited.insert(current_id.clone()) {
break; // Cycle detected
}
if let Some(ref_str) = &schema.obj.r#ref {
current_id = ref_str.clone();
depth += 1;
} else {
break;
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
current_id = t.clone();
depth += 1;
continue;
}
}
break;
}
depths.insert(id, depth);
}
@ -257,11 +259,13 @@ impl Database {
fn collect_descendants(&mut self) {
let mut direct_refs: HashMap<String, Vec<String>> = HashMap::new();
for (id, schema) in &self.schemas {
if let Some(ref_str) = &schema.obj.r#ref {
direct_refs
.entry(ref_str.clone())
.or_default()
.push(id.clone());
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
direct_refs
.entry(t.clone())
.or_default()
.push(id.clone());
}
}
}

View File

@ -33,15 +33,30 @@ where
Ok(Some(v))
}
pub fn is_primitive_type(t: &str) -> bool {
matches!(
t,
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub when: Option<Arc<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub then: Option<Arc<Schema>>,
#[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub else_: Option<Arc<Schema>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none")]
pub r#ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -150,24 +165,14 @@ pub struct SchemaObject {
pub exclusive_maximum: Option<f64>,
// Combining Keywords
#[serde(rename = "allOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Arc<Schema>>>,
pub cases: Option<Vec<Case>>,
#[serde(rename = "oneOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")]
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Arc<Schema>>,
#[serde(rename = "if")]
#[serde(skip_serializing_if = "Option::is_none")]
pub if_: Option<Arc<Schema>>,
#[serde(rename = "then")]
#[serde(skip_serializing_if = "Option::is_none")]
pub then_: Option<Arc<Schema>>,
#[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub else_: Option<Arc<Schema>>,
// Custom Vocabularies
#[serde(skip_serializing_if = "Option::is_none")]
@ -300,35 +305,45 @@ impl Schema {
let mut props = std::collections::BTreeMap::new();
// 1. Resolve INHERITANCE dependencies first
if let Some(ref_id) = &self.obj.r#ref {
if let Some(parent) = db.schemas.get(ref_id) {
parent.compile(db, visited, errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.compile(db, visited, errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
}
}
}
}
if let Some(all_of) = &self.obj.all_of {
for ao in all_of {
ao.compile(db, visited, errors);
if let Some(ao_props) = ao.obj.compiled_properties.get() {
props.extend(ao_props.clone());
if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
let mut custom_type_count = 0;
for t in types {
if !crate::database::schema::is_primitive_type(t) {
custom_type_count += 1;
}
}
}
if let Some(then_schema) = &self.obj.then_ {
then_schema.compile(db, visited, errors);
if let Some(t_props) = then_schema.obj.compiled_properties.get() {
props.extend(t_props.clone());
if custom_type_count > 1 {
errors.push(crate::drop::Error {
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
message: format!(
"Schema '{}' attempts to extend multiple custom object pointers in its type array. Use 'oneOf' for polymorphism and tagged unions.",
self.obj.identifier().unwrap_or("unknown".to_string())
),
details: crate::drop::ErrorDetails {
path: self.obj.identifier().unwrap_or("unknown".to_string()),
..Default::default()
}
});
}
}
if let Some(else_schema) = &self.obj.else_ {
else_schema.compile(db, visited, errors);
if let Some(e_props) = else_schema.obj.compiled_properties.get() {
props.extend(e_props.clone());
for t in types {
if !crate::database::schema::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.compile(db, visited, errors);
}
}
}
}
@ -382,11 +397,18 @@ impl Schema {
if let Some(child) = &self.obj.contains {
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.property_names {
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.if_ {
child.compile(db, visited, errors);
if let Some(cases) = &self.obj.cases {
for c in cases {
if let Some(child) = &c.when {
child.compile(db, visited, errors);
}
if let Some(child) = &c.then {
child.compile(db, visited, errors);
}
if let Some(child) = &c.else_ {
child.compile(db, visited, errors);
}
}
}
if let Some(id) = &self.obj.id {
@ -422,8 +444,10 @@ impl Schema {
Self::validate_identifier(id, "$id", errors);
to_insert.push((id.clone(), self.clone()));
}
if let Some(r#ref) = &self.obj.r#ref {
Self::validate_identifier(r#ref, "$ref", errors);
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
Self::validate_identifier(t, "type", errors);
}
}
if let Some(family) = &self.obj.family {
Self::validate_identifier(family, "$family", errors);
@ -431,9 +455,13 @@ impl Schema {
// Is this schema an inline ad-hoc composition?
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
if self.obj.id.is_none() && self.obj.r#ref.is_some() && self.obj.properties.is_some() {
if let Some(ref path) = tracking_path {
to_insert.push((path.clone(), self.clone()));
if self.obj.id.is_none() && self.obj.properties.is_some() {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(ref path) = tracking_path {
to_insert.push((path.clone(), self.clone()));
}
}
}
}
@ -478,9 +506,7 @@ impl Schema {
if let Some(arr) = &mut self.obj.prefix_items {
map_arr(arr);
}
if let Some(arr) = &mut self.obj.all_of {
map_arr(arr);
}
if let Some(arr) = &mut self.obj.one_of {
map_arr(arr);
}
@ -503,9 +529,25 @@ impl Schema {
map_opt(&mut self.obj.not, false);
map_opt(&mut self.obj.contains, false);
map_opt(&mut self.obj.property_names, false);
map_opt(&mut self.obj.if_, false);
map_opt(&mut self.obj.then_, false);
map_opt(&mut self.obj.else_, false);
if let Some(cases) = &mut self.obj.cases {
for c in cases.iter_mut() {
if let Some(when) = &mut c.when {
let mut inner = (**when).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*when = Arc::new(inner);
}
if let Some(then) = &mut c.then {
let mut inner = (**then).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*then = Arc::new(inner);
}
if let Some(else_) = &mut c.else_ {
let mut inner = (**else_).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*else_ = Arc::new(inner);
}
}
}
}
/// Dynamically infers and compiles all structural database relationships between this Schema
@ -823,13 +865,9 @@ impl<'de> Deserialize<'de> for Schema {
&& obj.format.is_none()
&& obj.enum_.is_none()
&& obj.const_.is_none()
&& obj.all_of.is_none()
&& obj.cases.is_none()
&& obj.one_of.is_none()
&& obj.not.is_none()
&& obj.if_.is_none()
&& obj.then_.is_none()
&& obj.else_.is_none()
&& obj.r#ref.is_none()
&& obj.family.is_none();
if is_empty && obj.extensible.is_none() {
@ -845,11 +883,15 @@ impl<'de> Deserialize<'de> for Schema {
impl SchemaObject {
pub fn identifier(&self) -> Option<String> {
if let Some(lookup_key) = self.id.as_ref().or(self.r#ref.as_ref()) {
Some(lookup_key.split('.').next_back().unwrap_or("").to_string())
} else {
None
if let Some(id) = &self.id {
return Some(id.split('.').next_back().unwrap_or("").to_string());
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
return Some(t.split('.').next_back().unwrap_or("").to_string());
}
}
None
}
}

View File

@ -47,7 +47,7 @@ impl<'a> Compiler<'a> {
};
let (sql, _) = compiler.compile_node(node)?;
Ok(sql)
Ok(format!("(SELECT jsonb_strip_nulls({}))", sql))
}
/// Recursively walks the schema AST emitting native PostgreSQL jsonb mapping
@ -63,7 +63,7 @@ impl<'a> Compiler<'a> {
}
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// 1. Array of DB Entities (`$ref` or `$family` pointing to a table limit)
// 1. Array of DB Entities (`type` or `$family` pointing to a table limit)
if let Some(items) = &node.schema.obj.items {
let mut resolved_type = None;
if let Some(family_target) = items.obj.family.as_ref() {
@ -112,15 +112,17 @@ impl<'a> Compiler<'a> {
return self.compile_entity(type_def, node.clone(), false);
}
// Handle Direct Refs
if let Some(ref_id) = &node.schema.obj.r#ref {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(ref_id) {
let mut ref_node = node.clone();
ref_node.schema = std::sync::Arc::new(target_schema.clone());
return self.compile_node(ref_node);
// Handle Direct Refs via type pointer
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(t) {
let mut ref_node = node.clone();
ref_node.schema = std::sync::Arc::new(target_schema.clone());
return self.compile_node(ref_node);
}
return Err(format!("Unresolved schema type pointer: {}", t));
}
return Err(format!("Unresolved $ref: {}", ref_id));
}
// Handle $family Polymorphism fallbacks for relations
if let Some(family_target) = &node.schema.obj.family {
@ -131,7 +133,7 @@ impl<'a> Compiler<'a> {
if all_targets.len() == 1 {
let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.r#ref = Some(all_targets[0].clone());
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(all_targets[0].clone()));
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(bypass_schema);
return self.compile_node(bypass_node);
@ -141,7 +143,7 @@ impl<'a> Compiler<'a> {
let mut family_schemas = Vec::new();
for variation in &all_targets {
let mut ref_schema = crate::database::schema::Schema::default();
ref_schema.obj.r#ref = Some(variation.clone());
ref_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(variation.clone()));
family_schemas.push(std::sync::Arc::new(ref_schema));
}
@ -185,9 +187,9 @@ impl<'a> Compiler<'a> {
select_args.append(&mut poly_args);
let jsonb_obj_sql = if select_args.is_empty() {
"jsonb_strip_nulls(jsonb_build_object())".to_string()
"jsonb_build_object()".to_string()
} else {
format!("jsonb_strip_nulls(jsonb_build_object({}))", select_args.join(", "))
format!("jsonb_build_object({})", select_args.join(", "))
};
// 3. Build WHERE clauses
@ -325,7 +327,7 @@ impl<'a> Compiler<'a> {
}
build_args.push(format!("'{}', {}", k, child_sql));
}
let combined = format!("jsonb_strip_nulls(jsonb_build_object({}))", build_args.join(", "));
let combined = format!("jsonb_build_object({})", build_args.join(", "));
Ok((combined, "object".to_string()))
}
@ -416,7 +418,14 @@ impl<'a> Compiler<'a> {
_ => false,
};
let is_primitive = prop_schema.obj.r#ref.is_none()
let is_custom_object_pointer = match &prop_schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => {
!crate::database::schema::is_primitive_type(s)
}
_ => false,
};
let is_primitive = !is_custom_object_pointer
&& !is_object_or_array
&& prop_schema.obj.family.is_none()
&& prop_schema.obj.one_of.is_none();

File diff suppressed because it is too large Load Diff

View File

@ -42,19 +42,21 @@ fn test_library_api() {
"types": [
{
"name": "source_schema",
"variations": ["source_schema"],
"hierarchy": ["source_schema", "entity"],
"schemas": [{
"$id": "source_schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"target": { "$ref": "target_schema" }
"target": { "type": "target_schema" }
},
"required": ["name"]
}]
},
{
"name": "target_schema",
"variations": ["target_schema"],
"hierarchy": ["target_schema", "entity"],
"schemas": [{
"$id": "target_schema",
@ -89,7 +91,7 @@ fn test_library_api() {
"properties": {
"name": { "type": "string" },
"target": {
"$ref": "target_schema",
"type": "target_schema",
"compiledProperties": ["value"]
}
},
@ -115,7 +117,7 @@ fn test_library_api() {
);
// 4. Validate Happy Path
let happy_drop = jspg_validate("source_schema", JsonB(json!({"name": "Neo"})));
let happy_drop = jspg_validate("source_schema", JsonB(json!({"type": "source_schema", "name": "Neo"})));
assert_eq!(
happy_drop.0,
json!({
@ -125,7 +127,7 @@ fn test_library_api() {
);
// 5. Validate Unhappy Path
let unhappy_drop = jspg_validate("source_schema", JsonB(json!({"wrong": "data"})));
let unhappy_drop = jspg_validate("source_schema", JsonB(json!({"type": "source_schema", "wrong": "data"})));
assert_eq!(
unhappy_drop.0,
json!({

View File

@ -19,6 +19,16 @@ impl Expect {
.map(|e| serde_json::to_value(e).unwrap())
.collect();
if expected_errors.len() != actual_values.len() {
return Err(format!(
"Expected {} errors, but got {}.\nExpected subset: {:?}\nActual full errors: {:?}",
expected_errors.len(),
actual_values.len(),
expected_errors,
drop.errors
));
}
for (i, expected_val) in expected_errors.iter().enumerate() {
let mut matched = false;

View File

@ -0,0 +1,45 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_cases(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(cases) = &self.schema.cases {
for case in cases {
if let Some(ref when_schema) = case.when {
let derived_when = self.derive_for_schema(when_schema, true);
let when_res = derived_when.validate()?;
// Evaluates all cases independently.
if when_res.is_valid() {
result
.evaluated_keys
.extend(when_res.evaluated_keys.clone());
result
.evaluated_indices
.extend(when_res.evaluated_indices.clone());
if let Some(ref then_schema) = case.then {
let derived_then = self.derive_for_schema(then_schema, true);
result.merge(derived_then.validate()?);
}
} else {
if let Some(ref else_schema) = case.else_ {
let derived_else = self.derive_for_schema(else_schema, true);
result.merge(derived_else.validate()?);
}
}
} else if let Some(ref else_schema) = case.else_ {
// A rule with a missing `when` fires the `else` indiscriminately
let derived_else = self.derive_for_schema(else_schema, true);
result.merge(derived_else.validate()?);
}
}
}
Ok(true)
}
}

View File

@ -1,96 +0,0 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_combinators(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(ref all_of) = self.schema.all_of {
for sub in all_of {
let derived = self.derive_for_schema(sub, true);
let res = derived.validate()?;
result.merge(res);
}
}
if let Some(ref one_of) = self.schema.one_of {
let mut passed_candidates: Vec<(Option<String>, usize, ValidationResult)> = Vec::new();
let mut failed_errors: Vec<ValidationError> = 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() {
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));
} else {
failed_errors.extend(sub_res.errors);
}
}
if passed_candidates.len() == 1 {
result.merge(passed_candidates.pop().unwrap().2);
} else if passed_candidates.is_empty() {
result.errors.push(ValidationError {
code: "NO_ONEOF_MATCH".to_string(),
message: "Matches none of oneOf schemas".to_string(),
path: self.path.to_string(),
});
result.errors.extend(failed_errors);
} 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: "AMBIGUOUS_ONEOF_MATCH".to_string(),
message: "Matches multiple oneOf schemas without a clear depth winner".to_string(),
path: self.path.to_string(),
});
}
}
if let Some(ref not_schema) = self.schema.not {
let derived = self.derive_for_schema(not_schema, true);
let sub_res = derived.validate()?;
if sub_res.is_valid() {
result.errors.push(ValidationError {
code: "NOT_VIOLATED".to_string(),
message: "Matched 'not' schema".to_string(),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -3,30 +3,14 @@ use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_conditionals(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(ref if_schema) = self.schema.if_ {
let derived_if = self.derive_for_schema(if_schema, true);
let if_res = derived_if.validate()?;
result.evaluated_keys.extend(if_res.evaluated_keys.clone());
result
.evaluated_indices
.extend(if_res.evaluated_indices.clone());
if if_res.is_valid() {
if let Some(ref then_schema) = self.schema.then_ {
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()?);
pub(crate) fn validate_extensible(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.extensible {
if let Some(obj) = self.instance.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = self.instance.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
Ok(true)
}
@ -40,6 +24,9 @@ impl<'a> ValidationContext<'a> {
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if key == "type" || key == "kind" {
continue; // Reserved keywords implicitly allowed
}
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),

View File

@ -3,10 +3,11 @@ use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
pub mod array;
pub mod combinators;
pub mod conditionals;
pub mod cases;
pub mod core;
pub mod extensible;
pub mod format;
pub mod not;
pub mod numeric;
pub mod object;
pub mod polymorphism;
@ -27,7 +28,7 @@ impl<'a> ValidationContext<'a> {
if !self.validate_family(&mut result)? {
return Ok(result);
}
if !self.validate_refs(&mut result)? {
if !self.validate_type_inheritance(&mut result)? {
return Ok(result);
}
@ -42,8 +43,11 @@ impl<'a> ValidationContext<'a> {
self.validate_array(&mut result)?;
// Multipliers & Conditionals
self.validate_combinators(&mut result)?;
self.validate_conditionals(&mut result)?;
if !self.validate_one_of(&mut result)? {
return Ok(result);
}
self.validate_not(&mut result)?;
self.validate_cases(&mut result)?;
// State Tracking
self.validate_extensible(&mut result)?;
@ -77,15 +81,4 @@ impl<'a> ValidationContext<'a> {
Ok(true)
}
}
fn validate_extensible(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.extensible {
if let Some(obj) = self.instance.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = self.instance.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
Ok(true)
}
}

View File

@ -0,0 +1,24 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_not(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(ref not_schema) = self.schema.not {
let derived = self.derive_for_schema(not_schema, true);
let sub_res = derived.validate()?;
if sub_res.is_valid() {
result.errors.push(ValidationError {
code: "NOT_VIOLATED".to_string(),
message: "Matched 'not' schema".to_string(),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -15,32 +15,68 @@ impl<'a> ValidationContext<'a> {
if let Some(obj) = current.as_object() {
// Entity implicit type validation
if let Some(schema_identifier) = self.schema.identifier() {
// Kick in if the data object has a type field
if let Some(type_val) = obj.get("type")
&& let Some(type_str) = type_val.as_str()
{
// Check if the identifier is a global type name
if let Some(type_def) = self.db.types.get(&schema_identifier) {
// Ensure the instance type is a variation of the global type
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: self.join_path("type"),
});
// We decompose identity string routing inherently
let expected_type = schema_identifier.split('.').last().unwrap_or(&schema_identifier);
// Check if the identifier represents a registered global database entity boundary mathematically
if let Some(type_def) = self.db.types.get(expected_type) {
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if type_def.variations.contains(type_str) {
// The instance is validly declaring a known structural descent
result.evaluated_keys.insert("type".to_string());
} else {
result.errors.push(ValidationError {
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively
message: format!(
"Type '{}' is not a valid descendant for this entity bound schema",
type_str
),
path: self.join_path("type"),
});
}
}
} else {
// Ad-Hoc schemas natively use strict schema discriminator strings instead of variation inheritance
if type_str == schema_identifier.as_str() {
result.evaluated_keys.insert("type".to_string());
// Because it's a global entity target, the payload must structurally provide a discriminator natively
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: format!("Schema mechanically requires type discrimination '{}'", expected_type),
path: self.path.clone(), // Empty boundary
});
}
// If the target mathematically declares a horizontal structural STI variation natively
if schema_identifier.contains('.') {
if obj.get("kind").is_none() {
result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination".to_string(),
path: self.path.clone(),
});
} else {
result.evaluated_keys.insert("kind".to_string());
}
}
} else {
// If it isn't registered globally, it might be a nested Ad-Hoc candidate running via O(1) union routers.
// Because they lack manual type property descriptors, we natively shield "type" and "kind" keys from
// triggering additionalProperty violations natively IF they precisely correspond to their fast-path boundaries
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if type_str == expected_type {
result.evaluated_keys.insert("type".to_string());
}
}
}
if let Some(kind_val) = obj.get("kind") {
if let Some((kind_str, _)) = schema_identifier.rsplit_once('.') {
if let Some(actual_kind) = kind_val.as_str() {
if actual_kind == kind_str {
result.evaluated_keys.insert("kind".to_string());
}
}
}
}
}
}
@ -67,11 +103,19 @@ impl<'a> ValidationContext<'a> {
if let Some(ref req) = self.schema.required {
for field in req {
if !obj.contains_key(field) {
result.errors.push(ValidationError {
code: "REQUIRED_FIELD_MISSING".to_string(),
message: format!("Missing {}", field),
path: self.join_path(field),
});
if field == "type" {
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: "Missing type discriminator".to_string(),
path: self.join_path(field),
});
} else {
result.errors.push(ValidationError {
code: "REQUIRED_FIELD_MISSING".to_string(),
message: format!("Missing {}", field),
path: self.join_path(field),
});
}
}
}
}
@ -110,7 +154,10 @@ impl<'a> ValidationContext<'a> {
if let Some(child_instance) = obj.get(key) {
let new_path = self.join_path(key);
let is_ref = sub_schema.r#ref.is_some();
let is_ref = match &sub_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
@ -121,21 +168,9 @@ impl<'a> ValidationContext<'a> {
next_extensible,
false,
);
let mut item_res = derived.validate()?;
let item_res = derived.validate()?;
// Entity Bound Implicit Type Interception
if key == "type"
&& let Some(schema_bound) = sub_schema.identifier()
{
if let Some(type_def) = self.db.types.get(&schema_bound)
&& 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");
}
}
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
@ -148,7 +183,10 @@ impl<'a> ValidationContext<'a> {
for (key, child_instance) in obj {
if compiled_re.0.is_match(key) {
let new_path = self.join_path(key);
let is_ref = sub_schema.r#ref.is_some();
let is_ref = match &sub_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
@ -187,7 +225,10 @@ impl<'a> ValidationContext<'a> {
if !locally_matched {
let new_path = self.join_path(key);
let is_ref = additional_schema.r#ref.is_some();
let is_ref = match &additional_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(

View File

@ -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(),
});