removed schema realms, fixed fixture generations, added dynamic type resolution for validation based on type and kind discriminators for filters
This commit is contained in:
@ -74,8 +74,8 @@ impl Schema {
|
||||
);
|
||||
|
||||
let mut wrapper_obj = SchemaObject::default();
|
||||
// Conceptually link this directly into the STI lineage of the base `filter` object
|
||||
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("filter".to_string()));
|
||||
// Filters are just plain objects containing conditions, no inheritance required
|
||||
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("object".to_string()));
|
||||
wrapper_obj.properties = Some(filter_props);
|
||||
|
||||
return Some(Schema {
|
||||
|
||||
@ -51,8 +51,8 @@ impl Schema {
|
||||
|
||||
// 1. Resolve INHERITANCE dependencies first
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(parent) = db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, t) {
|
||||
if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') {
|
||||
if let Some(parent) = db.schemas.get(t).cloned() {
|
||||
parent.as_ref().compile(db, t, t.clone(), errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
@ -85,8 +85,8 @@ impl Schema {
|
||||
}
|
||||
|
||||
for t in types {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(parent) = db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, t) {
|
||||
if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') {
|
||||
if let Some(parent) = db.schemas.get(t).cloned() {
|
||||
parent.as_ref().compile(db, t, t.clone(), errors);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,10 @@ impl Schema {
|
||||
let mut strategy = String::new();
|
||||
|
||||
if let Some(family) = &self.obj.family {
|
||||
// Formalize the <Variant>.<Base> topology
|
||||
// family_base extracts the 'Base' (e.g. 'widget', 'person')
|
||||
let family_base = family.split('.').next_back().unwrap_or(family).to_string();
|
||||
// family_prefix extracts the 'Variant' (e.g. 'stock', 'light')
|
||||
let family_prefix = family
|
||||
.strip_suffix(&family_base)
|
||||
.unwrap_or("")
|
||||
@ -29,7 +32,7 @@ impl Schema {
|
||||
format!("{}.{}", family_prefix, var)
|
||||
};
|
||||
|
||||
if db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, &target_id).is_some() {
|
||||
if db.schemas.get(&target_id).is_some() {
|
||||
options.insert(var.to_string(), (None, Some(target_id)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ pub mod formats;
|
||||
pub mod object;
|
||||
pub mod page;
|
||||
pub mod punc;
|
||||
pub mod realm;
|
||||
pub mod relation;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
@ -21,7 +20,6 @@ use executors::pgrx::SpiExecutor;
|
||||
use executors::mock::MockExecutor;
|
||||
|
||||
use punc::Punc;
|
||||
use realm::SchemaRealm;
|
||||
use relation::Relation;
|
||||
use schema::Schema;
|
||||
use serde_json::Value;
|
||||
@ -36,6 +34,8 @@ pub struct Database {
|
||||
pub puncs: HashMap<String, Punc>,
|
||||
pub relations: HashMap<String, Relation>,
|
||||
#[serde(skip)]
|
||||
pub schemas: HashMap<String, Arc<Schema>>,
|
||||
#[serde(skip)]
|
||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ impl Database {
|
||||
types: HashMap::new(),
|
||||
relations: HashMap::new(),
|
||||
puncs: HashMap::new(),
|
||||
schemas: HashMap::new(),
|
||||
#[cfg(not(test))]
|
||||
executor: Box::new(SpiExecutor::new()),
|
||||
#[cfg(test)]
|
||||
@ -194,22 +195,28 @@ impl Database {
|
||||
|
||||
// Formally evaluate properties with strict 3-pass Ordered Graph execution natively
|
||||
for (_, enum_def) in &self.enums {
|
||||
for (schema_id, schema_arc) in &enum_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
for (schema_id, schema_arc) in &enum_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc
|
||||
.as_ref()
|
||||
.compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
}
|
||||
for (_, type_def) in &self.types {
|
||||
for (schema_id, schema_arc) in &type_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
for (schema_id, schema_arc) in &type_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc
|
||||
.as_ref()
|
||||
.compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
}
|
||||
for (_, punc_def) in &self.puncs {
|
||||
for (schema_id, schema_arc) in &punc_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
for (schema_id, schema_arc) in &punc_def.schemas {
|
||||
let root_id = schema_id.split('/').next().unwrap_or(schema_id);
|
||||
schema_arc
|
||||
.as_ref()
|
||||
.compile(self, root_id, schema_id.clone(), errors);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Synthesize Composed Filter References
|
||||
@ -234,6 +241,7 @@ impl Database {
|
||||
let mut filter_ids = Vec::new();
|
||||
for (type_name, id, filter_arc) in filter_schemas {
|
||||
filter_ids.push((type_name.clone(), id.clone()));
|
||||
self.schemas.insert(id.clone(), filter_arc.clone());
|
||||
if let Some(t) = self.types.get_mut(&type_name) {
|
||||
t.schemas.insert(id, filter_arc);
|
||||
}
|
||||
@ -241,7 +249,12 @@ impl Database {
|
||||
|
||||
// Now actively compile the newly injected filters to lock all nested compose references natively
|
||||
for (type_name, id) in filter_ids {
|
||||
if let Some(filter_arc) = self.types.get(&type_name).and_then(|t| t.schemas.get(&id)).cloned() {
|
||||
if let Some(filter_arc) = self
|
||||
.types
|
||||
.get(&type_name)
|
||||
.and_then(|t| t.schemas.get(&id))
|
||||
.cloned()
|
||||
{
|
||||
let root_id = id.split('/').next().unwrap_or(&id);
|
||||
filter_arc
|
||||
.as_ref()
|
||||
@ -259,6 +272,7 @@ impl Database {
|
||||
// Validate every node recursively via string filters natively!
|
||||
for (type_name, type_def) in &self.types {
|
||||
for (id, schema_arc) in &type_def.schemas {
|
||||
self.schemas.insert(id.clone(), Arc::clone(schema_arc));
|
||||
let mut local_insert = Vec::new();
|
||||
crate::database::schema::Schema::collect_schemas(
|
||||
schema_arc,
|
||||
@ -275,6 +289,7 @@ impl Database {
|
||||
|
||||
for (punc_name, punc_def) in &self.puncs {
|
||||
for (id, schema_arc) in &punc_def.schemas {
|
||||
self.schemas.insert(id.clone(), Arc::clone(schema_arc));
|
||||
let mut local_insert = Vec::new();
|
||||
crate::database::schema::Schema::collect_schemas(
|
||||
schema_arc,
|
||||
@ -291,6 +306,7 @@ impl Database {
|
||||
|
||||
for (enum_name, enum_def) in &self.enums {
|
||||
for (id, schema_arc) in &enum_def.schemas {
|
||||
self.schemas.insert(id.clone(), Arc::clone(schema_arc));
|
||||
let mut local_insert = Vec::new();
|
||||
crate::database::schema::Schema::collect_schemas(
|
||||
schema_arc,
|
||||
@ -305,57 +321,27 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply local scopes
|
||||
// Apply local scopes and global schema map
|
||||
for (origin_name, id, schema_arc) in type_insert {
|
||||
self.schemas.insert(id.clone(), schema_arc.clone());
|
||||
if let Some(t) = self.types.get_mut(&origin_name) {
|
||||
t.schemas.insert(id, schema_arc);
|
||||
}
|
||||
}
|
||||
for (origin_name, id, schema_arc) in punc_insert {
|
||||
self.schemas.insert(id.clone(), schema_arc.clone());
|
||||
if let Some(p) = self.puncs.get_mut(&origin_name) {
|
||||
p.schemas.insert(id, schema_arc);
|
||||
}
|
||||
}
|
||||
for (origin_name, id, schema_arc) in enum_insert {
|
||||
self.schemas.insert(id.clone(), schema_arc.clone());
|
||||
if let Some(e) = self.enums.get_mut(&origin_name) {
|
||||
e.schemas.insert(id, schema_arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_scoped_schema(&self, realm: SchemaRealm, schema_id: &str) -> Option<Arc<Schema>> {
|
||||
// Punc Realm natively maps mathematically to `.request` and `.response` shapes
|
||||
if realm == SchemaRealm::Punc {
|
||||
if schema_id.ends_with(".request") || schema_id.ends_with(".response") {
|
||||
let punc_name = schema_id
|
||||
.trim_end_matches(".request")
|
||||
.trim_end_matches(".response");
|
||||
return self.puncs.get(punc_name).and_then(|p| p.schemas.get(schema_id).cloned());
|
||||
}
|
||||
}
|
||||
|
||||
let clean_id = schema_id.trim_end_matches(".filter");
|
||||
let root_id = clean_id.split('/').next().unwrap_or(clean_id);
|
||||
let base_name = root_id.split('.').next_back().unwrap_or(root_id);
|
||||
|
||||
// Puncs and Types can lookup Table boundaries
|
||||
if realm == SchemaRealm::Type || realm == SchemaRealm::Punc {
|
||||
if let Some(type_def) = self.types.get(base_name) {
|
||||
if let Some(schema) = type_def.schemas.get(schema_id) {
|
||||
return Some(schema.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All realms can intrinsically look up enumerations
|
||||
if let Some(enum_def) = self.enums.get(base_name) {
|
||||
if let Some(schema) = enum_def.schemas.get(schema_id) {
|
||||
return Some(schema.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
||||
/// the precise Foreign Key connecting a parent and child hierarchy path.
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SchemaRealm {
|
||||
Enum,
|
||||
Type,
|
||||
Punc,
|
||||
}
|
||||
@ -22,6 +22,27 @@ impl std::ops::DerefMut for Schema {
|
||||
}
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
/// Returns true if the schema acts purely as a type pointer (composition without overriding constraints)
|
||||
pub fn is_proxy(&self) -> bool {
|
||||
self.obj.properties.is_none()
|
||||
&& self.obj.pattern_properties.is_none()
|
||||
&& self.obj.additional_properties.is_none()
|
||||
&& self.obj.required.is_none()
|
||||
&& self.obj.dependencies.is_none()
|
||||
&& self.obj.items.is_none()
|
||||
&& self.obj.prefix_items.is_none()
|
||||
&& self.obj.contains.is_none()
|
||||
&& self.obj.format.is_none()
|
||||
&& self.obj.enum_.is_none()
|
||||
&& self.obj.const_.is_none()
|
||||
&& self.obj.cases.is_none()
|
||||
&& self.obj.one_of.is_none()
|
||||
&& self.obj.not.is_none()
|
||||
&& self.obj.family.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Schema {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
pub mod cache;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::database::realm::SchemaRealm;
|
||||
use crate::database::r#type::Type;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
@ -25,7 +24,7 @@ impl Merger {
|
||||
pub fn merge(&self, schema_id: &str, data: Value) -> crate::drop::Drop {
|
||||
let mut notifications_queue = Vec::new();
|
||||
|
||||
let target_schema = match self.db.get_scoped_schema(SchemaRealm::Type, schema_id) {
|
||||
let target_schema = match self.db.schemas.get(schema_id) {
|
||||
Some(s) => Arc::clone(&s),
|
||||
None => {
|
||||
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
@ -146,7 +145,7 @@ impl Merger {
|
||||
if let Some((idx_opt, target_id_opt)) = options.get(v) {
|
||||
if let Some(target_id) = target_id_opt {
|
||||
if let Some(target_schema) =
|
||||
self.db.get_scoped_schema(SchemaRealm::Type, target_id)
|
||||
self.db.schemas.get(target_id)
|
||||
{
|
||||
schema = target_schema.clone();
|
||||
} else {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::database::Database;
|
||||
use crate::database::realm::SchemaRealm;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Compiler<'a> {
|
||||
@ -25,15 +24,11 @@ pub struct Node<'a> {
|
||||
impl<'a> Compiler<'a> {
|
||||
/// Compiles a JSON schema into a nested PostgreSQL query returning JSONB
|
||||
pub fn compile(&self, schema_id: &str, filter_keys: &[String]) -> Result<String, String> {
|
||||
let realm = if schema_id.ends_with(".request") || schema_id.ends_with(".response") {
|
||||
SchemaRealm::Punc
|
||||
} else {
|
||||
SchemaRealm::Type
|
||||
};
|
||||
|
||||
let schema = self
|
||||
.db
|
||||
.get_scoped_schema(realm, schema_id)
|
||||
.schemas
|
||||
.get(schema_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Schema not found: {}", schema_id))?;
|
||||
|
||||
let target_schema = schema;
|
||||
@ -157,7 +152,7 @@ impl<'a> Compiler<'a> {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
// If it's just an ad-hoc struct ref, we should resolve it
|
||||
if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, t) {
|
||||
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
|
||||
let mut ref_node = node.clone();
|
||||
ref_node.schema = target_schema.clone();
|
||||
ref_node.schema_id = Some(t.clone());
|
||||
@ -312,7 +307,7 @@ impl<'a> Compiler<'a> {
|
||||
|
||||
for (disc_val, (idx_opt, target_id_opt)) in options {
|
||||
if let Some(target_id) = target_id_opt {
|
||||
if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, target_id) {
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id).cloned() {
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = target_schema.clone();
|
||||
child_node.schema_id = Some(target_id.clone());
|
||||
|
||||
@ -1247,6 +1247,36 @@ fn test_const_17_1() {
|
||||
crate::tests::runner::run_test_case(&path, 17, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_type_0_0() {
|
||||
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_type_0_1() {
|
||||
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_type_0_2() {
|
||||
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_type_0_3() {
|
||||
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 3).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_type_0_4() {
|
||||
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_property_names_0_0() {
|
||||
let path = format!("{}/fixtures/propertyNames.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
@ -160,7 +160,7 @@ fn test_library_api() {
|
||||
"target": { "type": ["target_schema.filter", "null"] },
|
||||
"type": { "type": ["string.condition", "null"] }
|
||||
},
|
||||
"type": "filter"
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"sensitive": false,
|
||||
@ -211,7 +211,7 @@ fn test_library_api() {
|
||||
},
|
||||
"value": { "type": ["number.condition", "null"] }
|
||||
},
|
||||
"type": "filter"
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"sensitive": false,
|
||||
|
||||
@ -35,12 +35,7 @@ impl Expect {
|
||||
if expected_val.is_object() && expected_val.as_object().unwrap().is_empty() {
|
||||
continue; // A `{}` means we just wanted to test it was collected/promoted, skip deep match
|
||||
}
|
||||
let schema_realm = if key.ends_with(".request") || key.ends_with(".response") {
|
||||
crate::database::realm::SchemaRealm::Punc
|
||||
} else {
|
||||
crate::database::realm::SchemaRealm::Type
|
||||
};
|
||||
let actual_ast = db.get_scoped_schema(schema_realm, key).unwrap();
|
||||
let actual_ast = db.schemas.get(key).cloned().unwrap();
|
||||
let actual_val = serde_json::to_value(actual_ast).unwrap();
|
||||
|
||||
if actual_val != *expected_val {
|
||||
|
||||
@ -15,6 +15,7 @@ pub struct ValidationContext<'a> {
|
||||
pub extensible: bool,
|
||||
pub reporter: bool,
|
||||
pub overrides: HashSet<String>,
|
||||
pub parent: Option<&'a serde_json::Value>,
|
||||
}
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
@ -38,6 +39,7 @@ impl<'a> ValidationContext<'a> {
|
||||
extensible: effective_extensible,
|
||||
reporter,
|
||||
overrides,
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +59,7 @@ impl<'a> ValidationContext<'a> {
|
||||
overrides: HashSet<String>,
|
||||
extensible: bool,
|
||||
reporter: bool,
|
||||
parent_instance: Option<&'a serde_json::Value>,
|
||||
) -> Self {
|
||||
let effective_extensible = schema.extensible.unwrap_or(extensible);
|
||||
|
||||
@ -70,6 +73,7 @@ impl<'a> ValidationContext<'a> {
|
||||
extensible: effective_extensible,
|
||||
reporter,
|
||||
overrides,
|
||||
parent: parent_instance,
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +85,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
reporter,
|
||||
self.parent,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ pub use error::ValidationError;
|
||||
pub use result::ValidationResult;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::database::realm::SchemaRealm;
|
||||
use crate::validator::rules::util::is_integer;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
@ -43,11 +42,7 @@ impl Validator {
|
||||
}
|
||||
|
||||
pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop {
|
||||
let schema_opt = if schema_id.ends_with(".request") || schema_id.ends_with(".response") {
|
||||
self.db.get_scoped_schema(SchemaRealm::Punc, schema_id)
|
||||
} else {
|
||||
self.db.get_scoped_schema(SchemaRealm::Type, schema_id)
|
||||
};
|
||||
let schema_opt = self.db.schemas.get(schema_id);
|
||||
|
||||
if let Some(schema) = schema_opt {
|
||||
let ctx = ValidationContext::new(
|
||||
|
||||
@ -57,6 +57,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
|
||||
let check = derived.validate()?;
|
||||
@ -108,6 +109,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
result.merge(item_res);
|
||||
@ -137,6 +139,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
result.merge(item_res);
|
||||
|
||||
@ -12,6 +12,7 @@ pub mod numeric;
|
||||
pub mod object;
|
||||
pub mod polymorphism;
|
||||
pub mod string;
|
||||
pub mod r#type;
|
||||
pub mod util;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
@ -28,7 +29,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !self.validate_family(&mut result)? {
|
||||
return Ok(result);
|
||||
}
|
||||
if !self.validate_type_inheritance(&mut result)? {
|
||||
if !self.validate_type(&mut result)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
|
||||
@ -191,6 +191,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
|
||||
@ -220,6 +221,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
result.merge(item_res);
|
||||
@ -265,6 +267,7 @@ impl<'a> ValidationContext<'a> {
|
||||
HashSet::new(),
|
||||
next_extensible,
|
||||
false,
|
||||
Some(self.instance),
|
||||
);
|
||||
let item_res = derived.validate()?;
|
||||
result.merge(item_res);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
use crate::database::realm::SchemaRealm;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_family(
|
||||
@ -100,8 +99,8 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(val) = instance_val {
|
||||
if let Some((idx_opt, target_id_opt)) = options.get(&val) {
|
||||
if let Some(target_id) = target_id_opt {
|
||||
if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, target_id) {
|
||||
let derived = self.derive_for_schema(&target_schema, false);
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id) {
|
||||
let derived = self.derive_for_schema(target_schema, false);
|
||||
let sub_res = derived.validate()?;
|
||||
let is_valid = sub_res.is_valid();
|
||||
result.merge(sub_res);
|
||||
@ -177,78 +176,4 @@ impl<'a> ValidationContext<'a> {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_type_inheritance(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
// Core inheritance logic replaces legacy routing
|
||||
let payload_primitive = match self.instance {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "boolean",
|
||||
serde_json::Value::Number(n) => {
|
||||
if n.is_i64() || n.is_u64() {
|
||||
"integer"
|
||||
} else {
|
||||
"number"
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
};
|
||||
|
||||
let mut custom_types = Vec::new();
|
||||
match &self.schema.type_ {
|
||||
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => {
|
||||
if arr.contains(&payload_primitive.to_string())
|
||||
|| (payload_primitive == "integer" && arr.contains(&"number".to_string()))
|
||||
{
|
||||
// It natively matched a primitive in the array options, skip forcing custom proxy fallback
|
||||
} else {
|
||||
for t in arr {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
for t in custom_types {
|
||||
if let Some(global_schema) = self.db.get_scoped_schema(SchemaRealm::Type, &t) {
|
||||
let mut new_overrides = self.overrides.clone();
|
||||
if let Some(props) = &self.schema.properties {
|
||||
new_overrides.extend(props.keys().map(|k| k.to_string()));
|
||||
}
|
||||
|
||||
let mut shadow = self.derive(
|
||||
&global_schema,
|
||||
self.instance,
|
||||
&self.path,
|
||||
new_overrides,
|
||||
self.extensible,
|
||||
true, // Reporter mode
|
||||
);
|
||||
shadow.root = &global_schema;
|
||||
result.merge(shadow.validate()?);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Inherited entity pointer '{}' was not found in schema registry",
|
||||
t
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
138
src/validator/rules/type.rs
Normal file
138
src/validator/rules/type.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_type(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
let payload_primitive = match self.instance {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "boolean",
|
||||
serde_json::Value::Number(n) => {
|
||||
if n.is_i64() || n.is_u64() {
|
||||
"integer"
|
||||
} else {
|
||||
"number"
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
};
|
||||
|
||||
let mut custom_types = Vec::new();
|
||||
match &self.schema.type_ {
|
||||
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => {
|
||||
if arr.contains(&payload_primitive.to_string())
|
||||
|| (payload_primitive == "integer" && arr.contains(&"number".to_string()))
|
||||
{
|
||||
// It natively matched a primitive in the array options, skip forcing custom proxy fallback
|
||||
} else {
|
||||
for t in arr {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
for t in custom_types {
|
||||
let mut target_id = t.clone();
|
||||
|
||||
// 1. DYNAMIC TYPE (Composition)
|
||||
if t.starts_with('$') {
|
||||
let parts: Vec<&str> = t.split('.').collect();
|
||||
let var_name = &parts[0][1..]; // Remove the $ prefix
|
||||
let suffix = if parts.len() > 1 {
|
||||
format!(".{}", parts[1..].join("."))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut resolved = false;
|
||||
if let Some(parent) = self.parent {
|
||||
if let Some(obj) = parent.as_object() {
|
||||
if let Some(val) = obj.get(var_name) {
|
||||
if let Some(str_val) = val.as_str() {
|
||||
target_id = format!("{}{}", str_val, suffix);
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !resolved {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Dynamic type pointer '{}' could not resolve discriminator property '{}' on parent instance",
|
||||
t, var_name
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch and apply
|
||||
if let Some(global_schema) = self.db.schemas.get(&target_id) {
|
||||
let mut new_overrides = self.overrides.clone();
|
||||
if let Some(props) = &self.schema.properties {
|
||||
new_overrides.extend(props.keys().map(|k| k.to_string()));
|
||||
}
|
||||
|
||||
let mut shadow = self.derive(
|
||||
&global_schema,
|
||||
self.instance,
|
||||
&self.path,
|
||||
new_overrides,
|
||||
self.extensible,
|
||||
true, // Reporter mode
|
||||
self.parent,
|
||||
);
|
||||
shadow.root = &global_schema;
|
||||
result.merge(shadow.validate()?);
|
||||
} else {
|
||||
// 3. Error handling pathways
|
||||
if t.starts_with('$') {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Resolved dynamic type pointer '{}' was not found in schema registry",
|
||||
target_id
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else if self.schema.is_proxy() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Composed proxy entity pointer '{}' was not found in schema registry",
|
||||
target_id
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Inherited entity pointer '{}' was not found in schema registry",
|
||||
target_id
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user