chore: JSPG Engine tuple decoupling and core routing optimizations
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
use crate::database::schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
@ -8,5 +9,6 @@ pub struct Enum {
|
||||
pub module: String,
|
||||
pub source: String,
|
||||
pub values: Vec<String>,
|
||||
pub schemas: Vec<Schema>,
|
||||
#[serde(default)]
|
||||
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
|
||||
}
|
||||
|
||||
@ -125,22 +125,16 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arr) = val.get("schemas").and_then(|v| v.as_array()) {
|
||||
for (i, item) in arr.iter().enumerate() {
|
||||
if let Some(map) = val.get("schemas").and_then(|v| v.as_object()) {
|
||||
for (key, item) in map.iter() {
|
||||
match serde_json::from_value::<Schema>(item.clone()) {
|
||||
Ok(mut schema) => {
|
||||
let id = schema
|
||||
.obj
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("schema_{}", i));
|
||||
schema.obj.id = Some(id.clone());
|
||||
db.schemas.insert(id, Arc::new(schema));
|
||||
Ok(schema) => {
|
||||
db.schemas.insert(key.clone(), Arc::new(schema));
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
|
||||
message: format!("Failed to parse database schema: {}", e),
|
||||
message: format!("Failed to parse database schema key '{}': {}", key, e),
|
||||
details: crate::drop::ErrorDetails::default(),
|
||||
});
|
||||
}
|
||||
@ -185,21 +179,21 @@ impl Database {
|
||||
|
||||
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||
let mut harvested = Vec::new();
|
||||
for schema_arc in self.schemas.values_mut() {
|
||||
if let Some(s) = std::sync::Arc::get_mut(schema_arc) {
|
||||
s.collect_schemas(None, &mut harvested, errors);
|
||||
}
|
||||
for (id, schema_arc) in &self.schemas {
|
||||
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut harvested, errors);
|
||||
}
|
||||
for (id, schema) in harvested {
|
||||
self.schemas.insert(id, Arc::new(schema));
|
||||
for (id, schema_arc) in harvested {
|
||||
self.schemas.insert(id, schema_arc);
|
||||
}
|
||||
|
||||
self.collect_schemas(errors);
|
||||
|
||||
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
for schema_arc in self.schemas.values() {
|
||||
schema_arc.as_ref().compile(self, &mut visited, errors);
|
||||
for (id, schema_arc) in &self.schemas {
|
||||
// First compile pass initializes exact structural root_id mapping to resolve DB constraints
|
||||
let root_id = id.split('/').next().unwrap_or(id);
|
||||
schema_arc.as_ref().compile(self, root_id, id.clone(), &mut visited, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,23 +203,26 @@ impl Database {
|
||||
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
||||
// Validate every node recursively via string filters natively!
|
||||
for type_def in self.types.values() {
|
||||
for mut schema in type_def.schemas.clone() {
|
||||
schema.collect_schemas(None, &mut to_insert, errors);
|
||||
for (id, schema_arc) in &type_def.schemas {
|
||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
|
||||
}
|
||||
}
|
||||
for punc_def in self.puncs.values() {
|
||||
for mut schema in punc_def.schemas.clone() {
|
||||
schema.collect_schemas(None, &mut to_insert, errors);
|
||||
for (id, schema_arc) in &punc_def.schemas {
|
||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
|
||||
}
|
||||
}
|
||||
for enum_def in self.enums.values() {
|
||||
for mut schema in enum_def.schemas.clone() {
|
||||
schema.collect_schemas(None, &mut to_insert, errors);
|
||||
for (id, schema_arc) in &enum_def.schemas {
|
||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
|
||||
}
|
||||
}
|
||||
|
||||
for (id, schema) in to_insert {
|
||||
self.schemas.insert(id, Arc::new(schema));
|
||||
for (id, schema_arc) in to_insert {
|
||||
self.schemas.insert(id, schema_arc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use crate::database::schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use crate::database::schema::Schema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Case {
|
||||
@ -19,9 +19,6 @@ pub struct Case {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SchemaObject {
|
||||
// Core Schema Keywords
|
||||
#[serde(rename = "$id")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@ -176,7 +173,7 @@ pub struct SchemaObject {
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
|
||||
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
|
||||
pub compiled_options: OnceLock<BTreeMap<String, String>>,
|
||||
pub compiled_options: OnceLock<BTreeMap<String, (Option<usize>, Option<String>)>>,
|
||||
|
||||
#[serde(rename = "compiledEdges")]
|
||||
#[serde(skip_deserializing)]
|
||||
@ -275,93 +272,49 @@ pub fn is_primitive_type(t: &str) -> bool {
|
||||
}
|
||||
|
||||
impl SchemaObject {
|
||||
pub fn identifier(&self) -> Option<String> {
|
||||
if let Some(id) = &self.id {
|
||||
return Some(id.split('.').next_back().unwrap_or("").to_string());
|
||||
}
|
||||
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !is_primitive_type(t) {
|
||||
return Some(t.split('.').next_back().unwrap_or("").to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_discriminator_value(&self, dim: &str) -> Option<String> {
|
||||
pub fn get_discriminator_value(&self, dim: &str, schema_id: &str) -> Option<String> {
|
||||
let is_split = self
|
||||
.compiled_properties
|
||||
.get()
|
||||
.map_or(false, |p| p.contains_key("kind"));
|
||||
if let Some(id) = &self.id {
|
||||
if id.contains("light.person") || id.contains("light.organization") {
|
||||
println!(
|
||||
"[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}",
|
||||
id,
|
||||
dim,
|
||||
is_split,
|
||||
self
|
||||
.compiled_properties
|
||||
.get()
|
||||
.map(|p| p.keys().cloned().collect::<Vec<_>>())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(props) = self.compiled_properties.get() {
|
||||
if let Some(prop_schema) = props.get(dim) {
|
||||
if let Some(c) = &prop_schema.obj.const_ {
|
||||
if let Some(s) = c.as_str() {
|
||||
return Some(s.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(e) = &prop_schema.obj.enum_ {
|
||||
if e.len() == 1 {
|
||||
if let Some(s) = e[0].as_str() {
|
||||
return Some(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dim == "kind" {
|
||||
if let Some(id) = &self.id {
|
||||
let base = id.split('/').last().unwrap_or(id);
|
||||
if let Some(idx) = base.rfind('.') {
|
||||
return Some(base[..idx].to_string());
|
||||
}
|
||||
}
|
||||
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !is_primitive_type(t) {
|
||||
let base = t.split('/').last().unwrap_or(t);
|
||||
if let Some(idx) = base.rfind('.') {
|
||||
return Some(base[..idx].to_string());
|
||||
}
|
||||
}
|
||||
let base = schema_id.split('/').last().unwrap_or(schema_id);
|
||||
if let Some(idx) = base.rfind('.') {
|
||||
return Some(base[..idx].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if dim == "type" {
|
||||
if let Some(id) = &self.id {
|
||||
let base = id.split('/').last().unwrap_or(id);
|
||||
if is_split {
|
||||
return Some(base.split('.').next_back().unwrap_or(base).to_string());
|
||||
} else {
|
||||
return Some(base.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !is_primitive_type(t) {
|
||||
let base = t.split('/').last().unwrap_or(t);
|
||||
if is_split {
|
||||
return Some(base.split('.').next_back().unwrap_or(base).to_string());
|
||||
} else {
|
||||
return Some(base.to_string());
|
||||
}
|
||||
}
|
||||
let base = schema_id.split('/').last().unwrap_or(schema_id);
|
||||
if is_split {
|
||||
return Some(base.split('.').next_back().unwrap_or(base).to_string());
|
||||
} else {
|
||||
return Some(base.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn requires_uuid_path(&self, db: &crate::database::Database) -> bool {
|
||||
// 1. Explicitly defines "id" either directly or via inheritance/extension?
|
||||
if self
|
||||
.compiled_properties
|
||||
.get()
|
||||
.map_or(false, |p| p.contains_key("id"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Implicit table-backed rule: Does its $family boundary map directly to the global database catalog?
|
||||
if let Some(family) = &self.family {
|
||||
let base = family.split('.').next_back().unwrap_or(family);
|
||||
if db.types.contains_key(base) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::database::page::Page;
|
||||
use crate::database::schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
@ -16,5 +17,5 @@ pub struct Punc {
|
||||
pub get: Option<String>,
|
||||
pub page: Option<Page>,
|
||||
#[serde(default)]
|
||||
pub schemas: Vec<Schema>,
|
||||
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::database::object::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use crate::database::object::*;
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct Schema {
|
||||
#[serde(flatten)]
|
||||
@ -26,6 +26,8 @@ impl Schema {
|
||||
pub fn compile(
|
||||
&self,
|
||||
db: &crate::database::Database,
|
||||
root_id: &str,
|
||||
path: String,
|
||||
visited: &mut std::collections::HashSet<String>,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
@ -33,9 +35,11 @@ impl Schema {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = &self.obj.id {
|
||||
if !visited.insert(id.clone()) {
|
||||
return; // Break cyclical resolution
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if !visited.insert(t.clone()) {
|
||||
return; // Break cyclical resolution
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +79,7 @@ impl Schema {
|
||||
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.schemas.get(t) {
|
||||
parent.as_ref().compile(db, visited, errors);
|
||||
parent.as_ref().compile(db, t, t.clone(), visited, errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
}
|
||||
@ -95,11 +99,12 @@ impl Schema {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
|
||||
message: format!(
|
||||
"Schema '{}' attempts to extend multiple custom object pointers in its type array. Use 'oneOf' for polymorphism and tagged unions.",
|
||||
self.obj.identifier().unwrap_or("unknown".to_string())
|
||||
"Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.",
|
||||
types
|
||||
),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: self.obj.identifier().unwrap_or("unknown".to_string()),
|
||||
path: path.clone(),
|
||||
schema: Some(root_id.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
@ -108,7 +113,7 @@ impl Schema {
|
||||
for t in types {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.as_ref().compile(db, visited, errors);
|
||||
parent.as_ref().compile(db, t, t.clone(), visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,60 +133,68 @@ impl Schema {
|
||||
let _ = self.obj.compiled_property_names.set(names);
|
||||
|
||||
// 4. Compute Edges natively
|
||||
let schema_edges = self.compile_edges(db, visited, &props, errors);
|
||||
let schema_edges = self.compile_edges(db, root_id, &path, visited, &props, errors);
|
||||
let _ = self.obj.compiled_edges.set(schema_edges);
|
||||
|
||||
// 5. Build our inline children properties recursively NOW! (Depth-first search)
|
||||
if let Some(local_props) = &self.obj.properties {
|
||||
for child in local_props.values() {
|
||||
child.compile(db, visited, errors);
|
||||
for (k, child) in local_props {
|
||||
child.compile(db, root_id, format!("{}/{}", path, k), visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(items) = &self.obj.items {
|
||||
items.compile(db, visited, errors);
|
||||
items.compile(db, root_id, format!("{}/items", path), visited, errors);
|
||||
}
|
||||
if let Some(pattern_props) = &self.obj.pattern_properties {
|
||||
for child in pattern_props.values() {
|
||||
child.compile(db, visited, errors);
|
||||
for (k, child) in pattern_props {
|
||||
child.compile(db, root_id, format!("{}/{}", path, k), visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(additional_props) = &self.obj.additional_properties {
|
||||
additional_props.compile(db, visited, errors);
|
||||
additional_props.compile(
|
||||
db,
|
||||
root_id,
|
||||
format!("{}/additionalProperties", path),
|
||||
visited,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
if let Some(one_of) = &self.obj.one_of {
|
||||
for child in one_of {
|
||||
child.compile(db, visited, errors);
|
||||
for (i, child) in one_of.iter().enumerate() {
|
||||
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(arr) = &self.obj.prefix_items {
|
||||
for child in arr {
|
||||
child.compile(db, visited, errors);
|
||||
for (i, child) in arr.iter().enumerate() {
|
||||
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(child) = &self.obj.not {
|
||||
child.compile(db, visited, errors);
|
||||
child.compile(db, root_id, format!("{}/not", path), visited, errors);
|
||||
}
|
||||
if let Some(child) = &self.obj.contains {
|
||||
child.compile(db, visited, errors);
|
||||
child.compile(db, root_id, format!("{}/contains", path), visited, errors);
|
||||
}
|
||||
if let Some(cases) = &self.obj.cases {
|
||||
for c in cases {
|
||||
for (i, c) in cases.iter().enumerate() {
|
||||
if let Some(child) = &c.when {
|
||||
child.compile(db, visited, errors);
|
||||
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), visited, errors);
|
||||
}
|
||||
if let Some(child) = &c.then {
|
||||
child.compile(db, visited, errors);
|
||||
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), visited, errors);
|
||||
}
|
||||
if let Some(child) = &c.else_ {
|
||||
child.compile(db, visited, errors);
|
||||
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.compile_polymorphism(db, errors);
|
||||
self.compile_polymorphism(db, root_id, &path, errors);
|
||||
|
||||
if let Some(id) = &self.obj.id {
|
||||
visited.remove(id);
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
visited.remove(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +205,8 @@ impl Schema {
|
||||
pub fn compile_edges(
|
||||
&self,
|
||||
db: &crate::database::Database,
|
||||
root_id: &str,
|
||||
path: &str,
|
||||
visited: &mut std::collections::HashSet<String>,
|
||||
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
@ -201,16 +216,33 @@ impl Schema {
|
||||
// Determine the physical Database Table Name this schema structurally represents
|
||||
// Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person")
|
||||
let mut parent_type_name = None;
|
||||
|
||||
if let Some(family) = &self.obj.family {
|
||||
// 1. Explicit horizontal routing
|
||||
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
||||
} else if let Some(identifier) = self.obj.identifier() {
|
||||
parent_type_name = Some(
|
||||
identifier
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(&identifier)
|
||||
.to_string(),
|
||||
);
|
||||
} else if !path.contains('/') {
|
||||
// 2. Root nodes trust their exact registry footprint
|
||||
let base_type_name = path.split('.').next_back().unwrap_or(path).to_string();
|
||||
if db.types.contains_key(&base_type_name) {
|
||||
parent_type_name = Some(base_type_name);
|
||||
}
|
||||
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
// 3. Nested graphs trust their explicit struct pointer reference
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if parent_type_name.is_none() {
|
||||
// 4. Absolute fallback for completely anonymous inline structures
|
||||
let base_type_name = root_id
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(root_id)
|
||||
.to_string();
|
||||
if db.types.contains_key(&base_type_name) {
|
||||
parent_type_name = Some(base_type_name);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(p_type) = parent_type_name {
|
||||
@ -237,13 +269,19 @@ impl Schema {
|
||||
// Determine the physical Postgres table backing the nested child schema recursively
|
||||
if let Some(family) = &target_schema.obj.family {
|
||||
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
||||
} else if let Some(ref_id) = target_schema.obj.identifier() {
|
||||
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
|
||||
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
||||
&target_schema.obj.type_
|
||||
{
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
|
||||
}
|
||||
} else if let Some(arr) = &target_schema.obj.one_of {
|
||||
if let Some(first) = arr.first() {
|
||||
if let Some(ref_id) = first.obj.identifier() {
|
||||
child_type_name =
|
||||
Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &first.obj.type_
|
||||
{
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,7 +290,7 @@ impl Schema {
|
||||
if db.types.contains_key(&c_type) {
|
||||
// Ensure the child Schema's AST has accurately compiled its own physical property keys so we can
|
||||
// inject them securely for Many-to-Many Twin Deduction disambiguation matching.
|
||||
target_schema.compile(db, visited, errors);
|
||||
target_schema.compile(db, root_id, format!("{}/{}", path, prop_name), visited, errors);
|
||||
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
|
||||
let keys_for_ambiguity: Vec<String> =
|
||||
compiled_target_props.keys().cloned().collect();
|
||||
@ -264,8 +302,8 @@ impl Schema {
|
||||
prop_name,
|
||||
Some(&keys_for_ambiguity),
|
||||
is_array,
|
||||
self.id.as_deref(),
|
||||
&format!("/{}", prop_name),
|
||||
Some(root_id),
|
||||
&format!("{}/{}", path, prop_name),
|
||||
errors,
|
||||
) {
|
||||
schema_edges.insert(
|
||||
@ -288,6 +326,8 @@ impl Schema {
|
||||
pub fn compile_polymorphism(
|
||||
&self,
|
||||
db: &crate::database::Database,
|
||||
root_id: &str,
|
||||
path: &str,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
let mut options = std::collections::BTreeMap::new();
|
||||
@ -312,7 +352,7 @@ impl Schema {
|
||||
};
|
||||
|
||||
if db.schemas.contains_key(&target_id) {
|
||||
options.insert(var.to_string(), target_id);
|
||||
options.insert(var.to_string(), (None, Some(target_id)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -321,12 +361,10 @@ impl Schema {
|
||||
|
||||
let suffix = format!(".{}", family_base);
|
||||
|
||||
for schema in &type_def.schemas {
|
||||
if let Some(id) = &schema.obj.id {
|
||||
if id.ends_with(&suffix) || id == &family_base {
|
||||
if let Some(kind_val) = schema.obj.get_discriminator_value("kind") {
|
||||
options.insert(kind_val, id.to_string());
|
||||
}
|
||||
for (id, schema) in &type_def.schemas {
|
||||
if id.ends_with(&suffix) || id == &family_base {
|
||||
if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) {
|
||||
options.insert(kind_val, (None, Some(id.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -335,65 +373,114 @@ impl Schema {
|
||||
} else if let Some(one_of) = &self.obj.one_of {
|
||||
let mut type_vals = std::collections::HashSet::new();
|
||||
let mut kind_vals = std::collections::HashSet::new();
|
||||
let mut disjoint_base = true;
|
||||
let mut structural_types = std::collections::HashSet::new();
|
||||
|
||||
for c in one_of {
|
||||
if let Some(t_val) = c.obj.get_discriminator_value("type") {
|
||||
type_vals.insert(t_val);
|
||||
let mut child_id = String::new();
|
||||
let mut child_is_primitive = false;
|
||||
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||
if crate::database::object::is_primitive_type(t) {
|
||||
child_is_primitive = true;
|
||||
structural_types.insert(t.clone());
|
||||
} else {
|
||||
child_id = t.clone();
|
||||
structural_types.insert("object".to_string());
|
||||
}
|
||||
} else {
|
||||
disjoint_base = false;
|
||||
}
|
||||
if let Some(k_val) = c.obj.get_discriminator_value("kind") {
|
||||
kind_vals.insert(k_val);
|
||||
|
||||
if !child_is_primitive {
|
||||
if let Some(t_val) = c.obj.get_discriminator_value("type", &child_id) {
|
||||
type_vals.insert(t_val);
|
||||
}
|
||||
if let Some(k_val) = c.obj.get_discriminator_value("kind", &child_id) {
|
||||
kind_vals.insert(k_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
|
||||
"type".to_string()
|
||||
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
|
||||
"kind".to_string()
|
||||
if disjoint_base && structural_types.len() == one_of.len() {
|
||||
strategy = "".to_string();
|
||||
for (i, c) in one_of.iter().enumerate() {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||
if crate::database::object::is_primitive_type(t) {
|
||||
options.insert(t.clone(), (Some(i), None));
|
||||
} else {
|
||||
options.insert("object".to_string(), (Some(i), None));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
|
||||
"type".to_string()
|
||||
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
|
||||
"kind".to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
if strategy.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for c in one_of {
|
||||
if let Some(val) = c.obj.get_discriminator_value(&strategy) {
|
||||
if options.contains_key(&val) {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "POLYMORPHIC_COLLISION".to_string(),
|
||||
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
|
||||
details: crate::drop::ErrorDetails::default()
|
||||
});
|
||||
continue;
|
||||
if strategy.is_empty() {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "AMBIGUOUS_POLYMORPHISM".to_string(),
|
||||
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: path.to_string(),
|
||||
schema: Some(root_id.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let mut target_id = c.obj.id.clone();
|
||||
if target_id.is_none() {
|
||||
for (i, c) in one_of.iter().enumerate() {
|
||||
let mut child_id = String::new();
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
target_id = Some(t.clone());
|
||||
child_id = t.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tid) = target_id {
|
||||
options.insert(val, tid);
|
||||
if let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) {
|
||||
if options.contains_key(&val) {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "POLYMORPHIC_COLLISION".to_string(),
|
||||
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: path.to_string(),
|
||||
schema: Some(root_id.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
options.insert(val, (Some(i), None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if !options.is_empty() {
|
||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||
if !strategy.is_empty() {
|
||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||
}
|
||||
let _ = self.obj.compiled_options.set(options);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn validate_identifier(id: &str, field_name: &str, errors: &mut Vec<crate::drop::Error>) {
|
||||
fn validate_identifier(
|
||||
id: &str,
|
||||
field_name: &str,
|
||||
root_id: &str,
|
||||
path: &str,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
#[cfg(not(test))]
|
||||
for c in id.chars() {
|
||||
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' {
|
||||
@ -401,9 +488,13 @@ impl Schema {
|
||||
code: "INVALID_IDENTIFIER".to_string(),
|
||||
message: format!(
|
||||
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]",
|
||||
c, field_name, id
|
||||
c, field_name, id
|
||||
),
|
||||
details: crate::drop::ErrorDetails::default(),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: path.to_string(),
|
||||
schema: Some(root_id.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -411,116 +502,124 @@ impl Schema {
|
||||
}
|
||||
|
||||
pub fn collect_schemas(
|
||||
&mut self,
|
||||
tracking_path: Option<String>,
|
||||
to_insert: &mut Vec<(String, Schema)>,
|
||||
schema_arc: &Arc<Schema>,
|
||||
root_id: &str,
|
||||
path: String,
|
||||
to_insert: &mut Vec<(String, Arc<Schema>)>,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
if let Some(id) = &self.obj.id {
|
||||
Self::validate_identifier(id, "$id", errors);
|
||||
to_insert.push((id.clone(), self.clone()));
|
||||
let mut should_push = false;
|
||||
|
||||
// Push ad-hoc inline composition into the addressable registry
|
||||
if schema_arc.obj.properties.is_some()
|
||||
|| schema_arc.obj.items.is_some()
|
||||
|| schema_arc.obj.family.is_some()
|
||||
|| schema_arc.obj.one_of.is_some()
|
||||
{
|
||||
should_push = true;
|
||||
}
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
Self::validate_identifier(t, "type", errors);
|
||||
}
|
||||
}
|
||||
if let Some(family) = &self.obj.family {
|
||||
Self::validate_identifier(family, "$family", errors);
|
||||
}
|
||||
|
||||
// Is this schema an inline ad-hoc composition?
|
||||
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
|
||||
if self.obj.id.is_none() && self.obj.properties.is_some() {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(ref path) = tracking_path {
|
||||
to_insert.push((path.clone(), self.clone()));
|
||||
}
|
||||
}
|
||||
Self::validate_identifier(t, "type", root_id, &path, errors);
|
||||
should_push = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the path origin to children natively, prioritizing the explicit `$id` boundary if one exists
|
||||
let origin_path = self.obj.id.clone().or(tracking_path);
|
||||
if let Some(family) = &schema_arc.obj.family {
|
||||
Self::validate_identifier(family, "$family", root_id, &path, errors);
|
||||
}
|
||||
|
||||
self.collect_child_schemas(origin_path, to_insert, errors);
|
||||
if should_push {
|
||||
to_insert.push((path.clone(), Arc::clone(schema_arc)));
|
||||
}
|
||||
|
||||
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
|
||||
}
|
||||
|
||||
pub fn collect_child_schemas(
|
||||
&mut self,
|
||||
origin_path: Option<String>,
|
||||
to_insert: &mut Vec<(String, Schema)>,
|
||||
schema_arc: &Arc<Schema>,
|
||||
root_id: &str,
|
||||
path: String,
|
||||
to_insert: &mut Vec<(String, Arc<Schema>)>,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
if let Some(props) = &mut self.obj.properties {
|
||||
for (k, v) in props.iter_mut() {
|
||||
let mut inner = (**v).clone();
|
||||
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
|
||||
inner.collect_schemas(next_path, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
if let Some(props) = &schema_arc.obj.properties {
|
||||
for (k, v) in props.iter() {
|
||||
let next_path = format!("{}/{}", path, k);
|
||||
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pattern_props) = &mut self.obj.pattern_properties {
|
||||
for (k, v) in pattern_props.iter_mut() {
|
||||
let mut inner = (**v).clone();
|
||||
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
|
||||
inner.collect_schemas(next_path, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
if let Some(pattern_props) = &schema_arc.obj.pattern_properties {
|
||||
for (k, v) in pattern_props.iter() {
|
||||
let next_path = format!("{}/{}", path, k);
|
||||
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
|
||||
}
|
||||
}
|
||||
|
||||
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
|
||||
for v in arr.iter_mut() {
|
||||
let mut inner = (**v).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
let mut map_arr = |arr: &Vec<Arc<Schema>>, sub: &str| {
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
Self::collect_schemas(v, root_id, format!("{}/{}/{}", path, sub, i), to_insert, errors);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(arr) = &mut self.obj.prefix_items {
|
||||
map_arr(arr);
|
||||
if let Some(arr) = &schema_arc.obj.prefix_items {
|
||||
map_arr(arr, "prefixItems");
|
||||
}
|
||||
|
||||
if let Some(arr) = &mut self.obj.one_of {
|
||||
map_arr(arr);
|
||||
if let Some(arr) = &schema_arc.obj.one_of {
|
||||
map_arr(arr, "oneOf");
|
||||
}
|
||||
|
||||
let mut map_opt = |opt: &mut Option<Arc<Schema>>, pass_path: bool| {
|
||||
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
|
||||
if let Some(v) = opt {
|
||||
let mut inner = (**v).clone();
|
||||
let next = if pass_path { origin_path.clone() } else { None };
|
||||
inner.collect_schemas(next, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
if pass_path {
|
||||
Self::collect_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
||||
} else {
|
||||
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
map_opt(&mut self.obj.additional_properties, false);
|
||||
map_opt(
|
||||
&schema_arc.obj.additional_properties,
|
||||
false,
|
||||
"additionalProperties",
|
||||
);
|
||||
map_opt(&schema_arc.obj.items, true, "items");
|
||||
map_opt(&schema_arc.obj.not, false, "not");
|
||||
map_opt(&schema_arc.obj.contains, false, "contains");
|
||||
map_opt(&schema_arc.obj.property_names, false, "propertyNames");
|
||||
|
||||
// `items` absolutely must inherit the EXACT property path assigned to the Array wrapper!
|
||||
// This allows nested Arrays enclosing bare Entity structs to correctly register as the boundary mapping.
|
||||
map_opt(&mut self.obj.items, true);
|
||||
|
||||
map_opt(&mut self.obj.not, false);
|
||||
map_opt(&mut self.obj.contains, false);
|
||||
map_opt(&mut self.obj.property_names, false);
|
||||
if let Some(cases) = &mut self.obj.cases {
|
||||
for c in cases.iter_mut() {
|
||||
if let Some(when) = &mut c.when {
|
||||
let mut inner = (**when).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*when = Arc::new(inner);
|
||||
if let Some(cases) = &schema_arc.obj.cases {
|
||||
for (i, c) in cases.iter().enumerate() {
|
||||
if let Some(when) = &c.when {
|
||||
Self::collect_schemas(
|
||||
when,
|
||||
root_id,
|
||||
format!("{}/cases/{}/when", path, i),
|
||||
to_insert,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
if let Some(then) = &mut c.then {
|
||||
let mut inner = (**then).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*then = Arc::new(inner);
|
||||
if let Some(then) = &c.then {
|
||||
Self::collect_schemas(
|
||||
then,
|
||||
root_id,
|
||||
format!("{}/cases/{}/then", path, i),
|
||||
to_insert,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
if let Some(else_) = &mut c.else_ {
|
||||
let mut inner = (**else_).clone();
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*else_ = Arc::new(inner);
|
||||
if let Some(else_) = &c.else_ {
|
||||
Self::collect_schemas(
|
||||
else_,
|
||||
root_id,
|
||||
format!("{}/cases/{}/else", path, i),
|
||||
to_insert,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use crate::database::schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@ -38,5 +39,5 @@ pub struct Type {
|
||||
pub default_fields: Vec<String>,
|
||||
pub field_types: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub schemas: Vec<Schema>,
|
||||
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
|
||||
}
|
||||
|
||||
@ -142,11 +142,21 @@ impl Merger {
|
||||
if let Some(disc) = schema.obj.compiled_discriminator.get() {
|
||||
let val = map.get(disc).and_then(|v| v.as_str());
|
||||
if let Some(v) = val {
|
||||
if let Some(target_id) = options.get(v) {
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id) {
|
||||
schema = Arc::clone(target_schema);
|
||||
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.schemas.get(target_id) {
|
||||
schema = Arc::clone(target_schema);
|
||||
} else {
|
||||
return Err(format!("Polymorphic mapped target '{}' not found in database registry", target_id));
|
||||
}
|
||||
} else if let Some(idx) = idx_opt {
|
||||
if let Some(target_schema) = schema.obj.one_of.as_ref().and_then(|options| options.get(*idx)) {
|
||||
schema = Arc::clone(target_schema);
|
||||
} else {
|
||||
return Err(format!("Polymorphic index target '{}' not found in local oneOf array", idx));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Polymorphic mapped target '{}' not found in database registry", target_id));
|
||||
return Err(format!("Polymorphic mapped target has no path"));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Polymorphic discriminator {}='{}' matched no compiled options", disc, v));
|
||||
@ -215,7 +225,7 @@ impl Merger {
|
||||
for (k, v) in obj {
|
||||
// Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables
|
||||
if k == "id" || k == "type" || k == "created" {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
entity_fields.insert(k, v);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -234,18 +244,18 @@ impl Merger {
|
||||
_ => "field", // Malformed edge data?
|
||||
};
|
||||
if typeof_v == "object" {
|
||||
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
entity_objects.insert(k, (v, prop_schema.clone()));
|
||||
} else if typeof_v == "array" {
|
||||
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
entity_arrays.insert(k, (v, prop_schema.clone()));
|
||||
} else {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
entity_fields.insert(k, v);
|
||||
}
|
||||
} else {
|
||||
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
entity_fields.insert(k, v);
|
||||
}
|
||||
} else if type_def.fields.contains(&k) {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
entity_fields.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,7 +534,7 @@ impl Merger {
|
||||
|
||||
entity_change_kind = Some("create".to_string());
|
||||
|
||||
let mut new_fields = changes.clone();
|
||||
let mut new_fields = changes;
|
||||
new_fields.insert("id".to_string(), id_val);
|
||||
new_fields.insert("type".to_string(), Value::String(type_name.to_string()));
|
||||
new_fields.insert("created_by".to_string(), Value::String(user_id.to_string()));
|
||||
@ -564,7 +574,7 @@ impl Merger {
|
||||
Some("update".to_string())
|
||||
};
|
||||
|
||||
let mut new_fields = changes.clone();
|
||||
let mut new_fields = changes;
|
||||
new_fields.insert(
|
||||
"id".to_string(),
|
||||
entity_fetched.as_ref().unwrap().get("id").unwrap().clone(),
|
||||
|
||||
@ -18,6 +18,7 @@ pub struct Node<'a> {
|
||||
pub depth: usize,
|
||||
pub ast_path: String,
|
||||
pub is_polymorphic_branch: bool,
|
||||
pub schema_id: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Compiler<'a> {
|
||||
@ -47,6 +48,7 @@ impl<'a> Compiler<'a> {
|
||||
depth: 0,
|
||||
ast_path: String::new(),
|
||||
is_polymorphic_branch: false,
|
||||
schema_id: Some(schema_id.to_string()),
|
||||
};
|
||||
|
||||
let (sql, _) = compiler.compile_node(node)?;
|
||||
@ -66,17 +68,31 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
// 1. Array of DB Entities (`type` or `$family` pointing to a table limit)
|
||||
if let Some(items) = &node.schema.obj.items {
|
||||
let mut resolved_type = None;
|
||||
if let Some(family_target) = items.obj.family.as_ref() {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target);
|
||||
resolved_type = self.db.types.get(base_type_name);
|
||||
} else if let Some(base_type_name) = items.obj.identifier() {
|
||||
resolved_type = self.db.types.get(&base_type_name);
|
||||
if let Some(sid) = &node.schema_id {
|
||||
resolved_type = self
|
||||
.db
|
||||
.types
|
||||
.get(&sid.split('.').next_back().unwrap_or(sid).to_string());
|
||||
}
|
||||
|
||||
if resolved_type.is_none() {
|
||||
if let Some(family_target) = items.obj.family.as_ref() {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target);
|
||||
resolved_type = self.db.types.get(base_type_name);
|
||||
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &items.obj.type_
|
||||
{
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
resolved_type = self
|
||||
.db
|
||||
.types
|
||||
.get(&t.split('.').next_back().unwrap_or(t).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(type_def) = resolved_type {
|
||||
@ -105,16 +121,28 @@ impl<'a> Compiler<'a> {
|
||||
// Determine if this schema represents a Database Entity
|
||||
let mut resolved_type = None;
|
||||
|
||||
if let Some(family_target) = node.schema.obj.family.as_ref() {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target);
|
||||
resolved_type = self.db.types.get(base_type_name);
|
||||
} else if let Some(base_type_name) = node.schema.obj.identifier() {
|
||||
if let Some(sid) = &node.schema_id {
|
||||
let base_type_name = sid.split('.').next_back().unwrap_or(sid).to_string();
|
||||
resolved_type = self.db.types.get(&base_type_name);
|
||||
}
|
||||
|
||||
if resolved_type.is_none() {
|
||||
if let Some(family_target) = node.schema.obj.family.as_ref() {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target);
|
||||
resolved_type = self.db.types.get(base_type_name);
|
||||
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
||||
&node.schema.obj.type_
|
||||
{
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
let base_type_name = t.split('.').next_back().unwrap_or(t).to_string();
|
||||
resolved_type = self.db.types.get(&base_type_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(type_def) = resolved_type {
|
||||
return self.compile_entity(type_def, node.clone(), false);
|
||||
}
|
||||
@ -126,6 +154,7 @@ impl<'a> Compiler<'a> {
|
||||
if let Some(target_schema) = self.db.schemas.get(t) {
|
||||
let mut ref_node = node.clone();
|
||||
ref_node.schema = Arc::clone(target_schema);
|
||||
ref_node.schema_id = Some(t.clone());
|
||||
return self.compile_node(ref_node);
|
||||
}
|
||||
return Err(format!("Unresolved schema type pointer: {}", t));
|
||||
@ -133,17 +162,21 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
// Handle Polymorphism fallbacks for relations
|
||||
if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
|
||||
if let Some(options) = node.schema.obj.compiled_options.get() {
|
||||
if options.len() == 1 {
|
||||
let target_id = options.values().next().unwrap();
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.type_ = Some(crate::database::object::SchemaTypeOrArray::Single(target_id.clone()));
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
return self.compile_node(bypass_node);
|
||||
if let Some(options) = node.schema.obj.compiled_options.get() {
|
||||
if options.len() == 1 {
|
||||
let (_, target_opt) = options.values().next().unwrap();
|
||||
if let Some(target_id) = target_opt {
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.type_ = Some(crate::database::object::SchemaTypeOrArray::Single(
|
||||
target_id.clone(),
|
||||
));
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
return self.compile_node(bypass_node);
|
||||
}
|
||||
}
|
||||
return self.compile_one_of(node);
|
||||
}
|
||||
}
|
||||
return self.compile_one_of(node);
|
||||
}
|
||||
|
||||
// Just an inline object definition?
|
||||
@ -171,27 +204,27 @@ impl<'a> Compiler<'a> {
|
||||
let (table_aliases, from_clauses) = self.compile_from_clause(r#type);
|
||||
|
||||
let jsonb_obj_sql = if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
|
||||
let base_alias = table_aliases
|
||||
.get(&r#type.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| node.parent_alias.to_string());
|
||||
|
||||
let mut case_node = node.clone();
|
||||
case_node.parent_alias = base_alias.clone();
|
||||
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
|
||||
case_node.parent_type_aliases = Some(arc_aliases);
|
||||
case_node.parent_type = Some(r#type);
|
||||
let base_alias = table_aliases
|
||||
.get(&r#type.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| node.parent_alias.to_string());
|
||||
|
||||
let (case_sql, _) = self.compile_one_of(case_node)?;
|
||||
case_sql
|
||||
let mut case_node = node.clone();
|
||||
case_node.parent_alias = base_alias.clone();
|
||||
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
|
||||
case_node.parent_type_aliases = Some(arc_aliases);
|
||||
case_node.parent_type = Some(r#type);
|
||||
|
||||
let (case_sql, _) = self.compile_one_of(case_node)?;
|
||||
case_sql
|
||||
} else {
|
||||
let select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
|
||||
|
||||
if select_args.is_empty() {
|
||||
"jsonb_build_object()".to_string()
|
||||
} else {
|
||||
format!("jsonb_build_object({})", select_args.join(", "))
|
||||
}
|
||||
let select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
|
||||
|
||||
if select_args.is_empty() {
|
||||
"jsonb_build_object()".to_string()
|
||||
} else {
|
||||
format!("jsonb_build_object({})", select_args.join(", "))
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Build WHERE clauses
|
||||
@ -249,14 +282,21 @@ impl<'a> Compiler<'a> {
|
||||
Ok((combined, "object".to_string()))
|
||||
}
|
||||
|
||||
fn compile_one_of(
|
||||
&mut self,
|
||||
node: Node<'a>,
|
||||
) -> Result<(String, String), String> {
|
||||
fn compile_one_of(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
let mut case_statements = Vec::new();
|
||||
|
||||
let options = node.schema.obj.compiled_options.get().ok_or("Missing compiled options for polymorphism")?;
|
||||
let disc = node.schema.obj.compiled_discriminator.get().ok_or("Missing compiled discriminator for polymorphism")?;
|
||||
let options = node
|
||||
.schema
|
||||
.obj
|
||||
.compiled_options
|
||||
.get()
|
||||
.ok_or("Missing compiled options for polymorphism")?;
|
||||
let disc = node
|
||||
.schema
|
||||
.obj
|
||||
.compiled_discriminator
|
||||
.get()
|
||||
.ok_or("Missing compiled discriminator for polymorphism")?;
|
||||
|
||||
let type_col = if let Some(prop) = &node.property_name {
|
||||
format!("{}_{}", prop, disc)
|
||||
@ -264,36 +304,73 @@ impl<'a> Compiler<'a> {
|
||||
disc.to_string()
|
||||
};
|
||||
|
||||
for (disc_val, target_id) in options {
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id) {
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = Arc::clone(target_schema);
|
||||
child_node.is_polymorphic_branch = true;
|
||||
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.schemas.get(target_id) {
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = Arc::clone(target_schema);
|
||||
child_node.schema_id = Some(target_id.clone());
|
||||
child_node.is_polymorphic_branch = true;
|
||||
|
||||
let val_sql = if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
|
||||
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
|
||||
let aliases = aliases_arc.as_ref();
|
||||
let p_type = node.parent_type.unwrap();
|
||||
|
||||
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
|
||||
|
||||
if select_args.is_empty() {
|
||||
let val_sql =
|
||||
if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
|
||||
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
|
||||
let aliases = aliases_arc.as_ref();
|
||||
let p_type = node.parent_type.unwrap();
|
||||
|
||||
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
|
||||
|
||||
if select_args.is_empty() {
|
||||
"jsonb_build_object()".to_string()
|
||||
} else {
|
||||
} else {
|
||||
format!("jsonb_build_object({})", select_args.join(", "))
|
||||
}
|
||||
} else {
|
||||
let (sql, _) = self.compile_node(child_node)?;
|
||||
sql
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let (sql, _) = self.compile_node(child_node)?;
|
||||
sql
|
||||
};
|
||||
|
||||
case_statements.push(format!(
|
||||
"WHEN {}.{} = '{}' THEN ({})",
|
||||
node.parent_alias, type_col, disc_val, val_sql
|
||||
));
|
||||
case_statements.push(format!(
|
||||
"WHEN {}.{} = '{}' THEN ({})",
|
||||
node.parent_alias, type_col, disc_val, val_sql
|
||||
));
|
||||
}
|
||||
} else if let Some(idx) = idx_opt {
|
||||
if let Some(target_schema) = node
|
||||
.schema
|
||||
.obj
|
||||
.one_of
|
||||
.as_ref()
|
||||
.and_then(|options| options.get(*idx))
|
||||
{
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = Arc::clone(target_schema);
|
||||
child_node.is_polymorphic_branch = true;
|
||||
|
||||
let val_sql = if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
|
||||
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
|
||||
let aliases = aliases_arc.as_ref();
|
||||
let p_type = node.parent_type.unwrap();
|
||||
|
||||
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
|
||||
|
||||
if select_args.is_empty() {
|
||||
"jsonb_build_object()".to_string()
|
||||
} else {
|
||||
format!("jsonb_build_object({})", select_args.join(", "))
|
||||
}
|
||||
} else {
|
||||
let (sql, _) = self.compile_node(child_node)?;
|
||||
sql
|
||||
};
|
||||
|
||||
case_statements.push(format!(
|
||||
"WHEN {}.{} = '{}' THEN ({})",
|
||||
node.parent_alias, type_col, disc_val, val_sql
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case_statements.is_empty() {
|
||||
return Ok(("NULL".to_string(), "string".to_string()));
|
||||
}
|
||||
@ -339,14 +416,14 @@ impl<'a> Compiler<'a> {
|
||||
let mut select_args = Vec::new();
|
||||
let grouped_fields = r#type.grouped_fields.as_ref().and_then(|v| v.as_object());
|
||||
let default_props = std::collections::BTreeMap::new();
|
||||
let merged_props = node.schema.obj.compiled_properties.get().unwrap_or(&default_props);
|
||||
|
||||
let mut sorted_keys: Vec<&String> = merged_props.keys().collect();
|
||||
sorted_keys.sort();
|
||||
|
||||
for prop_key in sorted_keys {
|
||||
let prop_schema = &merged_props[prop_key];
|
||||
let merged_props = node
|
||||
.schema
|
||||
.obj
|
||||
.compiled_properties
|
||||
.get()
|
||||
.unwrap_or(&default_props);
|
||||
|
||||
for (prop_key, prop_schema) in merged_props {
|
||||
let is_object_or_array = match &prop_schema.obj.type_ {
|
||||
Some(crate::database::object::SchemaTypeOrArray::Single(s)) => {
|
||||
s == "object" || s == "array"
|
||||
@ -410,6 +487,7 @@ impl<'a> Compiler<'a> {
|
||||
format!("{}/{}", node.ast_path, prop_key)
|
||||
},
|
||||
is_polymorphic_branch: false,
|
||||
schema_id: None,
|
||||
};
|
||||
|
||||
let (val_sql, val_type) = self.compile_node(child_node)?;
|
||||
@ -449,7 +527,7 @@ impl<'a> Compiler<'a> {
|
||||
|
||||
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
||||
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
|
||||
|
||||
|
||||
let start_len = where_clauses.len();
|
||||
self.compile_relation_conditions(
|
||||
r#type,
|
||||
@ -491,8 +569,12 @@ impl<'a> Compiler<'a> {
|
||||
.unwrap_or(family_target)
|
||||
.to_string(),
|
||||
);
|
||||
} else if let Some(lookup_key) = prop_schema.obj.identifier() {
|
||||
bound_type_name = Some(lookup_key);
|
||||
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
||||
&prop_schema.obj.type_
|
||||
{
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
bound_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(type_name) = bound_type_name {
|
||||
|
||||
@ -1553,24 +1553,6 @@ fn test_polymorphism_4_1() {
|
||||
crate::tests::runner::run_test_case(&path, 4, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_5_0() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 5, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_5_1() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 5, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_5_2() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_0_0() {
|
||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@ -3558,9 +3540,9 @@ fn test_paths_1_0() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_1_1() {
|
||||
fn test_paths_2_0() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 1, 1).unwrap();
|
||||
crate::tests::runner::run_test_case(&path, 2, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -7740,21 +7722,9 @@ fn test_object_types_2_1() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_object_types_3_0() {
|
||||
fn test_object_types_2_2() {
|
||||
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 3, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_object_types_3_1() {
|
||||
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 3, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_object_types_3_2() {
|
||||
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 3, 2).unwrap();
|
||||
crate::tests::runner::run_test_case(&path, 2, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -44,27 +44,30 @@ fn test_library_api() {
|
||||
"name": "source_schema",
|
||||
"variations": ["source_schema"],
|
||||
"hierarchy": ["source_schema", "entity"],
|
||||
"schemas": [{
|
||||
"$id": "source_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"target": { "type": "target_schema" }
|
||||
},
|
||||
"required": ["name"]
|
||||
}]
|
||||
"schemas": {
|
||||
"source_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"target": { "type": "target_schema" }
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "target_schema",
|
||||
"variations": ["target_schema"],
|
||||
"hierarchy": ["target_schema", "entity"],
|
||||
"schemas": [{
|
||||
"$id": "target_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": "number" }
|
||||
"schemas": {
|
||||
"target_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -86,9 +89,9 @@ fn test_library_api() {
|
||||
"type": "drop",
|
||||
"response": {
|
||||
"source_schema": {
|
||||
"$id": "source_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"target": {
|
||||
"type": "target_schema",
|
||||
@ -96,7 +99,7 @@ fn test_library_api() {
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"compiledProperties": ["name", "target"],
|
||||
"compiledProperties": ["name", "target", "type"],
|
||||
"compiledEdges": {
|
||||
"target": {
|
||||
"constraint": "fk_test_target",
|
||||
@ -104,8 +107,11 @@ fn test_library_api() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"source_schema/target": {
|
||||
"type": "target_schema",
|
||||
"compiledProperties": ["value"]
|
||||
},
|
||||
"target_schema": {
|
||||
"$id": "target_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": "number" }
|
||||
|
||||
@ -93,9 +93,12 @@ impl<'a> ValidationContext<'a> {
|
||||
if i < len {
|
||||
if let Some(child_instance) = arr.get(i) {
|
||||
let mut item_path = self.join_path(&i.to_string());
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = self.join_path(id_str);
|
||||
let is_topological = sub_schema.obj.requires_uuid_path(self.db);
|
||||
if is_topological {
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = self.join_path(id_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
let derived = self.derive(
|
||||
@ -116,12 +119,15 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
|
||||
if let Some(ref items_schema) = self.schema.items {
|
||||
let is_topological = items_schema.obj.requires_uuid_path(self.db);
|
||||
for i in validation_index..len {
|
||||
if let Some(child_instance) = arr.get(i) {
|
||||
let mut item_path = self.join_path(&i.to_string());
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = self.join_path(id_str);
|
||||
if is_topological {
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = self.join_path(id_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
let derived = self.derive(
|
||||
|
||||
@ -13,10 +13,17 @@ impl<'a> ValidationContext<'a> {
|
||||
) -> Result<bool, ValidationError> {
|
||||
let current = self.instance;
|
||||
if let Some(obj) = current.as_object() {
|
||||
let mut schema_identifier = None;
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.schema.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
schema_identifier = Some(t.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Entity implicit type validation
|
||||
if let Some(schema_identifier) = self.schema.identifier() {
|
||||
if let Some(ref schema_identifier_str) = schema_identifier {
|
||||
// We decompose identity string routing inherently
|
||||
let expected_type = schema_identifier.split('.').last().unwrap_or(&schema_identifier);
|
||||
let expected_type = schema_identifier_str.split('.').last().unwrap_or(schema_identifier_str);
|
||||
|
||||
// Check if the identifier represents a registered global database entity boundary mathematically
|
||||
if let Some(type_def) = self.db.types.get(expected_type) {
|
||||
@ -46,7 +53,7 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
|
||||
// If the target mathematically declares a horizontal structural STI variation natively
|
||||
if schema_identifier.contains('.') {
|
||||
if schema_identifier_str.contains('.') {
|
||||
if obj.get("kind").is_none() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_KIND".to_string(),
|
||||
@ -69,7 +76,7 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
if let Some(kind_val) = obj.get("kind") {
|
||||
if let Some((kind_str, _)) = schema_identifier.rsplit_once('.') {
|
||||
if let Some((kind_str, _)) = schema_identifier_str.rsplit_once('.') {
|
||||
if let Some(actual_kind) = kind_val.as_str() {
|
||||
if actual_kind == kind_str {
|
||||
result.evaluated_keys.insert("kind".to_string());
|
||||
|
||||
@ -30,77 +30,34 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
if self.schema.family.is_some() {
|
||||
if let Some(options) = self.schema.compiled_options.get() {
|
||||
if let Some(disc) = self.schema.compiled_discriminator.get() {
|
||||
return self.execute_polymorph(disc, options, result);
|
||||
}
|
||||
return self.execute_polymorph(options, result);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "UNCOMPILED_FAMILY".to_string(),
|
||||
message: "Encountered family block that could not be mapped to deterministic options during db schema compilation.".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn validate_one_of(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(one_of) = &self.schema.one_of {
|
||||
if self.schema.one_of.is_some() {
|
||||
if let Some(options) = self.schema.compiled_options.get() {
|
||||
if let Some(disc) = self.schema.compiled_discriminator.get() {
|
||||
return self.execute_polymorph(disc, options, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Native Draft2020-12 oneOf Evaluation Fallback
|
||||
let mut valid_count = 0;
|
||||
let mut final_successful_result = None;
|
||||
let mut failed_candidates = Vec::new();
|
||||
|
||||
for child_schema in one_of {
|
||||
let derived = self.derive_for_schema(child_schema, false);
|
||||
if let Ok(sub_res) = derived.validate_scoped() {
|
||||
if sub_res.is_valid() {
|
||||
valid_count += 1;
|
||||
final_successful_result = Some(sub_res.clone());
|
||||
} else {
|
||||
failed_candidates.push(sub_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid_count == 1 {
|
||||
if let Some(successful_res) = final_successful_result {
|
||||
result.merge(successful_res);
|
||||
}
|
||||
return Ok(true);
|
||||
} else if valid_count == 0 {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NO_ONEOF_MATCH".to_string(),
|
||||
message: "Payload matches none of the required candidate sub-schemas natively".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
if let Some(first) = failed_candidates.first() {
|
||||
let mut shared_errors = first.errors.clone();
|
||||
for sub_res in failed_candidates.iter().skip(1) {
|
||||
shared_errors.retain(|e1| {
|
||||
sub_res.errors.iter().any(|e2| e1.code == e2.code && e1.path == e2.path)
|
||||
});
|
||||
}
|
||||
for e in shared_errors {
|
||||
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
|
||||
result.errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
return self.execute_polymorph(options, result);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
|
||||
message: "Matches multiple polymorphic candidates inextricably natively".to_string(),
|
||||
result.errors.push(ValidationError {
|
||||
code: "UNCOMPILED_ONEOF".to_string(),
|
||||
message: "Encountered oneOf block that could not be mapped to deterministic compiled options natively.".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
@ -108,46 +65,115 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
pub(crate) fn execute_polymorph(
|
||||
&self,
|
||||
disc: &str,
|
||||
options: &std::collections::BTreeMap<String, String>,
|
||||
options: &std::collections::BTreeMap<String, (Option<usize>, Option<String>)>,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
// 1. O(1) Fast-Path Router & Extractor
|
||||
let instance_val = self.instance.as_object().and_then(|o| o.get(disc)).and_then(|t| t.as_str());
|
||||
let instance_val = if let Some(disc) = self.schema.compiled_discriminator.get() {
|
||||
let val = self
|
||||
.instance
|
||||
.as_object()
|
||||
.and_then(|o| o.get(disc))
|
||||
.and_then(|t| t.as_str());
|
||||
if val.is_some() {
|
||||
result.evaluated_keys.insert(disc.to_string());
|
||||
}
|
||||
val.map(|s| s.to_string())
|
||||
} else {
|
||||
match self.instance {
|
||||
serde_json::Value::Null => Some("null".to_string()),
|
||||
serde_json::Value::Bool(_) => Some("boolean".to_string()),
|
||||
serde_json::Value::Number(n) => {
|
||||
if n.is_i64() || n.is_u64() {
|
||||
Some("integer".to_string())
|
||||
} else {
|
||||
Some("number".to_string())
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(_) => Some("string".to_string()),
|
||||
serde_json::Value::Array(_) => Some("array".to_string()),
|
||||
serde_json::Value::Object(_) => Some("object".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(val) = instance_val {
|
||||
result.evaluated_keys.insert(disc.to_string());
|
||||
|
||||
if let Some(target_id) = options.get(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.schemas.get(target_id) {
|
||||
let derived = self.derive_for_schema(target_schema.as_ref(), false);
|
||||
let sub_res = derived.validate()?;
|
||||
let is_valid = sub_res.is_valid();
|
||||
result.merge(sub_res);
|
||||
return Ok(is_valid);
|
||||
let derived = self.derive_for_schema(target_schema.as_ref(), false);
|
||||
let sub_res = derived.validate()?;
|
||||
let is_valid = sub_res.is_valid();
|
||||
result.merge(sub_res);
|
||||
return Ok(is_valid);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
message: format!("Polymorphic router target '{}' does not exist in the database schemas map", target_id),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
message: format!(
|
||||
"Polymorphic router target '{}' does not exist in the database schemas map",
|
||||
target_id
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
|
||||
message: format!("Payload provided discriminator {}='{}' which matches none of the required candidate sub-schemas", disc, val),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else if let Some(idx) = idx_opt {
|
||||
if let Some(target_schema) = self
|
||||
.schema
|
||||
.one_of
|
||||
.as_ref()
|
||||
.and_then(|options| options.get(*idx))
|
||||
{
|
||||
let derived = self.derive_for_schema(target_schema.as_ref(), false);
|
||||
let sub_res = derived.validate()?;
|
||||
let is_valid = sub_res.is_valid();
|
||||
result.merge(sub_res);
|
||||
return Ok(is_valid);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
message: format!(
|
||||
"Polymorphic index target '{}' does not exist in the local oneOf array",
|
||||
idx
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
let disc_msg = if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
format!("discriminator {}='{}'", d, val)
|
||||
} else {
|
||||
format!("structural JSON base primitive '{}'", val)
|
||||
};
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
message: format!("Missing '{}' discriminator. Unable to resolve polymorphic boundaries", disc),
|
||||
code: if self.schema.family.is_some() {
|
||||
"NO_FAMILY_MATCH".to_string()
|
||||
} else {
|
||||
"NO_ONEOF_MATCH".to_string()
|
||||
},
|
||||
message: format!(
|
||||
"Payload matched no candidate boundaries based on its {}",
|
||||
disc_msg
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
message: format!(
|
||||
"Missing explicit '{}' discriminator. Unable to resolve polymorphic boundaries",
|
||||
d
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,14 +205,16 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
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
|
||||
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());
|
||||
}
|
||||
}
|
||||
for t in arr {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
|
||||
Reference in New Issue
Block a user