filter checkpoint
This commit is contained in:
163
src/database/compile/collection.rs
Normal file
163
src/database/compile/collection.rs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
use crate::database::schema::Schema;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
impl Schema {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub(crate) 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 != '.' {
|
||||||
|
errors.push(crate::drop::Error {
|
||||||
|
code: "INVALID_IDENTIFIER".to_string(),
|
||||||
|
message: format!(
|
||||||
|
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]",
|
||||||
|
c, field_name, id
|
||||||
|
),
|
||||||
|
details: crate::drop::ErrorDetails {
|
||||||
|
path: Some(path.to_string()),
|
||||||
|
schema: Some(root_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_schemas(
|
||||||
|
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(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
|
||||||
|
if t == "array" {
|
||||||
|
if let Some(items) = &schema_arc.obj.items {
|
||||||
|
if let Some(crate::database::object::SchemaTypeOrArray::Single(it)) = &items.obj.type_ {
|
||||||
|
if !crate::database::object::is_primitive_type(it) {
|
||||||
|
if items.obj.properties.is_some() || items.obj.cases.is_some() {
|
||||||
|
to_insert.push((path.clone(), Arc::clone(schema_arc)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !crate::database::object::is_primitive_type(t) {
|
||||||
|
Self::validate_identifier(t, "type", root_id, &path, errors);
|
||||||
|
|
||||||
|
// Is this an explicit inline ad-hoc composition?
|
||||||
|
if schema_arc.obj.properties.is_some() || schema_arc.obj.cases.is_some() {
|
||||||
|
to_insert.push((path.clone(), Arc::clone(schema_arc)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(family) = &schema_arc.obj.family {
|
||||||
|
Self::validate_identifier(family, "family", root_id, &path, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_child_schemas(
|
||||||
|
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) = &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) = &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: &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) = &schema_arc.obj.prefix_items {
|
||||||
|
map_arr(arr, "prefixItems");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(arr) = &schema_arc.obj.one_of {
|
||||||
|
map_arr(arr, "oneOf");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
|
||||||
|
if let Some(v) = opt {
|
||||||
|
if pass_path {
|
||||||
|
// Arrays explicitly push their wrapper natively.
|
||||||
|
// 'items' becomes a transparent conduit, bypassing self-promotion and skipping the '/items' suffix.
|
||||||
|
Self::collect_child_schemas(v, root_id, path.clone(), to_insert, errors);
|
||||||
|
} else {
|
||||||
|
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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) = &c.then {
|
||||||
|
Self::collect_schemas(
|
||||||
|
then,
|
||||||
|
root_id,
|
||||||
|
format!("{}/cases/{}/then", path, i),
|
||||||
|
to_insert,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(else_) = &c.else_ {
|
||||||
|
Self::collect_schemas(
|
||||||
|
else_,
|
||||||
|
root_id,
|
||||||
|
format!("{}/cases/{}/else", path, i),
|
||||||
|
to_insert,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/database/compile/edges.rs
Normal file
128
src/database/compile/edges.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use crate::database::schema::Schema;
|
||||||
|
|
||||||
|
impl Schema {
|
||||||
|
/// Dynamically infers and compiles all structural database relationships between this Schema
|
||||||
|
/// and its nested children. This functions recursively traverses the JSON Schema abstract syntax
|
||||||
|
/// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation
|
||||||
|
/// constraint paths directly onto the `compiled_edges` map in O(1) memory.
|
||||||
|
pub fn compile_edges(
|
||||||
|
&self,
|
||||||
|
db: &crate::database::Database,
|
||||||
|
root_id: &str,
|
||||||
|
path: &str,
|
||||||
|
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
|
||||||
|
errors: &mut Vec<crate::drop::Error>,
|
||||||
|
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
|
||||||
|
let mut schema_edges = std::collections::BTreeMap::new();
|
||||||
|
|
||||||
|
// 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 path == root_id {
|
||||||
|
// 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 let Some(p_type) = parent_type_name {
|
||||||
|
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
|
||||||
|
if let Some(type_def) = db.types.get(&p_type) {
|
||||||
|
// Iterate over all discovered schema boundaries mapped inside the object
|
||||||
|
for (prop_name, prop_schema) in props {
|
||||||
|
let mut child_type_name = None;
|
||||||
|
let mut target_schema = prop_schema.clone();
|
||||||
|
let mut is_array = false;
|
||||||
|
|
||||||
|
// Structurally unpack the inner target entity if the object maps to an array list
|
||||||
|
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
||||||
|
&prop_schema.obj.type_
|
||||||
|
{
|
||||||
|
if t == "array" {
|
||||||
|
is_array = true;
|
||||||
|
if let Some(items) = &prop_schema.obj.items {
|
||||||
|
target_schema = items.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(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(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c_type) = child_type_name {
|
||||||
|
// Skip edge compilation for JSONB columns — they store data inline, not relationally.
|
||||||
|
// The physical column type from field_types is the single source of truth.
|
||||||
|
if let Some(ft) = type_def
|
||||||
|
.field_types
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.get(prop_name.as_str()))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
{
|
||||||
|
if ft == "jsonb" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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, root_id, format!("{}/{}", path, prop_name), 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();
|
||||||
|
|
||||||
|
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
|
||||||
|
if let Some((relation, is_forward)) = db.resolve_relation(
|
||||||
|
&p_type,
|
||||||
|
&c_type,
|
||||||
|
prop_name,
|
||||||
|
Some(&keys_for_ambiguity),
|
||||||
|
is_array,
|
||||||
|
Some(root_id),
|
||||||
|
&format!("{}/{}", path, prop_name),
|
||||||
|
errors,
|
||||||
|
) {
|
||||||
|
schema_edges.insert(
|
||||||
|
prop_name.clone(),
|
||||||
|
crate::database::edge::Edge {
|
||||||
|
constraint: relation.constraint.clone(),
|
||||||
|
forward: is_forward,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema_edges
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/database/compile/mod.rs
Normal file
174
src/database/compile/mod.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
pub mod collection;
|
||||||
|
pub mod edges;
|
||||||
|
pub mod polymorphism;
|
||||||
|
|
||||||
|
use crate::database::schema::Schema;
|
||||||
|
|
||||||
|
impl Schema {
|
||||||
|
pub fn compile(
|
||||||
|
&self,
|
||||||
|
db: &crate::database::Database,
|
||||||
|
root_id: &str,
|
||||||
|
path: String,
|
||||||
|
errors: &mut Vec<crate::drop::Error>,
|
||||||
|
) {
|
||||||
|
if self.obj.compiled_properties.get().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(format_str) = &self.obj.format {
|
||||||
|
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
|
||||||
|
let _ = self
|
||||||
|
.obj
|
||||||
|
.compiled_format
|
||||||
|
.set(crate::database::object::CompiledFormat::Func(fmt.func));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pattern_str) = &self.obj.pattern {
|
||||||
|
if let Ok(re) = regex::Regex::new(pattern_str) {
|
||||||
|
let _ = self
|
||||||
|
.obj
|
||||||
|
.compiled_pattern
|
||||||
|
.set(crate::database::object::CompiledRegex(re));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pattern_props) = &self.obj.pattern_properties {
|
||||||
|
let mut compiled = Vec::new();
|
||||||
|
for (k, v) in pattern_props {
|
||||||
|
if let Ok(re) = regex::Regex::new(k) {
|
||||||
|
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !compiled.is_empty() {
|
||||||
|
let _ = self.obj.compiled_pattern_properties.set(compiled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut props = std::collections::BTreeMap::new();
|
||||||
|
|
||||||
|
// 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.schemas.get(t) {
|
||||||
|
parent.as_ref().compile(db, t, t.clone(), errors);
|
||||||
|
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||||
|
props.extend(p_props.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
|
||||||
|
let mut custom_type_count = 0;
|
||||||
|
for t in types {
|
||||||
|
if !crate::database::object::is_primitive_type(t) {
|
||||||
|
custom_type_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if custom_type_count > 1 {
|
||||||
|
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.",
|
||||||
|
types
|
||||||
|
),
|
||||||
|
details: crate::drop::ErrorDetails {
|
||||||
|
path: Some(path.clone()),
|
||||||
|
schema: Some(root_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, t, t.clone(), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Add local properties
|
||||||
|
if let Some(local_props) = &self.obj.properties {
|
||||||
|
for (k, v) in local_props {
|
||||||
|
props.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Add cases conditionally-defined properties recursively
|
||||||
|
if let Some(cases) = &self.obj.cases {
|
||||||
|
for (i, c) in cases.iter().enumerate() {
|
||||||
|
if let Some(child) = &c.when {
|
||||||
|
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), errors);
|
||||||
|
}
|
||||||
|
if let Some(child) = &c.then {
|
||||||
|
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), errors);
|
||||||
|
if let Some(t_props) = child.obj.compiled_properties.get() {
|
||||||
|
props.extend(t_props.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(child) = &c.else_ {
|
||||||
|
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), errors);
|
||||||
|
if let Some(e_props) = child.obj.compiled_properties.get() {
|
||||||
|
props.extend(e_props.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Set the OnceLock!
|
||||||
|
let _ = self.obj.compiled_properties.set(props.clone());
|
||||||
|
let mut names: Vec<String> = props.keys().cloned().collect();
|
||||||
|
names.sort();
|
||||||
|
let _ = self.obj.compiled_property_names.set(names);
|
||||||
|
|
||||||
|
// 5. Compute Edges natively
|
||||||
|
let schema_edges = self.compile_edges(db, root_id, &path, &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 (k, child) in local_props {
|
||||||
|
child.compile(db, root_id, format!("{}/{}", path, k), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(items) = &self.obj.items {
|
||||||
|
items.compile(db, root_id, format!("{}/items", path), errors);
|
||||||
|
}
|
||||||
|
if let Some(pattern_props) = &self.obj.pattern_properties {
|
||||||
|
for (k, child) in pattern_props {
|
||||||
|
child.compile(db, root_id, format!("{}/{}", path, k), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(additional_props) = &self.obj.additional_properties {
|
||||||
|
additional_props.compile(
|
||||||
|
db,
|
||||||
|
root_id,
|
||||||
|
format!("{}/additionalProperties", path),
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(one_of) = &self.obj.one_of {
|
||||||
|
for (i, child) in one_of.iter().enumerate() {
|
||||||
|
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(arr) = &self.obj.prefix_items {
|
||||||
|
for (i, child) in arr.iter().enumerate() {
|
||||||
|
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(child) = &self.obj.not {
|
||||||
|
child.compile(db, root_id, format!("{}/not", path), errors);
|
||||||
|
}
|
||||||
|
if let Some(child) = &self.obj.contains {
|
||||||
|
child.compile(db, root_id, format!("{}/contains", path), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.compile_polymorphism(db, root_id, &path, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/database/compile/polymorphism.rs
Normal file
153
src/database/compile/polymorphism.rs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
use crate::database::schema::Schema;
|
||||||
|
|
||||||
|
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();
|
||||||
|
let mut strategy = String::new();
|
||||||
|
|
||||||
|
if let Some(family) = &self.obj.family {
|
||||||
|
let family_base = family.split('.').next_back().unwrap_or(family).to_string();
|
||||||
|
let family_prefix = family
|
||||||
|
.strip_suffix(&family_base)
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_end_matches('.');
|
||||||
|
|
||||||
|
if let Some(type_def) = db.types.get(&family_base) {
|
||||||
|
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
|
||||||
|
// Scenario A / B: Table Variations
|
||||||
|
strategy = "type".to_string();
|
||||||
|
for var in &type_def.variations {
|
||||||
|
let target_id = if family_prefix.is_empty() {
|
||||||
|
var.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}.{}", family_prefix, var)
|
||||||
|
};
|
||||||
|
|
||||||
|
if db.schemas.contains_key(&target_id) {
|
||||||
|
options.insert(var.to_string(), (None, Some(target_id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Scenario C: Single Table Inheritance (Horizontal)
|
||||||
|
strategy = "kind".to_string();
|
||||||
|
|
||||||
|
let suffix = format!(".{}", family_base);
|
||||||
|
|
||||||
|
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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
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 !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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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() {
|
||||||
|
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: Some(path.to_string()),
|
||||||
|
schema: Some(root_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
child_id = t.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Some(path.to_string()),
|
||||||
|
schema: Some(root_id.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.insert(val, (Some(i), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !options.is_empty() {
|
||||||
|
if !strategy.is_empty() {
|
||||||
|
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||||
|
}
|
||||||
|
let _ = self.obj.compiled_options.set(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ pub mod punc;
|
|||||||
pub mod relation;
|
pub mod relation;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod r#type;
|
pub mod r#type;
|
||||||
|
pub mod compile;
|
||||||
|
|
||||||
// External mock exports inside the executor sub-folder
|
// External mock exports inside the executor sub-folder
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::database::object::*;
|
use crate::database::object::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
|
||||||
#[derive(Debug, Clone, Serialize, Default)]
|
#[derive(Debug, Clone, Serialize, Default)]
|
||||||
pub struct Schema {
|
pub struct Schema {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@ -22,609 +22,6 @@ impl std::ops::DerefMut for Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Schema {
|
|
||||||
pub fn compile(
|
|
||||||
&self,
|
|
||||||
db: &crate::database::Database,
|
|
||||||
root_id: &str,
|
|
||||||
path: String,
|
|
||||||
errors: &mut Vec<crate::drop::Error>,
|
|
||||||
) {
|
|
||||||
if self.obj.compiled_properties.get().is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(format_str) = &self.obj.format {
|
|
||||||
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
|
|
||||||
let _ = self
|
|
||||||
.obj
|
|
||||||
.compiled_format
|
|
||||||
.set(crate::database::object::CompiledFormat::Func(fmt.func));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pattern_str) = &self.obj.pattern {
|
|
||||||
if let Ok(re) = regex::Regex::new(pattern_str) {
|
|
||||||
let _ = self
|
|
||||||
.obj
|
|
||||||
.compiled_pattern
|
|
||||||
.set(crate::database::object::CompiledRegex(re));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pattern_props) = &self.obj.pattern_properties {
|
|
||||||
let mut compiled = Vec::new();
|
|
||||||
for (k, v) in pattern_props {
|
|
||||||
if let Ok(re) = regex::Regex::new(k) {
|
|
||||||
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !compiled.is_empty() {
|
|
||||||
let _ = self.obj.compiled_pattern_properties.set(compiled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut props = std::collections::BTreeMap::new();
|
|
||||||
|
|
||||||
// 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.schemas.get(t) {
|
|
||||||
parent.as_ref().compile(db, t, t.clone(), errors);
|
|
||||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
|
||||||
props.extend(p_props.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
|
|
||||||
let mut custom_type_count = 0;
|
|
||||||
for t in types {
|
|
||||||
if !crate::database::object::is_primitive_type(t) {
|
|
||||||
custom_type_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_type_count > 1 {
|
|
||||||
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.",
|
|
||||||
types
|
|
||||||
),
|
|
||||||
details: crate::drop::ErrorDetails {
|
|
||||||
path: Some(path.clone()),
|
|
||||||
schema: Some(root_id.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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, t, t.clone(), errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Add local properties
|
|
||||||
if let Some(local_props) = &self.obj.properties {
|
|
||||||
for (k, v) in local_props {
|
|
||||||
props.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Add cases conditionally-defined properties recursively
|
|
||||||
if let Some(cases) = &self.obj.cases {
|
|
||||||
for (i, c) in cases.iter().enumerate() {
|
|
||||||
if let Some(child) = &c.when {
|
|
||||||
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), errors);
|
|
||||||
}
|
|
||||||
if let Some(child) = &c.then {
|
|
||||||
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), errors);
|
|
||||||
if let Some(t_props) = child.obj.compiled_properties.get() {
|
|
||||||
props.extend(t_props.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(child) = &c.else_ {
|
|
||||||
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), errors);
|
|
||||||
if let Some(e_props) = child.obj.compiled_properties.get() {
|
|
||||||
props.extend(e_props.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Set the OnceLock!
|
|
||||||
let _ = self.obj.compiled_properties.set(props.clone());
|
|
||||||
let mut names: Vec<String> = props.keys().cloned().collect();
|
|
||||||
names.sort();
|
|
||||||
let _ = self.obj.compiled_property_names.set(names);
|
|
||||||
|
|
||||||
// 5. Compute Edges natively
|
|
||||||
let schema_edges = self.compile_edges(db, root_id, &path, &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 (k, child) in local_props {
|
|
||||||
child.compile(db, root_id, format!("{}/{}", path, k), errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(items) = &self.obj.items {
|
|
||||||
items.compile(db, root_id, format!("{}/items", path), errors);
|
|
||||||
}
|
|
||||||
if let Some(pattern_props) = &self.obj.pattern_properties {
|
|
||||||
for (k, child) in pattern_props {
|
|
||||||
child.compile(db, root_id, format!("{}/{}", path, k), errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(additional_props) = &self.obj.additional_properties {
|
|
||||||
additional_props.compile(
|
|
||||||
db,
|
|
||||||
root_id,
|
|
||||||
format!("{}/additionalProperties", path),
|
|
||||||
errors,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(one_of) = &self.obj.one_of {
|
|
||||||
for (i, child) in one_of.iter().enumerate() {
|
|
||||||
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(arr) = &self.obj.prefix_items {
|
|
||||||
for (i, child) in arr.iter().enumerate() {
|
|
||||||
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(child) = &self.obj.not {
|
|
||||||
child.compile(db, root_id, format!("{}/not", path), errors);
|
|
||||||
}
|
|
||||||
if let Some(child) = &self.obj.contains {
|
|
||||||
child.compile(db, root_id, format!("{}/contains", path), errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.compile_polymorphism(db, root_id, &path, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dynamically infers and compiles all structural database relationships between this Schema
|
|
||||||
/// and its nested children. This functions recursively traverses the JSON Schema abstract syntax
|
|
||||||
/// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation
|
|
||||||
/// constraint paths directly onto the `compiled_edges` map in O(1) memory.
|
|
||||||
pub fn compile_edges(
|
|
||||||
&self,
|
|
||||||
db: &crate::database::Database,
|
|
||||||
root_id: &str,
|
|
||||||
path: &str,
|
|
||||||
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
|
|
||||||
errors: &mut Vec<crate::drop::Error>,
|
|
||||||
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
|
|
||||||
let mut schema_edges = std::collections::BTreeMap::new();
|
|
||||||
|
|
||||||
// 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 path == root_id {
|
|
||||||
// 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 let Some(p_type) = parent_type_name {
|
|
||||||
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
|
|
||||||
if let Some(type_def) = db.types.get(&p_type) {
|
|
||||||
// Iterate over all discovered schema boundaries mapped inside the object
|
|
||||||
for (prop_name, prop_schema) in props {
|
|
||||||
let mut child_type_name = None;
|
|
||||||
let mut target_schema = prop_schema.clone();
|
|
||||||
let mut is_array = false;
|
|
||||||
|
|
||||||
// Structurally unpack the inner target entity if the object maps to an array list
|
|
||||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
|
||||||
&prop_schema.obj.type_
|
|
||||||
{
|
|
||||||
if t == "array" {
|
|
||||||
is_array = true;
|
|
||||||
if let Some(items) = &prop_schema.obj.items {
|
|
||||||
target_schema = items.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(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(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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(c_type) = child_type_name {
|
|
||||||
// Skip edge compilation for JSONB columns — they store data inline, not relationally.
|
|
||||||
// The physical column type from field_types is the single source of truth.
|
|
||||||
if let Some(ft) = type_def
|
|
||||||
.field_types
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|v| v.get(prop_name.as_str()))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
{
|
|
||||||
if ft == "jsonb" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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, root_id, format!("{}/{}", path, prop_name), 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();
|
|
||||||
|
|
||||||
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
|
|
||||||
if let Some((relation, is_forward)) = db.resolve_relation(
|
|
||||||
&p_type,
|
|
||||||
&c_type,
|
|
||||||
prop_name,
|
|
||||||
Some(&keys_for_ambiguity),
|
|
||||||
is_array,
|
|
||||||
Some(root_id),
|
|
||||||
&format!("{}/{}", path, prop_name),
|
|
||||||
errors,
|
|
||||||
) {
|
|
||||||
schema_edges.insert(
|
|
||||||
prop_name.clone(),
|
|
||||||
crate::database::edge::Edge {
|
|
||||||
constraint: relation.constraint.clone(),
|
|
||||||
forward: is_forward,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
schema_edges
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
let mut strategy = String::new();
|
|
||||||
|
|
||||||
if let Some(family) = &self.obj.family {
|
|
||||||
let family_base = family.split('.').next_back().unwrap_or(family).to_string();
|
|
||||||
let family_prefix = family
|
|
||||||
.strip_suffix(&family_base)
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim_end_matches('.');
|
|
||||||
|
|
||||||
if let Some(type_def) = db.types.get(&family_base) {
|
|
||||||
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
|
|
||||||
// Scenario A / B: Table Variations
|
|
||||||
strategy = "type".to_string();
|
|
||||||
for var in &type_def.variations {
|
|
||||||
let target_id = if family_prefix.is_empty() {
|
|
||||||
var.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}.{}", family_prefix, var)
|
|
||||||
};
|
|
||||||
|
|
||||||
if db.schemas.contains_key(&target_id) {
|
|
||||||
options.insert(var.to_string(), (None, Some(target_id)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Scenario C: Single Table Inheritance (Horizontal)
|
|
||||||
strategy = "kind".to_string();
|
|
||||||
|
|
||||||
let suffix = format!(".{}", family_base);
|
|
||||||
|
|
||||||
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())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 {
|
|
||||||
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 !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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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() {
|
|
||||||
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: Some(path.to_string()),
|
|
||||||
schema: Some(root_id.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
child_id = t.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Some(path.to_string()),
|
|
||||||
schema: Some(root_id.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.insert(val, (Some(i), None));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !options.is_empty() {
|
|
||||||
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,
|
|
||||||
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 != '.' {
|
|
||||||
errors.push(crate::drop::Error {
|
|
||||||
code: "INVALID_IDENTIFIER".to_string(),
|
|
||||||
message: format!(
|
|
||||||
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]",
|
|
||||||
c, field_name, id
|
|
||||||
),
|
|
||||||
details: crate::drop::ErrorDetails {
|
|
||||||
path: Some(path.to_string()),
|
|
||||||
schema: Some(root_id.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_schemas(
|
|
||||||
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(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
|
|
||||||
if t == "array" {
|
|
||||||
if let Some(items) = &schema_arc.obj.items {
|
|
||||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(it)) = &items.obj.type_ {
|
|
||||||
if !crate::database::object::is_primitive_type(it) {
|
|
||||||
if items.obj.properties.is_some() || items.obj.cases.is_some() {
|
|
||||||
to_insert.push((path.clone(), Arc::clone(schema_arc)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !crate::database::object::is_primitive_type(t) {
|
|
||||||
Self::validate_identifier(t, "type", root_id, &path, errors);
|
|
||||||
|
|
||||||
// Is this an explicit inline ad-hoc composition?
|
|
||||||
if schema_arc.obj.properties.is_some() || schema_arc.obj.cases.is_some() {
|
|
||||||
to_insert.push((path.clone(), Arc::clone(schema_arc)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(family) = &schema_arc.obj.family {
|
|
||||||
Self::validate_identifier(family, "family", root_id, &path, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_child_schemas(
|
|
||||||
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) = &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) = &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: &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) = &schema_arc.obj.prefix_items {
|
|
||||||
map_arr(arr, "prefixItems");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(arr) = &schema_arc.obj.one_of {
|
|
||||||
map_arr(arr, "oneOf");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
|
|
||||||
if let Some(v) = opt {
|
|
||||||
if pass_path {
|
|
||||||
// Arrays explicitly push their wrapper natively.
|
|
||||||
// 'items' becomes a transparent conduit, bypassing self-promotion and skipping the '/items' suffix.
|
|
||||||
Self::collect_child_schemas(v, root_id, path.clone(), to_insert, errors);
|
|
||||||
} else {
|
|
||||||
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
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) = &c.then {
|
|
||||||
Self::collect_schemas(
|
|
||||||
then,
|
|
||||||
root_id,
|
|
||||||
format!("{}/cases/{}/then", path, i),
|
|
||||||
to_insert,
|
|
||||||
errors,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(else_) = &c.else_ {
|
|
||||||
Self::collect_schemas(
|
|
||||||
else_,
|
|
||||||
root_id,
|
|
||||||
format!("{}/cases/{}/else", path, i),
|
|
||||||
to_insert,
|
|
||||||
errors,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Schema {
|
impl<'de> Deserialize<'de> for Schema {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
Reference in New Issue
Block a user