validator refactor progress

This commit is contained in:
2026-03-03 00:13:37 -05:00
parent e14f53e7d9
commit 3898c43742
81 changed files with 6331 additions and 7934 deletions

12
src/database/enum.rs Normal file
View File

@ -0,0 +1,12 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Enum {
pub name: String,
pub module: String,
pub source: String,
pub values: Vec<String>,
pub schemas: Vec<Schema>,
}

220
src/database/mod.rs Normal file
View File

@ -0,0 +1,220 @@
pub mod r#enum;
pub mod formats;
pub mod page;
pub mod punc;
pub mod schema;
pub mod r#type;
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;
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>>,
}
impl Database {
pub fn new(val: &serde_json::Value) -> Self {
let mut db = Self {
enums: HashMap::new(),
types: HashMap::new(),
puncs: HashMap::new(),
schemas: HashMap::new(),
descendants: HashMap::new(),
};
if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) {
for item in arr {
if let Ok(def) = serde_json::from_value::<Enum>(item.clone()) {
db.enums.insert(def.name.clone(), def);
}
}
}
if let Some(arr) = val.get("types").and_then(|v| v.as_array()) {
for item in arr {
if let Ok(def) = serde_json::from_value::<Type>(item.clone()) {
db.types.insert(def.name.clone(), def);
}
}
}
if let Some(arr) = val.get("puncs").and_then(|v| v.as_array()) {
for item in arr {
if let Ok(def) = serde_json::from_value::<Punc>(item.clone()) {
db.puncs.insert(def.name.clone(), def);
}
}
}
if let Some(arr) = val.get("schemas").and_then(|v| v.as_array()) {
for (i, item) in arr.iter().enumerate() {
if let Ok(mut schema) = serde_json::from_value::<Schema>(item.clone()) {
let id = schema
.obj
.id
.clone()
.unwrap_or_else(|| format!("schema_{}", i));
schema.obj.id = Some(id.clone());
db.schemas.insert(id, schema);
}
}
}
let _ = db.compile();
db
}
/// Organizes the graph of the database, compiling regex, format functions, and pointing schema references.
fn compile(&mut self) -> Result<(), String> {
self.collect_schemas();
// 1. Compile regex and formats sequentially
for schema in self.schemas.values_mut() {
schema.compile();
}
// 2. Compute the Unified Semantic Graph (descendants)
self.collect_descendents();
// 3. For any schema representing a Postgres table, cache its allowed subclasses
self.compile_allowed_types();
// 4. Finally, securely link all string $refs into memory pointers (Arc)
self.compile_pointers();
Ok(())
}
fn collect_schemas(&mut self) {
let mut to_insert = Vec::new();
for (_, type_def) in &self.types {
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
to_insert.push((id.clone(), schema.clone()));
}
}
}
for (_, punc_def) in &self.puncs {
for schema in &punc_def.schemas {
if let Some(id) = &schema.obj.id {
to_insert.push((id.clone(), schema.clone()));
}
}
}
for (_, enum_def) in &self.enums {
for schema in &enum_def.schemas {
if let Some(id) = &schema.obj.id {
to_insert.push((id.clone(), schema.clone()));
}
}
}
for (id, schema) in to_insert {
self.schemas.insert(id, schema);
}
}
fn collect_descendents(&mut self) {
let mut direct_children: HashMap<String, Vec<String>> = HashMap::new();
// First pass: Find all schemas that have a $ref to another schema
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
if let Some(ref_str) = self.schemas.get(&id).and_then(|s| s.obj.ref_string.clone()) {
if self.schemas.contains_key(&ref_str) {
direct_children.entry(ref_str).or_default().push(id.clone());
}
}
}
// Now compute descendants for all schemas
let mut descendants_map: HashMap<String, Vec<String>> = HashMap::new();
for key in self.schemas.keys() {
let mut descendants = Vec::new();
let mut queue = Vec::new();
if let Some(children) = direct_children.get(key) {
queue.extend(children.iter().cloned());
}
let mut visited = std::collections::HashSet::new();
while let Some(child) = queue.pop() {
if visited.insert(child.clone()) {
descendants.push(child.clone());
if let Some(grandchildren) = direct_children.get(&child) {
queue.extend(grandchildren.iter().cloned());
}
}
}
descendants_map.insert(key.clone(), descendants);
}
self.descendants = descendants_map;
}
fn compile_allowed_types(&mut self) {
// 1. Identify which types act as bases (table-backed schemas)
let mut entity_bases = HashMap::new();
for type_def in self.types.values() {
for type_schema in &type_def.schemas {
if let Some(id) = &type_schema.obj.id {
entity_bases.insert(id.clone(), type_def.name.clone());
}
}
}
// 2. Compute compiled_allowed_types for all descendants of entity bases
let mut allowed_types_map: HashMap<String, std::collections::HashSet<String>> = HashMap::new();
for base_id in entity_bases.keys() {
allowed_types_map.insert(
base_id.clone(),
self
.descendants
.get(base_id)
.unwrap_or(&vec![])
.iter()
.cloned()
.collect(),
);
if let Some(descendants) = self.descendants.get(base_id) {
let set: std::collections::HashSet<String> = descendants.iter().cloned().collect();
for desc_id in descendants {
allowed_types_map.insert(desc_id.clone(), set.clone());
}
}
}
// 3. Inject types into the schemas
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
if let Some(set) = allowed_types_map.get(&id) {
if let Some(schema) = self.schemas.get_mut(&id) {
schema.obj.compiled_allowed_types = Some(set.clone());
}
}
}
}
fn compile_pointers(&mut self) {
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
let mut compiled_ref = None;
if let Some(schema) = self.schemas.get(&id) {
if let Some(ref_str) = &schema.obj.ref_string {
if let Some(target) = self.schemas.get(ref_str) {
compiled_ref = Some(std::sync::Arc::new(target.clone()));
}
}
}
if let Some(schema) = self.schemas.get_mut(&id) {
schema.obj.compiled_ref = compiled_ref;
}
}
}
}

35
src/database/page.rs Normal file
View File

