checkpoint
This commit is contained in:
25
GEMINI.md
25
GEMINI.md
@ -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.
|
||||
|
||||
### Polymorphism (`$family` and `oneOf`)
|
||||
Polymorphism is how an object boundary can dynamically take on entirely different shapes based on the payload provided at runtime.
|
||||
* **`$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.
|
||||
* *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.
|
||||
* **`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`.
|
||||
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 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.
|
||||
* **Scenario A: Global Tables (Vertical Routing)**
|
||||
* *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`)
|
||||
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.
|
||||
|
||||
@ -196,6 +196,10 @@
|
||||
{
|
||||
"description": "Horizontal $family Routing (Virtual Variations)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "widget",
|
||||
"variations": ["widget"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "widget",
|
||||
@ -218,7 +222,11 @@
|
||||
"properties": {
|
||||
"super_amount": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "family_widget",
|
||||
"$family": "widget"
|
||||
|
||||
@ -25,11 +25,20 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_person",
|
||||
"name": "get_light_organizations",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "get_person.response",
|
||||
"$family": "person"
|
||||
"$id": "get_light_organizations.response",
|
||||
"$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": [],
|
||||
@ -260,7 +281,9 @@
|
||||
"type",
|
||||
"name",
|
||||
"archived",
|
||||
"created_at"
|
||||
"created_at",
|
||||
"token",
|
||||
"role"
|
||||
],
|
||||
"grouped_fields": {
|
||||
"entity": [
|
||||
@ -273,7 +296,8 @@
|
||||
"name"
|
||||
],
|
||||
"bot": [
|
||||
"token"
|
||||
"token",
|
||||
"role"
|
||||
]
|
||||
},
|
||||
"field_types": {
|
||||
@ -282,12 +306,25 @@
|
||||
"archived": "boolean",
|
||||
"name": "text",
|
||||
"token": "text",
|
||||
"role": "text",
|
||||
"created_at": "timestamptz"
|
||||
},
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "bot",
|
||||
"type": "organization",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.bot",
|
||||
"type": "organization",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
@ -360,8 +397,15 @@
|
||||
},
|
||||
{
|
||||
"$id": "light.person",
|
||||
"type": "person",
|
||||
"properties": {}
|
||||
"type": "organization",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "full.person",
|
||||
@ -850,6 +894,46 @@
|
||||
"variations": [
|
||||
"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",
|
||||
" WHEN entity_11.target_type = 'address' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'archived', entity_17.archived,",
|
||||
" 'city', address_16.city,",
|
||||
" 'created_at', entity_17.created_at,",
|
||||
" 'id', entity_17.id,",
|
||||
" 'type', entity_17.type",
|
||||
" 'archived', entity_13.archived,",
|
||||
" 'city', address_12.city,",
|
||||
" 'created_at', entity_13.created_at,",
|
||||
" 'id', entity_13.id,",
|
||||
" 'type', entity_13.type",
|
||||
" )",
|
||||
" FROM agreego.address address_16",
|
||||
" JOIN agreego.entity entity_17 ON entity_17.id = address_16.id",
|
||||
" FROM agreego.address address_12",
|
||||
" JOIN agreego.entity entity_13 ON entity_13.id = address_12.id",
|
||||
" WHERE",
|
||||
" NOT entity_17.archived",
|
||||
" AND relationship_10.target_id = entity_17.id))",
|
||||
" NOT entity_13.archived",
|
||||
" AND relationship_10.target_id = entity_13.id))",
|
||||
" WHEN entity_11.target_type = 'email_address' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'address', email_address_14.address,",
|
||||
@ -1030,17 +1114,17 @@
|
||||
" AND relationship_10.target_id = entity_15.id))",
|
||||
" WHEN entity_11.target_type = 'phone_number' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'archived', entity_13.archived,",
|
||||
" 'created_at', entity_13.created_at,",
|
||||
" 'id', entity_13.id,",
|
||||
" 'number', phone_number_12.number,",
|
||||
" 'type', entity_13.type",
|
||||
" 'archived', entity_17.archived,",
|
||||
" 'created_at', entity_17.created_at,",
|
||||
" 'id', entity_17.id,",
|
||||
" 'number', phone_number_16.number,",
|
||||
" 'type', entity_17.type",
|
||||
" )",
|
||||
" FROM agreego.phone_number phone_number_12",
|
||||
" JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id",
|
||||
" FROM agreego.phone_number phone_number_16",
|
||||
" JOIN agreego.entity entity_17 ON entity_17.id = phone_number_16.id",
|
||||
" WHERE",
|
||||
" NOT entity_13.archived",
|
||||
" AND relationship_10.target_id = entity_13.id))",
|
||||
" NOT entity_17.archived",
|
||||
" AND relationship_10.target_id = entity_17.id))",
|
||||
" ELSE NULL END,",
|
||||
" 'type', entity_11.type",
|
||||
" )), '[]'::jsonb)",
|
||||
@ -1240,17 +1324,17 @@
|
||||
" 'target', CASE",
|
||||
" WHEN entity_11.target_type = 'address' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'archived', entity_17.archived,",
|
||||
" 'city', address_16.city,",
|
||||
" 'created_at', entity_17.created_at,",
|
||||
" 'id', entity_17.id,",
|
||||
" 'type', entity_17.type",
|
||||
" 'archived', entity_13.archived,",
|
||||
" 'city', address_12.city,",
|
||||
" 'created_at', entity_13.created_at,",
|
||||
" 'id', entity_13.id,",
|
||||
" 'type', entity_13.type",
|
||||
" )",
|
||||
" FROM agreego.address address_16",
|
||||
" JOIN agreego.entity entity_17 ON entity_17.id = address_16.id",
|
||||
" FROM agreego.address address_12",
|
||||
" JOIN agreego.entity entity_13 ON entity_13.id = address_12.id",
|
||||
" WHERE",
|
||||
" NOT entity_17.archived",
|
||||
" AND relationship_10.target_id = entity_17.id))",
|
||||
" NOT entity_13.archived",
|
||||
" AND relationship_10.target_id = entity_13.id))",
|
||||
" WHEN entity_11.target_type = 'email_address' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'address', email_address_14.address,",
|
||||
@ -1266,17 +1350,17 @@
|
||||
" AND relationship_10.target_id = entity_15.id))",
|
||||
" WHEN entity_11.target_type = 'phone_number' THEN",
|
||||
" ((SELECT jsonb_build_object(",
|
||||
" 'archived', entity_13.archived,",
|
||||
" 'created_at', entity_13.created_at,",
|
||||
" 'id', entity_13.id,",
|
||||
" 'number', phone_number_12.number,",
|
||||
" 'type', entity_13.type",
|
||||
" 'archived', entity_17.archived,",
|
||||
" 'created_at', entity_17.created_at,",
|
||||
" 'id', entity_17.id,",
|
||||
" 'number', phone_number_16.number,",
|
||||
" 'type', entity_17.type",
|
||||
" )",
|
||||
" FROM agreego.phone_number phone_number_12",
|
||||
" JOIN agreego.entity entity_13 ON entity_13.id = phone_number_12.id",
|
||||
" FROM agreego.phone_number phone_number_16",
|
||||
" JOIN agreego.entity entity_17 ON entity_17.id = phone_number_16.id",
|
||||
" WHERE",
|
||||
" NOT entity_13.archived",
|
||||
" AND relationship_10.target_id = entity_13.id))",
|
||||
" NOT entity_17.archived",
|
||||
" AND relationship_10.target_id = entity_17.id))",
|
||||
" ELSE NULL END,",
|
||||
" 'type', entity_11.type",
|
||||
" )), '[]'::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",
|
||||
"schema_id": "get_person.response",
|
||||
"schema_id": "get_light_organizations.response",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"(SELECT jsonb_strip_nulls((SELECT jsonb_build_object(",
|
||||
" '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,",
|
||||
" 'name', organization_2.name,",
|
||||
" 'type', entity_3.type",
|
||||
")",
|
||||
"FROM agreego.person person_1",
|
||||
"JOIN agreego.organization organization_2 ON organization_2.id = person_1.id",
|
||||
"JOIN agreego.entity entity_3 ON entity_3.id = organization_2.id",
|
||||
"WHERE NOT entity_3.archived)))"
|
||||
"FIX ME"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Full organizations select via a punc response with family",
|
||||
"action": "query",
|
||||
"schema_id": "get_full_organizations.response",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"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"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -32,7 +32,6 @@ pub struct Database {
|
||||
pub puncs: HashMap<String, Punc>,
|
||||
pub relations: HashMap<String, Relation>,
|
||||
pub schemas: HashMap<String, Schema>,
|
||||
pub descendants: HashMap<String, Vec<String>>,
|
||||
pub depths: HashMap<String, usize>,
|
||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||
}
|
||||
@ -45,7 +44,6 @@ impl Database {
|
||||
relations: HashMap::new(),
|
||||
puncs: HashMap::new(),
|
||||
schemas: HashMap::new(),
|
||||
descendants: HashMap::new(),
|
||||
depths: HashMap::new(),
|
||||
#[cfg(not(test))]
|
||||
executor: Box::new(SpiExecutor::new()),
|
||||
@ -194,7 +192,6 @@ impl Database {
|
||||
|
||||
self.collect_schemas(errors);
|
||||
self.collect_depths();
|
||||
self.collect_descendants();
|
||||
|
||||
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
@ -256,43 +253,4 @@ impl Database {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
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.
|
||||
// It is an order-preserving representation of a JSON Schema.
|
||||
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
|
||||
@ -201,6 +205,18 @@ pub struct SchemaObject {
|
||||
#[serde(skip)]
|
||||
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(skip_deserializing)]
|
||||
#[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 {
|
||||
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)]
|
||||
fn validate_identifier(id: &str, field_name: &str, errors: &mut Vec<crate::drop::Error>) {
|
||||
#[cfg(not(test))]
|
||||
@ -893,6 +1038,72 @@ impl SchemaObject {
|
||||
}
|
||||
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)]
|
||||
|
||||
@ -131,13 +131,33 @@ impl Merger {
|
||||
|
||||
pub(crate) fn merge_internal(
|
||||
&self,
|
||||
schema: Arc<crate::database::schema::Schema>,
|
||||
mut schema: Arc<crate::database::schema::Schema>,
|
||||
data: Value,
|
||||
notifications: &mut Vec<String>,
|
||||
) -> Result<Value, String> {
|
||||
match data {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::database::Database;
|
||||
use std::sync::Arc;
|
||||
pub struct Compiler<'a> {
|
||||
pub db: &'a Database,
|
||||
pub filter_keys: &'a [String],
|
||||
@ -124,35 +123,19 @@ impl<'a> Compiler<'a> {
|
||||
return Err(format!("Unresolved schema type pointer: {}", t));
|
||||
}
|
||||
}
|
||||
// Handle $family Polymorphism fallbacks for relations
|
||||
if let Some(family_target) = &node.schema.obj.family {
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
all_targets.extend(descendants.clone());
|
||||
}
|
||||
|
||||
if all_targets.len() == 1 {
|
||||
// Handle Polymorphism fallbacks for relations
|
||||
if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
|
||||
if let Some(options) = node.schema.obj.compiled_options.get() {
|
||||
if options.len() == 1 {
|
||||
let target_id = options.values().next().unwrap();
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(all_targets[0].clone()));
|
||||
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(target_id.clone()));
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
return self.compile_node(bypass_node);
|
||||
}
|
||||
|
||||
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());
|
||||
return self.compile_one_of(node);
|
||||
}
|
||||
|
||||
// Just an inline object definition?
|
||||
@ -226,77 +209,43 @@ impl<'a> Compiler<'a> {
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut select_args = Vec::new();
|
||||
|
||||
if let Some(family_target) = node.schema.obj.family.as_ref() {
|
||||
let family_prefix = family_target.rfind('.').map(|idx| &family_target[..idx]);
|
||||
if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
|
||||
let base_alias = table_aliases
|
||||
.get(&r#type.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| node.parent_alias.to_string());
|
||||
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
all_targets.extend(descendants.clone());
|
||||
let disc = node.schema.obj.compiled_discriminator.get();
|
||||
if disc.is_none() {
|
||||
return Ok(select_args);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
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();
|
||||
|
||||
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) {
|
||||
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);
|
||||
} 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
|
||||
));
|
||||
return Ok(select_args);
|
||||
}
|
||||
}
|
||||
|
||||
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(&family_schemas, case_node)?;
|
||||
select_args.push(format!("'type', {}", 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));
|
||||
let (case_sql, _) = self.compile_one_of(case_node)?;
|
||||
select_args.push(format!("'{}', {}", disc.unwrap(), case_sql));
|
||||
}
|
||||
|
||||
Ok(select_args)
|
||||
@ -333,26 +282,28 @@ impl<'a> Compiler<'a> {
|
||||
|
||||
fn compile_one_of(
|
||||
&mut self,
|
||||
schemas: &[Arc<crate::database::schema::Schema>],
|
||||
node: Node<'a>,
|
||||
) -> Result<(String, String), String> {
|
||||
let mut case_statements = Vec::new();
|
||||
|
||||
let options = node.schema.obj.compiled_options.get().ok_or("Missing compiled options for polymorphism")?;
|
||||
let disc = node.schema.obj.compiled_discriminator.get().ok_or("Missing compiled discriminator for polymorphism")?;
|
||||
|
||||
let type_col = if let Some(prop) = &node.property_name {
|
||||
format!("{}_type", prop)
|
||||
format!("{}_{}", prop, disc)
|
||||
} else {
|
||||
"type".to_string()
|
||||
disc.to_string()
|
||||
};
|
||||
|
||||
for option_schema in schemas {
|
||||
if let Some(base_type_name) = option_schema.obj.identifier() {
|
||||
// Generate the nested SQL for this specific target type
|
||||
for (disc_val, target_id) in options {
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id) {
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = std::sync::Arc::clone(option_schema);
|
||||
child_node.schema = std::sync::Arc::new(target_schema.clone());
|
||||
let (val_sql, _) = self.compile_node(child_node)?;
|
||||
|
||||
case_statements.push(format!(
|
||||
"WHEN {}.{} = '{}' THEN ({})",
|
||||
node.parent_alias, type_col, base_type_name, val_sql
|
||||
node.parent_alias, type_col, disc_val, val_sql
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1439,6 +1439,18 @@ fn test_queryer_0_10() {
|
||||
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]
|
||||
fn test_polymorphism_0_0() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use crate::database::schema::Schema;
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -29,30 +28,10 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(family_target) = &self.schema.family {
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
// 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);
|
||||
if self.schema.family.is_some() {
|
||||
if let Some(options) = self.schema.compiled_options.get() {
|
||||
if let Some(disc) = self.schema.compiled_discriminator.get() {
|
||||
return self.execute_polymorph(disc, options, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,208 +43,39 @@ impl<'a> ValidationContext<'a> {
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(ref one_of) = self.schema.one_of {
|
||||
let mut candidates = Vec::new();
|
||||
for schema in one_of {
|
||||
candidates.push(schema.as_ref());
|
||||
}
|
||||
if !self.validate_polymorph(&candidates, None, result)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_polymorph(
|
||||
&self,
|
||||
candidates: &[&Schema],
|
||||
family_prefix: Option<&str>,
|
||||
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
|
||||
let instance_type = self.instance.as_object().and_then(|o| o.get("type")).and_then(|t| t.as_str());
|
||||
let instance_kind = self.instance.as_object().and_then(|o| o.get("kind")).and_then(|k| k.as_str());
|
||||
|
||||
let mut viable_candidates = Vec::new();
|
||||
|
||||
for sub in candidates {
|
||||
let _child_id = sub.identifier().unwrap_or_default();
|
||||
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;
|
||||
if let Some(one_of) = &self.schema.one_of {
|
||||
if let Some(options) = self.schema.compiled_options.get() {
|
||||
if let Some(disc) = self.schema.compiled_discriminator.get() {
|
||||
return self.execute_polymorph(disc, options, result);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Native Draft2020-12 oneOf Evaluation Fallback
|
||||
let mut valid_count = 0;
|
||||
let mut final_successful_result = None;
|
||||
let mut failed_candidates = Vec::new();
|
||||
|
||||
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 {
|
||||
viable_candidates.push(*sub);
|
||||
}
|
||||
}
|
||||
|
||||
println!("DEBUG VIABLE: {:?}", viable_candidates.iter().map(|s| s.id.clone()).collect::<Vec<_>>());
|
||||
// 2. Evaluate Viable Candidates
|
||||
// 2. Evaluate Viable Candidates
|
||||
// Composition validation is natively handled directly via type compilation.
|
||||
// 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()?;
|
||||
for child_schema in one_of {
|
||||
let derived = self.derive_for_schema(child_schema, false);
|
||||
if let Ok(sub_res) = derived.validate_scoped() {
|
||||
if sub_res.is_valid() {
|
||||
passed_candidates.push((sub.id.clone(), sub_res));
|
||||
valid_count += 1;
|
||||
final_successful_result = Some(sub_res.clone());
|
||||
} 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());
|
||||
if valid_count == 1 {
|
||||
if let Some(successful_res) = final_successful_result {
|
||||
result.merge(successful_res);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return Ok(true);
|
||||
} else if valid_count == 0 {
|
||||
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(),
|
||||
});
|
||||
|
||||
for sub_res in &failed_candidates {
|
||||
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(),
|
||||
code: "NO_ONEOF_MATCH".to_string(),
|
||||
message: "Payload matches none of the required candidate sub-schemas natively".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
@ -283,22 +93,62 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
for sub_res in failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
|
||||
message: "Matches multiple polymorphic candidates inextricably".to_string(),
|
||||
message: "Matches multiple polymorphic candidates inextricably natively".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user