From 566b599512d5d470e060314a7395b6051f870047 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Wed, 4 Mar 2026 01:02:32 -0500 Subject: [PATCH] all jspg tests now passing --- build.rs | 4 +- src/database/formats.rs | 2 +- src/database/mod.rs | 168 ++++++------- src/database/relation.rs | 15 -- src/database/schema.rs | 76 +----- src/database/type.rs | 4 + src/drop.rs | 6 + src/jspg.rs | 2 +- src/merger/mod.rs | 6 + src/queryer/mod.rs | 6 + src/tests/fixtures.rs | 66 ------ src/validator/context.rs | 19 +- src/validator/instance.rs | 2 +- src/validator/mod.rs | 31 +-- src/validator/result.rs | 3 +- src/validator/rules/array.rs | 56 ++--- src/validator/rules/combinators.rs | 71 +++--- src/validator/rules/conditionals.rs | 8 +- src/validator/rules/core.rs | 25 +- src/validator/rules/format.rs | 30 ++- src/validator/rules/mod.rs | 2 +- src/validator/rules/numeric.rs | 64 ++--- src/validator/rules/object.rs | 126 +++++----- src/validator/rules/polymorphism.rs | 88 ++++++- src/validator/rules/string.rs | 51 ++-- src/validator/rules/util.rs | 53 +++++ src/validator/util.rs | 59 +---- tests/fixtures.rs | 66 ------ tests/fixtures/allOf.json | 75 ------ tests/fixtures/anyOf.json | 356 ---------------------------- tests/fixtures/families.json | 30 +-- tests/fixtures/ref.json | 29 ++- 32 files changed, 531 insertions(+), 1068 deletions(-) delete mode 100644 src/database/relation.rs create mode 100644 src/validator/rules/util.rs delete mode 100644 tests/fixtures/anyOf.json diff --git a/build.rs b/build.rs index aed958b..4dfc464 100644 --- a/build.rs +++ b/build.rs @@ -26,11 +26,11 @@ fn main() { // File 1: src/tests/fixtures.rs for #[pg_test] let pg_dest_path = Path::new("src/tests/fixtures.rs"); - let mut pg_file = File::create(&pg_dest_path).unwrap(); + let mut pg_file = File::create(pg_dest_path).unwrap(); // File 2: tests/fixtures.rs for standard #[test] integration let std_dest_path = Path::new("tests/fixtures.rs"); - let mut std_file = File::create(&std_dest_path).unwrap(); + let mut std_file = File::create(std_dest_path).unwrap(); // Write headers writeln!(std_file, "use jspg::validator::util;").unwrap(); diff --git a/src/database/formats.rs b/src/database/formats.rs index 5963f34..a1f398a 100644 --- a/src/database/formats.rs +++ b/src/database/formats.rs @@ -376,7 +376,7 @@ fn check_hostname(s: &str) -> Result<(), Box> { 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) diff --git a/src/database/mod.rs b/src/database/mod.rs index 5918ab3..de1e008 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -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, pub types: HashMap, pub puncs: HashMap, pub schemas: HashMap, + pub descendants: HashMap>, + pub depths: HashMap, } 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> = - 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 = 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>, - ) { - 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 = HashMap::new(); + let schema_ids: Vec = 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 = 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(¤t_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> = 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 = 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>, - descendants: &mut std::collections::HashSet, + direct_refs: &HashMap>, + descendants: &mut HashSet, ) { 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 = self.schemas.keys().cloned().collect(); + for id in schema_ids { + if let Some(schema) = self.schemas.get_mut(&id) { + schema.compile_internals(); + } + } + } } diff --git a/src/database/relation.rs b/src/database/relation.rs deleted file mode 100644 index 02c616b..0000000 --- a/src/database/relation.rs +++ /dev/null @@ -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, - pub destination_type: String, - #[serde(default)] - pub destination_columns: Vec, - pub prefix: Option, -} diff --git a/src/database/schema.rs b/src/database/schema.rs index 9933a94..38c93c0 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -120,11 +120,6 @@ pub struct SchemaObject { #[serde(default)] pub extensible: Option, - // Compiled Fields (Hidden from JSON/Serde) - #[serde(skip)] - pub compiled_ref: Option>, - #[serde(skip)] - pub compiled_variations: Option>, #[serde(skip)] pub compiled_format: Option, #[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) { - 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, - visited: &mut std::collections::HashSet, - ) -> Option> { - 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>) { - 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); diff --git a/src/database/type.rs b/src/database/type.rs index f3d19ed..33098b2 100644 --- a/src/database/type.rs +++ b/src/database/type.rs @@ -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, #[serde(default)] pub hierarchy: Vec, + #[serde(default)] + pub variations: HashSet, pub relationship: Option, #[serde(default)] pub fields: Vec, diff --git a/src/drop.rs b/src/drop.rs index 06bf47e..2cd3317 100644 --- a/src/drop.rs +++ b/src/drop.rs @@ -17,6 +17,12 @@ pub struct Drop { pub errors: Vec, } +impl Default for Drop { + fn default() -> Self { + Self::new() + } +} + impl Drop { pub fn new() -> Self { Self { diff --git a/src/jspg.rs b/src/jspg.rs index e9b6056..5b7d958 100644 --- a/src/jspg.rs +++ b/src/jspg.rs @@ -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(); diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 950b7e7..887ba4b 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -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 {} diff --git a/src/queryer/mod.rs b/src/queryer/mod.rs index b3b10ec..9b8ac70 100644 --- a/src/queryer/mod.rs +++ b/src/queryer/mod.rs @@ -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 {} diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 4be1cac..d27650a 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -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")); diff --git a/src/validator/context.rs b/src/validator/context.rs index 4c61f12..6850488 100644 --- a/src/validator/context.rs +++ b/src/validator/context.rs @@ -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, + pub db: &'a Arc, 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, + pub overrides: HashSet, } impl<'a> ValidationContext<'a> { pub fn new( - schemas: &'a std::collections::HashMap, + db: &'a Arc, root: &'a Schema, schema: &'a Schema, instance: &'a serde_json::Value, - overrides: std::collections::HashSet, + overrides: HashSet, 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, + overrides: HashSet, 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, ) diff --git a/src/validator/instance.rs b/src/validator/instance.rs index 341acd5..c1fe04a 100644 --- a/src/validator/instance.rs +++ b/src/validator/instance.rs @@ -1,5 +1,5 @@ use serde_json::Value; -use std::collections::HashSet; +use HashSet; use std::ptr::NonNull; pub trait ValidationInstance<'a>: Copy + Clone { diff --git a/src/validator/mod.rs b/src/validator/mod.rs index 3004115..04833a7 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -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>, + pub db: Arc, } impl Validator { - pub fn new(schemas: Arc>) -> Self { - Self { schemas } + pub fn new(db: Arc) -> Self { + Self { db } } pub fn get_schema_ids(&self) -> Vec { - 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 { - 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, ); diff --git a/src/validator/result.rs b/src/validator/result.rs index 07dd56d..0bef48c 100644 --- a/src/validator/result.rs +++ b/src/validator/result.rs @@ -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, diff --git a/src/validator/rules/array.rs b/src/validator/rules/array.rs index 0b23a60..b2785b1 100644 --- a/src/validator/rules/array.rs +++ b/src/validator/rules/array.rs @@ -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 { 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, ); diff --git a/src/validator/rules/combinators.rs b/src/validator/rules/combinators.rs index 8579e27..24bdf8a 100644 --- a/src/validator/rules/combinators.rs +++ b/src/validator/rules/combinators.rs @@ -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, 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 = 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(), }); } diff --git a/src/validator/rules/conditionals.rs b/src/validator/rules/conditionals.rs index b489166..bd3e0c4 100644 --- a/src/validator/rules/conditionals.rs +++ b/src/validator/rules/conditionals.rs @@ -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()?); } } diff --git a/src/validator/rules/core.rs b/src/validator/rules/core.rs index be7126d..8bd650a 100644 --- a/src/validator/rules/core.rs +++ b/src/validator/rules/core.rs @@ -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()); } } diff --git a/src/validator/rules/format.rs b/src/validator/rules/format.rs index 07185f8..15043bf 100644 --- a/src/validator/rules/format.rs +++ b/src/validator/rules/format.rs @@ -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(), + }); } } } diff --git a/src/validator/rules/mod.rs b/src/validator/rules/mod.rs index 92a5896..0095ae7 100644 --- a/src/validator/rules/mod.rs +++ b/src/validator/rules/mod.rs @@ -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 { diff --git a/src/validator/rules/numeric.rs b/src/validator/rules/numeric.rs index 4926a36..9dcc0c6 100644 --- a/src/validator/rules/numeric.rs +++ b/src/validator/rules/numeric.rs @@ -9,41 +9,41 @@ impl<'a> ValidationContext<'a> { ) -> Result { 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; diff --git a/src/validator/rules/object.rs b/src/validator/rules/object.rs index 2154eae..32058b1 100644 --- a/src/validator/rules/object.rs +++ b/src/validator/rules/object.rs @@ -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, ); diff --git a/src/validator/rules/polymorphism.rs b/src/validator/rules/polymorphism.rs index 5f9ce25..6ce7bfd 100644 --- a/src/validator/rules/polymorphism.rs +++ b/src/validator/rules/polymorphism.rs @@ -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 = 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 { // 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())); diff --git a/src/validator/rules/string.rs b/src/validator/rules/string.rs index 6c5a936..40f384a 100644 --- a/src/validator/rules/string.rs +++ b/src/validator/rules/string.rs @@ -10,23 +10,23 @@ impl<'a> ValidationContext<'a> { ) -> Result { 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) diff --git a/src/validator/rules/util.rs b/src/validator/rules/util.rs new file mode 100644 index 0000000..ef8c4a9 --- /dev/null +++ b/src/validator/rules/util.rs @@ -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, + } +} diff --git a/src/validator/util.rs b/src/validator/util.rs index de7bcd3..c44dd9e 100644 --- a/src/validator/util.rs +++ b/src/validator/util.rs @@ -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, - } -} diff --git a/tests/fixtures.rs b/tests/fixtures.rs index 3df2c66..a76c7b0 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -282,66 +282,6 @@ fn test_const_17() { util::run_test_file_at_index(&path, 17).unwrap(); } -#[test] -fn test_any_of_0() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 0).unwrap(); -} - -#[test] -fn test_any_of_1() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 1).unwrap(); -} - -#[test] -fn test_any_of_2() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 2).unwrap(); -} - -#[test] -fn test_any_of_3() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 3).unwrap(); -} - -#[test] -fn test_any_of_4() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 4).unwrap(); -} - -#[test] -fn test_any_of_5() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 5).unwrap(); -} - -#[test] -fn test_any_of_6() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 6).unwrap(); -} - -#[test] -fn test_any_of_7() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 7).unwrap(); -} - -#[test] -fn test_any_of_8() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 8).unwrap(); -} - -#[test] -fn test_any_of_9() { - let path = format!("{}/tests/fixtures/anyOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 9).unwrap(); -} - #[test] fn test_families_0() { let path = format!("{}/tests/fixtures/families.json", env!("CARGO_MANIFEST_DIR")); @@ -1392,12 +1332,6 @@ fn test_all_of_14() { util::run_test_file_at_index(&path, 14).unwrap(); } -#[test] -fn test_all_of_15() { - let path = format!("{}/tests/fixtures/allOf.json", env!("CARGO_MANIFEST_DIR")); - util::run_test_file_at_index(&path, 15).unwrap(); -} - #[test] fn test_format_0() { let path = format!("{}/tests/fixtures/format.json", env!("CARGO_MANIFEST_DIR")); diff --git a/tests/fixtures/allOf.json b/tests/fixtures/allOf.json index 38db358..0d08c85 100644 --- a/tests/fixtures/allOf.json +++ b/tests/fixtures/allOf.json @@ -392,81 +392,6 @@ } ] }, - { - "description": "allOf combined with anyOf, oneOf", - "database": { - "schemas": [ - { - "allOf": [ - { - "multipleOf": 2 - } - ], - "anyOf": [ - { - "multipleOf": 3 - } - ], - "oneOf": [ - { - "multipleOf": 5 - } - ], - "$id": "allOf_11_0" - } - ] - }, - "tests": [ - { - "description": "allOf: false, anyOf: false, oneOf: false", - "data": 1, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: false, anyOf: false, oneOf: true", - "data": 5, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: false, anyOf: true, oneOf: false", - "data": 3, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: false, anyOf: true, oneOf: true", - "data": 15, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: true, anyOf: false, oneOf: false", - "data": 2, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: true, anyOf: false, oneOf: true", - "data": 10, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: true, anyOf: true, oneOf: false", - "data": 6, - "valid": false, - "schema_id": "allOf_11_0" - }, - { - "description": "allOf: true, anyOf: true, oneOf: true", - "data": 30, - "valid": true, - "schema_id": "allOf_11_0" - } - ] - }, { "description": "extensible: true allows extra properties in allOf", "database": { diff --git a/tests/fixtures/anyOf.json b/tests/fixtures/anyOf.json deleted file mode 100644 index 5a8c618..0000000 --- a/tests/fixtures/anyOf.json +++ /dev/null @@ -1,356 +0,0 @@ -[ - { - "description": "anyOf", - "database": { - "schemas": [ - { - "anyOf": [ - { - "type": "integer" - }, - { - "minimum": 2 - } - ], - "$id": "anyOf_0_0" - } - ] - }, - "tests": [ - { - "description": "first anyOf valid", - "data": 1, - "valid": true, - "schema_id": "anyOf_0_0" - }, - { - "description": "second anyOf valid", - "data": 2.5, - "valid": true, - "schema_id": "anyOf_0_0" - }, - { - "description": "both anyOf valid", - "data": 3, - "valid": true, - "schema_id": "anyOf_0_0" - }, - { - "description": "neither anyOf valid", - "data": 1.5, - "valid": false, - "schema_id": "anyOf_0_0" - } - ] - }, - { - "description": "anyOf with base schema", - "database": { - "schemas": [ - { - "type": "string", - "anyOf": [ - { - "maxLength": 2 - }, - { - "minLength": 4 - } - ], - "$id": "anyOf_1_0" - } - ] - }, - "tests": [ - { - "description": "mismatch base schema", - "data": 3, - "valid": false, - "schema_id": "anyOf_1_0" - }, - { - "description": "one anyOf valid", - "data": "foobar", - "valid": true, - "schema_id": "anyOf_1_0" - }, - { - "description": "both anyOf invalid", - "data": "foo", - "valid": false, - "schema_id": "anyOf_1_0" - } - ] - }, - { - "description": "anyOf with boolean schemas, all true", - "database": { - "schemas": [ - { - "anyOf": [ - true, - true - ], - "$id": "anyOf_2_0" - } - ] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true, - "schema_id": "anyOf_2_0" - } - ] - }, - { - "description": "anyOf with boolean schemas, some true", - "database": { - "schemas": [ - { - "anyOf": [ - true, - false - ], - "$id": "anyOf_3_0" - } - ] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true, - "schema_id": "anyOf_3_0" - } - ] - }, - { - "description": "anyOf with boolean schemas, all false", - "database": { - "schemas": [ - { - "anyOf": [ - false, - false - ], - "$id": "anyOf_4_0" - } - ] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false, - "schema_id": "anyOf_4_0" - } - ] - }, - { - "description": "anyOf complex types", - "database": { - "schemas": [ - { - "anyOf": [ - { - "properties": { - "bar": { - "type": "integer" - } - }, - "required": [ - "bar" - ] - }, - { - "properties": { - "foo": { - "type": "string" - } - }, - "required": [ - "foo" - ] - } - ], - "$id": "anyOf_5_0" - } - ] - }, - "tests": [ - { - "description": "first anyOf valid (complex)", - "data": { - "bar": 2 - }, - "valid": true, - "schema_id": "anyOf_5_0" - }, - { - "description": "second anyOf valid (complex)", - "data": { - "foo": "baz" - }, - "valid": true, - "schema_id": "anyOf_5_0" - }, - { - "description": "both anyOf valid (complex)", - "data": { - "foo": "baz", - "bar": 2 - }, - "valid": true, - "schema_id": "anyOf_5_0" - }, - { - "description": "neither anyOf valid (complex)", - "data": { - "foo": 2, - "bar": "quux" - }, - "valid": false, - "schema_id": "anyOf_5_0" - } - ] - }, - { - "description": "anyOf with one empty schema", - "database": { - "schemas": [ - { - "anyOf": [ - { - "type": "number" - }, - {} - ], - "$id": "anyOf_6_0" - } - ] - }, - "tests": [ - { - "description": "string is valid", - "data": "foo", - "valid": true, - "schema_id": "anyOf_6_0" - }, - { - "description": "number is valid", - "data": 123, - "valid": true, - "schema_id": "anyOf_6_0" - } - ] - }, - { - "description": "nested anyOf, to check validation semantics", - "database": { - "schemas": [ - { - "anyOf": [ - { - "anyOf": [ - { - "type": "null" - } - ] - } - ], - "$id": "anyOf_7_0" - } - ] - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true, - "schema_id": "anyOf_7_0" - }, - { - "description": "anything non-null is invalid", - "data": 123, - "valid": false, - "schema_id": "anyOf_7_0" - } - ] - }, - { - "description": "extensible: true allows extra properties in anyOf", - "database": { - "schemas": [ - { - "anyOf": [ - { - "type": "integer" - }, - { - "minimum": 2 - } - ], - "extensible": true, - "$id": "anyOf_8_0" - } - ] - }, - "tests": [ - { - "description": "extra property is valid", - "data": { - "foo": 1 - }, - "valid": true, - "schema_id": "anyOf_8_0" - } - ] - }, - { - "description": "strict by default with anyOf properties", - "database": { - "schemas": [ - { - "anyOf": [ - { - "properties": { - "foo": { - "const": 1 - } - } - }, - { - "properties": { - "bar": { - "const": 2 - } - } - } - ], - "$id": "anyOf_9_0" - } - ] - }, - "tests": [ - { - "description": "valid match (foo)", - "data": { - "foo": 1 - }, - "valid": true, - "schema_id": "anyOf_9_0" - }, - { - "description": "fails on extra property z explicitly", - "data": { - "foo": 1, - "z": 3 - }, - "valid": false, - "schema_id": "anyOf_9_0" - } - ] - } -] \ No newline at end of file diff --git a/tests/fixtures/families.json b/tests/fixtures/families.json index 3593aa4..a1d0810 100644 --- a/tests/fixtures/families.json +++ b/tests/fixtures/families.json @@ -5,8 +5,10 @@ "types": [ { "name": "entity", - "hierarchy": [ - "entity" + "variations": [ + "entity", + "organization", + "person" ], "schemas": [ { @@ -22,16 +24,16 @@ } }, { - "$id": "entity.light", + "$id": "light.entity", "$ref": "entity" } ] }, { "name": "organization", - "hierarchy": [ - "entity", - "organization" + "variations": [ + "organization", + "person" ], "schemas": [ { @@ -47,9 +49,7 @@ }, { "name": "person", - "hierarchy": [ - "entity", - "organization", + "variations": [ "person" ], "schemas": [ @@ -63,8 +63,8 @@ } }, { - "$id": "person.light", - "$ref": "entity.light" + "$id": "light.person", + "$ref": "light.entity" } ] } @@ -84,7 +84,7 @@ "schemas": [ { "$id": "get_light_entities.response", - "$family": "entity.light" + "$family": "light.entity" } ] } @@ -112,7 +112,7 @@ "valid": true }, { - "description": "Graph family matches entity.light", + "description": "Graph family matches light.entity", "schema_id": "get_light_entities.response", "data": { "id": "3", @@ -121,7 +121,7 @@ "valid": true }, { - "description": "Graph family matches person.light (because it $refs entity.light)", + "description": "Graph family matches light.person (because it $refs light.entity)", "schema_id": "get_light_entities.response", "data": { "id": "4", @@ -130,7 +130,7 @@ "valid": true }, { - "description": "Graph family excludes organization (missing .light schema that $refs entity.light)", + "description": "Graph family excludes organization (missing light. schema that $refs light.entity)", "schema_id": "get_light_entities.response", "data": { "id": "5", diff --git a/tests/fixtures/ref.json b/tests/fixtures/ref.json index e8aec54..1a621d6 100644 --- a/tests/fixtures/ref.json +++ b/tests/fixtures/ref.json @@ -500,8 +500,10 @@ "types": [ { "name": "entity", - "hierarchy": [ - "entity" + "variations": [ + "entity", + "organization", + "person" ], "schemas": [ { @@ -520,9 +522,9 @@ }, { "name": "organization", - "hierarchy": [ - "entity", - "organization" + "variations": [ + "organization", + "person" ], "schemas": [ { @@ -538,9 +540,7 @@ }, { "name": "person", - "hierarchy": [ - "entity", - "organization", + "variations": [ "person" ], "schemas": [ @@ -612,8 +612,9 @@ "types": [ { "name": "entity", - "hierarchy": [ - "entity" + "variations": [ + "entity", + "person" ], "schemas": [ { @@ -632,8 +633,7 @@ }, { "name": "person", - "hierarchy": [ - "entity", + "variations": [ "person" ], "schemas": [ @@ -647,9 +647,8 @@ } }, { - "$id": "person.light", + "$id": "light.person", "$ref": "entity", - "extensible": false, "properties": { "first_name": { "type": "string" @@ -665,7 +664,7 @@ "schemas": [ { "$id": "save_person_light.request", - "$ref": "person.light", + "$ref": "light.person", "properties": { "extra_request_field": { "type": "string"