@ -0,0 +1,35 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Page {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Sidebar {
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Action {
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub navigate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub present: Option<String>,
}

20
src/database/punc.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::database::page::Page;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Punc {
pub id: String,
pub r#type: String,
pub name: String,
pub module: String,
pub source: String,
pub description: Option<String>,
pub public: bool,
pub form: bool,
pub get: Option<String>,
pub page: Option<Page>,
#[serde(default)]
pub schemas: Vec<Schema>,
}

15
src/database/relation.rs Normal file
View File

@ -0,0 +1,15 @@
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

@ -12,12 +12,6 @@ pub struct SchemaObject {
pub id: Option<String>,
#[serde(rename = "$ref")]
pub ref_string: Option<String>,
#[serde(rename = "$anchor")]
pub anchor: Option<String>,
#[serde(rename = "$dynamicAnchor")]
pub dynamic_anchor: Option<String>,
#[serde(rename = "$dynamicRef")]
pub dynamic_ref: Option<String>,
/*
Note: The `Ref` field in the Go struct is a pointer populated by the linker.
In Rust, we might handle this differently (e.g., separate lookup or Rc/Arc),
@ -43,12 +37,6 @@ pub struct SchemaObject {
// dependencies can be schema dependencies or property dependencies
pub dependencies: Option<BTreeMap<String, Dependency>>,
// Definitions (for $ref resolution)
#[serde(rename = "$defs")]
pub defs: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "definitions")]
pub definitions: Option<BTreeMap<String, Arc<Schema>>>,
// Array Keywords
#[serde(rename = "items")]
pub items: Option<Arc<Schema>>,
@ -83,10 +71,6 @@ pub struct SchemaObject {
pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")]
pub property_names: Option<Arc<Schema>>,
#[serde(rename = "dependentRequired")]
pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
#[serde(rename = "dependentSchemas")]
pub dependent_schemas: Option<BTreeMap<String, Arc<Schema>>>,
// Numeric Validation
pub format: Option<String>,
@ -138,15 +122,42 @@ pub struct SchemaObject {
// Compiled Fields (Hidden from JSON/Serde)
#[serde(skip)]
pub compiled_format: Option<crate::validator::compiler::CompiledFormat>,
pub compiled_ref: Option<Arc<Schema>>,
#[serde(skip)]
pub compiled_pattern: Option<crate::validator::compiler::CompiledRegex>,
pub compiled_allowed_types: Option<std::collections::HashSet<String>>,
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(crate::validator::compiler::CompiledRegex, Arc<Schema>)>>,
pub compiled_format: Option<CompiledFormat>,
#[serde(skip)]
pub compiled_registry: Option<Arc<crate::validator::registry::Registry>>,
pub compiled_pattern: Option<CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(CompiledRegex, Arc<Schema>)>>,
}
pub enum ResolvedRef<'a> {
Local(&'a Schema),
Global(&'a Schema, &'a Schema),
}
/// Represents a compiled format validator
#[derive(Clone)]
pub enum CompiledFormat {
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
Regex(regex::Regex),
}
impl std::fmt::Debug for CompiledFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"),
CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r),
}
}
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub regex::Regex);
#[derive(Debug, Clone, Serialize)]
pub struct Schema {
#[serde(flatten)]
@ -176,6 +187,129 @@ impl std::ops::DerefMut for Schema {
}
}
impl Schema {
pub fn resolve_ref(&self, _ref_string: &str) -> Option<&Arc<Schema>> {
// This is vestigial for now. References are global pointers. We will remove this shortly.
None
}
pub fn compile(&mut self) {
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(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_props) = &self.obj.pattern_properties {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::schema::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
self.obj.compiled_pattern_properties = Some(compiled);
}
}
// Crawl children recursively to compile their internals
if let Some(props) = &mut self.obj.properties {
for (_, v) in props {
// Safe deep mutation workaround without unsafe Arc unwrap
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
if let Some(arr) = &mut self.obj.prefix_items {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
if let Some(arr) = &mut self.obj.all_of {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
if let Some(arr) = &mut self.obj.any_of {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
if let Some(arr) = &mut self.obj.one_of {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
if let Some(v) = &mut self.obj.additional_properties {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.items {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.contains {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.property_names {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.not {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.if_ {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.then_ {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
if let Some(v) = &mut self.obj.else_ {
let mut inner = (**v).clone();
inner.compile();
*v = Arc::new(inner);
}
}
}
impl<'de> Deserialize<'de> for Schema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where

35
src/database/type.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Type {
pub id: String,
pub r#type: String,
pub name: String,
pub module: String,
pub source: String,
#[serde(default)]
pub historical: bool,
#[serde(default)]
pub sensitive: bool,
#[serde(default)]
pub ownable: bool,
pub longevity: Option<i32>,
#[serde(default)]
pub hierarchy: Vec<String>,
pub relationship: Option<bool>,
#[serde(default)]
pub fields: Vec<String>,
pub grouped_fields: Option<Value>,
#[serde(default)]
pub lookup_fields: Vec<String>,
#[serde(default)]
pub null_fields: Vec<String>,
#[serde(default)]
pub default_fields: Vec<String>,
pub field_types: Option<Value>,
#[serde(default)]
pub schemas: Vec<Schema>,
}

29
src/jspg.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::database::Database;
use crate::merger::Merger;
use crate::queryer::Queryer;
use crate::validator::Validator;
use std::sync::Arc;
pub struct Jspg {
pub database: Arc<Database>,
pub validator: Validator,
pub queryer: Queryer,
pub merger: Merger,
}
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 queryer = Queryer::new();
let merger = Merger::new();
Self {
database,
validator,
queryer,
merger,
}
}
}

View File

@ -2,7 +2,11 @@ use pgrx::*;
pg_module_magic!();
pub mod database;
pub mod drop;
pub mod jspg;
pub mod merger;
pub mod queryer;
pub mod validator;
use serde_json::json;
@ -12,100 +16,38 @@ lazy_static::lazy_static! {
// Global Atomic Swap Container:
// - RwLock: To protect the SWAP of the Option.
// - Option: Because it starts empty.
// - Arc: Because multiple running threads might hold the OLD validator while we swap.
// - Validator: It immutably owns the Registry.
static ref GLOBAL_VALIDATOR: RwLock<Option<Arc<validator::Validator>>> = RwLock::new(None);
// - Arc: Because multiple running threads might hold the OLD engine while we swap.
// - Jspg: The root semantic engine encapsulating the database metadata, validator, queryer, and merger.
static ref GLOBAL_JSPG: RwLock<Option<Arc<jspg::Jspg>>> = RwLock::new(None);
}
#[pg_extern(strict)]
pub fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
// 1 & 2. Build Registry, Families, and Wrap in Validator all in one shot
let new_validator = crate::validator::Validator::from_punc_definition(
Some(&enums.0),
Some(&types.0),
Some(&puncs.0),
);
let new_arc = Arc::new(new_validator);
pub fn jspg_cache_database(database: JsonB) -> JsonB {
let new_jspg = crate::jspg::Jspg::new(&database.0);
let new_arc = Arc::new(new_jspg);
// 3. ATOMIC SWAP
{
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
let mut lock = GLOBAL_JSPG.write().unwrap();
*lock = Some(new_arc);
}
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
}
#[pg_extern(strict, parallel_safe)]
pub fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
// We need a mutable copy of the value to mask it
let mut mutable_instance = instance.0.clone();
match validator.mask(schema_id, &mut mutable_instance) {
Ok(result) => {
// If valid, return the MASKED instance
if result.is_valid() {
let drop = crate::drop::Drop::success_with_val(mutable_instance);
JsonB(serde_json::to_value(drop).unwrap())
} else {
// If invalid, return errors (Schema Validation Errors)
let errors: Vec<crate::drop::Error> = result
.errors
.into_iter()
.map(|e| crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
})
.collect();
let drop = crate::drop::Drop::with_errors(errors);
JsonB(serde_json::to_value(drop).unwrap())
}
}
Err(e) => {
// Schema Not Found or other fatal error
let error = crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
// `mask_json_schema` has been removed as the mask architecture is fully replaced by Spi string queries during DB interactions.
#[pg_extern(strict, parallel_safe)]
pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
let jspg_arc = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
match validator.validate(schema_id, &instance.0) {
if let Some(engine) = jspg_arc {
match engine.validator.validate(schema_id, &instance.0) {
Ok(result) => {
if result.is_valid() {
let drop = crate::drop::Drop::success();
@ -137,7 +79,7 @@ pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
message: "The JSPG database has not been cached yet. Run jspg_cache_database()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
@ -149,8 +91,11 @@ pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
#[pg_extern(strict, parallel_safe)]
pub fn json_schema_cached(schema_id: &str) -> bool {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
match validator.validate(schema_id, &serde_json::Value::Null) {
if let Some(engine) = GLOBAL_JSPG.read().unwrap().as_ref() {
match engine
.validator
.validate(schema_id, &serde_json::Value::Null)
{
Err(e) if e.code == "SCHEMA_NOT_FOUND" => false,
_ => true,
}
@ -161,7 +106,7 @@ pub fn json_schema_cached(schema_id: &str) -> bool {
#[pg_extern(strict)]
pub fn clear_json_schemas() -> JsonB {
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
let mut lock = GLOBAL_JSPG.write().unwrap();
*lock = None;
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
@ -169,8 +114,8 @@ pub fn clear_json_schemas() -> JsonB {
#[pg_extern(strict, parallel_safe)]
pub fn show_json_schemas() -> JsonB {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
let mut keys = validator.get_schema_ids();
if let Some(engine) = GLOBAL_JSPG.read().unwrap().as_ref() {
let mut keys = engine.validator.get_schema_ids();
keys.sort();
let drop = crate::drop::Drop::success_with_val(json!(keys));
JsonB(serde_json::to_value(drop).unwrap())

9
src/merger/mod.rs Normal file
View File

@ -0,0 +1,9 @@
pub struct Merger {
// To be implemented
}
impl Merger {
pub fn new() -> Self {
Self {}
}
}

9
src/queryer/mod.rs Normal file
View File

@ -0,0 +1,9 @@
pub struct Queryer {
// To be implemented
}
impl Queryer {
pub fn new() -> Self {
Self {}
}
}

View File

@ -1,28 +1,4 @@
#[pg_test]
fn test_anchor_0() {
let path = format!("{}/tests/fixtures/anchor.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_anchor_1() {
let path = format!("{}/tests/fixtures/anchor.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_anchor_2() {
let path = format!("{}/tests/fixtures/anchor.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_anchor_3() {
let path = format!("{}/tests/fixtures/anchor.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_content_0() {
let path = format!("{}/tests/fixtures/content.json", env!("CARGO_MANIFEST_DIR"));
@ -107,54 +83,6 @@ fn test_min_items_2() {
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_puncs_0() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_puncs_1() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_puncs_2() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_puncs_3() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_puncs_4() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 4).unwrap();
}
#[pg_test]
fn test_puncs_5() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 5).unwrap();
}
#[pg_test]
fn test_puncs_6() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 6).unwrap();
}
#[pg_test]
fn test_puncs_7() {
let path = format!("{}/tests/fixtures/puncs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 7).unwrap();
}
#[pg_test]
fn test_additional_properties_0() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
@ -347,6 +275,18 @@ fn test_any_of_9() {
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"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_families_1() {
let path = format!("{}/tests/fixtures/families.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_property_names_0() {
let path = format!("{}/tests/fixtures/propertyNames.json", env!("CARGO_MANIFEST_DIR"));
@ -389,18 +329,6 @@ fn test_property_names_6() {
crate::validator::util::run_test_file_at_index(&path, 6).unwrap();
}
#[pg_test]
fn test_boolean_schema_0() {
let path = format!("{}/tests/fixtures/boolean_schema.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_boolean_schema_1() {
let path = format!("{}/tests/fixtures/boolean_schema.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_not_0() {
let path = format!("{}/tests/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
@ -569,6 +497,30 @@ fn test_items_15() {
crate::validator::util::run_test_file_at_index(&path, 15).unwrap();
}
#[pg_test]
fn test_typed_refs_0() {
let path = format!("{}/tests/fixtures/typedRefs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_typed_refs_1() {
let path = format!("{}/tests/fixtures/typedRefs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_typed_refs_2() {
let path = format!("{}/tests/fixtures/typedRefs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_typed_refs_3() {
let path = format!("{}/tests/fixtures/typedRefs.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_enum_0() {
let path = format!("{}/tests/fixtures/enum.json", env!("CARGO_MANIFEST_DIR"));
@ -1013,6 +965,18 @@ fn test_one_of_12() {
crate::validator::util::run_test_file_at_index(&path, 12).unwrap();
}
#[pg_test]
fn test_boolean_schema_0() {
let path = format!("{}/tests/fixtures/booleanSchema.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_boolean_schema_1() {
let path = format!("{}/tests/fixtures/booleanSchema.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_if_then_else_0() {
let path = format!("{}/tests/fixtures/if-then-else.json", env!("CARGO_MANIFEST_DIR"));
@ -1960,129 +1924,3 @@ fn test_contains_8() {
let path = format!("{}/tests/fixtures/contains.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 8).unwrap();
}
#[pg_test]
fn test_dynamic_ref_0() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_dynamic_ref_1() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_dynamic_ref_2() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_dynamic_ref_3() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_dynamic_ref_4() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 4).unwrap();
}
#[pg_test]
fn test_dynamic_ref_5() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 5).unwrap();
}
#[pg_test]
fn test_dynamic_ref_6() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 6).unwrap();
}
#[pg_test]
fn test_dynamic_ref_7() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 7).unwrap();
}
#[pg_test]
fn test_dynamic_ref_8() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 8).unwrap();
}
#[pg_test]
fn test_dynamic_ref_9() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 9).unwrap();
}
#[pg_test]
fn test_dynamic_ref_10() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 10).unwrap();
}
#[pg_test]
fn test_dynamic_ref_11() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 11).unwrap();
}
#[pg_test]
fn test_dynamic_ref_12() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 12).unwrap();
}
#[pg_test]
fn test_dynamic_ref_13() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 13).unwrap();
}
#[pg_test]
fn test_dynamic_ref_14() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 14).unwrap();
}
#[pg_test]
fn test_dynamic_ref_15() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 15).unwrap();
}
#[pg_test]
fn test_dynamic_ref_16() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 16).unwrap();
}
#[pg_test]
fn test_dynamic_ref_17() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 17).unwrap();
}
#[pg_test]
fn test_dynamic_ref_18() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 18).unwrap();
}
#[pg_test]
fn test_dynamic_ref_19() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 19).unwrap();
}
#[pg_test]
fn test_dynamic_ref_20() {
let path = format!("{}/tests/fixtures/dynamicRef.json", env!("CARGO_MANIFEST_DIR"));
crate::validator::util::run_test_file_at_index(&path, 20).unwrap();
}

View File

@ -1,394 +0,0 @@
use crate::validator::schema::Schema;
use regex::Regex;
use serde_json::Value;
// use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
/// Represents a compiled format validator
#[derive(Debug, Clone)]
pub enum CompiledFormat {
/// A simple function pointer validator
Func(fn(&Value) -> Result<(), Box<dyn Error + Send + Sync>>),
/// A regex-based validator
Regex(Regex),
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub Regex);
/// The Compiler is responsible for pre-calculating high-cost schema operations
pub struct Compiler;
impl Compiler {
/// Internal: Compiles formats and regexes in-place
fn compile_formats_and_regexes(schema: &mut Schema) {
// 1. Compile Format
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::validator::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
// 2. Compile Pattern (regex)
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// 2.5 Compile Pattern Properties
if let Some(pp) = &schema.pattern_properties {
let mut compiled_pp = Vec::new();
for (pattern, sub_schema) in pp {
if let Ok(re) = Regex::new(pattern) {
compiled_pp.push((CompiledRegex(re), sub_schema.clone()));
} else {
eprintln!(
"Invalid patternProperty regex in schema (compile time): {}",
pattern
);
}
}
if !compiled_pp.is_empty() {
schema.compiled_pattern_properties = Some(compiled_pp);
}
}
// 3. Recurse
Self::compile_recursive(schema);
}
fn normalize_dependencies(schema: &mut Schema) {
if let Some(deps) = schema.dependencies.take() {
for (key, dep) in deps {
match dep {
crate::validator::schema::Dependency::Props(props) => {
schema
.dependent_required
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, props);
}
crate::validator::schema::Dependency::Schema(sub_schema) => {
schema
.dependent_schemas
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, sub_schema);
}
}
}
}
}
fn compile_recursive(schema: &mut Schema) {
Self::normalize_dependencies(schema);
// Compile self
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::validator::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// Recurse
if let Some(defs) = &mut schema.definitions {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(defs) = &mut schema.defs {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(props) = &mut schema.properties {
for s in props.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(add_props) = &mut schema.additional_properties {
Self::compile_recursive(Arc::make_mut(add_props));
}
// ... Recurse logic ...
if let Some(items) = &mut schema.items {
Self::compile_recursive(Arc::make_mut(items));
}
if let Some(prefix_items) = &mut schema.prefix_items {
for s in prefix_items {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(not) = &mut schema.not {
Self::compile_recursive(Arc::make_mut(not));
}
if let Some(all_of) = &mut schema.all_of {
for s in all_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(any_of) = &mut schema.any_of {
for s in any_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(one_of) = &mut schema.one_of {
for s in one_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(s) = &mut schema.if_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.then_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.else_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(ds) = &mut schema.dependent_schemas {
for s in ds.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(pn) = &mut schema.property_names {
Self::compile_recursive(Arc::make_mut(pn));
}
}
/// Recursively traverses the schema tree to build the local registry index.
fn compile_index(
schema: &Arc<Schema>,
registry: &mut crate::validator::registry::Registry,
parent_base: Option<String>,
pointer: json_pointer::JsonPointer<String, Vec<String>>,
) {
// 1. Index using Parent Base (Path from Parent)
if let Some(base) = &parent_base {
// We use the pointer's string representation (e.g., "/definitions/foo")
// and append it to the base.
let fragment = pointer.to_string();
let ptr_uri = if fragment.is_empty() {
base.clone()
} else {
format!("{}#{}", base, fragment)
};
registry.insert(ptr_uri, schema.clone());
}
// 2. Determine Current Scope... (unchanged logic)
let mut current_base = parent_base.clone();
let mut child_pointer = pointer.clone();
if let Some(id) = &schema.obj.id {
let mut new_base = None;
if let Ok(_) = url::Url::parse(id) {
new_base = Some(id.clone());
} else if let Some(base) = &current_base {
if let Ok(base_url) = url::Url::parse(base) {
if let Ok(joined) = base_url.join(id) {
new_base = Some(joined.to_string());
}
}
} else {
new_base = Some(id.clone());
}
if let Some(base) = new_base {
// println!("DEBUG: Compiling index for path: {}", base); // Added println
registry.insert(base.clone(), schema.clone());
current_base = Some(base);
child_pointer = json_pointer::JsonPointer::new(vec![]); // Reset
}
}
// 3. Index by Anchor
if let Some(anchor) = &schema.obj.anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// Index by Dynamic Anchor
if let Some(d_anchor) = &schema.obj.dynamic_anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, d_anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// 4. Recurse (unchanged logic structure, just passing registry)
if let Some(defs) = schema.defs.as_ref().or(schema.definitions.as_ref()) {
let segment = if schema.defs.is_some() {
"$defs"
} else {
"definitions"
};
for (key, sub_schema) in defs {
let mut sub = child_pointer.clone();
sub.push(segment.to_string());
let decoded_key = percent_encoding::percent_decode_str(key).decode_utf8_lossy();
sub.push(decoded_key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(props) = &schema.properties {
for (key, sub_schema) in props {
let mut sub = child_pointer.clone();
sub.push("properties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(items) = &schema.items {
let mut sub = child_pointer.clone();
sub.push("items".to_string());
Self::compile_index(items, registry, current_base.clone(), sub);
}
if let Some(prefix_items) = &schema.prefix_items {
for (i, sub_schema) in prefix_items.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("prefixItems".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(all_of) = &schema.all_of {
for (i, sub_schema) in all_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("allOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(any_of) = &schema.any_of {
for (i, sub_schema) in any_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("anyOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(one_of) = &schema.one_of {
for (i, sub_schema) in one_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("oneOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(not) = &schema.not {
let mut sub = child_pointer.clone();
sub.push("not".to_string());
Self::compile_index(not, registry, current_base.clone(), sub);
}
if let Some(if_) = &schema.if_ {
let mut sub = child_pointer.clone();
sub.push("if".to_string());
Self::compile_index(if_, registry, current_base.clone(), sub);
}
if let Some(then_) = &schema.then_ {
let mut sub = child_pointer.clone();
sub.push("then".to_string());
Self::compile_index(then_, registry, current_base.clone(), sub);
}
if let Some(else_) = &schema.else_ {
let mut sub = child_pointer.clone();
sub.push("else".to_string());
Self::compile_index(else_, registry, current_base.clone(), sub);
}
if let Some(deps) = &schema.dependent_schemas {
for (key, sub_schema) in deps {
let mut sub = child_pointer.clone();
sub.push("dependentSchemas".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(pp) = &schema.pattern_properties {
for (key, sub_schema) in pp {
let mut sub = child_pointer.clone();
sub.push("patternProperties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(add_props) = &schema.additional_properties {
let mut sub = child_pointer.clone();
sub.push("additionalProperties".to_string());
Self::compile_index(add_props, registry, current_base.clone(), sub);
}
if let Some(contains) = &schema.contains {
let mut sub = child_pointer.clone();
sub.push("contains".to_string());
Self::compile_index(contains, registry, current_base.clone(), sub);
}
if let Some(property_names) = &schema.property_names {
let mut sub = child_pointer.clone();
sub.push("propertyNames".to_string());
Self::compile_index(property_names, registry, current_base.clone(), sub);
}
}
pub fn compile(mut root_schema: Schema, root_id: Option<String>) -> Arc<Schema> {
// 1. Compile in-place (formats/regexes/normalization)
Self::compile_formats_and_regexes(&mut root_schema);
// Apply root_id override if schema ID is missing
if let Some(rid) = &root_id {
if root_schema.obj.id.is_none() {
root_schema.obj.id = Some(rid.clone());
}
}
// 2. Build ID/Pointer Index
let mut registry = crate::validator::registry::Registry::new();
// We need a temporary Arc to satisfy compile_index recursion
// But we are modifying root_schema.
// This is tricky. compile_index takes &Arc<Schema>.
// We should build the index first, THEN attach it.
let root = Arc::new(root_schema);
// Default base_uri to ""
let base_uri = root_id
.clone()
.or_else(|| root.obj.id.clone())
.or(Some("".to_string()));
Self::compile_index(
&root,
&mut registry,
base_uri,
json_pointer::JsonPointer::new(vec![]),
);
// Also ensure root id is indexed if present
if let Some(rid) = root_id {
registry.insert(rid, root.clone());
}
// Now we need to attach this registry to the root schema.
// Since root is an Arc, we might need to recreate it if we can't mutate.
// Schema struct modifications require &mut.
let mut final_schema = Arc::try_unwrap(root).unwrap_or_else(|arc| (*arc).clone());
final_schema.obj.compiled_registry = Some(Arc::new(registry));
Arc::new(final_schema)
}
}

View File

@ -1,44 +1,60 @@
use crate::validator::schema::Schema;
use crate::validator::Validator;
use crate::database::schema::Schema;
use crate::validator::error::ValidationError;
use crate::validator::instance::ValidationInstance;
use crate::validator::result::ValidationResult;
use std::collections::HashSet;
pub struct ValidationContext<'a, I: ValidationInstance<'a>> {
pub validator: &'a Validator,
pub struct ValidationContext<'a> {
pub schemas: &'a std::collections::HashMap<String, Schema>,
pub root: &'a Schema,
pub schema: &'a Schema,
pub instance: I,
pub instance: &'a serde_json::Value,
pub path: String,
pub depth: usize,
pub scope: Vec<String>,
pub overrides: HashSet<String>,
pub extensible: bool,
pub reporter: bool,
}
impl<'a, I: ValidationInstance<'a>> ValidationContext<'a, I> {
impl<'a> ValidationContext<'a> {
pub fn resolve_ref(
&self,
ref_string: &str,
) -> Option<(crate::database::schema::ResolvedRef<'a>, String)> {
if let Some(local_schema_arc) = self.root.resolve_ref(ref_string) {
if ref_string.starts_with('#') {
return Some((
crate::database::schema::ResolvedRef::Local(local_schema_arc.as_ref()),
ref_string.to_string(),
));
}
}
// We will replace all of this with `self.schema.compiled_ref` heavily shortly.
// For now, doing a basic map lookup to pass compilation.
if let Some(s) = self.schemas.get(ref_string) {
return Some((
crate::database::schema::ResolvedRef::Global(s, s),
ref_string.to_string(),
));
}
None
}
pub fn new(
validator: &'a Validator,
schemas: &'a std::collections::HashMap<String, Schema>,
root: &'a Schema,
schema: &'a Schema,
instance: I,
scope: Vec<String>,
overrides: HashSet<String>,
instance: &'a serde_json::Value,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator,
schemas,
root,
schema,
instance,
path: String::new(),
depth: 0,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
@ -47,72 +63,30 @@ impl<'a, I: ValidationInstance<'a>> ValidationContext<'a, I> {
pub fn derive(
&self,
schema: &'a Schema,
instance: I,
instance: &'a serde_json::Value,
path: &str,
scope: Vec<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator: self.validator,
schemas: self.schemas,
root: self.root,
schema,
instance,
path: path.to_string(),
depth: self.depth + 1,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
}
pub fn derive_for_schema(&self, schema: &'a Schema, reporter: bool) -> Self {
self.derive(
schema,
self.instance,
&self.path,
self.scope.clone(),
HashSet::new(),
self.extensible,
reporter,
)
self.derive(schema, self.instance, &self.path, self.extensible, reporter)
}
pub fn validate(&self) -> Result<ValidationResult, ValidationError> {
let mut effective_scope = self.scope.clone();
if let Some(id) = &self.schema.obj.id {
let current_base = self.scope.last().map(|s| s.as_str()).unwrap_or("");
let mut new_base = id.clone().to_string();
if !current_base.is_empty() {
if let Ok(base_url) = url::Url::parse(current_base) {
if let Ok(joined) = base_url.join(id) {
new_base = joined.to_string();
}
}
}
effective_scope.push(new_base);
let shadow = ValidationContext {
validator: self.validator,
root: self.root,
schema: self.schema,
instance: self.instance,
path: self.path.clone(),
depth: self.depth,
scope: effective_scope,
overrides: self.overrides.clone(),
extensible: self.extensible,
reporter: self.reporter,
};
return shadow.validate_scoped();
}
self.validate_scoped()
}
}

View File

@ -1,101 +1,29 @@
pub mod compiler;
pub mod context;
pub mod error;
pub mod formats;
pub mod instance;
pub mod registry;
pub mod result;
pub mod rules;
pub mod schema;
pub mod util;
pub use context::ValidationContext;
pub use error::ValidationError;
pub use instance::{MutableInstance, ReadOnlyInstance};
pub use result::ValidationResult;
use crate::validator::registry::Registry;
use crate::validator::schema::Schema;
use serde_json::Value;
use std::collections::HashSet;
use std::sync::Arc;
pub enum ResolvedRef<'a> {
Local(&'a Schema),
Global(&'a Schema, &'a Schema),
}
pub struct Validator {
pub registry: Registry,
pub families: std::collections::HashMap<String, Arc<Schema>>,
pub schemas: std::sync::Arc<std::collections::HashMap<String, crate::database::schema::Schema>>,
}
impl Validator {
pub fn from_punc_definition(
enums: Option<&Value>,
types: Option<&Value>,
puncs: Option<&Value>,
pub fn new(
schemas: std::sync::Arc<std::collections::HashMap<String, crate::database::schema::Schema>>,
) -> Self {
let mut registry = Registry::new();
let mut families = std::collections::HashMap::new();
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
if let Some(Value::Array(arr)) = types {
for item in arr {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
for ancestor in hierarchy {
if let Some(anc_str) = ancestor.as_str() {
family_map
.entry(anc_str.to_string())
.or_default()
.insert(name.to_string());
}
}
}
}
}
}
for (family_name, members) in family_map {
let object_refs: Vec<Value> = members
.iter()
.map(|s| serde_json::json!({ "$ref": s }))
.collect();
let schema_json = serde_json::json!({
"oneOf": object_refs
});
if let Ok(schema) = serde_json::from_value::<Schema>(schema_json) {
let compiled = crate::validator::compiler::Compiler::compile(schema, None);
families.insert(family_name, compiled);
}
}
let mut cache_items = |items_val: Option<&Value>| {
if let Some(Value::Array(arr)) = items_val {
for item in arr {
if let Some(Value::Array(schemas)) = item.get("schemas") {
for schema_val in schemas {
if let Ok(schema) = serde_json::from_value::<Schema>(schema_val.clone()) {
registry.add(schema);
}
}
}
}
}
};
cache_items(enums);
cache_items(types);
cache_items(puncs);
Self { registry, families }
Self { schemas }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.registry.schemas.keys().cloned().collect()
self.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
@ -116,148 +44,14 @@ impl Validator {
}
}
pub fn resolve_ref<'a>(
&'a self,
root: &'a Schema,
ref_string: &str,
scope: &str,
) -> Option<(ResolvedRef<'a>, String)> {
if ref_string.starts_with('#') {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(ref_string) {
return Some((ResolvedRef::Local(s.as_ref()), ref_string.to_string()));
}
}
}
if let Ok(base) = url::Url::parse(scope) {
if let Ok(joined) = base.join(ref_string) {
let joined_str = joined.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
} else {
if ref_string.starts_with('#') {
let joined_str = format!("{}{}", scope, ref_string);
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref() as &Schema), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
}
if let Ok(parsed) = url::Url::parse(ref_string) {
let absolute = parsed.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Local(s.as_ref()), absolute));
}
}
let resource_base = if let Some((base, _)) = absolute.split_once('#') {
base
} else {
&absolute
};
if let Some(compiled) = self.registry.schemas.get(resource_base) {
if let Some(indexrs) = &compiled.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Global(compiled.as_ref(), s.as_ref()), absolute));
}
}
}
}
if let Some(compiled) = self.registry.schemas.get(ref_string) {
return Some((
ResolvedRef::Global(compiled.as_ref(), compiled.as_ref()),
ref_string.to_string(),
));
}
None
}
pub fn validate(
&self,
schema_id: &str,
instance: &Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
ReadOnlyInstance(instance),
vec![],
HashSet::new(),
false,
false,
);
ctx.validate()
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
path: "".to_string(),
})
}
}
pub fn mask(
&self,
schema_id: &str,
instance: &mut Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
MutableInstance::new(instance),
vec![],
HashSet::new(),
false,
false,
);
let res = ctx.validate()?;
Ok(res)
if let Some(schema) = self.schemas.get(schema_id) {
let ctx = ValidationContext::new(&self.schemas, schema, schema, instance, false, false);
ctx.validate_scoped()
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),

View File

@ -1,50 +0,0 @@
use crate::validator::schema::Schema;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
pub static ref REGISTRY: RwLock<Registry> = RwLock::new(Registry::new());
}
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct Registry {
pub schemas: HashMap<String, Arc<Schema>>,
}
impl Registry {
pub fn new() -> Self {
Registry {
schemas: HashMap::new(),
}
}
pub fn add(&mut self, schema: crate::validator::schema::Schema) {
let id = schema
.obj
.id
.clone()
.expect("Schema must have an $id to be registered");
let compiled = crate::validator::compiler::Compiler::compile(schema, Some(id.clone()));
self.schemas.insert(id, compiled);
}
pub fn insert(&mut self, id: String, schema: Arc<Schema>) {
// We allow overwriting for now to support re-compilation in tests/dev
self.schemas.insert(id, schema);
}
pub fn get(&self, id: &str) -> Option<Arc<Schema>> {
self.schemas.get(id).cloned()
}
pub fn clear(&mut self) {
self.schemas.clear();
}
pub fn len(&self) -> usize {
self.schemas.len()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,119 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_array(
&self,
result: &mut ValidationResult,
) -> 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(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 self.schema.unique_items.unwrap_or(false) {
let mut seen: Vec<&Value> = Vec::new();
for item in arr {
if seen.contains(&item) {
result.errors.push(ValidationError {
code: "UNIQUE_ITEMS_VIOLATED".to_string(),
message: "Array has duplicate items".to_string(),
path: self.path.to_string(),
});
break;
}
seen.push(item);
}
}
if let Some(ref contains_schema) = self.schema.contains {
let mut _match_count = 0;
for (i, child_instance) in arr.iter().enumerate() {
let derived = self.derive(
contains_schema,
child_instance,
&self.path,
self.extensible,
false,
);
let check = derived.validate()?;
if check.is_valid() {
_match_count += 1;
result.evaluated_indices.insert(i);
}
}
let min = self.schema.min_contains.unwrap_or(1.0) as usize;
if _match_count < min {
result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(),
message: format!("Contains matches {} < min {}", _match_count, min),
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(),
});
}
}
}
let len = arr.len();
let mut validation_index = 0;
if let Some(ref prefix) = self.schema.prefix_items {
for (i, sub_schema) in prefix.iter().enumerate() {
if i < len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(sub_schema, child_instance, &path, self.extensible, false);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
validation_index += 1;
}
}
}
}
if let Some(ref items_schema) = self.schema.items {
for i in validation_index..len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(items_schema, child_instance, &path, self.extensible, false);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
}
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,83 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_combinators(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(ref all_of) = self.schema.all_of {
for sub in all_of {
let derived = self.derive_for_schema(sub, true);
let res = derived.validate()?;
result.merge(res);
}
}
if let Some(ref 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();
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;
}
}
if valid_count == 1 {
result.merge(valid_res);
} else if valid_count == 0 {
result.errors.push(ValidationError {
code: "ONE_OF_VIOLATED".to_string(),
message: "Matches none of oneOf schemas".to_string(),
path: self.path.to_string(),
});
} else {
result.errors.push(ValidationError {
code: "ONE_OF_VIOLATED".to_string(),
message: format!("Matches {} of oneOf schemas (expected 1)", valid_count),
path: self.path.to_string(),
});
}
}
if let Some(ref not_schema) = self.schema.not {
let derived = self.derive_for_schema(not_schema, true);
let sub_res = derived.validate()?;
if sub_res.is_valid() {
result.errors.push(ValidationError {
code: "NOT_VIOLATED".to_string(),
message: "Matched 'not' schema".to_string(),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -0,0 +1,69 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_conditionals(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(ref if_schema) = self.schema.if_ {
let derived_if = self.derive_for_schema(if_schema, true);
let if_res = derived_if.validate()?;
result.evaluated_keys.extend(if_res.evaluated_keys.clone());
result
.evaluated_indices
.extend(if_res.evaluated_indices.clone());
if if_res.is_valid() {
if let Some(ref then_schema) = self.schema.then_ {
let derived_then = self.derive_for_schema(then_schema, true);
result.merge(derived_then.validate()?);
}
} else {
if let Some(ref else_schema) = self.schema.else_ {
let derived_else = self.derive_for_schema(else_schema, true);
result.merge(derived_else.validate()?);
}
}
}
Ok(true)
}
pub(crate) fn validate_strictness(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if self.extensible || self.reporter {
return Ok(true);
}
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if !result.evaluated_keys.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),
message: format!("Unexpected property '{}'", key),
path: format!("{}/{}", self.path, key),
});
}
}
}
if let Some(arr) = self.instance.as_array() {
for i in 0..arr.len() {
if !result.evaluated_indices.contains(&i) {
result.errors.push(ValidationError {
code: "STRICT_ITEM_VIOLATION".to_string(),
message: format!("Unexpected item at index {}", i),
path: format!("{}/{}", self.path, i),
});
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,84 @@
use crate::validator::Validator;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_core(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(ref type_) = self.schema.type_ {
match type_ {
crate::database::schema::SchemaTypeOrArray::Single(t) => {
if !Validator::check_type(t, current) {
result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(),
message: format!("Expected type '{}'", t),
path: self.path.to_string(),
});
}
}
crate::database::schema::SchemaTypeOrArray::Multiple(types) => {
let mut valid = false;
for t in types {
if Validator::check_type(t, current) {
valid = true;
break;
}
}
if !valid {
result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(),
message: format!("Expected one of types {:?}", types),
path: self.path.to_string(),
});
}
}
}
}
if let Some(ref const_val) = self.schema.const_ {
if !crate::validator::util::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());
}
}
}
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) {
found = true;
break;
}
}
if !found {
result.errors.push(ValidationError {
code: "ENUM_MISMATCH".to_string(),
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());
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,44 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_format(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(ref compiled_fmt) = self.schema.compiled_format {
match compiled_fmt {
crate::database::schema::CompiledFormat::Func(f) => {
let should = if let Some(s) = current.as_str() {
!s.is_empty()
} 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(),
});
}
}
}
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(),
});
}
}
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,93 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
pub mod array;
pub mod combinators;
pub mod conditionals;
pub mod core;
pub mod format;
pub mod numeric;
pub mod object;
pub mod polymorphism;
pub mod string;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_scoped(&self) -> Result<ValidationResult, ValidationError> {
let mut result = ValidationResult::new();
// Structural Limits
if !self.validate_depth(&mut result)? {
return Ok(result);
}
if !self.validate_always_fail(&mut result)? {
return Ok(result);
}
if !self.validate_family(&mut result)? {
return Ok(result);
}
if !self.validate_refs(&mut result)? {
return Ok(result);
}
// Core Type Constraints
self.validate_core(&mut result)?;
self.validate_numeric(&mut result)?;
self.validate_string(&mut result)?;
self.validate_format(&mut result)?;
// Complex Structures
self.validate_object(&mut result)?;
self.validate_array(&mut result)?;
// Multipliers & Conditionals
self.validate_combinators(&mut result)?;
self.validate_conditionals(&mut result)?;
// State Tracking
self.validate_extensible(&mut result)?;
self.validate_strictness(&mut result)?;
Ok(result)
}
fn validate_depth(&self, _result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.depth > 100 {
Err(ValidationError {
code: "RECURSION_LIMIT_EXCEEDED".to_string(),
message: "Recursion limit exceeded".to_string(),
path: self.path.to_string(),
})
} else {
Ok(true)
}
}
fn validate_always_fail(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.schema.always_fail {
result.errors.push(ValidationError {
code: "FALSE_SCHEMA".to_string(),
message: "Schema is false".to_string(),
path: self.path.to_string(),
});
// Short-circuit
Ok(false)
} else {
Ok(true)
}
}
fn validate_extensible(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.extensible {
if let Some(obj) = self.instance.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = self.instance.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
Ok(true)
}
}

View File

@ -0,0 +1,61 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_numeric(
&self,
result: &mut ValidationResult,
) -> 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(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(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_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(multiple_of) = self.schema.multiple_of {
let val: f64 = num / multiple_of;
if (val - val.round()).abs() > f64::EPSILON {
result.errors.push(ValidationError {
code: "MULTIPLE_OF_VIOLATED".to_string(),
message: format!("Value {} not multiple of {}", num, multiple_of),
path: self.path.to_string(),
});
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,183 @@
use serde_json::Value;
use std::collections::HashSet;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_object(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(obj) = current.as_object() {
// Entity Bound Implicit Type Validation
if let Some(allowed_types) = &self.schema.obj.compiled_allowed_types {
if let Some(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(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(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(ref req) = self.schema.required {
for field in req {
if !obj.contains_key(field) {
result.errors.push(ValidationError {
code: "REQUIRED_FIELD_MISSING".to_string(),
message: format!("Missing {}", field),
path: format!("{}/{}", self.path, field),
});
}
}
}
if let Some(props) = &self.schema.properties {
for (key, sub_schema) in props {
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 next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
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_allowed_types {
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");
}
}
}
}
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties {
for (compiled_re, sub_schema) in compiled_pp {
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 next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
next_extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
}
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 !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 new_path = format!("{}/{}", self.path, key);
let is_ref = additional_schema.ref_string.is_some()
|| additional_schema.obj.compiled_ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
additional_schema,
child_instance,
&new_path,
next_extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
if let Some(ref property_names) = self.schema.property_names {
for key in obj.keys() {
let _new_path = format!("{}/propertyNames/{}", self.path, key);
let val_str = Value::String(key.to_string());
let ctx = ValidationContext::new(
self.schemas,
self.root,
property_names,
&val_str,
self.extensible,
self.reporter,
);
result.merge(ctx.validate()?);
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,64 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_family(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if self.schema.family.is_some() {
let conflicts = self.schema.type_.is_some()
|| self.schema.properties.is_some()
|| self.schema.required.is_some()
|| self.schema.additional_properties.is_some()
|| 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();
if conflicts {
result.errors.push(ValidationError {
code: "INVALID_SCHEMA".to_string(),
message: "$family must be used exclusively without other constraints".to_string(),
path: self.path.to_string(),
});
// Short-circuit: the schema formulation is broken
return Ok(false);
}
}
// Family specific runtime validation will go here later if needed
Ok(true)
}
pub(crate) fn validate_refs(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
// 1. Core $ref logic fully transitioned to memory pointer resolutions.
if let Some(_ref_str) = &self.schema.ref_string {
if let Some(global_schema) = &self.schema.compiled_ref {
let mut shadow = self.derive(
global_schema,
self.instance,
&self.path,
self.extensible,
false,
);
shadow.root = global_schema;
result.merge(shadow.validate()?);
} else {
result.errors.push(ValidationError {
code: "REF_RESOLUTION_FAILED".to_string(),
message: format!("Reference pointer was not compiled inside Database graph"),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -0,0 +1,53 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use regex::Regex;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_string(
&self,
result: &mut ValidationResult,
) -> 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(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(ref compiled_re) = self.schema.compiled_pattern {
if !compiled_re.0.is_match(s) {
result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(),
message: format!("Pattern mismatch {:?}", self.schema.pattern),
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(),
});
}
}
}
}
Ok(true)
}
}

View File

@ -5,11 +5,7 @@ use std::fs;
struct TestSuite {
#[allow(dead_code)]
description: String,
schema: Option<serde_json::Value>,
// Support JSPG-style test suites with explicit types/enums/puncs
types: Option<serde_json::Value>,
enums: Option<serde_json::Value>,
puncs: Option<serde_json::Value>,
database: serde_json::Value,
tests: Vec<TestCase>,
}
@ -20,9 +16,6 @@ struct TestCase {
valid: bool,
// Support explicit schema ID target for test case
schema_id: Option<String>,
// Expected output for masking tests
#[allow(dead_code)]
expected: Option<serde_json::Value>,
}
// use crate::validator::registry::REGISTRY; // No longer used directly for tests!
@ -50,64 +43,34 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
let group = &suite[index];
let mut failures = Vec::<String>::new();
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
// 3. Register root 'schemas' if present (generic test support)
// Some tests use a raw 'schema' or 'schemas' field at the group level
if let Some(schema_val) = &group.schema {
match serde_json::from_value::<crate::validator::schema::Schema>(schema_val.clone()) {
Ok(mut schema) => {
let id_clone = schema.obj.id.clone();
if id_clone.is_some() {
validator.registry.add(schema);
} else {
// Fallback ID if none provided in schema
let id = format!("test:{}:{}", path, index);
schema.obj.id = Some(id);
validator.registry.add(schema);
}
}
Err(e) => {
eprintln!(
"DEBUG: FAILED to deserialize group schema for index {}: {}",
index, e
);
}
}
}
let db_json = group.database.clone();
let db = crate::database::Database::new(&db_json);
let validator = Validator::new(std::sync::Arc::new(db.schemas));
// 4. Run Tests
for (_test_index, test) in group.tests.iter().enumerate() {
let mut schema_id = test.schema_id.clone();
// If no explicit schema_id, try to infer from the single schema in the group
// If no explicit schema_id, infer from the database structure
if schema_id.is_none() {
if let Some(s) = &group.schema {
// If 'schema' is a single object, use its ID or "root"
if let Some(obj) = s.as_object() {
if let Some(id_val) = obj.get("$id") {
schema_id = id_val.as_str().map(|s| s.to_string());
if let Some(schemas) = db_json.get("schemas").and_then(|v| v.as_array()) {
if let Some(first) = schemas.first() {
if let Some(id) = first.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
} else {
schema_id = Some("schema_0".to_string());
}
}
if schema_id.is_none() {
schema_id = Some(format!("test:{}:{}", path, index));
}
}
}
// Default to the first punc if present (for puncs.json style)
if schema_id.is_none() {
if let Some(Value::Array(puncs)) = &group.puncs {
if let Some(first_punc) = puncs.first() {
if let Some(Value::Array(schemas)) = first_punc.get("schemas") {
if let Some(first_schema) = schemas.first() {
if let Some(id) = first_schema.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
if schema_id.is_none() {
if let Some(puncs) = db_json.get("puncs").and_then(|v| v.as_array()) {
if let Some(first_punc) = puncs.first() {
if let Some(schemas) = first_punc.get("schemas").and_then(|v| v.as_array()) {
if let Some(first) = schemas.first() {
if let Some(id) = first.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
}
}
}
}
@ -127,42 +90,16 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
}
};
if let Some(expected) = &test.expected {
// Masking Test
let mut data_for_mask = test.data.clone();
match validator.mask(&sid, &mut data_for_mask) {
Ok(_) => {
if !equals(&data_for_mask, expected) {
let msg = format!(
"Masking Test '{}' failed.\nExpected: {:?}\nGot: {:?}",
test.description, expected, data_for_mask
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Masking Test '{}' failed with execution error: {:?}",
test.description, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
// Standard Validation Test
if got_valid != test.valid {
let error_msg = match &result {
Ok(res) => format!("{:?}", res.errors),
Err(e) => format!("Execution Error: {:?}", e),
};
if got_valid != test.valid {
let error_msg = match &result {
Ok(res) => format!("{:?}", res.errors),
Err(e) => format!("Execution Error: {:?}", e),
};
failures.push(format!(
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
group.description, test.description, test.valid, got_valid, error_msg
));
}
failures.push(format!(
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
group.description, test.description, test.valid, got_valid, error_msg
));
}
} else {
failures.push(format!(
@ -178,96 +115,6 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
Ok(())
}
pub fn run_test_file(path: &str) -> Result<(), String> {
let content =
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
let suite: Vec<TestSuite> = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
let mut failures = Vec::<String>::new();
for (group_index, group) in suite.into_iter().enumerate() {
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
let unique_id = format!("test:{}:{}", path, group_index);
// Register main 'schema' if present (Standard style)
if let Some(ref schema_val) = group.schema {
let mut schema: crate::validator::schema::Schema =
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
// If schema has no ID, assign unique_id and use add() or manual insert?
// Compiler needs ID. Registry::add needs ID.
if schema.obj.id.is_none() {
schema.obj.id = Some(unique_id.clone());
}
validator.registry.add(schema);
}
for test in group.tests {
// Use explicit schema_id from test, or default to unique_id
let schema_id = test.schema_id.as_deref().unwrap_or(&unique_id).to_string();
let result = validator.validate(&schema_id, &test.data);
if test.valid {
match result {
Ok(res) => {
if !res.is_valid() {
let msg = format!(
"Test failed (expected valid): {}\nSchema: {:?}\nData: {:?}\nErrors: {:?}",
test.description,
group.schema, // We might need to find the actual schema used if schema_id is custom
test.data,
res.errors
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Test failed (expected valid) but got execution error: {}\nSchema: {:?}\nData: {:?}\nError: {:?}",
test.description, group.schema, test.data, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
match result {
Ok(res) => {
if res.is_valid() {
let msg = format!(
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}",
test.description, group.schema, test.data
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(_) => {
// Expected invalid, got error (which implies invalid/failure), so this is PASS.
}
}
}
}
}
if !failures.is_empty() {
return Err(format!(
"{} tests failed in file {}:\n\n{}",
failures.len(),
path,
failures.join("\n\n")
));
}
Ok(())
}
pub fn is_integer(v: &Value) -> bool {
match v {