significantly simplified the validator and work continues on query

This commit is contained in:
2026-03-03 17:58:31 -05:00
parent 3898c43742
commit e7f20e2cb6
58 changed files with 5446 additions and 5693 deletions

View File

@ -11,39 +11,16 @@ pub struct ValidationContext<'a> {
pub depth: usize,
pub extensible: bool,
pub reporter: bool,
pub overrides: std::collections::HashSet<String>,
}
impl<'a> ValidationContext<'a> {
pub fn resolve_ref(
&self,
ref_string: &str,
) -> Option<(crate::database::schema::ResolvedRef<'a>, String)> {
if let Some(local_schema_arc) = self.root.resolve_ref(ref_string) {
if ref_string.starts_with('#') {
return Some((
crate::database::schema::ResolvedRef::Local(local_schema_arc.as_ref()),
ref_string.to_string(),
));
}
}
// We will replace all of this with `self.schema.compiled_ref` heavily shortly.
// For now, doing a basic map lookup to pass compilation.
if let Some(s) = self.schemas.get(ref_string) {
return Some((
crate::database::schema::ResolvedRef::Global(s, s),
ref_string.to_string(),
));
}
None
}
pub fn new(
schemas: &'a std::collections::HashMap<String, Schema>,
root: &'a Schema,
schema: &'a Schema,
instance: &'a serde_json::Value,
overrides: std::collections::HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
@ -57,6 +34,7 @@ impl<'a> ValidationContext<'a> {
depth: 0,
extensible: effective_extensible,
reporter,
overrides,
}
}
@ -65,6 +43,7 @@ impl<'a> ValidationContext<'a> {
schema: &'a Schema,
instance: &'a serde_json::Value,
path: &str,
overrides: std::collections::HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
@ -79,11 +58,19 @@ impl<'a> ValidationContext<'a> {
depth: self.depth + 1,
extensible: effective_extensible,
reporter,
overrides,
}
}
pub fn derive_for_schema(&self, schema: &'a Schema, reporter: bool) -> Self {
self.derive(schema, self.instance, &self.path, self.extensible, reporter)
self.derive(
schema,
self.instance,
&self.path,
std::collections::HashSet::new(),
self.extensible,
reporter,
)
}
pub fn validate(&self) -> Result<ValidationResult, ValidationError> {

View File

@ -8,17 +8,16 @@ pub use context::ValidationContext;
pub use error::ValidationError;
pub use result::ValidationResult;
use crate::database::schema::Schema;
use serde_json::Value;
use std::collections::HashSet;
use std::collections::HashMap;
use std::sync::Arc;
pub struct Validator {
pub schemas: std::sync::Arc<std::collections::HashMap<String, crate::database::schema::Schema>>,
pub schemas: Arc<HashMap<String, Schema>>,
}
impl Validator {
pub fn new(
schemas: std::sync::Arc<std::collections::HashMap<String, crate::database::schema::Schema>>,
) -> Self {
pub fn new(schemas: Arc<HashMap<String, Schema>>) -> Self {
Self { schemas }
}
@ -50,7 +49,15 @@ impl Validator {
instance: &Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.schemas.get(schema_id) {
let ctx = ValidationContext::new(&self.schemas, schema, schema, instance, false, false);
let ctx = ValidationContext::new(
&self.schemas,
schema,
schema,
instance,
std::collections::HashSet::new(),
false,
false,
);
ctx.validate_scoped()
} else {
Err(ValidationError {

View File

@ -1,5 +1,4 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
@ -53,6 +52,7 @@ impl<'a> ValidationContext<'a> {
contains_schema,
child_instance,
&self.path,
std::collections::HashSet::new(),
self.extensible,
false,
);
@ -91,7 +91,14 @@ impl<'a> ValidationContext<'a> {
if i < len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(sub_schema, child_instance, &path, self.extensible, false);
let derived = self.derive(
sub_schema,
child_instance,
&path,
std::collections::HashSet::new(),
self.extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
@ -105,7 +112,14 @@ impl<'a> ValidationContext<'a> {
for i in validation_index..len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(items_schema, child_instance, &path, self.extensible, false);
let derived = self.derive(
items_schema,
child_instance,
&path,
std::collections::HashSet::new(),
self.extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);

View File

@ -42,7 +42,7 @@ impl<'a> ValidationContext<'a> {
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if !result.evaluated_keys.contains(key) {
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),
message: format!("Unexpected property '{}'", key),

View File

@ -1,5 +1,3 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;

View File

@ -1,5 +1,4 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
@ -13,7 +12,7 @@ impl<'a> ValidationContext<'a> {
let current = self.instance;
if let Some(obj) = current.as_object() {
// Entity Bound Implicit Type Validation
if let Some(allowed_types) = &self.schema.obj.compiled_allowed_types {
if let Some(allowed_types) = &self.schema.obj.compiled_variations {
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if allowed_types.contains(type_str) {
@ -62,8 +61,38 @@ impl<'a> ValidationContext<'a> {
}
}
if let Some(ref deps) = self.schema.dependencies {
for (prop, dep) in deps {
if obj.contains_key(prop) {
match dep {
crate::database::schema::Dependency::Props(required_props) => {
for req_prop in required_props {
if !obj.contains_key(req_prop) {
result.errors.push(ValidationError {
code: "DEPENDENCY_MISSING".to_string(),
message: format!("Property '{}' requires property '{}'", prop, req_prop),
path: self.path.to_string(),
});
}
}
}
crate::database::schema::Dependency::Schema(dep_schema) => {
let derived = self.derive_for_schema(dep_schema, false);
let dep_res = derived.validate()?;
result.evaluated_keys.extend(dep_res.evaluated_keys.clone());
result.merge(dep_res);
}
}
}
}
}
if let Some(props) = &self.schema.properties {
for (key, sub_schema) in props {
if self.overrides.contains(key) {
continue; // Skip validation if exactly this property was overridden by a child
}
if let Some(child_instance) = obj.get(key) {
let new_path = format!("{}/{}", self.path, key);
let is_ref = sub_schema.ref_string.is_some() || sub_schema.obj.compiled_ref.is_some();
@ -73,6 +102,7 @@ impl<'a> ValidationContext<'a> {
sub_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
next_extensible,
false,
);
@ -80,7 +110,7 @@ impl<'a> ValidationContext<'a> {
// Entity Bound Implicit Type Interception
if key == "type" {
if let Some(allowed_types) = &self.schema.obj.compiled_allowed_types {
if let Some(allowed_types) = &self.schema.obj.compiled_variations {
if let Some(instance_type) = child_instance.as_str() {
if allowed_types.contains(instance_type) {
item_res
@ -109,6 +139,7 @@ impl<'a> ValidationContext<'a> {
sub_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
next_extensible,
false,
);
@ -149,6 +180,7 @@ impl<'a> ValidationContext<'a> {
additional_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
next_extensible,
false,
);
@ -169,6 +201,7 @@ impl<'a> ValidationContext<'a> {
self.root,
property_names,
&val_str,
std::collections::HashSet::new(),
self.extensible,
self.reporter,
);

View File

@ -39,22 +39,31 @@ impl<'a> ValidationContext<'a> {
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
// 1. Core $ref logic fully transitioned to memory pointer resolutions.
if let Some(_ref_str) = &self.schema.ref_string {
if let Some(global_schema) = &self.schema.compiled_ref {
// 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.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,
false,
true,
);
shadow.root = global_schema;
result.merge(shadow.validate()?);
} else {
result.errors.push(ValidationError {
code: "REF_RESOLUTION_FAILED".to_string(),
message: format!("Reference pointer was not compiled inside Database graph"),
message: format!(
"Reference pointer to '{}' was not found in schema registry",
ref_str
),
path: self.path.to_string(),
});
}

View File

@ -15,7 +15,7 @@ struct TestCase {
data: serde_json::Value,
valid: bool,
// Support explicit schema ID target for test case
schema_id: Option<String>,
schema_id: String,
}
// use crate::validator::registry::REGISTRY; // No longer used directly for tests!
@ -49,62 +49,36 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
// 4. Run Tests
for (_test_index, test) in group.tests.iter().enumerate() {
let mut schema_id = test.schema_id.clone();
let schema_id = &test.schema_id;
// If no explicit schema_id, infer from the database structure
if schema_id.is_none() {
if let Some(schemas) = db_json.get("schemas").and_then(|v| v.as_array()) {
if let Some(first) = schemas.first() {
if let Some(id) = first.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
} else {
schema_id = Some("schema_0".to_string());
}
}
}
if schema_id.is_none() {
if let Some(puncs) = db_json.get("puncs").and_then(|v| v.as_array()) {
if let Some(first_punc) = puncs.first() {
if let Some(schemas) = first_punc.get("schemas").and_then(|v| v.as_array()) {
if let Some(first) = schemas.first() {
if let Some(id) = first.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
}
}
}
}
}
}
if validator.schemas.get(schema_id).is_none() {
failures.push(format!(
"[{}] Missing Schema: Cannot find schema ID '{}'",
group.description, schema_id
));
continue;
}
if let Some(sid) = schema_id {
let result = validator.validate(&sid, &test.data);
let result = validator.validate(schema_id, &test.data);
let (got_valid, _errors) = match &result {
Ok(res) => (res.is_valid(), &res.errors),
Err(_e) => {
// If we encounter an execution error (e.g. Schema Not Found),
// we treat it as a test failure.
(false, &vec![])
}
let (got_valid, _errors) = match &result {
Ok(res) => (res.is_valid(), &res.errors),
Err(_e) => {
// If we encounter an execution error (e.g. Schema Not Found),
// we treat it as a test failure.
(false, &vec![])
}
};
if got_valid != test.valid {
let error_msg = match &result {
Ok(res) => format!("{:?}", res.errors),
Err(e) => format!("Execution Error: {:?}", e),
};
if got_valid != test.valid {
let error_msg = match &result {
Ok(res) => format!("{:?}", res.errors),
Err(e) => format!("Execution Error: {:?}", e),
};
failures.push(format!(
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
group.description, test.description, test.valid, got_valid, error_msg
));
}
} else {
failures.push(format!(
"[{}] Test '{}' skipped: No schema ID found.",
group.description, test.description
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
group.description, test.description, test.valid, got_valid, error_msg
));
}
}