From 4e2cb488ccd36e6575cdce46e917d80a07be7ef0 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Tue, 21 Apr 2026 10:50:01 -0400 Subject: [PATCH] removed schema realms, fixed fixture generations, added dynamic type resolution for validation based on type and kind discriminators for filters --- GEMINI.md | 13 ++- fixtures/dynamicType.json | 155 +++++++++++++++++++++++++++ fixtures/filter.json | 26 +++-- log.txt | 23 ---- log_test.txt | 23 ---- src/database/compile/filter.rs | 4 +- src/database/compile/mod.rs | 8 +- src/database/compile/polymorphism.rs | 5 +- src/database/mod.rs | 84 ++++++--------- src/database/realm.rs | 6 -- src/database/schema.rs | 21 ++++ src/merger/mod.rs | 5 +- src/queryer/compiler.rs | 15 +-- src/tests/fixtures.rs | 30 ++++++ src/tests/mod.rs | 4 +- src/tests/types/expect/schema.rs | 7 +- src/validator/context.rs | 5 + src/validator/mod.rs | 7 +- src/validator/rules/array.rs | 3 + src/validator/rules/mod.rs | 3 +- src/validator/rules/object.rs | 3 + src/validator/rules/polymorphism.rs | 79 +------------- src/validator/rules/type.rs | 138 ++++++++++++++++++++++++ test_failures.log | 81 -------------- test_merge.log | 23 ---- 25 files changed, 440 insertions(+), 331 deletions(-) create mode 100644 fixtures/dynamicType.json delete mode 100644 log.txt delete mode 100644 log_test.txt delete mode 100644 src/database/realm.rs create mode 100644 src/validator/rules/type.rs delete mode 100644 test_failures.log delete mode 100644 test_merge.log diff --git a/GEMINI.md b/GEMINI.md index 52fb8ae..0ad6189 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -56,12 +56,14 @@ The core execution engines natively enforce these boundaries: * **Global Schema Registration**: Roots must be attached to the top-level keys mapped from the `types`, `enums`, or `puncs` database tables. * They can re-use the standard `type` discriminator locally for `oneOf` polymorphism without conflicting with global Postgres Table constraints. -### Discriminators & The Dot Convention (A.B) -In Punc, polymorphic targets like explicit tagged unions or STI (Single Table Inheritance) rely on discriminators. Because Punc favors universal consistency, a schema's data contract must be explicit and mathematically identical regardless of the routing context an endpoint consumes it through. +### Discriminators & The `.` Convention +In Punc, polymorphic targets like explicit tagged unions or STI (Single Table Inheritance) rely on discriminators. The system heavily leverages a standard `.` dot-notation to enforce topological boundaries deterministically. -**The 2-Tier Paradigm**: The system inherently prevents "God Tables" by restricting routing to exactly two dimensions, guaranteeing absolute $O(1)$ lookups without ambiguity: -1. **Vertical Routing (`type`)**: Identifies the specific Postgres Table lineage (e.g. `person` vs `organization`). -2. **Horizontal Routing (`kind.type`)**: Natively evaluates Single Table Inheritance. The runtime dynamically concatenates `$kind.$type` to yield the namespace-protected schema key (e.g. `light.person`), maintaining collision-free schema registration. +**The 2-Tier Paradigm**: The system prevents "God Tables" by restricting routing to exactly two dimensions, guaranteeing absolute $O(1)$ lookups without ambiguity: +1. **Base (Vertical Routing)**: Represents the core physical lineage or foundational structural boundary. For entities, this is the table `type` (e.g. `person` or `widget`). For composed schemas, this is the root structural archetype (e.g., `filter`). +2. **Variant (Horizontal Routing)**: Represents the specific contextual projection or runtime mutation applied to the Base. For STI entities, this is the `kind` (e.g., `light`, `heavy`, `stock`). For composed filters, the variant identifies the entity it targets (e.g., `person`, `invoice`). + +When an object is evaluated for STI polymorphism, the runtime natively extracts its `$kind` and `$type` values, dynamically concatenating them as `.` (e.g. `light.person` or `stock.widget`) to yield the namespace-protected schema key. Therefore, any schema that participates in polymorphic discrimination MUST explicitly define its discriminator properties natively inside its `properties` block. However, to stay DRY and maintain flexible APIs, you **DO NOT** need to hardcode `const` values, nor should you add them to your `required` array. The Punc engine treats `type` and `kind` as **magic properties**. @@ -91,6 +93,7 @@ Punc completely abandons the standard JSON Schema `$ref` keyword. Instead, it ov * **Implicit Keyword Shadowing**: Unlike standard JSON Schema inheritance, local property definitions natively override and shadow inherited properties. * **Primitive Array Shorthand (Optionality)**: The `type` array syntax is heavily optimized for nullable fields. Defining `"type": ["budget", "null"]` natively builds a nullable strict, generating `Budget? budget;` in Dart. You can freely mix primitives like `["string", "number", "null"]`. * **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. +* **Dynamic Type Bindings (`"$sibling.[suffix]"`)**: If a `type` string begins with a `$` (e.g., `"type": "$kind.filter"`), the JSPG engine treats it as a Dynamic Pointer. During compile time, it safely defers boundary checks. During runtime validation, the engine dynamically reads the literal string value of the referenced sibling property (`kind`) on the *current parent JSON object*, evaluates the substitution (e.g., `"person.filter"`), and instantly routes execution to that schema in $O(1)$ time. This enables incredibly powerful dynamic JSONB shapes (like a generic `filter` column inside a `search` table) without forcing downstream code generators to build unmaintainable unions. ### Polymorphism (`family` and `oneOf`) 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. diff --git a/fixtures/dynamicType.json b/fixtures/dynamicType.json new file mode 100644 index 0000000..0419cab --- /dev/null +++ b/fixtures/dynamicType.json @@ -0,0 +1,155 @@ +[ + { + "description": "Dynamic type binding ($sibling.suffix) validation", + "database": { + "types": [ + { + "name": "person", + "schemas": { + "person.filter": { + "properties": { + "age": { + "type": "integer" + } + } + } + } + }, + { + "name": "widget", + "schemas": { + "widget.filter": { + "properties": { + "weight": { + "type": "integer" + } + } + } + } + }, + { + "name": "search", + "schemas": { + "search": { + "properties": { + "kind": { + "type": "string" + }, + "filter": { + "type": "$kind.filter" + } + } + } + } + } + ] + }, + "tests": [ + { + "description": "Valid person filter payload", + "data": { + "kind": "person", + "filter": { + "age": 30 + } + }, + "schema_id": "search", + "action": "validate", + "expect": { + "success": true + } + }, + { + "description": "Invalid person filter payload (fails constraint)", + "data": { + "kind": "person", + "filter": { + "age": "thirty" + } + }, + "schema_id": "search", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "INVALID_TYPE", + "details": { + "path": "filter/age" + } + } + ] + } + }, + { + "description": "Valid widget filter payload", + "data": { + "kind": "widget", + "filter": { + "weight": 500 + } + }, + "schema_id": "search", + "action": "validate", + "expect": { + "success": true + } + }, + { + "description": "Fails resolution if kind doesn't match an existing schema", + "data": { + "kind": "unknown", + "filter": { + "weight": 500 + } + }, + "schema_id": "search", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "DYNAMIC_TYPE_RESOLUTION_FAILED", + "details": { + "path": "filter" + } + }, + { + "code": "STRICT_PROPERTY_VIOLATION", + "details": { + "path": "filter/weight" + } + } + ] + } + }, + { + "description": "Fails resolution if discriminator is missing", + "data": { + "filter": { + "weight": 500 + } + }, + "schema_id": "search", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "DYNAMIC_TYPE_RESOLUTION_FAILED", + "details": { + "path": "filter" + } + }, + { + "code": "STRICT_PROPERTY_VIOLATION", + "details": { + "path": "filter/weight" + } + } + ] + } + } + ] + } +] diff --git a/fixtures/filter.json b/fixtures/filter.json index c15a502..2a2aa72 100644 --- a/fixtures/filter.json +++ b/fixtures/filter.json @@ -107,17 +107,17 @@ "search": { "type": "object", "properties": { + "kind": { + "type": "string" + }, "name": { "type": "string" }, "filter": { - "type": "filter" + "type": "$kind.filter" } } }, - "filter": { - "type": "object" - }, "condition": { "type": "object", "properties": { @@ -172,7 +172,7 @@ "schemas": { "person": {}, "person.filter": { - "type": "filter", + "type": "object", "compiledPropertyNames": [ "$and", "$or", @@ -244,7 +244,7 @@ }, "address": {}, "address.filter": { - "type": "filter", + "type": "object", "compiledPropertyNames": [ "$and", "$or", @@ -287,18 +287,18 @@ } } }, - "filter": {}, "condition": {}, "string.condition": {}, "integer.condition": {}, "date.condition": {}, "search": {}, "search.filter": { - "type": "filter", + "type": "object", "compiledPropertyNames": [ "$and", "$or", "filter", + "kind", "name" ], "properties": { @@ -312,6 +312,7 @@ "$and", "$or", "filter", + "kind", "name" ], "type": "search.filter" @@ -327,6 +328,7 @@ "$and", "$or", "filter", + "kind", "name" ], "type": "search.filter" @@ -334,7 +336,13 @@ }, "filter": { "type": [ - "filter.filter", + "$kind.filter.filter", + "null" + ] + }, + "kind": { + "type": [ + "string.condition", "null" ] }, diff --git a/log.txt b/log.txt deleted file mode 100644 index 794a356..0000000 --- a/log.txt +++ /dev/null @@ -1,23 +0,0 @@ - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s - Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386) - -running 1 test -test tests::test_library_api ... FAILED - -failures: - ----- tests::test_library_api stdout ---- - -thread 'tests::test_library_api' (110325727) panicked at src/tests/mod.rs:86:3: -assertion `left == right` failed - left: Object {"response": Object {"enums": Object {}, "puncs": Object {}, "relations": Object {"fk_test_target": Object {"constraint": String("fk_test_target"), "destination_columns": Array [String("id")], "destination_type": String("target_schema"), "id": String("11111111-1111-1111-1111-111111111111"), "prefix": String("target"), "source_columns": Array [String("target_id")], "source_type": String("source_schema"), "type": String("relation")}}, "types": Object {"source_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("source_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("source_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"source_schema": Object {"compiledEdges": Object {"target": Object {"constraint": String("fk_test_target"), "forward": Bool(true)}}, "compiledPropertyNames": Array [String("name"), String("target"), String("type")], "properties": Object {"name": Object {"type": String("string")}, "target": Object {"compiledPropertyNames": Array [String("value")], "type": String("target_schema")}, "type": Object {"type": String("string")}}, "required": Array [String("name")], "type": String("object")}, "source_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "properties": Object {"$and": Object {"items": Object {"type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "name": Object {"type": Array [String("string.condition"), String("null")]}, "target": Object {"type": Array [String("target_schema.filter"), String("null")]}, "type": Object {"type": Array [String("string.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("source_schema")]}, "target_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("target_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("target_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"target_schema": Object {"compiledPropertyNames": Array [String("value")], "properties": Object {"value": Object {"type": String("number")}}, "type": String("object")}, "target_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "properties": Object {"$and": Object {"items": Object {"type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "value": Object {"type": Array [String("number.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("target_schema")]}}}, "type": String("drop")} - right: Object {"response": Object {"enums": Object {}, "puncs": Object {}, "relations": Object {"fk_test_target": Object {"constraint": String("fk_test_target"), "destination_columns": Array [String("id")], "destination_type": String("target_schema"), "id": String("11111111-1111-1111-1111-111111111111"), "prefix": String("target"), "source_columns": Array [String("target_id")], "source_type": String("source_schema"), "type": String("relation")}}, "types": Object {"source_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("source_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("source_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"source_schema": Object {"compiledEdges": Object {"target": Object {"constraint": String("fk_test_target"), "forward": Bool(true)}}, "compiledPropertyNames": Array [String("name"), String("target"), String("type")], "properties": Object {"name": Object {"type": String("string")}, "target": Object {"compiledPropertyNames": Array [String("value")], "type": String("target_schema")}, "type": Object {"type": String("string")}}, "required": Array [String("name")], "type": String("object")}, "source_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "properties": Object {"$and": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "name": Object {"type": Array [String("string.condition"), String("null")]}, "target": Object {"type": Array [String("target_schema.filter"), String("null")]}, "type": Object {"type": Array [String("string.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("source_schema")]}, "target_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("target_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("target_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"target_schema": Object {"compiledPropertyNames": Array [String("value")], "properties": Object {"value": Object {"type": String("number")}}, "type": String("object")}, "target_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "properties": Object {"$and": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "value": Object {"type": Array [String("number.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("target_schema")]}}}, "type": String("drop")} -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - tests::test_library_api - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1357 filtered out; finished in 0.00s - -error: test failed, to rerun pass `--lib` diff --git a/log_test.txt b/log_test.txt deleted file mode 100644 index bdd9215..0000000 --- a/log_test.txt +++ /dev/null @@ -1,23 +0,0 @@ - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.35s - Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386) - -running 1 test -test tests::test_library_api ... FAILED - -failures: - ----- tests::test_library_api stdout ---- - -thread 'tests::test_library_api' (110334696) panicked at src/tests/mod.rs:86:3: -assertion `left == right` failed - left: Object {"response": Object {"enums": Object {}, "puncs": Object {}, "relations": Object {"fk_test_target": Object {"constraint": String("fk_test_target"), "destination_columns": Array [String("id")], "destination_type": String("target_schema"), "id": String("11111111-1111-1111-1111-111111111111"), "prefix": String("target"), "source_columns": Array [String("target_id")], "source_type": String("source_schema"), "type": String("relation")}}, "types": Object {"source_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("source_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("source_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"source_schema": Object {"compiledEdges": Object {"target": Object {"constraint": String("fk_test_target"), "forward": Bool(true)}}, "compiledPropertyNames": Array [String("name"), String("target"), String("type")], "properties": Object {"name": Object {"type": String("string")}, "target": Object {"compiledPropertyNames": Array [String("value")], "type": String("target_schema")}, "type": Object {"type": String("string")}}, "required": Array [String("name")], "type": String("object")}, "source_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "properties": Object {"$and": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "name": Object {"type": Array [String("string.condition"), String("null")]}, "target": Object {"type": Array [String("target_schema.filter"), String("null")]}, "type": Object {"type": Array [String("string.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("source_schema")]}, "target_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("target_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("target_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"target_schema": Object {"compiledPropertyNames": Array [String("value")], "properties": Object {"value": Object {"type": String("number")}}, "type": String("object")}, "target_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "properties": Object {"$and": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "value": Object {"type": Array [String("number.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("target_schema")]}}}, "type": String("drop")} - right: Object {"response": Object {"enums": Object {}, "puncs": Object {}, "relations": Object {"fk_test_target": Object {"constraint": String("fk_test_target"), "destination_columns": Array [String("id")], "destination_type": String("target_schema"), "id": String("11111111-1111-1111-1111-111111111111"), "prefix": String("target"), "source_columns": Array [String("target_id")], "source_type": String("source_schema"), "type": String("relation")}}, "types": Object {"source_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("source_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("source_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"source_schema": Object {"compiledEdges": Object {"target": Object {"constraint": String("fk_test_target"), "forward": Bool(true)}}, "compiledPropertyNames": Array [String("name"), String("target"), String("type")], "properties": Object {"name": Object {"type": String("string")}, "target": Object {"compiledPropertyNames": Array [String("value")], "type": String("target_schema")}, "type": Object {"type": String("string")}}, "required": Array [String("name")], "type": String("object")}, "source_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("name"), String("target"), String("type")], "properties": Object {"$and": Object {"items": Object {"type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"type": String("source_schema.filter")}, "type": Array [String("array"), String("null")]}, "name": Object {"type": Array [String("string.condition"), String("null")]}, "target": Object {"type": Array [String("target_schema.filter"), String("null")]}, "type": Object {"type": Array [String("string.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("source_schema")]}, "target_schema": Object {"default_fields": Array [], "field_types": Null, "fields": Array [], "grouped_fields": Null, "hierarchy": Array [String("target_schema"), String("entity")], "historical": Bool(false), "id": String(""), "longevity": Null, "lookup_fields": Array [], "module": String(""), "name": String("target_schema"), "notify": Bool(false), "null_fields": Array [], "ownable": Bool(false), "relationship": Bool(false), "schemas": Object {"target_schema": Object {"compiledPropertyNames": Array [String("value")], "properties": Object {"value": Object {"type": String("number")}}, "type": String("object")}, "target_schema.filter": Object {"compiledPropertyNames": Array [String("$and"), String("$or"), String("value")], "properties": Object {"$and": Object {"items": Object {"type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "$or": Object {"items": Object {"type": String("target_schema.filter")}, "type": Array [String("array"), String("null")]}, "value": Object {"type": Array [String("number.condition"), String("null")]}}, "type": String("filter")}}, "sensitive": Bool(false), "source": String(""), "type": String(""), "variations": Array [String("target_schema")]}}}, "type": String("drop")} -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - tests::test_library_api - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1357 filtered out; finished in 0.00s - -error: test failed, to rerun pass `--lib` diff --git a/src/database/compile/filter.rs b/src/database/compile/filter.rs index 67befaa..388c286 100644 --- a/src/database/compile/filter.rs +++ b/src/database/compile/filter.rs @@ -74,8 +74,8 @@ impl Schema { ); let mut wrapper_obj = SchemaObject::default(); - // Conceptually link this directly into the STI lineage of the base `filter` object - wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("filter".to_string())); + // Filters are just plain objects containing conditions, no inheritance required + wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("object".to_string())); wrapper_obj.properties = Some(filter_props); return Some(Schema { diff --git a/src/database/compile/mod.rs b/src/database/compile/mod.rs index 35ed18e..5a5c3cf 100644 --- a/src/database/compile/mod.rs +++ b/src/database/compile/mod.rs @@ -51,8 +51,8 @@ impl Schema { // 1. Resolve INHERITANCE dependencies first if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { - if !crate::database::object::is_primitive_type(t) { - if let Some(parent) = db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, t) { + if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') { + if let Some(parent) = db.schemas.get(t).cloned() { parent.as_ref().compile(db, t, t.clone(), errors); if let Some(p_props) = parent.obj.compiled_properties.get() { props.extend(p_props.clone()); @@ -85,8 +85,8 @@ impl Schema { } for t in types { - if !crate::database::object::is_primitive_type(t) { - if let Some(parent) = db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, t) { + if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') { + if let Some(parent) = db.schemas.get(t).cloned() { parent.as_ref().compile(db, t, t.clone(), errors); } } diff --git a/src/database/compile/polymorphism.rs b/src/database/compile/polymorphism.rs index cfb51a5..9da88aa 100644 --- a/src/database/compile/polymorphism.rs +++ b/src/database/compile/polymorphism.rs @@ -12,7 +12,10 @@ impl Schema { let mut strategy = String::new(); if let Some(family) = &self.obj.family { + // Formalize the . topology + // family_base extracts the 'Base' (e.g. 'widget', 'person') let family_base = family.split('.').next_back().unwrap_or(family).to_string(); + // family_prefix extracts the 'Variant' (e.g. 'stock', 'light') let family_prefix = family .strip_suffix(&family_base) .unwrap_or("") @@ -29,7 +32,7 @@ impl Schema { format!("{}.{}", family_prefix, var) }; - if db.get_scoped_schema(crate::database::realm::SchemaRealm::Type, &target_id).is_some() { + if db.schemas.get(&target_id).is_some() { options.insert(var.to_string(), (None, Some(target_id))); } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 6953e7c..f27332d 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -6,7 +6,6 @@ pub mod formats; pub mod object; pub mod page; pub mod punc; -pub mod realm; pub mod relation; pub mod schema; pub mod r#type; @@ -21,7 +20,6 @@ use executors::pgrx::SpiExecutor; use executors::mock::MockExecutor; use punc::Punc; -use realm::SchemaRealm; use relation::Relation; use schema::Schema; use serde_json::Value; @@ -36,6 +34,8 @@ pub struct Database { pub puncs: HashMap, pub relations: HashMap, #[serde(skip)] + pub schemas: HashMap>, + #[serde(skip)] pub executor: Box, } @@ -46,6 +46,7 @@ impl Database { types: HashMap::new(), relations: HashMap::new(), puncs: HashMap::new(), + schemas: HashMap::new(), #[cfg(not(test))] executor: Box::new(SpiExecutor::new()), #[cfg(test)] @@ -194,22 +195,28 @@ impl Database { // Formally evaluate properties with strict 3-pass Ordered Graph execution natively for (_, enum_def) in &self.enums { - for (schema_id, schema_arc) in &enum_def.schemas { - let root_id = schema_id.split('/').next().unwrap_or(schema_id); - schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors); - } + for (schema_id, schema_arc) in &enum_def.schemas { + let root_id = schema_id.split('/').next().unwrap_or(schema_id); + schema_arc + .as_ref() + .compile(self, root_id, schema_id.clone(), errors); + } } for (_, type_def) in &self.types { - for (schema_id, schema_arc) in &type_def.schemas { - let root_id = schema_id.split('/').next().unwrap_or(schema_id); - schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors); - } + for (schema_id, schema_arc) in &type_def.schemas { + let root_id = schema_id.split('/').next().unwrap_or(schema_id); + schema_arc + .as_ref() + .compile(self, root_id, schema_id.clone(), errors); + } } for (_, punc_def) in &self.puncs { - for (schema_id, schema_arc) in &punc_def.schemas { - let root_id = schema_id.split('/').next().unwrap_or(schema_id); - schema_arc.as_ref().compile(self, root_id, schema_id.clone(), errors); - } + for (schema_id, schema_arc) in &punc_def.schemas { + let root_id = schema_id.split('/').next().unwrap_or(schema_id); + schema_arc + .as_ref() + .compile(self, root_id, schema_id.clone(), errors); + } } // Phase 2: Synthesize Composed Filter References @@ -234,6 +241,7 @@ impl Database { let mut filter_ids = Vec::new(); for (type_name, id, filter_arc) in filter_schemas { filter_ids.push((type_name.clone(), id.clone())); + self.schemas.insert(id.clone(), filter_arc.clone()); if let Some(t) = self.types.get_mut(&type_name) { t.schemas.insert(id, filter_arc); } @@ -241,7 +249,12 @@ impl Database { // Now actively compile the newly injected filters to lock all nested compose references natively for (type_name, id) in filter_ids { - if let Some(filter_arc) = self.types.get(&type_name).and_then(|t| t.schemas.get(&id)).cloned() { + if let Some(filter_arc) = self + .types + .get(&type_name) + .and_then(|t| t.schemas.get(&id)) + .cloned() + { let root_id = id.split('/').next().unwrap_or(&id); filter_arc .as_ref() @@ -259,6 +272,7 @@ impl Database { // Validate every node recursively via string filters natively! for (type_name, type_def) in &self.types { for (id, schema_arc) in &type_def.schemas { + self.schemas.insert(id.clone(), Arc::clone(schema_arc)); let mut local_insert = Vec::new(); crate::database::schema::Schema::collect_schemas( schema_arc, @@ -275,6 +289,7 @@ impl Database { for (punc_name, punc_def) in &self.puncs { for (id, schema_arc) in &punc_def.schemas { + self.schemas.insert(id.clone(), Arc::clone(schema_arc)); let mut local_insert = Vec::new(); crate::database::schema::Schema::collect_schemas( schema_arc, @@ -291,6 +306,7 @@ impl Database { for (enum_name, enum_def) in &self.enums { for (id, schema_arc) in &enum_def.schemas { + self.schemas.insert(id.clone(), Arc::clone(schema_arc)); let mut local_insert = Vec::new(); crate::database::schema::Schema::collect_schemas( schema_arc, @@ -305,57 +321,27 @@ impl Database { } } - // Apply local scopes + // Apply local scopes and global schema map for (origin_name, id, schema_arc) in type_insert { + self.schemas.insert(id.clone(), schema_arc.clone()); if let Some(t) = self.types.get_mut(&origin_name) { t.schemas.insert(id, schema_arc); } } for (origin_name, id, schema_arc) in punc_insert { + self.schemas.insert(id.clone(), schema_arc.clone()); if let Some(p) = self.puncs.get_mut(&origin_name) { p.schemas.insert(id, schema_arc); } } for (origin_name, id, schema_arc) in enum_insert { + self.schemas.insert(id.clone(), schema_arc.clone()); if let Some(e) = self.enums.get_mut(&origin_name) { e.schemas.insert(id, schema_arc); } } } - - pub fn get_scoped_schema(&self, realm: SchemaRealm, schema_id: &str) -> Option> { - // Punc Realm natively maps mathematically to `.request` and `.response` shapes - if realm == SchemaRealm::Punc { - if schema_id.ends_with(".request") || schema_id.ends_with(".response") { - let punc_name = schema_id - .trim_end_matches(".request") - .trim_end_matches(".response"); - return self.puncs.get(punc_name).and_then(|p| p.schemas.get(schema_id).cloned()); - } - } - let clean_id = schema_id.trim_end_matches(".filter"); - let root_id = clean_id.split('/').next().unwrap_or(clean_id); - let base_name = root_id.split('.').next_back().unwrap_or(root_id); - - // Puncs and Types can lookup Table boundaries - if realm == SchemaRealm::Type || realm == SchemaRealm::Punc { - if let Some(type_def) = self.types.get(base_name) { - if let Some(schema) = type_def.schemas.get(schema_id) { - return Some(schema.clone()); - } - } - } - - // All realms can intrinsically look up enumerations - if let Some(enum_def) = self.enums.get(base_name) { - if let Some(schema) = enum_def.schemas.get(schema_id) { - return Some(schema.clone()); - } - } - - None - } /// Inspects the Postgres pg_constraint relations catalog to securely identify /// the precise Foreign Key connecting a parent and child hierarchy path. diff --git a/src/database/realm.rs b/src/database/realm.rs deleted file mode 100644 index 4ec6ac7..0000000 --- a/src/database/realm.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SchemaRealm { - Enum, - Type, - Punc, -} diff --git a/src/database/schema.rs b/src/database/schema.rs index bf96551..0e78705 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -22,6 +22,27 @@ impl std::ops::DerefMut for Schema { } } +impl Schema { + /// Returns true if the schema acts purely as a type pointer (composition without overriding constraints) + pub fn is_proxy(&self) -> bool { + self.obj.properties.is_none() + && self.obj.pattern_properties.is_none() + && self.obj.additional_properties.is_none() + && self.obj.required.is_none() + && self.obj.dependencies.is_none() + && self.obj.items.is_none() + && self.obj.prefix_items.is_none() + && self.obj.contains.is_none() + && self.obj.format.is_none() + && self.obj.enum_.is_none() + && self.obj.const_.is_none() + && self.obj.cases.is_none() + && self.obj.one_of.is_none() + && self.obj.not.is_none() + && self.obj.family.is_none() + } +} + impl<'de> Deserialize<'de> for Schema { fn deserialize(deserializer: D) -> Result where diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 115e1eb..867997e 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -4,7 +4,6 @@ pub mod cache; use crate::database::Database; -use crate::database::realm::SchemaRealm; use crate::database::r#type::Type; use serde_json::Value; use std::sync::Arc; @@ -25,7 +24,7 @@ impl Merger { pub fn merge(&self, schema_id: &str, data: Value) -> crate::drop::Drop { let mut notifications_queue = Vec::new(); - let target_schema = match self.db.get_scoped_schema(SchemaRealm::Type, schema_id) { + let target_schema = match self.db.schemas.get(schema_id) { Some(s) => Arc::clone(&s), None => { return crate::drop::Drop::with_errors(vec![crate::drop::Error { @@ -146,7 +145,7 @@ impl Merger { if let Some((idx_opt, target_id_opt)) = options.get(v) { if let Some(target_id) = target_id_opt { if let Some(target_schema) = - self.db.get_scoped_schema(SchemaRealm::Type, target_id) + self.db.schemas.get(target_id) { schema = target_schema.clone(); } else { diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index dd9413b..de25481 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -1,5 +1,4 @@ use crate::database::Database; -use crate::database::realm::SchemaRealm; use std::sync::Arc; pub struct Compiler<'a> { @@ -25,15 +24,11 @@ pub struct Node<'a> { impl<'a> Compiler<'a> { /// Compiles a JSON schema into a nested PostgreSQL query returning JSONB pub fn compile(&self, schema_id: &str, filter_keys: &[String]) -> Result { - let realm = if schema_id.ends_with(".request") || schema_id.ends_with(".response") { - SchemaRealm::Punc - } else { - SchemaRealm::Type - }; - let schema = self .db - .get_scoped_schema(realm, schema_id) + .schemas + .get(schema_id) + .cloned() .ok_or_else(|| format!("Schema not found: {}", schema_id))?; let target_schema = schema; @@ -157,7 +152,7 @@ impl<'a> Compiler<'a> { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ { if !crate::database::object::is_primitive_type(t) { // If it's just an ad-hoc struct ref, we should resolve it - if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, t) { + if let Some(target_schema) = self.db.schemas.get(t).cloned() { let mut ref_node = node.clone(); ref_node.schema = target_schema.clone(); ref_node.schema_id = Some(t.clone()); @@ -312,7 +307,7 @@ impl<'a> Compiler<'a> { for (disc_val, (idx_opt, target_id_opt)) in options { if let Some(target_id) = target_id_opt { - if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, target_id) { + if let Some(target_schema) = self.db.schemas.get(target_id).cloned() { let mut child_node = node.clone(); child_node.schema = target_schema.clone(); child_node.schema_id = Some(target_id.clone()); diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 8899f3c..316dd12 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -1247,6 +1247,36 @@ fn test_const_17_1() { crate::tests::runner::run_test_case(&path, 17, 1).unwrap(); } +#[test] +fn test_dynamic_type_0_0() { + let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 0).unwrap(); +} + +#[test] +fn test_dynamic_type_0_1() { + let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 1).unwrap(); +} + +#[test] +fn test_dynamic_type_0_2() { + let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 2).unwrap(); +} + +#[test] +fn test_dynamic_type_0_3() { + let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 3).unwrap(); +} + +#[test] +fn test_dynamic_type_0_4() { + let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 4).unwrap(); +} + #[test] fn test_property_names_0_0() { let path = format!("{}/fixtures/propertyNames.json", env!("CARGO_MANIFEST_DIR")); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 6d85e4b..093756b 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -160,7 +160,7 @@ fn test_library_api() { "target": { "type": ["target_schema.filter", "null"] }, "type": { "type": ["string.condition", "null"] } }, - "type": "filter" + "type": "object" } }, "sensitive": false, @@ -211,7 +211,7 @@ fn test_library_api() { }, "value": { "type": ["number.condition", "null"] } }, - "type": "filter" + "type": "object" } }, "sensitive": false, diff --git a/src/tests/types/expect/schema.rs b/src/tests/types/expect/schema.rs index d60ab15..e130c34 100644 --- a/src/tests/types/expect/schema.rs +++ b/src/tests/types/expect/schema.rs @@ -35,12 +35,7 @@ impl Expect { if expected_val.is_object() && expected_val.as_object().unwrap().is_empty() { continue; // A `{}` means we just wanted to test it was collected/promoted, skip deep match } - let schema_realm = if key.ends_with(".request") || key.ends_with(".response") { - crate::database::realm::SchemaRealm::Punc - } else { - crate::database::realm::SchemaRealm::Type - }; - let actual_ast = db.get_scoped_schema(schema_realm, key).unwrap(); + let actual_ast = db.schemas.get(key).cloned().unwrap(); let actual_val = serde_json::to_value(actual_ast).unwrap(); if actual_val != *expected_val { diff --git a/src/validator/context.rs b/src/validator/context.rs index 0efe036..6133ccf 100644 --- a/src/validator/context.rs +++ b/src/validator/context.rs @@ -15,6 +15,7 @@ pub struct ValidationContext<'a> { pub extensible: bool, pub reporter: bool, pub overrides: HashSet, + pub parent: Option<&'a serde_json::Value>, } impl<'a> ValidationContext<'a> { @@ -38,6 +39,7 @@ impl<'a> ValidationContext<'a> { extensible: effective_extensible, reporter, overrides, + parent: None, } } @@ -57,6 +59,7 @@ impl<'a> ValidationContext<'a> { overrides: HashSet, extensible: bool, reporter: bool, + parent_instance: Option<&'a serde_json::Value>, ) -> Self { let effective_extensible = schema.extensible.unwrap_or(extensible); @@ -70,6 +73,7 @@ impl<'a> ValidationContext<'a> { extensible: effective_extensible, reporter, overrides, + parent: parent_instance, } } @@ -81,6 +85,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), self.extensible, reporter, + self.parent, ) } diff --git a/src/validator/mod.rs b/src/validator/mod.rs index dc13a1b..87b2efd 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -10,7 +10,6 @@ pub use error::ValidationError; pub use result::ValidationResult; use crate::database::Database; -use crate::database::realm::SchemaRealm; use crate::validator::rules::util::is_integer; use serde_json::Value; use std::sync::Arc; @@ -43,11 +42,7 @@ impl Validator { } pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop { - let schema_opt = if schema_id.ends_with(".request") || schema_id.ends_with(".response") { - self.db.get_scoped_schema(SchemaRealm::Punc, schema_id) - } else { - self.db.get_scoped_schema(SchemaRealm::Type, schema_id) - }; + let schema_opt = self.db.schemas.get(schema_id); if let Some(schema) = schema_opt { let ctx = ValidationContext::new( diff --git a/src/validator/rules/array.rs b/src/validator/rules/array.rs index 010068a..f3d73c8 100644 --- a/src/validator/rules/array.rs +++ b/src/validator/rules/array.rs @@ -57,6 +57,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), self.extensible, false, + Some(self.instance), ); let check = derived.validate()?; @@ -108,6 +109,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), self.extensible, false, + Some(self.instance), ); let item_res = derived.validate()?; result.merge(item_res); @@ -137,6 +139,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), self.extensible, false, + Some(self.instance), ); let item_res = derived.validate()?; result.merge(item_res); diff --git a/src/validator/rules/mod.rs b/src/validator/rules/mod.rs index c78a744..33b0c66 100644 --- a/src/validator/rules/mod.rs +++ b/src/validator/rules/mod.rs @@ -12,6 +12,7 @@ pub mod numeric; pub mod object; pub mod polymorphism; pub mod string; +pub mod r#type; pub mod util; impl<'a> ValidationContext<'a> { @@ -28,7 +29,7 @@ impl<'a> ValidationContext<'a> { if !self.validate_family(&mut result)? { return Ok(result); } - if !self.validate_type_inheritance(&mut result)? { + if !self.validate_type(&mut result)? { return Ok(result); } diff --git a/src/validator/rules/object.rs b/src/validator/rules/object.rs index 159a307..779d7b9 100644 --- a/src/validator/rules/object.rs +++ b/src/validator/rules/object.rs @@ -191,6 +191,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), next_extensible, false, + Some(self.instance), ); let item_res = derived.validate()?; @@ -220,6 +221,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), next_extensible, false, + Some(self.instance), ); let item_res = derived.validate()?; result.merge(item_res); @@ -265,6 +267,7 @@ impl<'a> ValidationContext<'a> { HashSet::new(), next_extensible, false, + Some(self.instance), ); let item_res = derived.validate()?; result.merge(item_res); diff --git a/src/validator/rules/polymorphism.rs b/src/validator/rules/polymorphism.rs index 096d1f2..d45dc69 100644 --- a/src/validator/rules/polymorphism.rs +++ b/src/validator/rules/polymorphism.rs @@ -1,7 +1,6 @@ use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; -use crate::database::realm::SchemaRealm; impl<'a> ValidationContext<'a> { pub(crate) fn validate_family( @@ -100,8 +99,8 @@ impl<'a> ValidationContext<'a> { if let Some(val) = instance_val { if let Some((idx_opt, target_id_opt)) = options.get(&val) { if let Some(target_id) = target_id_opt { - if let Some(target_schema) = self.db.get_scoped_schema(SchemaRealm::Type, target_id) { - let derived = self.derive_for_schema(&target_schema, false); + 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); @@ -177,78 +176,4 @@ impl<'a> ValidationContext<'a> { return Ok(false); } } - - pub(crate) fn validate_type_inheritance( - &self, - result: &mut ValidationResult, - ) -> Result { - // Core inheritance logic replaces legacy routing - let payload_primitive = match self.instance { - serde_json::Value::Null => "null", - serde_json::Value::Bool(_) => "boolean", - serde_json::Value::Number(n) => { - if n.is_i64() || n.is_u64() { - "integer" - } else { - "number" - } - } - serde_json::Value::String(_) => "string", - serde_json::Value::Array(_) => "array", - serde_json::Value::Object(_) => "object", - }; - - let mut custom_types = Vec::new(); - match &self.schema.type_ { - Some(crate::database::object::SchemaTypeOrArray::Single(t)) => { - if !crate::database::object::is_primitive_type(t) { - custom_types.push(t.clone()); - } - } - Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => { - if arr.contains(&payload_primitive.to_string()) - || (payload_primitive == "integer" && arr.contains(&"number".to_string())) - { - // It natively matched a primitive in the array options, skip forcing custom proxy fallback - } else { - for t in arr { - if !crate::database::object::is_primitive_type(t) { - custom_types.push(t.clone()); - } - } - } - } - None => {} - } - - for t in custom_types { - if let Some(global_schema) = self.db.get_scoped_schema(SchemaRealm::Type, &t) { - let mut new_overrides = self.overrides.clone(); - if let Some(props) = &self.schema.properties { - new_overrides.extend(props.keys().map(|k| k.to_string())); - } - - let mut shadow = self.derive( - &global_schema, - self.instance, - &self.path, - new_overrides, - self.extensible, - true, // Reporter mode - ); - shadow.root = &global_schema; - result.merge(shadow.validate()?); - } else { - result.errors.push(ValidationError { - code: "INHERITANCE_RESOLUTION_FAILED".to_string(), - message: format!( - "Inherited entity pointer '{}' was not found in schema registry", - t - ), - path: self.path.to_string(), - }); - } - } - Ok(true) - } } diff --git a/src/validator/rules/type.rs b/src/validator/rules/type.rs new file mode 100644 index 0000000..37a43f6 --- /dev/null +++ b/src/validator/rules/type.rs @@ -0,0 +1,138 @@ +use crate::validator::context::ValidationContext; +use crate::validator::error::ValidationError; +use crate::validator::result::ValidationResult; + +impl<'a> ValidationContext<'a> { + pub(crate) fn validate_type( + &self, + result: &mut ValidationResult, + ) -> Result { + let payload_primitive = match self.instance { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "boolean", + serde_json::Value::Number(n) => { + if n.is_i64() || n.is_u64() { + "integer" + } else { + "number" + } + } + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + }; + + let mut custom_types = Vec::new(); + match &self.schema.type_ { + Some(crate::database::object::SchemaTypeOrArray::Single(t)) => { + if !crate::database::object::is_primitive_type(t) { + custom_types.push(t.clone()); + } + } + Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => { + if arr.contains(&payload_primitive.to_string()) + || (payload_primitive == "integer" && arr.contains(&"number".to_string())) + { + // It natively matched a primitive in the array options, skip forcing custom proxy fallback + } else { + for t in arr { + if !crate::database::object::is_primitive_type(t) { + custom_types.push(t.clone()); + } + } + } + } + None => {} + } + + for t in custom_types { + let mut target_id = t.clone(); + + // 1. DYNAMIC TYPE (Composition) + if t.starts_with('$') { + let parts: Vec<&str> = t.split('.').collect(); + let var_name = &parts[0][1..]; // Remove the $ prefix + let suffix = if parts.len() > 1 { + format!(".{}", parts[1..].join(".")) + } else { + String::new() + }; + + let mut resolved = false; + if let Some(parent) = self.parent { + if let Some(obj) = parent.as_object() { + if let Some(val) = obj.get(var_name) { + if let Some(str_val) = val.as_str() { + target_id = format!("{}{}", str_val, suffix); + resolved = true; + } + } + } + } + + if !resolved { + result.errors.push(ValidationError { + code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), + message: format!( + "Dynamic type pointer '{}' could not resolve discriminator property '{}' on parent instance", + t, var_name + ), + path: self.path.to_string(), + }); + continue; + } + } + + // 2. Fetch and apply + if let Some(global_schema) = self.db.schemas.get(&target_id) { + let mut new_overrides = self.overrides.clone(); + if let Some(props) = &self.schema.properties { + new_overrides.extend(props.keys().map(|k| k.to_string())); + } + + let mut shadow = self.derive( + &global_schema, + self.instance, + &self.path, + new_overrides, + self.extensible, + true, // Reporter mode + self.parent, + ); + shadow.root = &global_schema; + result.merge(shadow.validate()?); + } else { + // 3. Error handling pathways + if t.starts_with('$') { + result.errors.push(ValidationError { + code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), + message: format!( + "Resolved dynamic type pointer '{}' was not found in schema registry", + target_id + ), + path: self.path.to_string(), + }); + } else if self.schema.is_proxy() { + result.errors.push(ValidationError { + code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(), + message: format!( + "Composed proxy entity pointer '{}' was not found in schema registry", + target_id + ), + path: self.path.to_string(), + }); + } else { + result.errors.push(ValidationError { + code: "INHERITANCE_RESOLUTION_FAILED".to_string(), + message: format!( + "Inherited entity pointer '{}' was not found in schema registry", + target_id + ), + path: self.path.to_string(), + }); + } + } + } + Ok(true) + } +} diff --git a/test_failures.log b/test_failures.log deleted file mode 100644 index cfe234b..0000000 --- a/test_failures.log +++ /dev/null @@ -1,81 +0,0 @@ - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s - Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386) - -running 11 tests -test tests::test_minimum_0_2 ... ok -test tests::test_minimum_1_4 ... ok -test tests::test_minimum_1_0 ... FAILED -test tests::test_minimum_1_1 ... FAILED -test tests::test_minimum_0_3 ... FAILED -test tests::test_minimum_1_5 ... ok -test tests::test_minimum_1_3 ... FAILED -test tests::test_minimum_0_0 ... FAILED -test tests::test_minimum_0_1 ... FAILED -test tests::test_minimum_1_2 ... FAILED -test tests::test_minimum_1_6 ... FAILED - -failures: - ----- tests::test_minimum_1_0 stdout ---- -TEST VALIDATE ERROR FOR 'negative above the minimum is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_1_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_1_0' (110318318) panicked at src/tests/fixtures.rs:3503:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation with signed integer] Validate Test 'negative above the minimum is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_1_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - ----- tests::test_minimum_1_1 stdout ---- -TEST VALIDATE ERROR FOR 'positive above the minimum is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_1_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_1_1' (110318319) panicked at src/tests/fixtures.rs:3509:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation with signed integer] Validate Test 'positive above the minimum is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_1_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_0_3 stdout ---- -TEST VALIDATE ERROR FOR 'ignores non-numbers': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_0_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_0_3' (110318317) panicked at src/tests/fixtures.rs:3497:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation] Validate Test 'ignores non-numbers' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_0_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_1_3 stdout ---- -TEST VALIDATE ERROR FOR 'boundary point with float is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_1_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_1_3' (110318321) panicked at src/tests/fixtures.rs:3521:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation with signed integer] Validate Test 'boundary point with float is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_1_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_0_0 stdout ---- -TEST VALIDATE ERROR FOR 'above the minimum is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_0_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_0_0' (110318314) panicked at src/tests/fixtures.rs:3479:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation] Validate Test 'above the minimum is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_0_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_0_1 stdout ---- -TEST VALIDATE ERROR FOR 'boundary point is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_0_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_0_1' (110318315) panicked at src/tests/fixtures.rs:3485:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation] Validate Test 'boundary point is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_0_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_1_2 stdout ---- -TEST VALIDATE ERROR FOR 'boundary point is valid': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_1_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_1_2' (110318320) panicked at src/tests/fixtures.rs:3515:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation with signed integer] Validate Test 'boundary point is valid' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_1_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - ----- tests::test_minimum_1_6 stdout ---- -TEST VALIDATE ERROR FOR 'ignores non-numbers': Expected success: true, Got: false. Actual Errors: [Error { code: "SCHEMA_NOT_FOUND", message: "Schema minimum_1_0 not found", details: ErrorDetails { path: Some("/"), cause: None, context: None, schema: None } }] - -thread 'tests::test_minimum_1_6' (110318324) panicked at src/tests/fixtures.rs:3539:54: -called `Result::unwrap()` on an `Err` value: "[minimum validation with signed integer] Validate Test 'ignores non-numbers' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"SCHEMA_NOT_FOUND\", message: \"Schema minimum_1_0 not found\", details: ErrorDetails { path: Some(\"/\"), cause: None, context: None, schema: None } }]" - - -failures: - tests::test_minimum_0_0 - tests::test_minimum_0_1 - tests::test_minimum_0_3 - tests::test_minimum_1_0 - tests::test_minimum_1_1 - tests::test_minimum_1_2 - tests::test_minimum_1_3 - tests::test_minimum_1_6 - -test result: FAILED. 3 passed; 8 failed; 0 ignored; 0 measured; 1347 filtered out; finished in 0.00s - -error: test failed, to rerun pass `--lib` diff --git a/test_merge.log b/test_merge.log deleted file mode 100644 index 84a54e6..0000000 --- a/test_merge.log +++ /dev/null @@ -1,23 +0,0 @@ - Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg) - Finished `test` profile [unoptimized + debuginfo] target(s) in 7.59s - Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386) - -running 1 test -test tests::test_merge_0_0 ... FAILED - -failures: - ----- tests::test_merge_0_0 stdout ---- -TEST VALIDATE ERROR FOR 'valid with both properties': Expected success: true, Got: false. Actual Errors: [Error { code: "MISSING_TYPE", message: "Schema mechanically requires type discrimination 'base_0'", details: ErrorDetails { path: Some(""), cause: None, context: None, schema: None } }] - -thread 'tests::test_merge_0_0' (110369726) panicked at src/tests/fixtures.rs:4307:54: -called `Result::unwrap()` on an `Err` value: "[merging: properties accumulate] Validate Test 'valid with both properties' failed. Error: Expected success: true, Got: false. Actual Errors: [Error { code: \"MISSING_TYPE\", message: \"Schema mechanically requires type discrimination 'base_0'\", details: ErrorDetails { path: Some(\"\"), cause: None, context: None, schema: None } }]" -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace - - -failures: - tests::test_merge_0_0 - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1357 filtered out; finished in 0.00s - -error: test failed, to rerun pass `--lib`