all jspg tests now passing

This commit is contained in:
2026-03-04 01:02:32 -05:00
parent e7f20e2cb6
commit 566b599512
32 changed files with 531 additions and 1068 deletions

View File

@ -376,7 +376,7 @@ fn check_hostname(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
Err("label has -- in 3rd/4th position but does not start with xn--")?;
} else {
let (unicode, errors) = idna::domain_to_unicode(label);
if let Err(_) = errors {
if errors.is_err() {
Err("invalid punycode")?;
}
check_unicode_idn_constraints(&unicode)

View File

@ -9,13 +9,15 @@ use crate::database::r#enum::Enum;
use crate::database::punc::Punc;
use crate::database::schema::Schema;
use crate::database::r#type::Type;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
pub struct Database {
pub enums: HashMap<String, Enum>,
pub types: HashMap<String, Type>,
pub puncs: HashMap<String, Punc>,
pub schemas: HashMap<String, Schema>,
pub descendants: HashMap<String, Vec<String>>,
pub depths: HashMap<String, usize>,
}
impl Database {
@ -25,6 +27,8 @@ impl Database {
types: HashMap::new(),
puncs: HashMap::new(),
schemas: HashMap::new(),
descendants: HashMap::new(),
depths: HashMap::new(),
};
if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) {
@ -69,43 +73,12 @@ impl Database {
db
}
/// Organizes the graph of the database, compiling regex, format functions, and pointing schema references.
/// Organizes the graph of the database, compiling regex, format functions, and caching relationships.
fn compile(&mut self) -> Result<(), String> {
self.collect_schemas();
// 1. Build a structural descendant graph for $family macro expansion
let mut direct_refs: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (id, schema) in &self.schemas {
if let Some(ref_str) = &schema.obj.ref_string {
direct_refs
.entry(ref_str.clone())
.or_default()
.push(id.clone());
}
}
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
// 2. Expand $family macros into oneOf blocks
for id in &schema_ids {
if let Some(schema) = self.schemas.get_mut(id) {
schema.map_children(|mut child| {
Self::expand_family(&mut child, &direct_refs);
});
Self::expand_family(schema, &direct_refs);
}
}
let schemas_snap = self.schemas.clone();
// 3. Compile internals and link memory pointers
for id in schema_ids {
if let Some(schema) = self.schemas.get_mut(&id) {
schema.compile_internals();
schema.link_refs(&schemas_snap);
}
}
self.collect_depths();
self.collect_descendants();
self.compile_schemas();
Ok(())
}
@ -113,38 +86,18 @@ impl Database {
fn collect_schemas(&mut self) {
let mut to_insert = Vec::new();
// Pass A: Entities - Compute Variations from hierarchies
// `hierarchy` is an array of ancestors. E.g. `person` -> `['entity', 'user', 'person']`.
// We map this backward so that `user`'s allowed variations = `['user', 'person']`.
let mut variations_by_entity = std::collections::HashMap::new();
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
for type_def in self.types.values() {
for ancestor in &type_def.hierarchy {
variations_by_entity
.entry(ancestor.clone())
.or_insert_with(std::collections::HashSet::new)
.insert(type_def.name.clone());
}
}
// Now stamp all exported entity schemas with their precise physical variations
for (_, type_def) in &self.types {
let allowed_strings = variations_by_entity
.get(&type_def.name)
.cloned()
.unwrap_or_default();
for mut schema in type_def.schemas.clone() {
schema.stamp_variations(Some(allowed_strings.clone()));
schema.harvest(&mut to_insert);
}
}
// Pass B: APIs and Enums (No initial variations stamped)
for (_, punc_def) in &self.puncs {
for punc_def in self.puncs.values() {
for mut schema in punc_def.schemas.clone() {
schema.harvest(&mut to_insert);
}
}
for (_, enum_def) in &self.enums {
for enum_def in self.enums.values() {
for mut schema in enum_def.schemas.clone() {
schema.harvest(&mut to_insert);
}
@ -155,55 +108,80 @@ impl Database {
}
}
fn expand_family(
schema: &mut crate::database::schema::Schema,
direct_refs: &std::collections::HashMap<String, Vec<String>>,
) {
if let Some(family_target) = &schema.obj.family {
let mut descendants = std::collections::HashSet::new();
Self::collect_descendants(family_target, direct_refs, &mut descendants);
fn collect_depths(&mut self) {
let mut depths: HashMap<String, usize> = HashMap::new();
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
// the "$family" macro is logically replaced by an anyOf of its descendants + itself
let mut derived_any_of = Vec::new();
for id in schema_ids {
let mut current_id = id.clone();
let mut depth = 0;
let mut visited = HashSet::new();
// Include the target base itself if valid (which it always is structurally)
let mut base_ref = crate::database::schema::SchemaObject::default();
base_ref.ref_string = Some(family_target.clone());
derived_any_of.push(std::sync::Arc::new(crate::database::schema::Schema {
obj: base_ref,
always_fail: false,
}));
// Sort descendants for determinism during testing
let mut desc_vec: Vec<String> = descendants.into_iter().collect();
desc_vec.sort();
for child_id in desc_vec {
let mut child_ref = crate::database::schema::SchemaObject::default();
child_ref.ref_string = Some(child_id);
derived_any_of.push(std::sync::Arc::new(crate::database::schema::Schema {
obj: child_ref,
always_fail: false,
}));
while let Some(schema) = self.schemas.get(&current_id) {
if !visited.insert(current_id.clone()) {
break; // Cycle detected
}
if let Some(ref_str) = &schema.obj.ref_string {
current_id = ref_str.clone();
depth += 1;
} else {
break;
}
}
schema.obj.any_of = Some(derived_any_of);
// Remove family so it doesn't cause conflicts or fail the simple validation
schema.obj.family = None;
depths.insert(id, depth);
}
self.depths = depths;
}
fn collect_descendants(
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.ref_string {
direct_refs
.entry(ref_str.clone())
.or_default()
.push(id.clone());
}
}
// Cache generic descendants for $family runtime lookups
let mut descendants = HashMap::new();
for (id, schema) in &self.schemas {
if let Some(family_target) = &schema.obj.family {
let mut desc_set = HashSet::new();
Self::collect_descendants_recursively(family_target, &direct_refs, &mut desc_set);
let mut desc_vec: Vec<String> = desc_set.into_iter().collect();
desc_vec.sort();
// By placing all descendants directly onto the ID mapped location of the Family declaration,
// we can lookup descendants natively in ValidationContext without AST replacement overrides.
descendants.insert(id.clone(), desc_vec);
}
}
self.descendants = descendants;
}
fn collect_descendants_recursively(
target: &str,
direct_refs: &std::collections::HashMap<String, Vec<String>>,
descendants: &mut std::collections::HashSet<String>,
direct_refs: &HashMap<String, Vec<String>>,
descendants: &mut HashSet<String>,
) {
if let Some(children) = direct_refs.get(target) {
for child in children {
if descendants.insert(child.clone()) {
Self::collect_descendants(child, direct_refs, descendants);
Self::collect_descendants_recursively(child, direct_refs, descendants);
}
}
}
}
fn compile_schemas(&mut self) {
// Pass 3: compile_internals across pure structure
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
if let Some(schema) = self.schemas.get_mut(&id) {
schema.compile_internals();
}
}
}
}

View File

@ -1,15 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Relation {
pub id: String,
pub constraint_name: String,
pub source_type: String,
#[serde(default)]
pub source_columns: Vec<String>,
pub destination_type: String,
#[serde(default)]
pub destination_columns: Vec<String>,
pub prefix: Option<String>,
}

View File

@ -120,11 +120,6 @@ pub struct SchemaObject {
#[serde(default)]
pub extensible: Option<bool>,
// Compiled Fields (Hidden from JSON/Serde)
#[serde(skip)]
pub compiled_ref: Option<Arc<Schema>>,
#[serde(skip)]
pub compiled_variations: Option<std::collections::HashSet<String>>,
#[serde(skip)]
pub compiled_format: Option<CompiledFormat>,
#[serde(skip)]
@ -153,7 +148,7 @@ impl std::fmt::Debug for CompiledFormat {
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub regex::Regex);
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Default)]
pub struct Schema {
#[serde(flatten)]
pub obj: SchemaObject,
@ -161,15 +156,6 @@ pub struct Schema {
pub always_fail: bool,
}
impl Default for Schema {
fn default() -> Self {
Schema {
obj: SchemaObject::default(),
always_fail: false,
}
}
}
impl std::ops::Deref for Schema {
type Target = SchemaObject;
fn deref(&self) -> &Self::Target {
@ -186,16 +172,16 @@ impl Schema {
pub fn compile_internals(&mut self) {
self.map_children(|child| child.compile_internals());
if let Some(format_str) = &self.obj.format {
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
self.obj.compiled_format = Some(crate::database::schema::CompiledFormat::Func(fmt.func));
}
if let Some(format_str) = &self.obj.format
&& let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str())
{
self.obj.compiled_format = Some(crate::database::schema::CompiledFormat::Func(fmt.func));
}
if let Some(pattern_str) = &self.obj.pattern {
if let Ok(re) = regex::Regex::new(pattern_str) {
self.obj.compiled_pattern = Some(crate::database::schema::CompiledRegex(re));
}
if let Some(pattern_str) = &self.obj.pattern
&& let Ok(re) = regex::Regex::new(pattern_str)
{
self.obj.compiled_pattern = Some(crate::database::schema::CompiledRegex(re));
}
if let Some(pattern_props) = &self.obj.pattern_properties {
@ -211,46 +197,6 @@ impl Schema {
}
}
pub fn link_refs(&mut self, schemas: &std::collections::HashMap<String, Schema>) {
if let Some(ref_str) = &self.obj.ref_string {
if let Some(target) = schemas.get(ref_str) {
self.obj.compiled_ref = Some(Arc::new(target.clone()));
// Viral Infection: Inherit physical entity boundaries across the $ref pointer recursively
if self.obj.compiled_variations.is_none() {
let mut visited = std::collections::HashSet::new();
self.obj.compiled_variations = Self::resolve_variations(ref_str, schemas, &mut visited);
}
}
}
self.map_children(|child| child.link_refs(schemas));
}
fn resolve_variations(
ref_str: &str,
schemas: &std::collections::HashMap<String, Schema>,
visited: &mut std::collections::HashSet<String>,
) -> Option<std::collections::HashSet<String>> {
if !visited.insert(ref_str.to_string()) {
return None; // Cycle detected
}
if let Some(target) = schemas.get(ref_str) {
if let Some(vars) = &target.obj.compiled_variations {
return Some(vars.clone());
}
if let Some(next_ref) = &target.obj.ref_string {
return Self::resolve_variations(next_ref, schemas, visited);
}
}
None
}
pub fn stamp_variations(&mut self, variations: Option<std::collections::HashSet<String>>) {
self.obj.compiled_variations = variations.clone();
self.map_children(|child| child.stamp_variations(variations.clone()));
}
pub fn harvest(&mut self, to_insert: &mut Vec<(String, Schema)>) {
if let Some(id) = &self.obj.id {
to_insert.push((id.clone(), self.clone()));
@ -263,7 +209,7 @@ impl Schema {
F: FnMut(&mut Schema),
{
if let Some(props) = &mut self.obj.properties {
for (_, v) in props {
for v in props.values_mut() {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);
@ -271,7 +217,7 @@ impl Schema {
}
if let Some(pattern_props) = &mut self.obj.pattern_properties {
for (_, v) in pattern_props {
for v in pattern_props.values_mut() {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -19,6 +21,8 @@ pub struct Type {
pub longevity: Option<i32>,
#[serde(default)]
pub hierarchy: Vec<String>,
#[serde(default)]
pub variations: HashSet<String>,
pub relationship: Option<bool>,
#[serde(default)]
pub fields: Vec<String>,

View File

@ -17,6 +17,12 @@ pub struct Drop {
pub errors: Vec<Error>,
}
impl Default for Drop {
fn default() -> Self {
Self::new()
}
}
impl Drop {
pub fn new() -> Self {
Self {

View File

@ -15,7 +15,7 @@ impl Jspg {
pub fn new(database_val: &serde_json::Value) -> Self {
let database_instance = Database::new(database_val);
let database = Arc::new(database_instance);
let validator = Validator::new(std::sync::Arc::new(database.schemas.clone()));
let validator = Validator::new(database.clone());
let queryer = Queryer::new();
let merger = Merger::new();

View File

@ -2,6 +2,12 @@ pub struct Merger {
// To be implemented
}
impl Default for Merger {
fn default() -> Self {
Self::new()
}
}
impl Merger {
pub fn new() -> Self {
Self {}

View File

@ -2,6 +2,12 @@ pub struct Queryer {
// To be implemented
}
impl Default for Queryer {
fn default() -> Self {
Self::new()
}
}
impl Queryer {
pub fn new() -> Self {
Self {}

View File

@ -281,66 +281,6 @@ fn test_const_17() {
crate::validator::util::run_test_file_at_index(&path, 17).unwrap();
}
#[pg_test]
fn test_any_of_0() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_any_of_1() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_any_of_2() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_any_of_3() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_any_of_4() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 4).unwrap();
}
#[pg_test]
fn test_any_of_5() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 5).unwrap();
}
#[pg_test]
fn test_any_of_6() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 6).unwrap();
}
#[pg_test]
fn test_any_of_7() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 7).unwrap();
}
#[pg_test]
fn test_any_of_8() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 8).unwrap();
}
#[pg_test]
fn test_any_of_9() {
let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 9).unwrap();
}
#[pg_test]
fn test_families_0() {
let path = format!("{}/tests/fixtures/families.json", env!("CARGO_MANIFEST_DIR"));
@ -1391,12 +1331,6 @@ fn test_all_of_14() {
crate::validator::util::run_test_file_at_index(&path, 14).unwrap();
}
#[pg_test]
fn test_all_of_15() {
let path = format!("{}/tests/fixtures/allOf.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 15).unwrap();
}
#[pg_test]
fn test_format_0() {
let path = format!("{}/tests/fixtures/format.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -1,9 +1,12 @@
use crate::database::Database;
use crate::database::schema::Schema;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use std::collections::HashSet;
use std::sync::Arc;
pub struct ValidationContext<'a> {
pub schemas: &'a std::collections::HashMap<String, Schema>,
pub db: &'a Arc<Database>,
pub root: &'a Schema,
pub schema: &'a Schema,
pub instance: &'a serde_json::Value,
@ -11,22 +14,22 @@ pub struct ValidationContext<'a> {
pub depth: usize,
pub extensible: bool,
pub reporter: bool,
pub overrides: std::collections::HashSet<String>,
pub overrides: HashSet<String>,
}
impl<'a> ValidationContext<'a> {
pub fn new(
schemas: &'a std::collections::HashMap<String, Schema>,
db: &'a Arc<Database>,
root: &'a Schema,
schema: &'a Schema,
instance: &'a serde_json::Value,
overrides: std::collections::HashSet<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
schemas,
db,
root,
schema,
instance,
@ -43,14 +46,14 @@ impl<'a> ValidationContext<'a> {
schema: &'a Schema,
instance: &'a serde_json::Value,
path: &str,
overrides: std::collections::HashSet<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
schemas: self.schemas,
db: self.db,
root: self.root,
schema,
instance,
@ -67,7 +70,7 @@ impl<'a> ValidationContext<'a> {
schema,
self.instance,
&self.path,
std::collections::HashSet::new(),
HashSet::new(),
self.extensible,
reporter,
)

View File

@ -1,5 +1,5 @@
use serde_json::Value;
use std::collections::HashSet;
use HashSet;
use std::ptr::NonNull;
pub trait ValidationInstance<'a>: Copy + Clone {

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
pub mod context;
pub mod error;
pub mod result;
@ -8,35 +10,36 @@ pub use context::ValidationContext;
pub use error::ValidationError;
pub use result::ValidationResult;
use crate::database::schema::Schema;
use crate::database::Database;
use crate::validator::rules::util::is_integer;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
pub struct Validator {
pub schemas: Arc<HashMap<String, Schema>>,
pub db: Arc<Database>,
}
impl Validator {
pub fn new(schemas: Arc<HashMap<String, Schema>>) -> Self {
Self { schemas }
pub fn new(db: Arc<Database>) -> Self {
Self { db }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.schemas.keys().cloned().collect()
self.db.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val {
if s.is_empty() {
return true;
}
if let Value::String(s) = val
&& s.is_empty()
{
return true;
}
match t {
"null" => val.is_null(),
"boolean" => val.is_boolean(),
"string" => val.is_string(),
"number" => val.is_number(),
"integer" => crate::validator::util::is_integer(val),
"integer" => is_integer(val),
"object" => val.is_object(),
"array" => val.is_array(),
_ => true,
@ -48,13 +51,13 @@ impl Validator {
schema_id: &str,
instance: &Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.schemas.get(schema_id) {
if let Some(schema) = self.db.schemas.get(schema_id) {
let ctx = ValidationContext::new(
&self.schemas,
&self.db,
schema,
schema,
instance,
std::collections::HashSet::new(),
HashSet::new(),
false,
false,
);

View File

@ -1,6 +1,7 @@
use crate::validator::error::ValidationError;
use std::collections::HashSet;
use crate::validator::error::ValidationError;
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ValidationResult {
pub errors: Vec<ValidationError>,

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use serde_json::Value;
use crate::validator::context::ValidationContext;
@ -11,23 +13,23 @@ impl<'a> ValidationContext<'a> {
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(arr) = current.as_array() {
if let Some(min) = self.schema.min_items {
if (arr.len() as f64) < min {
result.errors.push(ValidationError {
code: "MIN_ITEMS".to_string(),
message: "Too few items".to_string(),
path: self.path.to_string(),
});
}
if let Some(min) = self.schema.min_items
&& (arr.len() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_ITEMS".to_string(),
message: "Too few items".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_items {
if (arr.len() as f64) > max {
result.errors.push(ValidationError {
code: "MAX_ITEMS".to_string(),
message: "Too many items".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_items
&& (arr.len() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_ITEMS".to_string(),
message: "Too many items".to_string(),
path: self.path.to_string(),
});
}
if self.schema.unique_items.unwrap_or(false) {
@ -52,7 +54,7 @@ impl<'a> ValidationContext<'a> {
contains_schema,
child_instance,
&self.path,
std::collections::HashSet::new(),
HashSet::new(),
self.extensible,
false,
);
@ -72,14 +74,14 @@ impl<'a> ValidationContext<'a> {
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_contains {
if _match_count > max as usize {
result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(),
message: format!("Contains matches {} > max {}", _match_count, max),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_contains
&& _match_count > max as usize
{
result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(),
message: format!("Contains matches {} > max {}", _match_count, max),
path: self.path.to_string(),
});
}
}
@ -95,7 +97,7 @@ impl<'a> ValidationContext<'a> {
sub_schema,
child_instance,
&path,
std::collections::HashSet::new(),
HashSet::new(),
self.extensible,
false,
);
@ -116,7 +118,7 @@ impl<'a> ValidationContext<'a> {
items_schema,
child_instance,
&path,
std::collections::HashSet::new(),
HashSet::new(),
self.extensible,
false,
);

View File

@ -15,52 +15,61 @@ impl<'a> ValidationContext<'a> {
}
}
if let Some(ref any_of) = self.schema.any_of {
let mut valid = false;
for sub in any_of {
let derived = self.derive_for_schema(sub, true);
let sub_res = derived.validate()?;
if sub_res.is_valid() {
valid = true;
result.merge(sub_res);
}
}
if !valid {
result.errors.push(ValidationError {
code: "ANY_OF_VIOLATED".to_string(),
message: "Matches none of anyOf schemas".to_string(),
path: self.path.to_string(),
});
}
}
if let Some(ref one_of) = self.schema.one_of {
let mut valid_count = 0;
let mut valid_res = ValidationResult::new();
let mut passed_candidates: Vec<(Option<String>, usize, ValidationResult)> = 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() {
valid_count += 1;
valid_res = sub_res;
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));
}
}
if valid_count == 1 {
result.merge(valid_res);
} else if valid_count == 0 {
if passed_candidates.len() == 1 {
result.merge(passed_candidates.pop().unwrap().2);
} else if passed_candidates.is_empty() {
result.errors.push(ValidationError {
code: "ONE_OF_VIOLATED".to_string(),
code: "NO_ONEOF_MATCH".to_string(),
message: "Matches none of oneOf schemas".to_string(),
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;
} 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: "ONE_OF_VIOLATED".to_string(),
message: format!("Matches {} of oneOf schemas (expected 1)", valid_count),
code: "AMBIGUOUS_ONEOF_MATCH".to_string(),
message: "Matches multiple oneOf schemas without a clear depth winner".to_string(),
path: self.path.to_string(),
});
}

View File

@ -21,11 +21,9 @@ impl<'a> ValidationContext<'a> {
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()?);
}
} 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()?);
}
}

View File

@ -2,6 +2,7 @@ use crate::validator::Validator;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use crate::validator::rules::util::equals;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_core(
@ -41,25 +42,23 @@ impl<'a> ValidationContext<'a> {
}
if let Some(ref const_val) = self.schema.const_ {
if !crate::validator::util::equals(current, const_val) {
if !equals(current, const_val) {
result.errors.push(ValidationError {
code: "CONST_VIOLATED".to_string(),
message: "Value does not match const".to_string(),
path: self.path.to_string(),
});
} else {
if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
} else if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
if let Some(ref enum_vals) = self.schema.enum_ {
let mut found = false;
for val in enum_vals {
if crate::validator::util::equals(current, val) {
if equals(current, val) {
found = true;
break;
}
@ -70,12 +69,10 @@ impl<'a> ValidationContext<'a> {
message: "Value is not in enum".to_string(),
path: self.path.to_string(),
});
} else {
if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
} else if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}

View File

@ -16,25 +16,23 @@ impl<'a> ValidationContext<'a> {
} else {
true
};
if should {
if let Err(e) = f(current) {
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: format!("Format error: {}", e),
path: self.path.to_string(),
});
}
if should && let Err(e) = f(current) {
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: format!("Format error: {}", e),
path: self.path.to_string(),
});
}
}
crate::database::schema::CompiledFormat::Regex(re) => {
if let Some(s) = current.as_str() {
if !re.is_match(s) {
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: "Format regex mismatch".to_string(),
path: self.path.to_string(),
});
}
if let Some(s) = current.as_str()
&& !re.is_match(s)
{
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: "Format regex mismatch".to_string(),
path: self.path.to_string(),
});
}
}
}

View File

@ -1,4 +1,3 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
@ -12,6 +11,7 @@ pub mod numeric;
pub mod object;
pub mod polymorphism;
pub mod string;
pub mod util;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_scoped(&self) -> Result<ValidationResult, ValidationError> {

View File

@ -9,41 +9,41 @@ impl<'a> ValidationContext<'a> {
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(num) = current.as_f64() {
if let Some(min) = self.schema.minimum {
if num < min {
result.errors.push(ValidationError {
code: "MINIMUM_VIOLATED".to_string(),
message: format!("Value {} < min {}", num, min),
path: self.path.to_string(),
});
}
if let Some(min) = self.schema.minimum
&& num < min
{
result.errors.push(ValidationError {
code: "MINIMUM_VIOLATED".to_string(),
message: format!("Value {} < min {}", num, min),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.maximum {
if num > max {
result.errors.push(ValidationError {
code: "MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} > max {}", num, max),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.maximum
&& num > max
{
result.errors.push(ValidationError {
code: "MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} > max {}", num, max),
path: self.path.to_string(),
});
}
if let Some(ex_min) = self.schema.exclusive_minimum {
if num <= ex_min {
result.errors.push(ValidationError {
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
message: format!("Value {} <= ex_min {}", num, ex_min),
path: self.path.to_string(),
});
}
if let Some(ex_min) = self.schema.exclusive_minimum
&& num <= ex_min
{
result.errors.push(ValidationError {
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
message: format!("Value {} <= ex_min {}", num, ex_min),
path: self.path.to_string(),
});
}
if let Some(ex_max) = self.schema.exclusive_maximum {
if num >= ex_max {
result.errors.push(ValidationError {
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} >= ex_max {}", num, ex_max),
path: self.path.to_string(),
});
}
if let Some(ex_max) = self.schema.exclusive_maximum
&& num >= ex_max
{
result.errors.push(ValidationError {
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} >= ex_max {}", num, ex_max),
path: self.path.to_string(),
});
}
if let Some(multiple_of) = self.schema.multiple_of {
let val: f64 = num / multiple_of;

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use serde_json::Value;
use crate::validator::context::ValidationContext;
@ -12,42 +14,44 @@ 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_variations {
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if allowed_types.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: format!("{}/type", self.path),
});
}
if let Some(lookup_key) = self.schema.id.as_ref().or(self.schema.ref_string.as_ref()) {
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
if let Some(type_def) = self.db.types.get(&base_type_name)
&& let Some(type_val) = obj.get("type")
&& let Some(type_str) = type_val.as_str()
{
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: format!("{}/type", self.path),
});
}
}
}
if let Some(min) = self.schema.min_properties {
if (obj.len() as f64) < min {
result.errors.push(ValidationError {
code: "MIN_PROPERTIES".to_string(),
message: "Too few properties".to_string(),
path: self.path.to_string(),
});
}
if let Some(min) = self.schema.min_properties
&& (obj.len() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_PROPERTIES".to_string(),
message: "Too few properties".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_properties {
if (obj.len() as f64) > max {
result.errors.push(ValidationError {
code: "MAX_PROPERTIES".to_string(),
message: "Too many properties".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_properties
&& (obj.len() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_PROPERTIES".to_string(),
message: "Too many properties".to_string(),
path: self.path.to_string(),
});
}
if let Some(ref req) = self.schema.required {
for field in req {
@ -95,29 +99,31 @@ impl<'a> ValidationContext<'a> {
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();
let is_ref = sub_schema.ref_string.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
HashSet::new(),
next_extensible,
false,
);
let mut item_res = derived.validate()?;
// Entity Bound Implicit Type Interception
if key == "type" {
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
.errors
.retain(|e| e.code != "CONST_VIOLATED" && e.code != "ENUM_VIOLATED");
}
}
if key == "type"
&& let Some(lookup_key) = sub_schema.id.as_ref().or(sub_schema.ref_string.as_ref())
{
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
if let Some(type_def) = self.db.types.get(&base_type_name)
&& 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");
}
}
@ -132,14 +138,14 @@ impl<'a> ValidationContext<'a> {
for (key, child_instance) in obj {
if compiled_re.0.is_match(key) {
let new_path = format!("{}/{}", self.path, key);
let is_ref = sub_schema.ref_string.is_some() || sub_schema.obj.compiled_ref.is_some();
let is_ref = sub_schema.ref_string.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
HashSet::new(),
next_extensible,
false,
);
@ -154,33 +160,31 @@ impl<'a> ValidationContext<'a> {
if let Some(ref additional_schema) = self.schema.additional_properties {
for (key, child_instance) in obj {
let mut locally_matched = false;
if let Some(props) = &self.schema.properties {
if props.contains_key(&key.to_string()) {
locally_matched = true;
}
if let Some(props) = &self.schema.properties
&& props.contains_key(&key.to_string())
{
locally_matched = true;
}
if !locally_matched {
if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties {
for (compiled_re, _) in compiled_pp {
if compiled_re.0.is_match(key) {
locally_matched = true;
break;
}
if !locally_matched && let Some(ref compiled_pp) = self.schema.compiled_pattern_properties
{
for (compiled_re, _) in compiled_pp {
if compiled_re.0.is_match(key) {
locally_matched = true;
break;
}
}
}
if !locally_matched {
let new_path = format!("{}/{}", self.path, key);
let is_ref = additional_schema.ref_string.is_some()
|| additional_schema.obj.compiled_ref.is_some();
let is_ref = additional_schema.ref_string.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
additional_schema,
child_instance,
&new_path,
std::collections::HashSet::new(),
HashSet::new(),
next_extensible,
false,
);
@ -197,11 +201,11 @@ impl<'a> ValidationContext<'a> {
let val_str = Value::String(key.to_string());
let ctx = ValidationContext::new(
self.schemas,
self.db,
self.root,
property_names,
&val_str,
std::collections::HashSet::new(),
HashSet::new(),
self.extensible,
self.reporter,
);

View File

@ -15,7 +15,6 @@ impl<'a> ValidationContext<'a> {
|| self.schema.items.is_some()
|| self.schema.ref_string.is_some()
|| self.schema.one_of.is_some()
|| self.schema.any_of.is_some()
|| self.schema.all_of.is_some()
|| self.schema.enum_.is_some()
|| self.schema.const_.is_some();
@ -31,7 +30,90 @@ impl<'a> ValidationContext<'a> {
}
}
// Family specific runtime validation will go here later if needed
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)
}
@ -41,7 +123,7 @@ impl<'a> ValidationContext<'a> {
) -> 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.schemas.get(ref_str) {
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()));

View File

@ -10,23 +10,23 @@ impl<'a> ValidationContext<'a> {
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(s) = current.as_str() {
if let Some(min) = self.schema.min_length {
if (s.chars().count() as f64) < min {
result.errors.push(ValidationError {
code: "MIN_LENGTH_VIOLATED".to_string(),
message: format!("Length < min {}", min),
path: self.path.to_string(),
});
}
if let Some(min) = self.schema.min_length
&& (s.chars().count() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_LENGTH_VIOLATED".to_string(),
message: format!("Length < min {}", min),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_length {
if (s.chars().count() as f64) > max {
result.errors.push(ValidationError {
code: "MAX_LENGTH_VIOLATED".to_string(),
message: format!("Length > max {}", max),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_length
&& (s.chars().count() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_LENGTH_VIOLATED".to_string(),
message: format!("Length > max {}", max),
path: self.path.to_string(),
});
}
if let Some(ref compiled_re) = self.schema.compiled_pattern {
if !compiled_re.0.is_match(s) {
@ -36,16 +36,15 @@ impl<'a> ValidationContext<'a> {
path: self.path.to_string(),
});
}
} else if let Some(ref pattern) = self.schema.pattern {
if let Ok(re) = Regex::new(pattern) {
if !re.is_match(s) {
result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(),
message: format!("Pattern mismatch {}", pattern),
path: self.path.to_string(),
});
}
}
} else if let Some(ref pattern) = self.schema.pattern
&& let Ok(re) = Regex::new(pattern)
&& !re.is_match(s)
{
result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(),
message: format!("Pattern mismatch {}", pattern),
path: self.path.to_string(),
});
}
}
Ok(true)

View File

@ -0,0 +1,53 @@
use serde_json::Value;
pub fn is_integer(v: &Value) -> bool {
match v {
Value::Number(n) => {
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
}
_ => false,
}
}
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub fn equals(v1: &Value, v2: &Value) -> bool {
match (v1, v2) {
(Value::Null, Value::Null) => true,
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Number(n1), Value::Number(n2)) => {
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
return (n1 - n2).abs() < f64::EPSILON;
}
false
}
(Value::String(s1), Value::String(s2)) => s1 == s2,
(Value::Array(arr1), Value::Array(arr2)) => {
if arr1.len() != arr2.len() {
return false;
}
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
}
(Value::Object(obj1), Value::Object(obj2)) => {
if obj1.len() != obj2.len() {
return false;
}
for (k1, v1) in obj1 {
if let Some(v2) = obj2.get(k1) {
if !equals(v1, v2) {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
}
}

View File

@ -45,13 +45,13 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
let db_json = group.database.clone();
let db = crate::database::Database::new(&db_json);
let validator = Validator::new(std::sync::Arc::new(db.schemas));
let validator = Validator::new(std::sync::Arc::new(db));
// 4. Run Tests
for (_test_index, test) in group.tests.iter().enumerate() {
for test in group.tests.iter() {
let schema_id = &test.schema_id;
if validator.schemas.get(schema_id).is_none() {
if !validator.db.schemas.contains_key(schema_id) {
failures.push(format!(
"[{}] Missing Schema: Cannot find schema ID '{}'",
group.description, schema_id
@ -89,56 +89,3 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
Ok(())
}
pub fn is_integer(v: &Value) -> bool {
match v {
Value::Number(n) => {
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
}
_ => false,
}
}
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub fn equals(v1: &Value, v2: &Value) -> bool {
// eprintln!("Comparing {:?} with {:?}", v1, v2);
match (v1, v2) {
(Value::Null, Value::Null) => true,
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Number(n1), Value::Number(n2)) => {
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
return (n1 - n2).abs() < f64::EPSILON;
}
false
}
(Value::String(s1), Value::String(s2)) => s1 == s2,
(Value::Array(arr1), Value::Array(arr2)) => {
if arr1.len() != arr2.len() {
return false;
}
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
}
(Value::Object(obj1), Value::Object(obj2)) => {
if obj1.len() != obj2.len() {
return false;
}
for (k1, v1) in obj1 {
if let Some(v2) = obj2.get(k1) {
if !equals(v1, v2) {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
}
}