checkpoint

This commit is contained in:
2026-04-09 18:39:52 -04:00
parent 9387152859
commit 5f45df6c11
11 changed files with 677 additions and 467 deletions

View File

@ -84,11 +84,26 @@ Punc completely abandons the standard JSON Schema `$ref` keyword. Instead, it ov
* **Strict Array Constraint**: To explicitly prevent mathematically ambiguous Multiple Inheritance, a `type` array is strictly constrained to at most **ONE** Custom Object Pointer. Defining `"type": ["person", "organization"]` will intentionally trigger a fatal database compilation error natively instructing developers to build a proper tagged union (`oneOf`) instead. * **Strict Array Constraint**: To explicitly prevent mathematically ambiguous Multiple Inheritance, a `type` array is strictly constrained to at most **ONE** Custom Object Pointer. Defining `"type": ["person", "organization"]` will intentionally trigger a fatal database compilation error natively instructing developers to build a proper tagged union (`oneOf`) instead.
### Polymorphism (`$family` and `oneOf`) ### Polymorphism (`$family` and `oneOf`)
Polymorphism is how an object boundary can dynamically take on entirely different shapes based on the payload provided at runtime. Polymorphism is how an object boundary can dynamically take on entirely different shapes based on the payload provided at runtime. Punc utilizes the static database metadata generated from Postgres (`db.types`) to enforce these boundaries deterministically, rather than relying on ambiguous tree-traversals.
* **`$family` (Target-Based Polymorphism)**: An explicit Punc compiler macro instructing the database compiler to dynamically search its internal `db.descendants` registry and find all physical schemas that mathematically resolve to the target.
* *Across Tables (Vertical)*: If `$family: entity` is requested, the payload's `type` field acts as the discriminator, dynamically routing to standard variations like `organization` or `person` spanning multiple Postgres tables. * **`$family` (Target-Based Polymorphism)**: An explicit Punc compiler macro instructing the engine to resolve dynamic options against the registered database `types` variations or its inner schema registry. It uses the exact physical constraints of the database to build SQL and validation routes.
* *Single Table (Horizontal)*: If `$family: widget` is requested, the router explicitly evaluates the Dot Convention dynamically. If the payload possesses `"type": "widget"` and `"kind": "stock"`, the router mathematically resolves to the string `"stock.widget"` and routes exclusively to that explicit `JSPG` schema. * **Scenario A: Global Tables (Vertical Routing)**
* **`oneOf` (Strict Tagged Unions)**: A hardcoded array of JSON Schema candidate options. Punc strictly bans mathematical "Union of Sets" evaluation. Every `oneOf` candidate item MUST either be a pure primitive (`{ "type": "null" }`) or a user-defined Object Pointer providing a specific discriminator (e.g., `{ "type": "invoice_metadata" }`). This ensures validations remain pure $O(1)$ fast-paths and allows the Dart generator to emit pristine `sealed classes`. * *Setup*: `{ "$family": "organization" }`
* *Execution*: The engine queries `db.types.get("organization").variations` and finds `["bot", "organization", "person"]`. Because organizations are structurally table-backed, the `$family` automatically uses `type` as the discriminator.
* *Options*: `bot` -> `bot`, `person` -> `person`, `organization` -> `organization`.
* **Scenario B: Prefixed Tables (Vertical Projection)**
* *Setup*: `{ "$family": "light.organization" }`
* *Execution*: The engine sees the prefix `light.` and base `organization`. It queries `db.types.get("organization").variations` and dynamically prepends the prefix to discover the relevant UI schemas.
* *Options*: `person` -> `light.person`, `organization` -> `light.organization`. (If a projection like `light.bot` does not exist in `db.schemas`, it is safely ignored).
* **Scenario C: Single Table Inheritance (Horizontal Routing)**
* *Setup*: `{ "$family": "widget" }` (Where `widget` is a table type but has no external variations).
* *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any `$id` terminating in the base `.widget` (e.g., `stock.widget`). The `$family` automatically uses `kind` as the discriminator.
* *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`.
* **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `$family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing.
* **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`).
* **STI Types**: `oneOf: [{ "type": "heavy.person" }, { "type": "light.person" }]`. The engine succeeds. Even though both share `"type": "person"`, their explicit discriminator is `kind` (`"heavy"` vs `"light"`), ensuring unique $O(1)$ fast-paths.
* **Conflicting Types**: `oneOf: [{ "type": "person" }, { "type": "light.person" }]`. The engine **fails compilation natively**. Both schemas evaluate to `"type": "person"` and neither provides a disjoint `kind` constraint, making them mathematically ambiguous and impossible to route in $O(1)$ time.
### Conditionals (`cases`) ### Conditionals (`cases`)
Standard JSON Schema forces developers to write deeply nested `allOf` -> `if` -> `properties` blocks just to execute conditional branching. **JSPG completely abandons `allOf` and this practice.** For declarative business logic and structural mutations conditionally based upon property bounds, use the top-level `cases` array. Standard JSON Schema forces developers to write deeply nested `allOf` -> `if` -> `properties` blocks just to execute conditional branching. **JSPG completely abandons `allOf` and this practice.** For declarative business logic and structural mutations conditionally based upon property bounds, use the top-level `cases` array.

43
debug.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -196,29 +196,37 @@
{ {
"description": "Horizontal $family Routing (Virtual Variations)", "description": "Horizontal $family Routing (Virtual Variations)",
"database": { "database": {
"types": [
{
"name": "widget",
"variations": ["widget"],
"schemas": [
{
"$id": "widget",
"type": "object",
"properties": {
"type": { "type": "string" }
}
},
{
"$id": "stock.widget",
"type": "widget",
"properties": {
"kind": { "type": "string" },
"amount": { "type": "integer" }
}
},
{
"$id": "super_stock.widget",
"type": "stock.widget",
"properties": {
"super_amount": { "type": "integer" }
}
}
]
}
],
"schemas": [ "schemas": [
{
"$id": "widget",
"type": "object",
"properties": {
"type": { "type": "string" }
}
},
{
"$id": "stock.widget",
"type": "widget",
"properties": {
"kind": { "type": "string" },
"amount": { "type": "integer" }
}
},
{
"$id": "super_stock.widget",
"type": "stock.widget",
"properties": {
"super_amount": { "type": "integer" }
}
},
{ {
"$id": "family_widget", "$id": "family_widget",
"$family": "widget" "$family": "widget"

View File

@ -25,11 +25,20 @@
] ]
}, },
{ {
"name": "get_person", "name": "get_light_organizations",
"schemas": [ "schemas": [
{ {
"$id": "get_person.response", "$id": "get_light_organizations.response",
"$family": "person" "$family": "light.organization"
}
]
},
{
"name": "get_full_organizations",
"schemas": [
{
"$id": "get_full_organizations.response",
"$family": "full.organization"
} }
] ]
}, },
@ -44,6 +53,18 @@
} }
} }
] ]
},
{
"name": "get_widgets",
"schemas": [
{
"$id": "get_widgets.response",
"type": "array",
"items": {
"$family": "widget"
}
}
]
} }
], ],
"enums": [], "enums": [],
@ -260,7 +281,9 @@
"type", "type",
"name", "name",
"archived", "archived",
"created_at" "created_at",
"token",
"role"
], ],
"grouped_fields": { "grouped_fields": {
"entity": [ "entity": [
@ -273,7 +296,8 @@
"name" "name"
], ],
"bot": [ "bot": [
"token" "token",
"role"
] ]
}, },
"field_types": { "field_types": {
@ -282,12 +306,25 @@
"archived": "boolean", "archived": "boolean",
"name": "text", "name": "text",
"token": "text", "token": "text",
"role": "text",
"created_at": "timestamptz" "created_at": "timestamptz"
}, },
"schemas": [ "schemas": [
{ {
"$id": "bot", "$id": "bot",
"type": "organization", "type": "organization",
"properties": {
"token": {
"type": "string"
},
"role": {
"type": "string"
}
}
},
{
"$id": "light.bot",
"type": "organization",
"properties": { "properties": {
"token": { "token": {
"type": "string" "type": "string"
@ -360,8 +397,15 @@
}, },
{ {
"$id": "light.person", "$id": "light.person",
"type": "person", "type": "organization",
"properties": {} "properties": {
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
}
}
}, },
{ {
"$id": "full.person", "$id": "full.person",
@ -850,6 +894,46 @@
"variations": [ "variations": [
"order_line" "order_line"
] ]
},
{
"name": "widget",
"hierarchy": ["widget", "entity"],
"fields": ["id", "type", "kind", "archived", "created_at"],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"],
"widget": ["kind"]
},
"field_types": {
"id": "uuid",
"type": "text",
"kind": "text",
"archived": "boolean",
"created_at": "timestamptz"
},
"variations": ["widget"],
"schemas": [
{
"$id": "widget",
"type": "entity",
"properties": {
"kind": { "type": "string" }
}
},
{
"$id": "stock.widget",
"type": "widget",
"properties": {
"kind": { "const": "stock" }
}
},
{
"$id": "tasks.widget",
"type": "widget",
"properties": {
"kind": { "const": "tasks" }
}
}
]
} }
] ]
}, },
@ -1004,17 +1088,17 @@
" 'target', CASE", " 'target', CASE",
" WHEN entity_11.target_type = 'address' THEN", " WHEN entity_11.target_type = 'address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_17.archived,", " 'archived', entity_13.archived,",
" 'city', address_16.city,", " 'city', address_12.city,",
" 'created_at', entity_17.created_at,", " 'created_at', entity_13.created_at,",
" 'id', entity_17.id,", " 'id', entity_13.id,",
" 'type', entity_17.type", " 'type', entity_13.type",
" )", " )",
" FROM agreego.address address_16", " FROM agreego.address address_12",
" JOIN agreego.entity entity_17 ON entity_17.id = address_16.id", " JOIN agreego.entity entity_13 ON entity_13.id = address_12.id",
" WHERE", " WHERE",
" NOT entity_17.archived", " NOT entity_13.archived",
" AND relationship_10.target_id = entity_17.id))", " AND relationship_10.target_id = entity_13.id))",
" WHEN entity_11.target_type = 'email_address' THEN", " WHEN entity_11.target_type = 'email_address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'address', email_address_14.address,", " 'address', email_address_14.address,",
@ -1030,17 +1114,17 @@
" AND relationship_10.target_id = entity_15.id))", " AND relationship_10.target_id = entity_15.id))",
" WHEN entity_11.target_type = 'phone_number' THEN", " WHEN entity_11.target_type = 'phone_number' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_13.archived,", " 'archived', entity_17.archived,",
" 'created_at', entity_13.created_at,", " 'created_at', entity_17.created_at,",
" 'id', entity_13.id,", " 'id', entity_17.id,",
" 'number', phone_number_12.number,", " 'number', phone_number_16.number,",
" 'type', entity_13.type", " 'type', entity_17.type",
" )", " )",
" FROM agreego.phone_number phone_number_12", " FROM agreego.phone_number phone_number_16",
" JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id", " JOIN agreego.entity entity_17 ON entity_17.id = phone_number_16.id",
" WHERE", " WHERE",
" NOT entity_13.archived", " NOT entity_17.archived",
" AND relationship_10.target_id = entity_13.id))", " AND relationship_10.target_id = entity_17.id))",
" ELSE NULL END,", " ELSE NULL END,",
" 'type', entity_11.type", " 'type', entity_11.type",
" )), '[]'::jsonb)", " )), '[]'::jsonb)",
@ -1240,17 +1324,17 @@
" 'target', CASE", " 'target', CASE",
" WHEN entity_11.target_type = 'address' THEN", " WHEN entity_11.target_type = 'address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_17.archived,", " 'archived', entity_13.archived,",
" 'city', address_16.city,", " 'city', address_12.city,",
" 'created_at', entity_17.created_at,", " 'created_at', entity_13.created_at,",
" 'id', entity_17.id,", " 'id', entity_13.id,",
" 'type', entity_17.type", " 'type', entity_13.type",
" )", " )",
" FROM agreego.address address_16", " FROM agreego.address address_12",
" JOIN agreego.entity entity_17 ON entity_17.id = address_16.id", " JOIN agreego.entity entity_13 ON entity_13.id = address_12.id",
" WHERE", " WHERE",
" NOT entity_17.archived", " NOT entity_13.archived",
" AND relationship_10.target_id = entity_17.id))", " AND relationship_10.target_id = entity_13.id))",
" WHEN entity_11.target_type = 'email_address' THEN", " WHEN entity_11.target_type = 'email_address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'address', email_address_14.address,", " 'address', email_address_14.address,",
@ -1266,17 +1350,17 @@
" AND relationship_10.target_id = entity_15.id))", " AND relationship_10.target_id = entity_15.id))",
" WHEN entity_11.target_type = 'phone_number' THEN", " WHEN entity_11.target_type = 'phone_number' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_13.archived,", " 'archived', entity_17.archived,",
" 'created_at', entity_13.created_at,", " 'created_at', entity_17.created_at,",
" 'id', entity_13.id,", " 'id', entity_17.id,",
" 'number', phone_number_12.number,", " 'number', phone_number_16.number,",
" 'type', entity_13.type", " 'type', entity_17.type",
" )", " )",
" FROM agreego.phone_number phone_number_12", " FROM agreego.phone_number phone_number_16",
" JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id", " JOIN agreego.entity entity_17 ON entity_17.id = phone_number_16.id",
" WHERE", " WHERE",
" NOT entity_13.archived", " NOT entity_17.archived",
" AND relationship_10.target_id = entity_13.id))", " AND relationship_10.target_id = entity_17.id))",
" ELSE NULL END,", " ELSE NULL END,",
" 'type', entity_11.type", " 'type', entity_11.type",
" )), '[]'::jsonb)", " )), '[]'::jsonb)",
@ -1565,27 +1649,27 @@
} }
}, },
{ {
"description": "Person select via a punc response with family", "description": "Light organizations select via a punc response with family",
"action": "query", "action": "query",
"schema_id": "get_person.response", "schema_id": "get_light_organizations.response",
"expect": { "expect": {
"success": true, "success": true,
"sql": [ "sql": [
[ [
"(SELECT jsonb_strip_nulls((SELECT jsonb_build_object(", "FIX ME"
" 'age', person_1.age,", ]
" 'archived', entity_3.archived,", ]
" 'created_at', entity_3.created_at,", }
" 'first_name', person_1.first_name,", },
" 'id', entity_3.id,", {
" 'last_name', person_1.last_name,", "description": "Full organizations select via a punc response with family",
" 'name', organization_2.name,", "action": "query",
" 'type', entity_3.type", "schema_id": "get_full_organizations.response",
")", "expect": {
"FROM agreego.person person_1", "success": true,
"JOIN agreego.organization organization_2 ON organization_2.id = person_1.id", "sql": [
"JOIN agreego.entity entity_3 ON entity_3.id = organization_2.id", [
"WHERE NOT entity_3.archived)))" "FIX ME"
] ]
] ]
} }
@ -1629,6 +1713,19 @@
] ]
] ]
} }
},
{
"description": "Widgets select via a punc response with family (STI)",
"action": "query",
"schema_id": "get_widgets.response",
"expect": {
"success": true,
"sql": [
[
"FIX ME"
]
]
}
} }
] ]
} }

45
out.txt Normal file

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,6 @@ pub struct Database {
pub puncs: HashMap<String, Punc>, pub puncs: HashMap<String, Punc>,
pub relations: HashMap<String, Relation>, pub relations: HashMap<String, Relation>,
pub schemas: HashMap<String, Schema>, pub schemas: HashMap<String, Schema>,
pub descendants: HashMap<String, Vec<String>>,
pub depths: HashMap<String, usize>, pub depths: HashMap<String, usize>,
pub executor: Box<dyn DatabaseExecutor + Send + Sync>, pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
} }
@ -45,7 +44,6 @@ impl Database {
relations: HashMap::new(), relations: HashMap::new(),
puncs: HashMap::new(), puncs: HashMap::new(),
schemas: HashMap::new(), schemas: HashMap::new(),
descendants: HashMap::new(),
depths: HashMap::new(), depths: HashMap::new(),
#[cfg(not(test))] #[cfg(not(test))]
executor: Box::new(SpiExecutor::new()), executor: Box::new(SpiExecutor::new()),
@ -194,7 +192,6 @@ impl Database {
self.collect_schemas(errors); self.collect_schemas(errors);
self.collect_depths(); self.collect_depths();
self.collect_descendants();
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks // Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
let mut visited = std::collections::HashSet::new(); let mut visited = std::collections::HashSet::new();
@ -256,43 +253,4 @@ impl Database {
self.depths = depths; self.depths = depths;
} }
fn collect_descendants(&mut self) {
let mut direct_refs: HashMap<String, Vec<String>> = HashMap::new();
for (id, schema) in &self.schemas {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
direct_refs
.entry(t.clone())
.or_default()
.push(id.clone());
}
}
}
// Cache exhaustive descendants matrix for generic $family string lookups natively
let mut descendants = HashMap::new();
for id in self.schemas.keys() {
let mut desc_set = HashSet::new();
Self::collect_descendants_recursively(id, &direct_refs, &mut desc_set);
let mut desc_vec: Vec<String> = desc_set.into_iter().collect();
desc_vec.sort();
descendants.insert(id.clone(), desc_vec);
}
self.descendants = descendants;
}
fn collect_descendants_recursively(
target: &str,
direct_refs: &std::collections::HashMap<String, Vec<String>>,
descendants: &mut std::collections::HashSet<String>,
) {
if let Some(children) = direct_refs.get(target) {
for child in children {
if descendants.insert(child.clone()) {
Self::collect_descendants_recursively(child, direct_refs, descendants);
}
}
}
}
} }

View File

@ -23,6 +23,10 @@ pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
lock.get().map_or(true, |v| v.is_empty()) lock.get().map_or(true, |v| v.is_empty())
} }
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> bool {
lock.get().map_or(true, |s| s.is_empty())
}
// Schema mirrors the Go Punc Generator's schema struct for consistency. // Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema. // It is an order-preserving representation of a JSON Schema.
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
@ -201,6 +205,18 @@ pub struct SchemaObject {
#[serde(skip)] #[serde(skip)]
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>, pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "compiledDiscriminator")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_string_empty")]
#[serde(serialize_with = "crate::database::schema::serialize_once_lock")]
pub compiled_discriminator: OnceLock<String>,
#[serde(rename = "compiledOptions")]
#[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_options: OnceLock<BTreeMap<String, String>>,
#[serde(rename = "compiledEdges")] #[serde(rename = "compiledEdges")]
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")] #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")]
@ -411,11 +427,140 @@ impl Schema {
} }
} }
self.compile_polymorphism(db, errors);
if let Some(id) = &self.obj.id { if let Some(id) = &self.obj.id {
visited.remove(id); visited.remove(id);
} }
} }
pub fn compile_polymorphism(
&self,
db: &crate::database::Database,
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(), target_id);
}
}
} else {
// Scenario C: Single Table Inheritance (Horizontal)
strategy = "kind".to_string();
let mut target_family_ids = std::collections::HashSet::new();
target_family_ids.insert(family.clone());
// Iteratively build local descendants since db.descendants is removed natively
let mut added = true;
while added {
added = false;
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
if !target_family_ids.contains(id) {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if target_family_ids.contains(t) {
target_family_ids.insert(id.clone());
added = true;
}
}
}
}
}
}
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
if target_family_ids.contains(id) {
if let Some(kind_val) = schema.obj.get_discriminator_value("kind") {
options.insert(kind_val, 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();
for c in one_of {
if let Some(t_val) = c.obj.get_discriminator_value("type") {
type_vals.insert(t_val);
}
if let Some(k_val) = c.obj.get_discriminator_value("kind") {
kind_vals.insert(k_val);
}
}
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
"type".to_string()
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
"kind".to_string()
} else {
"".to_string()
};
if strategy.is_empty() {
return;
}
for c in one_of {
if let Some(val) = c.obj.get_discriminator_value(&strategy) {
if options.contains_key(&val) {
errors.push(crate::drop::Error {
code: "POLYMORPHIC_COLLISION".to_string(),
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
details: crate::drop::ErrorDetails::default()
});
continue;
}
let mut target_id = c.obj.id.clone();
if target_id.is_none() {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
target_id = Some(t.clone());
}
}
}
if let Some(tid) = target_id {
options.insert(val, tid);
}
}
}
} else {
return;
}
if let Some(id) = &self.obj.id {
println!("[DEBUG POLYMORPHISM] ID: {}, Strategy: {}, Options: {:?}", id, strategy, options);
}
if !options.is_empty() {
let _ = self.obj.compiled_discriminator.set(strategy);
let _ = self.obj.compiled_options.set(options);
}
}
#[allow(unused_variables)] #[allow(unused_variables)]
fn validate_identifier(id: &str, field_name: &str, errors: &mut Vec<crate::drop::Error>) { fn validate_identifier(id: &str, field_name: &str, errors: &mut Vec<crate::drop::Error>) {
#[cfg(not(test))] #[cfg(not(test))]
@ -893,6 +1038,72 @@ impl SchemaObject {
} }
None None
} }
pub fn get_discriminator_value(&self, dim: &str) -> Option<String> {
let is_split = self.compiled_properties.get().map_or(false, |p| p.contains_key("kind"));
if let Some(id) = &self.id {
if id.contains("light.person") || id.contains("light.organization") {
println!("[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}", id, dim, is_split, self.compiled_properties.get().map(|p| p.keys().cloned().collect::<Vec<_>>()));
}
}
if let Some(props) = self.compiled_properties.get() {
if let Some(prop_schema) = props.get(dim) {
if let Some(c) = &prop_schema.obj.const_ {
if let Some(s) = c.as_str() {
return Some(s.to_string());
}
}
if let Some(e) = &prop_schema.obj.enum_ {
if e.len() == 1 {
if let Some(s) = e[0].as_str() {
return Some(s.to_string());
}
}
}
}
}
if dim == "kind" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ {
if !crate::database::schema::is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
}
}
if dim == "type" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ {
if !crate::database::schema::is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
}
}
None
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -131,13 +131,33 @@ impl Merger {
pub(crate) fn merge_internal( pub(crate) fn merge_internal(
&self, &self,
schema: Arc<crate::database::schema::Schema>, mut schema: Arc<crate::database::schema::Schema>,
data: Value, data: Value,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
) -> Result<Value, String> { ) -> Result<Value, String> {
match data { match data {
Value::Array(items) => self.merge_array(schema, items, notifications), Value::Array(items) => self.merge_array(schema, items, notifications),
Value::Object(map) => self.merge_object(schema, map, notifications), Value::Object(map) => {
if let Some(options) = schema.obj.compiled_options.get() {
if let Some(disc) = schema.obj.compiled_discriminator.get() {
let val = map.get(disc).and_then(|v| v.as_str());
if let Some(v) = val {
if let Some(target_id) = options.get(v) {
if let Some(target_schema) = self.db.schemas.get(target_id) {
schema = Arc::new(target_schema.clone());
} else {
return Err(format!("Polymorphic mapped target '{}' not found in database registry", target_id));
}
} else {
return Err(format!("Polymorphic discriminator {}='{}' matched no compiled options", disc, v));
}
} else {
return Err(format!("Polymorphic merging failed: missing required discriminator '{}'", disc));
}
}
}
self.merge_object(schema, map, notifications)
},
_ => Err("Invalid merge payload: root must be an Object or Array".to_string()), _ => Err("Invalid merge payload: root must be an Object or Array".to_string()),
} }
} }

View File

@ -1,5 +1,4 @@
use crate::database::Database; use crate::database::Database;
use std::sync::Arc;
pub struct Compiler<'a> { pub struct Compiler<'a> {
pub db: &'a Database, pub db: &'a Database,
pub filter_keys: &'a [String], pub filter_keys: &'a [String],
@ -124,35 +123,19 @@ impl<'a> Compiler<'a> {
return Err(format!("Unresolved schema type pointer: {}", t)); return Err(format!("Unresolved schema type pointer: {}", t));
} }
} }
// Handle $family Polymorphism fallbacks for relations // Handle Polymorphism fallbacks for relations
if let Some(family_target) = &node.schema.obj.family { if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
let mut all_targets = vec![family_target.clone()]; if let Some(options) = node.schema.obj.compiled_options.get() {
if let Some(descendants) = self.db.descendants.get(family_target) { if options.len() == 1 {
all_targets.extend(descendants.clone()); let target_id = options.values().next().unwrap();
} let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(target_id.clone()));
if all_targets.len() == 1 { let mut bypass_node = node.clone();
let mut bypass_schema = crate::database::schema::Schema::default(); bypass_node.schema = std::sync::Arc::new(bypass_schema);
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(all_targets[0].clone())); return self.compile_node(bypass_node);
let mut bypass_node = node.clone(); }
bypass_node.schema = std::sync::Arc::new(bypass_schema); }
return self.compile_node(bypass_node); return self.compile_one_of(node);
}
all_targets.sort();
let mut family_schemas = Vec::new();
for variation in &all_targets {
let mut ref_schema = crate::database::schema::Schema::default();
ref_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(variation.clone()));
family_schemas.push(std::sync::Arc::new(ref_schema));
}
return self.compile_one_of(&family_schemas, node);
}
// Handle oneOf Polymorphism fallbacks for relations
if let Some(one_of) = &node.schema.obj.one_of {
return self.compile_one_of(one_of, node.clone());
} }
// Just an inline object definition? // Just an inline object definition?
@ -226,77 +209,43 @@ impl<'a> Compiler<'a> {
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut select_args = Vec::new(); let mut select_args = Vec::new();
if let Some(family_target) = node.schema.obj.family.as_ref() { if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
let family_prefix = family_target.rfind('.').map(|idx| &family_target[..idx]);
let mut all_targets = vec![family_target.clone()];
if let Some(descendants) = self.db.descendants.get(family_target) {
all_targets.extend(descendants.clone());
}
// Filter targets to EXACTLY match the family_target prefix
let mut final_targets = Vec::new();
for target in all_targets {
let target_prefix = target.rfind('.').map(|idx| &target[..idx]);
if target_prefix == family_prefix {
final_targets.push(target);
}
}
final_targets.sort();
final_targets.dedup();
if final_targets.len() == 1 {
let variation = &final_targets[0];
if let Some(target_schema) = self.db.schemas.get(variation) {
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(target_schema.clone());
let mut bypassed_args = self.compile_select_clause(r#type, table_aliases, bypass_node)?;
select_args.append(&mut bypassed_args);
} else {
return Err(format!("Could not find schema for variation {}", variation));
}
} else {
let mut family_schemas = Vec::new();
for variation in &final_targets {
if let Some(target_schema) = self.db.schemas.get(variation) {
family_schemas.push(std::sync::Arc::new(target_schema.clone()));
} else {
return Err(format!(
"Could not find schema metadata for variation {}",
variation
));
}
}
let base_alias = table_aliases let base_alias = table_aliases
.get(&r#type.name) .get(&r#type.name)
.cloned() .cloned()
.unwrap_or_else(|| node.parent_alias.to_string()); .unwrap_or_else(|| node.parent_alias.to_string());
let disc = node.schema.obj.compiled_discriminator.get();
if disc.is_none() {
return Ok(select_args);
}
let options = node.schema.obj.compiled_options.get();
println!("[DEBUG QUERYER] Evaluating node. Target family: {:?}, disc: {:?}, options: {:?}", node.schema.obj.family, disc, options);
if options.is_none() {
return Ok(select_args);
}
let options = options.unwrap();
if options.len() == 1 {
let target_id = options.values().next().unwrap();
if let Some(target_schema) = self.db.schemas.get(target_id) {
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(target_schema.clone());
let mut bypassed_args = self.compile_select_clause(r#type, table_aliases, bypass_node)?;
select_args.append(&mut bypassed_args);
return Ok(select_args);
}
}
select_args.push(format!("'id', {}.id", base_alias)); select_args.push(format!("'id', {}.id", base_alias));
let mut case_node = node.clone(); let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone(); case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone()); let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases); case_node.parent_type_aliases = Some(arc_aliases);
let (case_sql, _) = self.compile_one_of(&family_schemas, case_node)?; let (case_sql, _) = self.compile_one_of(case_node)?;
select_args.push(format!("'type', {}", case_sql)); select_args.push(format!("'{}', {}", disc.unwrap(), case_sql));
}
} else if let Some(one_of) = &node.schema.obj.one_of {
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
select_args.push(format!("'id', {}.id", base_alias));
let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases);
let (case_sql, _) = self.compile_one_of(one_of, case_node)?;
select_args.push(format!("'type', {}", case_sql));
} }
Ok(select_args) Ok(select_args)
@ -333,26 +282,28 @@ impl<'a> Compiler<'a> {
fn compile_one_of( fn compile_one_of(
&mut self, &mut self,
schemas: &[Arc<crate::database::schema::Schema>],
node: Node<'a>, node: Node<'a>,
) -> Result<(String, String), String> { ) -> Result<(String, String), String> {
let mut case_statements = Vec::new(); let mut case_statements = Vec::new();
let options = node.schema.obj.compiled_options.get().ok_or("Missing compiled options for polymorphism")?;
let disc = node.schema.obj.compiled_discriminator.get().ok_or("Missing compiled discriminator for polymorphism")?;
let type_col = if let Some(prop) = &node.property_name { let type_col = if let Some(prop) = &node.property_name {
format!("{}_type", prop) format!("{}_{}", prop, disc)
} else { } else {
"type".to_string() disc.to_string()
}; };
for option_schema in schemas { for (disc_val, target_id) in options {
if let Some(base_type_name) = option_schema.obj.identifier() { if let Some(target_schema) = self.db.schemas.get(target_id) {
// Generate the nested SQL for this specific target type
let mut child_node = node.clone(); let mut child_node = node.clone();
child_node.schema = std::sync::Arc::clone(option_schema); child_node.schema = std::sync::Arc::new(target_schema.clone());
let (val_sql, _) = self.compile_node(child_node)?; let (val_sql, _) = self.compile_node(child_node)?;
case_statements.push(format!( case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})", "WHEN {}.{} = '{}' THEN ({})",
node.parent_alias, type_col, base_type_name, val_sql node.parent_alias, type_col, disc_val, val_sql
)); ));
} }
} }

View File

@ -1439,6 +1439,18 @@ fn test_queryer_0_10() {
crate::tests::runner::run_test_case(&path, 0, 10).unwrap(); crate::tests::runner::run_test_case(&path, 0, 10).unwrap();
} }
#[test]
fn test_queryer_0_11() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 11).unwrap();
}
#[test]
fn test_queryer_0_12() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 12).unwrap();
}
#[test] #[test]
fn test_polymorphism_0_0() { fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -1,4 +1,3 @@
use crate::database::schema::Schema;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -29,30 +28,10 @@ impl<'a> ValidationContext<'a> {
} }
} }
if let Some(family_target) = &self.schema.family { if self.schema.family.is_some() {
if let Some(descendants) = self.db.descendants.get(family_target) { if let Some(options) = self.schema.compiled_options.get() {
let mut candidates = Vec::new(); if let Some(disc) = self.schema.compiled_discriminator.get() {
return self.execute_polymorph(disc, options, result);
// Add the target base schema itself
if let Some(base_schema) = self.db.schemas.get(family_target) {
candidates.push(base_schema);
}
// Add all descendants
for child_id in descendants {
if let Some(child_schema) = self.db.schemas.get(child_id) {
candidates.push(child_schema);
}
}
// Use prefix from family string (e.g. `light.`)
let prefix = family_target
.rsplit_once('.')
.map(|(p, _)| format!("{}.", p))
.unwrap_or_default();
if !self.validate_polymorph(&candidates, Some(&prefix), result)? {
return Ok(false);
} }
} }
} }
@ -64,212 +43,43 @@ impl<'a> ValidationContext<'a> {
&self, &self,
result: &mut ValidationResult, result: &mut ValidationResult,
) -> Result<bool, ValidationError> { ) -> Result<bool, ValidationError> {
if let Some(ref one_of) = self.schema.one_of { if let Some(one_of) = &self.schema.one_of {
let mut candidates = Vec::new(); if let Some(options) = self.schema.compiled_options.get() {
for schema in one_of { if let Some(disc) = self.schema.compiled_discriminator.get() {
candidates.push(schema.as_ref()); return self.execute_polymorph(disc, options, result);
}
} }
if !self.validate_polymorph(&candidates, None, result)? {
return Ok(false);
}
}
Ok(true)
}
pub(crate) fn validate_polymorph( // Native Draft2020-12 oneOf Evaluation Fallback
&self, let mut valid_count = 0;
candidates: &[&Schema], let mut final_successful_result = None;
family_prefix: Option<&str>, let mut failed_candidates = Vec::new();
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let mut passed_candidates: Vec<(Option<String>, ValidationResult)> = Vec::new();
let mut failed_candidates: Vec<ValidationResult> = Vec::new();
// 1. O(1) Fast-Path Router & Extractor for child_schema in one_of {
let instance_type = self.instance.as_object().and_then(|o| o.get("type")).and_then(|t| t.as_str()); let derived = self.derive_for_schema(child_schema, false);
let instance_kind = self.instance.as_object().and_then(|o| o.get("kind")).and_then(|k| k.as_str()); if let Ok(sub_res) = derived.validate_scoped() {
if sub_res.is_valid() {
let mut viable_candidates = Vec::new(); valid_count += 1;
final_successful_result = Some(sub_res.clone());
for sub in candidates { } else {
let _child_id = sub.identifier().unwrap_or_default(); failed_candidates.push(sub_res);
let mut can_match = true;
if let Some(t) = instance_type {
// Fast Path 1: Pure Ad-Hoc Match (schema identifier == type)
// If it matches exactly, it's our golden candidate. Make all others non-viable manually?
// Wait, we loop through all and filter down. If exact match is found, we should ideally break and use ONLY that.
// Let's implement the logic safely.
let mut exact_match_found = false;
if let Some(schema_id) = &sub.id {
// Compute Vertical Exact Target (e.g. "person" or "light.person")
let exact_target = if let Some(prefix) = family_prefix {
format!("{}{}", prefix, t)
} else {
t.to_string()
};
// Fast Path 1 & 2: Vertical Exact Match
if schema_id == &exact_target {
if instance_kind.is_none() {
exact_match_found = true;
}
}
// Fast Path 3: Horizontal Sibling Match (kind + . + type)
if let Some(k) = instance_kind {
let sibling_target = format!("{}.{}", k, t);
if schema_id == &sibling_target {
exact_match_found = true;
}
}
}
if exact_match_found {
// We found an exact literal structural identity match!
// Wipe the existing viable_candidates and only yield this guy!
viable_candidates.clear();
viable_candidates.push(*sub);
break;
}
// Fast Path 4: Vertical Inheritance Fallback (Physical DB constraint)
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t_ptr)) = &sub.type_ {
if !crate::database::schema::is_primitive_type(t_ptr) {
if let Some(base_type) = t_ptr.split('.').last() {
if let Some(type_def) = self.db.types.get(base_type) {
if !type_def.variations.contains(&t.to_string()) {
can_match = false;
}
} else {
if t_ptr != t {
can_match = false;
}
}
}
}
}
// Fast Path 5: Explicit Schema JSON `const` values check
if can_match {
if let Some(props) = &sub.properties {
if let Some(type_prop) = props.get("type") {
if let Some(const_val) = &type_prop.const_ {
if let Some(const_str) = const_val.as_str() {
if const_str != t {
can_match = false;
}
}
}
}
} }
} }
} }
if can_match { if valid_count == 1 {
viable_candidates.push(*sub); if let Some(successful_res) = final_successful_result {
} result.merge(successful_res);
} }
return Ok(true);
println!("DEBUG VIABLE: {:?}", viable_candidates.iter().map(|s| s.id.clone()).collect::<Vec<_>>()); } else if valid_count == 0 {
// 2. Evaluate Viable Candidates result.errors.push(ValidationError {
// 2. Evaluate Viable Candidates code: "NO_ONEOF_MATCH".to_string(),
// Composition validation is natively handled directly via type compilation. message: "Payload matches none of the required candidate sub-schemas natively".to_string(),
// The deprecated allOf JSON structure is no longer supported nor traversed.
for sub in viable_candidates.clone() {
let derived = self.derive_for_schema(sub, false);
let sub_res = derived.validate()?;
if sub_res.is_valid() {
passed_candidates.push((sub.id.clone(), sub_res));
} else {
failed_candidates.push(sub_res);
}
}
for f in &failed_candidates {
println!(" - Failed candidate errors: {:?}", f.errors.iter().map(|e| e.code.clone()).collect::<Vec<_>>());
}
if passed_candidates.len() == 1 {
result.merge(passed_candidates.pop().unwrap().1);
} else if passed_candidates.is_empty() {
// 3. Discriminator Pathing (Failure Analytics)
let type_path = self.join_path("type");
if instance_type.is_some() {
// Filter to candidates that didn't explicitly throw a CONST violation on `type`
let mut genuinely_failed = Vec::new();
for res in &failed_candidates {
let rejected_type = res.errors.iter().any(|e| {
(e.code == "CONST_VIOLATED" || e.code == "ENUM_VIOLATED") && e.path == type_path
});
if !rejected_type {
genuinely_failed.push(res.clone());
}
}
println!("DEBUG genuinely_failed len: {}", genuinely_failed.len());
if genuinely_failed.len() == 1 {
// Golden Type Match (1 candidate was structurally possible but failed property validation)
let sub_res = genuinely_failed.pop().unwrap();
result.errors.extend(sub_res.errors);
result.evaluated_keys.extend(sub_res.evaluated_keys);
return Ok(false);
} else {
// Pure Ad-Hoc Union
result.errors.push(ValidationError {
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
message: "Payload matches none of the required candidate sub-schemas".to_string(),
path: self.path.to_string(), path: self.path.to_string(),
}); });
for sub_res in &failed_candidates { if let Some(first) = failed_candidates.first() {
result.evaluated_keys.extend(sub_res.evaluated_keys.clone());
}
println!("DEBUG ELSE NO_FAMILY_MATCH RUNNING. Genuinely Failed len: {}", genuinely_failed.len());
if viable_candidates.is_empty() {
if let Some(obj) = self.instance.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
}
}
for sub_res in genuinely_failed {
for e in sub_res.errors {
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
result.errors.push(e);
}
}
}
return Ok(false);
}
} else {
// Instance missing type
// Instance missing type
let expects_type = viable_candidates.iter().any(|c| {
c.compiled_property_names.get().map_or(false, |props| props.contains(&"type".to_string()))
});
if expects_type {
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: "Missing type discriminator. Unable to resolve polymorphic boundaries".to_string(),
path: self.path.to_string(),
});
for sub_res in failed_candidates {
result.evaluated_keys.extend(sub_res.evaluated_keys);
}
return Ok(false);
} else {
// Pure Ad-Hoc Union
result.errors.push(ValidationError {
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
message: "Payload matches none of the required candidate sub-schemas".to_string(),
path: self.path.to_string(),
});
if let Some(first) = failed_candidates.first() {
let mut shared_errors = first.errors.clone(); let mut shared_errors = first.errors.clone();
for sub_res in failed_candidates.iter().skip(1) { for sub_res in failed_candidates.iter().skip(1) {
shared_errors.retain(|e1| { shared_errors.retain(|e1| {
@ -281,26 +91,66 @@ impl<'a> ValidationContext<'a> {
result.errors.push(e); result.errors.push(e);
} }
} }
} }
for sub_res in failed_candidates { return Ok(false);
result.evaluated_keys.extend(sub_res.evaluated_keys); } else {
} result.errors.push(ValidationError {
return Ok(false); code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
} message: "Matches multiple polymorphic candidates inextricably natively".to_string(),
path: self.path.to_string(),
});
return Ok(false);
} }
} else {
result.errors.push(ValidationError {
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
message: "Matches multiple polymorphic candidates inextricably".to_string(),
path: self.path.to_string(),
});
} }
Ok(true) Ok(true)
} }
pub(crate) fn execute_polymorph(
&self,
disc: &str,
options: &std::collections::BTreeMap<String, String>,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
// 1. O(1) Fast-Path Router & Extractor
let instance_val = self.instance.as_object().and_then(|o| o.get(disc)).and_then(|t| t.as_str());
if let Some(val) = instance_val {
result.evaluated_keys.insert(disc.to_string());
if let Some(target_id) = options.get(val) {
if let Some(target_schema) = self.db.schemas.get(target_id) {
let derived = self.derive_for_schema(target_schema, false);
let sub_res = derived.validate()?;
let is_valid = sub_res.is_valid();
result.merge(sub_res);
return Ok(is_valid);
} else {
result.errors.push(ValidationError {
code: "MISSING_COMPILED_SCHEMA".to_string(),
message: format!("Polymorphic router target '{}' does not exist in the database schemas map", target_id),
path: self.path.to_string(),
});
return Ok(false);
}
} else {
result.errors.push(ValidationError {
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
message: format!("Payload provided discriminator {}='{}' which matches none of the required candidate sub-schemas", disc, val),
path: self.path.to_string(),
});
return Ok(false);
}
} else {
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: format!("Missing '{}' discriminator. Unable to resolve polymorphic boundaries", disc),
path: self.path.to_string(),
});
return Ok(false);
}
}
pub(crate) fn validate_type_inheritance( pub(crate) fn validate_type_inheritance(
&self, &self,
result: &mut ValidationResult, result: &mut ValidationResult,