diff --git a/d1.json b/d1.json new file mode 100644 index 0000000..b5be71c --- /dev/null +++ b/d1.json @@ -0,0 +1,819 @@ +{ + "database": { + "puncs": [], + "enums": [ + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation_type", + "enum": "relation_type", + "values": [ + "foreign_key", + "polymorphic", + "graph" + ] + } + ], + "relations": [ + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "relation", + "constraint": "fk_order_customer", + "source_type": "order", + "source_columns": [ + "customer_id" + ], + "destination_type": "person", + "destination_columns": [ + "id" + ], + "prefix": "customer" + }, + { + "id": "33333333-3333-3333-3333-333333333333", + "type": "relation", + "constraint": "fk_order_line_order", + "source_type": "order_line", + "source_columns": [ + "order_id" + ], + "destination_type": "order", + "destination_columns": [ + "id" + ], + "prefix": "lines" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "relation", + "constraint": "fk_relationship_source_entity", + "source_type": "relationship", + "source_columns": [ + "source_id", + "source_type" + ], + "destination_type": "entity", + "destination_columns": [ + "id", + "type" + ], + "prefix": "source" + }, + { + "id": "55555555-5555-5555-5555-555555555555", + "type": "relation", + "constraint": "fk_relationship_target_entity", + "source_type": "relationship", + "source_columns": [ + "target_id", + "target_type" + ], + "destination_type": "entity", + "destination_columns": [ + "id", + "type" + ], + "prefix": "target" + } + ], + "types": [ + { + "name": "entity", + "schemas": [ + { + "$id": "entity", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "archived": { + "type": "boolean" + }, + "created_by": { + "type": "string" + }, + "modified_by": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "modified_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "type", + "created_by", + "created_at", + "modified_by", + "modified_at" + ] + } + ], + "hierarchy": [ + "entity" + ], + "fields": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "organization", + "schemas": [ + { + "$id": "organization", + "$ref": "entity", + "properties": { + "name": { + "type": "string" + } + } + } + ], + "hierarchy": [ + "organization", + "entity" + ], + "fields": [ + "id", + "type", + "name", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "organization": [ + "id", + "type", + "name" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "user", + "schemas": [ + { + "$id": "user", + "$ref": "organization", + "properties": {} + } + ], + "hierarchy": [ + "user", + "organization", + "entity" + ], + "fields": [ + "id", + "type", + "name", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "user": [ + "id", + "type" + ], + "organization": [ + "id", + "type", + "name" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "person", + "schemas": [ + { + "$id": "person", + "$ref": "user", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "date_of_birth": { + "type": "string" + }, + "pronouns": { + "type": "string" + }, + "contact_id": { + "type": "string" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "contact", + "properties": { + "target": { + "oneOf": [ + { + "$ref": "phone_number" + }, + { + "$ref": "email_address" + } + ] + } + } + } + } + } + } + ], + "hierarchy": [ + "person", + "user", + "organization", + "entity" + ], + "fields": [ + "id", + "type", + "first_name", + "last_name", + "date_of_birth", + "pronouns", + "contact_id", + "name", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "person": [ + "id", + "type", + "first_name", + "last_name", + "date_of_birth", + "pronouns", + "contact_id" + ], + "user": [ + "id", + "type" + ], + "organization": [ + "id", + "type", + "name" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [ + "first_name", + "last_name", + "date_of_birth", + "pronouns" + ], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "order", + "schemas": [ + { + "$id": "order", + "$ref": "entity", + "properties": { + "total": { + "type": "number" + }, + "customer_id": { + "type": "string" + }, + "customer": { + "$ref": "person" + }, + "lines": { + "type": "array", + "items": { + "$ref": "order_line" + } + } + } + } + ], + "hierarchy": [ + "order", + "entity" + ], + "fields": [ + "id", + "type", + "total", + "customer_id", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "order": [ + "id", + "type", + "total", + "customer_id" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [ + "id" + ], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "order_line", + "schemas": [ + { + "$id": "order_line", + "$ref": "entity", + "properties": { + "order_id": { + "type": "string" + }, + "product": { + "type": "string" + }, + "price": { + "type": "number" + } + } + } + ], + "hierarchy": [ + "order_line", + "entity" + ], + "fields": [ + "id", + "type", + "order_id", + "product", + "price", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "order_line": [ + "id", + "type", + "order_id", + "product", + "price" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "relationship", + "relationship": true, + "hierarchy": [ + "relationship", + "entity" + ], + "fields": [ + "source_id", + "source_type", + "target_id", + "target_type", + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "relationship": [ + "source_id", + "source_type", + "target_id", + "target_type" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "source_id": "uuid", + "source_type": "text", + "target_id": "uuid", + "target_type": "text", + "name": "text", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid" + }, + "schemas": [ + { + "$id": "relationship", + "$ref": "entity", + "properties": {} + } + ], + "lookup_fields": [], + "historical": true, + "notify": true + }, + { + "name": "contact", + "relationship": true, + "hierarchy": [ + "contact", + "relationship", + "entity" + ], + "fields": [ + "is_primary", + "source_id", + "source_type", + "target_id", + "target_type", + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "relationship": [ + "source_id", + "source_type", + "target_id", + "target_type" + ], + "contact": [ + "is_primary" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "source_id": "uuid", + "source_type": "text", + "target_id": "uuid", + "target_type": "text", + "is_primary": "boolean", + "name": "text", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid" + }, + "schemas": [ + { + "$id": "contact", + "$ref": "relationship", + "properties": { + "is_primary": { + "type": "boolean" + } + } + } + ], + "lookup_fields": [], + "historical": true, + "notify": true + }, + { + "name": "phone_number", + "hierarchy": [ + "phone_number", + "entity" + ], + "fields": [ + "number", + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "phone_number": [ + "number" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "number": "text", + "name": "text", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid" + }, + "schemas": [ + { + "$id": "phone_number", + "$ref": "entity", + "properties": { + "number": { + "type": "string" + } + } + } + ], + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "email_address", + "hierarchy": [ + "email_address", + "entity" + ], + "fields": [ + "address", + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "email_address": [ + "address" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "address": "text", + "name": "text", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid" + }, + "schemas": [ + { + "$id": "email_address", + "$ref": "entity", + "properties": { + "address": { + "type": "string" + } + } + } + ], + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, + { + "name": "attachment", + "schemas": [ + { + "$id": "type_metadata", + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + { + "$id": "other_metadata", + "type": "object", + "properties": { + "other": { + "type": "string" + } + } + }, + { + "$id": "attachment", + "$ref": "entity", + "properties": { + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "type_metadata": { + "$ref": "type_metadata" + }, + "other_metadata": { + "$ref": "other_metadata" + } + } + } + ], + "hierarchy": [ + "attachment", + "entity" + ], + "fields": [ + "id", + "type", + "flags", + "type_metadata", + "other_metadata", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ], + "grouped_fields": { + "attachment": [ + "id", + "type", + "flags", + "type_metadata", + "other_metadata" + ], + "entity": [ + "id", + "type", + "created_at", + "created_by", + "modified_at", + "modified_by", + "archived" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "flags": "_text", + "type_metadata": "jsonb", + "other_metadata": "jsonb", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid", + "archived": "boolean" + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + } + ] + } +} diff --git a/fixtures/merger.json b/fixtures/merger.json index f035ff0..96ac37d 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -359,6 +359,15 @@ }, "customer_id": { "type": "string" + }, + "customer": { + "$ref": "person" + }, + "lines": { + "type": "array", + "items": { + "$ref": "order_line" + } } } } @@ -829,6 +838,7 @@ "contact_id": "old-contact" } ], + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -952,6 +962,7 @@ "contact_id": "old-contact" } ], + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -1045,6 +1056,7 @@ "last_name": "OldLast" } ], + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -1134,6 +1146,7 @@ "date_of_birth": "1990-01-01T00:00:00Z", "pronouns": "" }, + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -1266,6 +1279,7 @@ "date_of_birth": "2000-01-01" } }, + "schema_id": "order", "expect": { "success": true, "sql": [ @@ -1462,6 +1476,7 @@ } ] }, + "schema_id": "order", "expect": { "success": true, "sql": [ @@ -1657,6 +1672,7 @@ } ] }, + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -2220,6 +2236,7 @@ "archived": false } ], + "schema_id": "person", "expect": { "success": true, "sql": [ @@ -2298,6 +2315,7 @@ "type": "type_metadata" } }, + "schema_id": "attachment", "expect": { "success": true, "sql": [ diff --git a/src/database/edge.rs b/src/database/edge.rs new file mode 100644 index 0000000..a1a7c14 --- /dev/null +++ b/src/database/edge.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Edge { + pub constraint: String, + pub forward: bool, +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 191f9f3..485d0db 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,4 @@ +pub mod edge; pub mod r#enum; pub mod executors; pub mod formats; @@ -23,14 +24,13 @@ use relation::Relation; use schema::Schema; use serde_json::Value; use std::collections::{HashMap, HashSet}; -use std::sync::Arc; use r#type::Type; pub struct Database { pub enums: HashMap, pub types: HashMap, pub puncs: HashMap, - pub relations: Vec, + pub relations: HashMap, pub schemas: HashMap, pub descendants: HashMap>, pub depths: HashMap, @@ -42,7 +42,7 @@ impl Database { let mut db = Self { enums: HashMap::new(), types: HashMap::new(), - relations: Vec::new(), + relations: HashMap::new(), puncs: HashMap::new(), schemas: HashMap::new(), descendants: HashMap::new(), @@ -76,7 +76,7 @@ impl Database { if db.types.contains_key(&def.source_type) && db.types.contains_key(&def.destination_type) { - db.relations.push(def); + db.relations.insert(def.constraint.clone(), def); } } Err(e) => println!("DATABASE RELATION PARSE FAILED: {:?}", e), @@ -140,7 +140,12 @@ impl Database { self.collect_schemas(); self.collect_depths(); self.collect_descendants(); - self.compile_schemas(); + + // Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks + let mut visited = std::collections::HashSet::new(); + for schema in self.schemas.values() { + schema.compile(self, &mut visited); + } Ok(()) } @@ -223,7 +228,7 @@ impl Database { self.descendants = descendants; } - pub fn get_relation( + fn resolve_relation( &self, parent_type: &str, child_type: &str, @@ -240,7 +245,7 @@ impl Database { let mut matching_rels = Vec::new(); let mut directions = Vec::new(); - for rel in &self.relations { + for rel in self.relations.values() { let is_forward = p_def.hierarchy.contains(&rel.source_type) && c_def.hierarchy.contains(&rel.destination_type); let is_reverse = p_def.hierarchy.contains(&rel.destination_type) @@ -269,7 +274,10 @@ impl Database { // Reduce ambiguity with prefix for (i, rel) in matching_rels.iter().enumerate() { if let Some(prefix) = &rel.prefix { - if prefix == prop_name { + if prop_name.starts_with(prefix) + || prefix.starts_with(prop_name) + || prefix.replace("_", "") == prop_name.replace("_", "") + { chosen_idx = i; resolved = true; break; @@ -298,8 +306,8 @@ impl Database { fn collect_descendants_recursively( target: &str, - direct_refs: &HashMap>, - descendants: &mut HashSet, + direct_refs: &std::collections::HashMap>, + descendants: &mut std::collections::HashSet, ) { if let Some(children) = direct_refs.get(target) { for child in children { @@ -309,86 +317,4 @@ impl Database { } } } - - fn compile_schemas(&mut self) { - let schema_ids: Vec = self.schemas.keys().cloned().collect(); - let mut compiled_names_map: HashMap> = HashMap::new(); - let mut compiled_props_map: HashMap>> = - HashMap::new(); - - for id in &schema_ids { - if let Some(schema) = self.schemas.get(id) { - let mut visited = HashSet::new(); - let merged = self.merged_properties(schema, &mut visited); - let mut names: Vec = merged.keys().cloned().collect(); - if !names.is_empty() { - names.sort(); - compiled_names_map.insert(id.clone(), names); - compiled_props_map.insert(id.clone(), merged); - } - } - } - - for id in schema_ids { - if let Some(schema) = self.schemas.get_mut(&id) { - if let Some(names) = compiled_names_map.remove(&id) { - schema.obj.compiled_property_names = Some(names); - } - if let Some(props) = compiled_props_map.remove(&id) { - schema.obj.compiled_properties = Some(props); - } - schema.compile_internals(); - } - } - } - - pub fn merged_properties( - &self, - schema: &Schema, - visited: &mut HashSet, - ) -> std::collections::BTreeMap> { - if let Some(props) = &schema.obj.compiled_properties { - return props.clone(); - } - - let mut props = std::collections::BTreeMap::new(); - - if let Some(id) = &schema.obj.id { - if !visited.insert(id.clone()) { - return props; - } - } - - if let Some(ref_id) = &schema.obj.r#ref { - if let Some(parent_schema) = self.schemas.get(ref_id) { - props.extend(self.merged_properties(parent_schema, visited)); - } - } - - if let Some(all_of) = &schema.obj.all_of { - for ao in all_of { - props.extend(self.merged_properties(ao, visited)); - } - } - - if let Some(then_schema) = &schema.obj.then_ { - props.extend(self.merged_properties(then_schema, visited)); - } - - if let Some(else_schema) = &schema.obj.else_ { - props.extend(self.merged_properties(else_schema, visited)); - } - - if let Some(local_props) = &schema.obj.properties { - for (k, v) in local_props { - props.insert(k.clone(), v.clone()); - } - } - - if let Some(id) = &schema.obj.id { - visited.remove(id); - } - - props - } } diff --git a/src/database/schema.rs b/src/database/schema.rs index 90ef0a9..6bf1bb5 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -2,6 +2,26 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; use std::sync::Arc; +use std::sync::OnceLock; + +pub fn serialize_once_lock( + lock: &OnceLock, + serializer: S, +) -> Result { + if let Some(val) = lock.get() { + val.serialize(serializer) + } else { + serializer.serialize_none() + } +} + +pub fn is_once_lock_map_empty(lock: &OnceLock>) -> bool { + lock.get().map_or(true, |m| m.is_empty()) +} + +pub fn is_once_lock_vec_empty(lock: &OnceLock>) -> bool { + lock.get().map_or(true, |v| v.is_empty()) +} // Schema mirrors the Go Punc Generator's schema struct for consistency. // It is an order-preserving representation of a JSON Schema. @@ -168,18 +188,26 @@ pub struct SchemaObject { pub extensible: Option, #[serde(rename = "compiledProperties")] - #[serde(skip_serializing_if = "Option::is_none")] - pub compiled_property_names: Option>, + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_vec_empty")] + #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] + pub compiled_property_names: OnceLock>, #[serde(skip)] - pub compiled_properties: Option>>, + pub compiled_properties: OnceLock>>, + + #[serde(rename = "compiledEdges")] + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")] + #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] + pub compiled_edges: OnceLock>, #[serde(skip)] - pub compiled_format: Option, + pub compiled_format: OnceLock, #[serde(skip)] - pub compiled_pattern: Option, + pub compiled_pattern: OnceLock, #[serde(skip)] - pub compiled_pattern_properties: Option)>>, + pub compiled_pattern_properties: OnceLock)>>, } /// Represents a compiled format validator @@ -223,19 +251,37 @@ impl std::ops::DerefMut for Schema { } impl Schema { - pub fn compile_internals(&mut self) { - self.map_children(|child| child.compile_internals()); - - if let Some(format_str) = &self.obj.format - && let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) - { - self.obj.compiled_format = Some(crate::database::schema::CompiledFormat::Func(fmt.func)); + pub fn compile( + &self, + db: &crate::database::Database, + visited: &mut std::collections::HashSet, + ) { + if self.obj.compiled_properties.get().is_some() { + return; } - if let Some(pattern_str) = &self.obj.pattern - && let Ok(re) = regex::Regex::new(pattern_str) - { - self.obj.compiled_pattern = Some(crate::database::schema::CompiledRegex(re)); + if let Some(id) = &self.obj.id { + if !visited.insert(id.clone()) { + return; // Break cyclical resolution + } + } + + 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::schema::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::schema::CompiledRegex(re)); + } } if let Some(pattern_props) = &self.obj.pattern_properties { @@ -246,19 +292,115 @@ impl Schema { } } if !compiled.is_empty() { - self.obj.compiled_pattern_properties = Some(compiled); + let _ = self.obj.compiled_pattern_properties.set(compiled); } } + + let mut props = std::collections::BTreeMap::new(); + + // 1. Resolve INHERITANCE dependencies first + if let Some(ref_id) = &self.obj.r#ref { + if let Some(parent) = db.schemas.get(ref_id) { + parent.compile(db, visited); + if let Some(p_props) = parent.obj.compiled_properties.get() { + props.extend(p_props.clone()); + } + } + } + + if let Some(all_of) = &self.obj.all_of { + for ao in all_of { + ao.compile(db, visited); + if let Some(ao_props) = ao.obj.compiled_properties.get() { + props.extend(ao_props.clone()); + } + } + } + + if let Some(then_schema) = &self.obj.then_ { + then_schema.compile(db, visited); + if let Some(t_props) = then_schema.obj.compiled_properties.get() { + props.extend(t_props.clone()); + } + } + + if let Some(else_schema) = &self.obj.else_ { + else_schema.compile(db, visited); + if let Some(e_props) = else_schema.obj.compiled_properties.get() { + props.extend(e_props.clone()); + } + } + + // 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. Set the OnceLock! + let _ = self.obj.compiled_properties.set(props.clone()); + let mut names: Vec = props.keys().cloned().collect(); + names.sort(); + let _ = self.obj.compiled_property_names.set(names); + + // 4. Compute Edges natively + let schema_edges = self.compile_edges(db, visited, &props); + 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); + } + } + if let Some(items) = &self.obj.items { + items.compile(db, visited); + } + if let Some(pattern_props) = &self.obj.pattern_properties { + for child in pattern_props.values() { + child.compile(db, visited); + } + } + if let Some(additional_props) = &self.obj.additional_properties { + additional_props.compile(db, visited); + } + if let Some(one_of) = &self.obj.one_of { + for child in one_of { + child.compile(db, visited); + } + } + if let Some(arr) = &self.obj.prefix_items { + for child in arr { + child.compile(db, visited); + } + } + if let Some(child) = &self.obj.not { + child.compile(db, visited); + } + if let Some(child) = &self.obj.contains { + child.compile(db, visited); + } + if let Some(child) = &self.obj.property_names { + child.compile(db, visited); + } + if let Some(child) = &self.obj.if_ { + child.compile(db, visited); + } + + if let Some(id) = &self.obj.id { + visited.remove(id); + } } pub fn harvest(&mut self, to_insert: &mut Vec<(String, Schema)>) { if let Some(id) = &self.obj.id { to_insert.push((id.clone(), self.clone())); } - self.map_children(|child| child.harvest(to_insert)); + self.harvest_children(|child| child.harvest(to_insert)); } - pub fn map_children(&mut self, mut f: F) + pub fn harvest_children(&mut self, mut f: F) where F: FnMut(&mut Schema), { @@ -313,6 +455,76 @@ impl Schema { map_opt(&mut self.obj.then_); map_opt(&mut self.obj.else_); } + + pub fn compile_edges( + &self, + db: &crate::database::Database, + visited: &mut std::collections::HashSet, + props: &std::collections::BTreeMap>, + ) -> std::collections::BTreeMap { + let mut schema_edges = std::collections::BTreeMap::new(); + let mut parent_type_name = None; + if let Some(family) = &self.obj.family { + parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); + } else if let Some(id) = &self.obj.id { + parent_type_name = Some(id.split('.').next_back().unwrap_or("").to_string()); + } else if let Some(ref_id) = &self.obj.r#ref { + parent_type_name = Some(ref_id.split('.').next_back().unwrap_or("").to_string()); + } + + if let Some(p_type) = parent_type_name { + if db.types.contains_key(&p_type) { + for (prop_name, prop_schema) in props { + let mut child_type_name = None; + let mut target_schema = prop_schema.clone(); + + if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = + &prop_schema.obj.type_ + { + if t == "array" { + if let Some(items) = &prop_schema.obj.items { + target_schema = items.clone(); + } + } + } + + 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.r#ref.as_ref() { + child_type_name = Some(ref_id.split('.').next_back().unwrap_or("").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.id.as_ref().or(first.obj.r#ref.as_ref()) { + child_type_name = Some(ref_id.split('.').next_back().unwrap_or("").to_string()); + } + } + } + + if let Some(c_type) = child_type_name { + if db.types.contains_key(&c_type) { + target_schema.compile(db, visited); + if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { + let keys_for_ambiguity: Vec = + compiled_target_props.keys().cloned().collect(); + if let Some((relation, is_forward)) = + db.resolve_relation(&p_type, &c_type, prop_name, Some(&keys_for_ambiguity)) + { + schema_edges.insert( + prop_name.clone(), + crate::database::edge::Edge { + constraint: relation.constraint.clone(), + forward: is_forward, + }, + ); + } + } + } + } + } + } + } + schema_edges + } } impl<'de> Deserialize<'de> for Schema { diff --git a/src/lib.rs b/src/lib.rs index 5958854..129f18e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ pub fn jspg_setup(database: JsonB) -> JsonB { } #[cfg_attr(not(test), pg_extern)] -pub fn jspg_merge(data: JsonB) -> JsonB { +pub fn jspg_merge(schema_id: &str, data: JsonB) -> JsonB { // Try to acquire a read lock to get a clone of the Engine Arc let engine_opt = { let lock = GLOBAL_JSPG.read().unwrap(); @@ -69,7 +69,7 @@ pub fn jspg_merge(data: JsonB) -> JsonB { match engine_opt { Some(engine) => { - let drop = engine.merger.merge(data.0); + let drop = engine.merger.merge(schema_id, data.0); JsonB(serde_json::to_value(drop).unwrap()) } None => jspg_failure(), diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 72accb2..4d9bf2b 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -21,10 +21,26 @@ impl Merger { } } - pub fn merge(&self, data: Value) -> crate::drop::Drop { + pub fn merge(&self, schema_id: &str, data: Value) -> crate::drop::Drop { let mut notifications_queue = Vec::new(); - let result = self.merge_internal(data.clone(), &mut notifications_queue); + let target_schema = match self.db.schemas.get(schema_id) { + Some(s) => Arc::new(s.clone()), + None => { + return crate::drop::Drop::with_errors(vec![crate::drop::Error { + code: "MERGE_FAILED".to_string(), + message: format!("Unknown schema_id: {}", schema_id), + details: crate::drop::ErrorDetails { + path: "".to_string(), + cause: None, + context: Some(data), + schema: None, + }, + }]); + } + }; + + let result = self.merge_internal(target_schema, data.clone(), &mut notifications_queue); let val_resolved = match result { Ok(val) => val, @@ -88,24 +104,35 @@ impl Merger { pub(crate) fn merge_internal( &self, + schema: Arc, data: Value, notifications: &mut Vec, ) -> Result { match data { - Value::Array(items) => self.merge_array(items, notifications), - Value::Object(map) => self.merge_object(map, notifications), + Value::Array(items) => self.merge_array(schema, items, notifications), + Value::Object(map) => self.merge_object(schema, map, notifications), _ => Err("Invalid merge payload: root must be an Object or Array".to_string()), } } fn merge_array( &self, + schema: Arc, items: Vec, notifications: &mut Vec, ) -> Result { + let mut item_schema = schema.clone(); + if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ { + if t == "array" { + if let Some(items_def) = &schema.obj.items { + item_schema = items_def.clone(); + } + } + } + let mut resolved_items = Vec::new(); for item in items { - let resolved = self.merge_internal(item, notifications)?; + let resolved = self.merge_internal(item_schema.clone(), item, notifications)?; resolved_items.push(resolved); } Ok(Value::Array(resolved_items)) @@ -113,6 +140,7 @@ impl Merger { fn merge_object( &self, + schema: Arc, obj: serde_json::Map, notifications: &mut Vec, ) -> Result { @@ -128,25 +156,49 @@ impl Merger { None => return Err(format!("Unknown entity type: {}", type_name)), }; - // 1. Segment the entity: fields in type_def.fields are database fields, others are relationships + let compiled_props = match schema.obj.compiled_properties.get() { + Some(props) => props, + None => return Err("Schema has no compiled properties for merging".to_string()), + }; + let mut entity_fields = serde_json::Map::new(); - let mut entity_objects = serde_json::Map::new(); - let mut entity_arrays = serde_json::Map::new(); + let mut entity_objects = std::collections::BTreeMap::new(); + let mut entity_arrays = std::collections::BTreeMap::new(); for (k, v) in obj { - let is_field = type_def.fields.contains(&k) || k == "created"; - let typeof_v = match &v { - Value::Object(_) => "object", - Value::Array(_) => "array", - _ => "other", - }; + // 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()); + continue; + } - if is_field { - entity_fields.insert(k, v); - } else if typeof_v == "object" { - entity_objects.insert(k, v); - } else if typeof_v == "array" { - entity_arrays.insert(k, v); + if let Some(prop_schema) = compiled_props.get(&k) { + let mut is_edge = false; + if let Some(edges) = schema.obj.compiled_edges.get() { + if edges.contains_key(&k) { + is_edge = true; + } + } + + if is_edge { + let typeof_v = match &v { + Value::Object(_) => "object", + Value::Array(_) => "array", + _ => "field", // Malformed edge data? + }; + if typeof_v == "object" { + entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone())); + } else if typeof_v == "array" { + entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone())); + } else { + entity_fields.insert(k.clone(), v.clone()); + } + } else { + // Not an edge! It's a raw Postgres column (e.g., JSONB, text[]) + entity_fields.insert(k.clone(), v.clone()); + } + } else if type_def.fields.contains(&k) { + entity_fields.insert(k.clone(), v.clone()); } } @@ -156,7 +208,6 @@ impl Merger { let mut entity_change_kind = None; let mut entity_fetched = None; - // 2. Pre-stage the entity (for non-relationships) if !type_def.relationship { let (fields, kind, fetched) = self.stage_entity(entity_fields.clone(), type_def, &user_id, ×tamp)?; @@ -167,82 +218,74 @@ impl Merger { let mut entity_response = serde_json::Map::new(); - // 3. Handle related objects - for (relation_name, relative_val) in entity_objects { + for (relation_name, (relative_val, rel_schema)) in entity_objects { let mut relative = match relative_val { Value::Object(m) => m, _ => continue, }; - // Attempt to extract relative object type name let relative_type_name = match relative.get("type").and_then(|v| v.as_str()) { Some(t) => t.to_string(), None => continue, }; - let relative_keys: Vec = relative.keys().cloned().collect(); + if let Some(compiled_edges) = schema.obj.compiled_edges.get() { + println!("Compiled Edges keys for relation {}: {:?}", relation_name, compiled_edges.keys().collect::>()); + if let Some(edge) = compiled_edges.get(&relation_name) { + println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint); + if let Some(relation) = self.db.relations.get(&edge.constraint) { + let parent_is_source = edge.forward; + + if parent_is_source { + if !relative.contains_key("organization_id") { + if let Some(org_id) = entity_fields.get("organization_id") { + relative.insert("organization_id".to_string(), org_id.clone()); + } + } - // Call central Database O(1) graph logic - let relative_relation = self.db.get_relation( - &type_def.name, - &relative_type_name, - &relation_name, - Some(&relative_keys), - ); + let mut merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { + Value::Object(m) => m, + _ => continue, + }; - if let Some((relation, parent_is_source)) = relative_relation { + merged_relative.insert( + "type".to_string(), + Value::String(relative_type_name), + ); - if parent_is_source { - // Parent holds FK to Child. Child MUST be generated FIRST. - if !relative.contains_key("organization_id") { - if let Some(org_id) = entity_fields.get("organization_id") { - relative.insert("organization_id".to_string(), org_id.clone()); + Self::apply_entity_relation( + &mut entity_fields, + &relation.source_columns, + &relation.destination_columns, + &merged_relative, + ); + entity_response.insert(relation_name, Value::Object(merged_relative)); + } else { + if !relative.contains_key("organization_id") { + if let Some(org_id) = entity_fields.get("organization_id") { + relative.insert("organization_id".to_string(), org_id.clone()); + } + } + + Self::apply_entity_relation( + &mut relative, + &relation.source_columns, + &relation.destination_columns, + &entity_fields, + ); + + let merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { + Value::Object(m) => m, + _ => continue, + }; + + entity_response.insert(relation_name, Value::Object(merged_relative)); } } - - let mut merged_relative = match self.merge_internal(Value::Object(relative), notifications)? { - Value::Object(m) => m, - _ => continue, - }; - - merged_relative.insert( - "type".to_string(), - Value::String(relative_type_name), - ); - - Self::apply_entity_relation( - &mut entity_fields, - &relation.source_columns, - &relation.destination_columns, - &merged_relative, - ); - entity_response.insert(relation_name, Value::Object(merged_relative)); - } else { - // Child holds FK back to Parent. - if !relative.contains_key("organization_id") { - if let Some(org_id) = entity_fields.get("organization_id") { - relative.insert("organization_id".to_string(), org_id.clone()); - } - } - - Self::apply_entity_relation( - &mut relative, - &relation.source_columns, - &relation.destination_columns, - &entity_fields, - ); - - let merged_relative = match self.merge_internal(Value::Object(relative), notifications)? { - Value::Object(m) => m, - _ => continue, - }; - - entity_response.insert(relation_name, Value::Object(merged_relative)); } } } - // 4. Post-stage the entity (for relationships) if type_def.relationship { let (fields, kind, fetched) = self.stage_entity(entity_fields.clone(), type_def, &user_id, ×tamp)?; @@ -251,7 +294,6 @@ impl Merger { entity_fetched = fetched; } - // 5. Process the main entity fields self.merge_entity_fields( entity_change_kind.as_deref().unwrap_or(""), &type_name, @@ -260,13 +302,11 @@ impl Merger { entity_fetched.as_ref(), )?; - // Add main entity fields to response for (k, v) in &entity_fields { entity_response.insert(k.clone(), v.clone()); } - // 6. Handle related arrays - for (relation_name, relative_val) in entity_arrays { + for (relation_name, (relative_val, rel_schema)) in entity_arrays { let relative_arr = match relative_val { Value::Array(a) => a, _ => continue, @@ -276,54 +316,46 @@ impl Merger { continue; } - let first_relative = match &relative_arr[0] { - Value::Object(m) => m, - _ => continue, - }; + if let Some(compiled_edges) = schema.obj.compiled_edges.get() { + if let Some(edge) = compiled_edges.get(&relation_name) { + if let Some(relation) = self.db.relations.get(&edge.constraint) { + let mut relative_responses = Vec::new(); + for relative_item_val in relative_arr { + if let Value::Object(mut relative_item) = relative_item_val { + if !relative_item.contains_key("organization_id") { + if let Some(org_id) = entity_fields.get("organization_id") { + relative_item.insert("organization_id".to_string(), org_id.clone()); + } + } - // Attempt to extract relative object type name - let relative_type_name = match first_relative.get("type").and_then(|v| v.as_str()) { - Some(t) => t, - None => continue, - }; + Self::apply_entity_relation( + &mut relative_item, + &relation.source_columns, + &relation.destination_columns, + &entity_fields, + ); - let relative_keys: Vec = first_relative.keys().cloned().collect(); + let mut item_schema = rel_schema.clone(); + if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &rel_schema.obj.type_ { + if t == "array" { + if let Some(items_def) = &rel_schema.obj.items { + item_schema = items_def.clone(); + } + } + } - // Call central Database O(1) graph logic - let relative_relation = self.db.get_relation( - &type_def.name, - relative_type_name, - &relation_name, - Some(&relative_keys), - ); + let merged_relative = + match self.merge_internal(item_schema, Value::Object(relative_item), notifications)? { + Value::Object(m) => m, + _ => continue, + }; - if let Some((relation, _)) = relative_relation { - let mut relative_responses = Vec::new(); - for relative_item_val in relative_arr { - if let Value::Object(mut relative_item) = relative_item_val { - if !relative_item.contains_key("organization_id") { - if let Some(org_id) = entity_fields.get("organization_id") { - relative_item.insert("organization_id".to_string(), org_id.clone()); + relative_responses.push(Value::Object(merged_relative)); } } - - Self::apply_entity_relation( - &mut relative_item, - &relation.source_columns, - &relation.destination_columns, - &entity_fields, - ); - - let merged_relative = - match self.merge_internal(Value::Object(relative_item), notifications)? { - Value::Object(m) => m, - _ => continue, - }; - - relative_responses.push(Value::Object(merged_relative)); + entity_response.insert(relation_name, Value::Array(relative_responses)); } } - entity_response.insert(relation_name, Value::Array(relative_responses)); } } diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index b3dddf4..ab7c024 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -12,6 +12,7 @@ pub struct Node<'a> { pub parent_alias: String, pub parent_type_aliases: Option>>, pub parent_type: Option<&'a crate::database::r#type::Type>, + pub parent_schema: Option>, pub property_name: Option, pub depth: usize, pub ast_path: String, @@ -39,6 +40,7 @@ impl<'a> Compiler<'a> { parent_alias: "t1".to_string(), parent_type_aliases: None, parent_type: None, + parent_schema: None, property_name: None, depth: 0, ast_path: String::new(), @@ -243,6 +245,7 @@ impl<'a> Compiler<'a> { if fam_type_def.variations.len() == 1 { let mut bypass_schema = crate::database::schema::Schema::default(); bypass_schema.obj.r#ref = Some(family_target.clone()); + bypass_schema.compile(self.db, &mut std::collections::HashSet::new()); let mut bypass_node = node.clone(); bypass_node.schema = std::sync::Arc::new(bypass_schema); @@ -258,6 +261,7 @@ impl<'a> Compiler<'a> { for variation in &sorted_fam_variations { let mut ref_schema = crate::database::schema::Schema::default(); ref_schema.obj.r#ref = Some(variation.clone()); + ref_schema.compile(self.db, &mut std::collections::HashSet::new()); family_schemas.push(std::sync::Arc::new(ref_schema)); } @@ -395,9 +399,7 @@ impl<'a> Compiler<'a> { ) -> Result, String> { let mut select_args = Vec::new(); let grouped_fields = r#type.grouped_fields.as_ref().and_then(|v| v.as_object()); - let merged_props = self - .db - .merged_properties(node.schema.as_ref(), &mut std::collections::HashSet::new()); + let merged_props = node.schema.obj.compiled_properties.get().unwrap(); let mut sorted_keys: Vec<&String> = merged_props.keys().collect(); sorted_keys.sort(); @@ -451,6 +453,7 @@ impl<'a> Compiler<'a> { let arc_aliases = std::sync::Arc::new(table_aliases.clone()); child_node.parent_type_aliases = Some(arc_aliases); child_node.parent_type = Some(r#type); + child_node.parent_schema = Some(std::sync::Arc::clone(&node.schema)); child_node.property_name = Some(prop_key.clone()); child_node.depth += 1; let next_path = if node.ast_path.is_empty() { @@ -663,60 +666,61 @@ impl<'a> Compiler<'a> { ) -> Result<(), String> { if let Some(prop_ref) = &node.property_name { let prop = prop_ref.as_str(); + println!("DEBUG: Eval prop: {}", prop); + let mut parent_relation_alias = node.parent_alias.clone(); let mut child_relation_alias = base_alias.to_string(); if let Some(parent_type) = node.parent_type { - let merged_props = self - .db - .merged_properties(node.schema.as_ref(), &mut std::collections::HashSet::new()); - let relative_keys: Vec = merged_props.keys().cloned().collect(); + if let Some(parent_schema) = &node.parent_schema { + if let Some(compiled_edges) = parent_schema.obj.compiled_edges.get() { + if let Some(edge) = compiled_edges.get(prop) { + let is_parent_source = edge.forward; + let relation = self.db.relations.get(&edge.constraint).ok_or_else(|| { + format!( + "Could not find exact relation constraint {} statically mapped from {} -> {} property {}", + edge.constraint, parent_type.name, r#type.name, prop + ) + })?; - let (relation, is_parent_source) = self - .db - .get_relation(&parent_type.name, &r#type.name, prop, Some(&relative_keys)) - .ok_or_else(|| { - format!( - "Could not dynamically resolve database relation mapping for {} -> {} on property {}", - parent_type.name, r#type.name, prop - ) - })?; + let source_col = &relation.source_columns[0]; + let dest_col = &relation.destination_columns[0]; - let source_col = &relation.source_columns[0]; - let dest_col = &relation.destination_columns[0]; + if let Some(pta) = &node.parent_type_aliases { + let p_search_type = if is_parent_source { + &relation.source_type + } else { + &relation.destination_type + }; + if let Some(a) = pta.get(p_search_type) { + parent_relation_alias = a.clone(); + } + } - if let Some(pta) = &node.parent_type_aliases { - let p_search_type = if is_parent_source { - &relation.source_type - } else { - &relation.destination_type - }; - if let Some(a) = pta.get(p_search_type) { - parent_relation_alias = a.clone(); + let c_search_type = if is_parent_source { + &relation.destination_type + } else { + &relation.source_type + }; + if let Some(a) = type_aliases.get(c_search_type) { + child_relation_alias = a.clone(); + } + + let sql_string = if is_parent_source { + format!( + "{}.{} = {}.{}", + parent_relation_alias, source_col, child_relation_alias, dest_col + ) + } else { + format!( + "{}.{} = {}.{}", + child_relation_alias, source_col, parent_relation_alias, dest_col + ) + }; + where_clauses.push(sql_string); + } } } - - let c_search_type = if is_parent_source { - &relation.destination_type - } else { - &relation.source_type - }; - if let Some(a) = type_aliases.get(c_search_type) { - child_relation_alias = a.clone(); - } - - let sql_string = if is_parent_source { - format!( - "{}.{} = {}.{}", - parent_relation_alias, source_col, child_relation_alias, dest_col - ) - } else { - format!( - "{}.{} = {}.{}", - child_relation_alias, source_col, parent_relation_alias, dest_col - ) - }; - where_clauses.push(sql_string); } } Ok(()) diff --git a/src/tests/mod.rs b/src/tests/mod.rs index cbe39e3..bfff180 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -10,7 +10,7 @@ fn test_library_api() { // 1. Initially, schemas are not cached. // Expected uninitialized drop format: errors + null response - let uninitialized_drop = jspg_validate("test_schema", JsonB(json!({}))); + let uninitialized_drop = jspg_validate("source_schema", JsonB(json!({}))); assert_eq!( uninitialized_drop.0, json!({ @@ -27,17 +27,44 @@ fn test_library_api() { let db_json = json!({ "puncs": [], "enums": [], - "relations": [], - "types": [{ - "schemas": [{ - "$id": "test_schema", - "type": "object", - "properties": { - "name": { "type": "string" } - }, - "required": ["name"] - }] - }] + "relations": [ + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "relation", + "constraint": "fk_test_target", + "source_type": "source_schema", + "source_columns": ["target_id"], + "destination_type": "target_schema", + "destination_columns": ["id"], + "prefix": "target" + } + ], + "types": [ + { + "name": "source_schema", + "hierarchy": ["source_schema", "entity"], + "schemas": [{ + "$id": "source_schema", + "type": "object", + "properties": { + "name": { "type": "string" }, + "target": { "$ref": "target_schema" } + }, + "required": ["name"] + }] + }, + { + "name": "target_schema", + "hierarchy": ["target_schema", "entity"], + "schemas": [{ + "$id": "target_schema", + "type": "object", + "properties": { + "value": { "type": "number" } + } + }] + } + ] }); let cache_drop = jspg_setup(JsonB(db_json)); @@ -56,21 +83,39 @@ fn test_library_api() { json!({ "type": "drop", "response": { - "test_schema": { - "$id": "test_schema", + "source_schema": { + "$id": "source_schema", "type": "object", "properties": { - "name": { "type": "string" } + "name": { "type": "string" }, + "target": { + "$ref": "target_schema", + "compiledProperties": ["value"] + } }, "required": ["name"], - "compiledProperties": ["name"] + "compiledProperties": ["name", "target"], + "compiledEdges": { + "target": { + "constraint": "fk_test_target", + "forward": true + } + } + }, + "target_schema": { + "$id": "target_schema", + "type": "object", + "properties": { + "value": { "type": "number" } + }, + "compiledProperties": ["value"] } } }) ); // 4. Validate Happy Path - let happy_drop = jspg_validate("test_schema", JsonB(json!({"name": "Neo"}))); + let happy_drop = jspg_validate("source_schema", JsonB(json!({"name": "Neo"}))); assert_eq!( happy_drop.0, json!({ @@ -80,7 +125,7 @@ fn test_library_api() { ); // 5. Validate Unhappy Path - let unhappy_drop = jspg_validate("test_schema", JsonB(json!({"wrong": "data"}))); + let unhappy_drop = jspg_validate("source_schema", JsonB(json!({"wrong": "data"}))); assert_eq!( unhappy_drop.0, json!({ diff --git a/src/tests/types/case.rs b/src/tests/types/case.rs index 8e9bac2..edc14ee 100644 --- a/src/tests/types/case.rs +++ b/src/tests/types/case.rs @@ -99,7 +99,7 @@ impl Case { let merger = Merger::new(db.clone()); let test_data = self.data.clone().unwrap_or(Value::Null); - let result = merger.merge(test_data); + let result = merger.merge(&self.schema_id, test_data); let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false); let got_success = result.errors.is_empty(); diff --git a/src/validator/rules/format.rs b/src/validator/rules/format.rs index 15043bf..03d3cf3 100644 --- a/src/validator/rules/format.rs +++ b/src/validator/rules/format.rs @@ -8,7 +8,7 @@ impl<'a> ValidationContext<'a> { result: &mut ValidationResult, ) -> Result { let current = self.instance; - if let Some(ref compiled_fmt) = self.schema.compiled_format { + if let Some(compiled_fmt) = self.schema.compiled_format.get() { match compiled_fmt { crate::database::schema::CompiledFormat::Func(f) => { let should = if let Some(s) = current.as_str() { diff --git a/src/validator/rules/object.rs b/src/validator/rules/object.rs index 86d64d2..53175b8 100644 --- a/src/validator/rules/object.rs +++ b/src/validator/rules/object.rs @@ -151,7 +151,7 @@ impl<'a> ValidationContext<'a> { } } - if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties { + if let Some(compiled_pp) = self.schema.compiled_pattern_properties.get() { for (compiled_re, sub_schema) in compiled_pp { for (key, child_instance) in obj { if compiled_re.0.is_match(key) { @@ -183,7 +183,7 @@ impl<'a> ValidationContext<'a> { { locally_matched = true; } - if !locally_matched && let Some(ref compiled_pp) = self.schema.compiled_pattern_properties + if !locally_matched && let Some(compiled_pp) = self.schema.compiled_pattern_properties.get() { for (compiled_re, _) in compiled_pp { if compiled_re.0.is_match(key) { diff --git a/src/validator/rules/string.rs b/src/validator/rules/string.rs index 40f384a..42e11fa 100644 --- a/src/validator/rules/string.rs +++ b/src/validator/rules/string.rs @@ -28,7 +28,7 @@ impl<'a> ValidationContext<'a> { path: self.path.to_string(), }); } - if let Some(ref compiled_re) = self.schema.compiled_pattern { + if let Some(compiled_re) = self.schema.compiled_pattern.get() { if !compiled_re.0.is_match(s) { result.errors.push(ValidationError { code: "PATTERN_VIOLATED".to_string(),