Compare commits

...

47 Commits

Author SHA1 Message Date
e45265b242 version: 1.0.132 2026-04-24 11:54:01 -04:00
ec867f142f fixed more filtering issues and promoted enum condition generation 2026-04-24 11:53:54 -04:00
e9b5c82809 version: 1.0.131 2026-04-24 10:49:44 -04:00
628471e5d5 version: 1.0.130 2026-04-24 10:48:51 -04:00
0093aea790 fixed nested filters 2026-04-24 10:48:38 -04:00
45ebc57e0c version: 1.0.129 2026-04-21 11:54:03 -04:00
00319b570b fixed $ validation in schema ids 2026-04-21 11:53:51 -04:00
9ab3689808 version: 1.0.128 2026-04-21 10:51:08 -04:00
bff0884ad2 version: 1.0.127 2026-04-21 10:50:13 -04:00
4e2cb488cc removed schema realms, fixed fixture generations, added dynamic type resolution for validation based on type and kind discriminators for filters 2026-04-21 10:50:01 -04:00
a1e6ac8cb0 doc update 2026-04-20 09:56:46 -04:00
120f488d93 cleanup 2026-04-17 18:27:49 -04:00
3928174fe7 version: 1.0.126 2026-04-17 18:25:24 -04:00
f450f8ab8b added realm to jspg processing 2026-04-17 18:25:14 -04:00
8ebf6a69bf updates 2026-04-17 17:20:07 -04:00
3a4c53dc7d version: 1.0.125 2026-04-17 07:43:30 -04:00
69bd726b25 more filter fixes 2026-04-17 07:43:19 -04:00
c2267b68d8 version: 1.0.124 2026-04-17 05:53:54 -04:00
f58d1a32a3 full database extracton 2026-04-17 05:53:44 -04:00
d9f4a90225 version: 1.0.123 2026-04-17 02:24:09 -04:00
509194b55f in, nin to of, nof for go and dart generator compatibility 2026-04-17 02:24:03 -04:00
87a845e85a filters are now entities and auto-generated for all table backed types 2026-04-17 01:46:02 -04:00
8175b10a97 filter checkpoint 2026-04-17 00:38:54 -04:00
0b072d66e7 version: 1.0.122 2026-04-16 11:54:43 -04:00
41649766db to family 2026-04-16 11:54:37 -04:00
61a8c5eed7 version: 1.0.121 2026-04-16 11:00:32 -04:00
77af67aef5 beefed up schema compiled properties with cases properties and added tests with cases to queryer and merger 2026-04-16 11:00:26 -04:00
cd85a8a2c3 version: 1.0.120 2026-04-16 00:19:24 -04:00
d3cb72a5e2 -m fixed bug with STI and different type shapes that don't require kind discrimination 2026-04-16 00:19:10 -04:00
57baa389b6 version: 1.0.119 2026-04-15 03:09:43 -04:00
8ceb4f05a2 version: 1.0.118 2026-04-15 03:08:26 -04:00
a3bd79deef jspg stabilized again 2026-04-15 03:08:11 -04:00
c38d81efa4 version: 1.0.117 2026-04-14 13:38:24 -04:00
3e7fafd736 version: 1.0.116 2026-04-14 13:23:08 -04:00
8984acaa5f added jsonb field tests to queryer and merger and fixed a bug there 2026-04-14 13:23:01 -04:00
24adf3ffc6 checkpoint 2026-04-14 13:06:53 -04:00
bfe3dd2e8c version: 1.0.115 2026-04-14 09:36:12 -04:00
609371c03c fixed drop errors for database initialization 2026-04-14 09:36:04 -04:00
93d1315f0b version: 1.0.114 2026-04-14 06:09:30 -04:00
e407adf6a1 version: 1.0.113 2026-04-14 06:09:14 -04:00
0b4607b7d4 fixed and tested subschema promotions for beat processing 2026-04-14 06:09:00 -04:00
a53e89df52 version: 1.0.112 2026-04-13 22:48:27 -04:00
57bbe11013 version: 1.0.111 2026-04-13 22:48:12 -04:00
386b7ad012 docs(jspg): document scalar tuple routing boundary 2026-04-13 22:47:21 -04:00
a91460b390 cleanup 2026-04-13 22:44:18 -04:00
0017c598e1 chore: JSPG Engine tuple decoupling and core routing optimizations 2026-04-13 22:41:32 -04:00
665a821bf9 version: 1.0.110 2026-04-10 01:06:16 -04:00
81 changed files with 11508 additions and 6960 deletions

BIN
.DS_Store vendored

Binary file not shown.

107
GEMINI.md
View File

@ -13,7 +13,7 @@ JSPG operates by deeply integrating the JSON Schema Draft 2020-12 specification
1. **Draft 2020-12 Based**: Attempt to adhere to the official JSON Schema Draft 2020-12 specification, while heavily augmenting it for strict structural typing.
2. **Ultra-Fast Execution**: Compile schemas into optimized in-memory validation trees and cached SQL SPIs to bypass Postgres Query Builder overheads.
3. **Connection-Bound Caching**: Leverage the PostgreSQL session lifecycle using an **Atomic Swap** pattern. Schemas are 100% frozen, completely eliminating locks during read access.
4. **Structural Inheritance**: Support object-oriented schema design via Implicit Keyword Shadowing and virtual `$family` references natively mapped to Postgres table constraints.
4. **Structural Inheritance**: Support object-oriented schema design via Implicit Keyword Shadowing and virtual `family` references natively mapped to Postgres table constraints.
5. **Reactive Beats**: Provide ultra-fast natively generated flat payloads mapping directly to the Dart topological state for dynamic websocket reactivity.
### Concurrency & Threading ("Immutable Graphs")
@ -28,7 +28,7 @@ These functions operate on the global `GLOBAL_JSPG` engine instance and provide
* `jspg_setup(database jsonb) -> jsonb`: Initializes the engine. Deserializes the full database schema registry (types, enums, puncs, relations) from Postgres and compiles them into memory atomically.
* `jspg_teardown() -> jsonb`: Clears the current session's engine instance from `GLOBAL_JSPG`, resetting the cache.
* `jspg_schemas() -> jsonb`: Exports the fully compiled AST snapshot (including all inherited dependencies) out of `GLOBAL_JSPG` into standard JSON Schema representations.
* `jspg_database() -> jsonb`: Exports the fully compiled snapshot of the database registry (including Types, Puncs, Enums, and Relations) out of `GLOBAL_JSPG` into standard JSON Schema representations.
---
@ -36,45 +36,56 @@ These functions operate on the global `GLOBAL_JSPG` engine instance and provide
JSPG augments standard JSON Schema 2020-12 to provide an opinionated, strict, and highly ergonomic Object-Oriented paradigm. Developers defining Punc Data Models should follow these conventions.
### Realms (Topological Boundaries)
JSPG strictly organizes schemas into three distinct topological boundaries called **Realms** to prevent cross-contamination and ensure secure API generation:
* **Type Realm (`database.types`)**: Represents physical Postgres tables or structural JSONB bubbles. Table-backed entities here are strictly evaluated for their `type` or `kind` discriminators if they possess polymorphic variations.
* **Punc Realm (`database.puncs`)**: Represents API endpoint Contracts (functions). Contains strictly `.request` and `.response` shapes. These cannot be inherited by standard data models.
* **Enum Realm (`database.enums`)**: Represents simple restricted value lists. Handled universally across all lookups.
The core execution engines natively enforce these boundaries:
* **Validator**: Routes dynamically using a single schema key, transparently switching domains to validate Punc requests/responses from the `Punc` realm, or raw instance payloads from the `Type` realm.
* **Merger**: Strictly bounded to the `Type` Realm. It is philosophically impossible and mathematically illegal to attempt to UPSERT an API endpoint.
* **Queryer**: Routes recursively. Safely evaluates API boundary inputs directly from the `Punc` realm, while tracing underlying table targets back through the `Type` realm to physically compile SQL `SELECT` statements.
### Types of Types
* **Table-Backed (Entity Types)**: Primarily defined in root type schemas. These represent physical Postgres tables.
* They absolutely **require** an `$id`.
* **Table-Backed (Entity Types)**: Primarily defined in root `types` schemas. These represent physical Postgres tables.
* They are implicitly registered in the Global Registry using their precise key name mapped from the database compilation phase.
* The schema conceptually requires a `type` discriminator at runtime so the engine knows what physical variation to interact with.
* Can inherit other entity types to build lineage (e.g. `person` -> `organization` -> `entity`).
* Can inherit other entity types to build lineage (e.g. `person` -> `organization` -> `entity`) natively using the `type` property.
* **Field-Backed (JSONB Bubbles)**: These are shapes that live entirely inside a Postgres JSONB column without being tied to a top-level table constraint.
* **Global `$id` Promotion**: Utilizing explicit `$id` declarations promotes the schema to the Global Registry. This effectively creates strictly-typed code-generator universes (e.g., generating an `InvoiceNotificationMetadata` Dart class) operating cleanly inside unstructured Postgres JSONB columns.
* **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 `<Variant>.<Base>` Convention
In Punc, polymorphic targets like explicit tagged unions or STI (Single Table Inheritance) rely on discriminators. The system heavily leverages a standard `<Variant>.<Base>` 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 `$id` (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 `<Variant>.<Base>` (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**.
**Magic Validation Constraints**:
* **Dynamically Required**: The system inherently drives the need for their requirement. The Validator dynamically expects the discriminators and structurally bubbles `MISSING_TYPE` ultimata ONLY when a polymorphic router (`$family` / `oneOf`) dynamically requires them to resolve a path. You never manually put them in the JSON schema `required` block.
* **Implicit Resolution**: When wrapped in `$family` or `oneOf`, the polymorphic router can mathematically parse the schema `$id` (e.g. `light.person`) and natively validate that `type` equals `"person"` and `kind` equals `"light"`, bubbling `CONST_VIOLATED` if they mismatch, all without you ever hardcoding `const` limitations.
* **Dynamically Required**: The system inherently drives the need for their requirement. The Validator dynamically expects the discriminators and structurally bubbles `MISSING_TYPE` ultimata ONLY when a polymorphic router (`family` / `oneOf`) dynamically requires them to resolve a path. You never manually put them in the JSON schema `required` block.
* **Implicit Resolution**: When wrapped in `family` or `oneOf`, the polymorphic router can mathematically parse the schema key (e.g. `light.person`) and natively validate that `type` equals `"person"` and `kind` equals `"light"`, bubbling `CONST_VIOLATED` if they mismatch, all without you ever hardcoding `const` limitations.
* **Generator Explicitness**: Because Postgres is the Single Source of Truth, forcing the explicit definition in `properties` initially guarantees the downstream Dart/Go code generators observe the fields and can cleanly serialize them dynamically back to the server.
For example, a schema representing `$id: "light.person"` must natively define its own structural boundaries:
For example, a schema registered under the exact key `"light.person"` inside the database registry must natively define its own structural boundaries:
```json
{
"$id": "light.person",
"type": "person",
"properties": {
"type": { "type": "string" },
"kind": { "type": "string" }
},
"required": ["type", "kind"]
}
}
```
* **The Object Contract (Presence)**: The Object enforces its own structural integrity mechanically. Standard JSON Validation natively ensures `type` and `kind` are present, bubbling `REQUIRED_FIELD_MISSING` organically if omitted.
* **The Dynamic Values (`db.types`)**: Because the `type` and `kind` properties technically exist, the Punc engine dynamically intercepts them during `validate_object`. It mathematically parses the schema `$id` (e.g. `light.person`) and natively validates that `type` equals `"person"` (or a valid descendant in `db.types`) and `kind` equals `"light"`, bubbling `CONST_VIOLATED` if they mismatch.
* **The Routing Contract**: When wrapped in `$family` or `oneOf`, the polymorphic router can execute Lightning Fast $O(1)$ fast-paths by reading the payload's `type`/`kind` identifiers, and gracefully fallback to standard structural failure if omitted.
* **The Object Contract (Presence)**: The Object enforces its own structural integrity mechanically. Standard JSON Validation natively ensures `type` and `kind` are dynamically present as expected.
* **The Dynamic Values (`db.types`)**: Because the `type` and `kind` properties technically exist, the Punc engine dynamically intercepts them during `validate_object`. It mathematically parses the schema key (e.g. `light.person`) and natively validates that `type` equals `"person"` (or a valid descendant in `db.types`) and `kind` equals `"light"`, bubbling `CONST_VIOLATED` if they mismatch.
* **The Routing Contract**: When wrapped in `family` or `oneOf`, the polymorphic router can execute Lightning Fast $O(1)$ fast-paths by reading the payload's `type`/`kind` identifiers, and gracefully fallback to standard structural failure if omitted.
### Composition & Inheritance (The `type` keyword)
Punc completely abandons the standard JSON Schema `$ref` keyword. Instead, it overloads the exact same `type` keyword used for primitives. A `"type"` in Punc is mathematically evaluated as either a Native Primitive (`"string"`, `"null"`) or a Custom Object Pointer (`"budget"`, `"user"`).
@ -82,25 +93,26 @@ 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 (`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.
* **`$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.
* **`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.
* *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" }`
* *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).
* *Options*: `person` -> `light.person`, `organization` -> `light.organization`. (If a projection like `light.bot` does not exist in the Type Registry, 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.
* *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 registered key 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.
* **`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.
@ -112,7 +124,6 @@ It evaluates as an **Independent Declarative Rules Engine**. Every `Case` block
```json
{
"$id": "save_external_account",
"cases": [
{
"when": {
@ -155,6 +166,17 @@ It evaluates as an **Independent Declarative Rules Engine**. Every `Case` block
### Format Leniency for Empty Strings
To simplify frontend form validation, format validators specifically for `uuid`, `date-time`, and `email` explicitly allow empty strings (`""`), treating them as "present but unset".
### Filters & Conditions
In the Punc architecture, filters are automatically synthesized, strongly-typed JSON Schema boundaries that dictate the exact querying capabilities for any given entity or enum. They are completely generated for you; you never write them manually.
* **Conditions**: A condition schema is the contract defining the mathematical operations allowed on a primitive field. For example, a `string.condition` allows `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$of` (IN), and `$nof` (NOT IN).
* **Enum Conditions**: When JSPG synthesizes an enum, it dynamically generates an `<enum>.condition` (e.g., `address_kind.condition`). This strongly-typed condition perfectly mirrors the operations of a `string.condition`, but strictly limits the arrays and inputs of `$eq`, `$ne`, `$of`, and `$nof` to the exact variations defined by that Enum. This context ensures that UI generators know exactly when to render `<Select>` dropdowns instead of generic `<Text>` boxes.
* **Filters**: A filter schema (e.g., `person.filter`) is an object containing condition properties used to filter entities. It natively supports structural composition:
* **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas.
* **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively.
* **Logical Operators (`$and`, `$or`)**: Every filter automatically includes `$and` and `$or` arrays, which recursively accept the exact same filter schema, allowing complex logical grouping.
* **Ad-Hoc Extensions (`ad_hoc`)**: Fields stored purely in JSONB bubbles that lack formal database columns can still be queried using the `ad_hoc` object, which passes standard, unvalidated string conditions.
---
## 3. Database
@ -171,10 +193,11 @@ When compiling nested object graphs or arrays, the JSPG engine must dynamically
5. **Implicit Base Fallback (1:M)**: If no explicit prefix matches, and M:M deduction fails, the compiler filters for exactly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the core structural parent-child ownership edge and is used safely as a fallback.
6. **Deterministic Abort**: If the engine exhausts all deduction pathways and the edge remains ambiguous, it explicitly aborts schema compilation (`returns None`) rather than silently generating unpredictable SQL.
### Ad-Hoc Schema Promotion
To seamlessly support deeply nested, inline Object definitions that don't declare an explicit `$id`, JSPG aggressively promotes them to standalone topological entities during the database compilation phase.
* **Hash Generation:** While evaluating the unified graph, if the compiler enters an `Object` or `Array` structure completely lacking an `$id`, it dynamically calculates a localized hash alias representing exactly its structural constraints.
* **Promotion:** This inline chunk is mathematically elevated to its own `$id` in the `db.schemas` cache registry. This guarantees that $O(1)$ WebSockets or isolated queries can natively target any arbitrary sub-object of a massive database topology directly without recursively re-parsing its parent's AST block every read.
### Subschema Promotion
To seamlessly support deeply nested Object and Array structures, JSPG aggressively promotes them to standalone topological entities during the database compilation phase.
* **Path Generation:** While evaluating a unified graph originating from a base `types`, `enums`, or `puncs` key, the compiler tracks its exact path descent into nested objects and arrays. It dynamically calculates a localized alias string by appending a `/` pathing syntax (e.g., `base_schema_key/nested/path`) representing exactly its structural constraints.
* **Promotion:** This nested subschema chunk is mathematically elevated to an independent subschema entry natively within its parent's internal scope (e.g. inside `db.types.get("base").schemas`) using its full path. This guarantees that $O(1)$ WebSockets or isolated queries can natively target any arbitrary nested sub-object of a massive database topology directly without recursively re-parsing its parent's AST block every read. Note that you cannot use the `type` attribute to statically inherit from these automatically promoted subschemas.
* **Primitive Confinement:** Purely scalar or primitive branches (like `oneOf: [{type: "string"}, {type: "null"}]`) bypass global topological promotion. They are evaluated directly within the execution engine via isolated Tuple Indexes to explicitly protect the global DB Registry and Go Mixer from memory bloat.
---
@ -189,9 +212,10 @@ The Validator provides strict, schema-driven evaluation for the "Punc" architect
JSPG implements specific extensions to the Draft 2020-12 standard to support the Punc architecture's object-oriented needs while heavily optimizing for zero-runtime lookups.
* **Caching Strategy**: The Validator caches the pre-compiled `Database` registry in memory upon initialization (`jspg_setup`). This registry holds the comprehensive graph of schema boundaries, Types, ENUMs, and Foreign Key relationships, acting as the Single Source of Truth for all validation operations without polling Postgres.
* **Discriminator Fast Paths & Extraction**: When executing a polymorphic node (`oneOf` or `$family`), the engine statically analyzes the incoming JSON payload for the literal `type` and `kind` string coordinates. It routes the evaluation specifically to matching candidates in $O(1)$ while returning `MISSING_TYPE` ultimata directly.
* **Discriminator Fast Paths & Extraction**: When executing a polymorphic node (`oneOf` or `family`), the engine statically analyzes the incoming JSON payload for the literal `type` and `kind` string coordinates. It routes the evaluation specifically to matching candidates in $O(1)$ while returning `MISSING_TYPE` ultimata directly.
* **Missing Type Ultimatum**: If an entity logically requires a discriminator and the JSON payload omits it, JSPG short-circuits branch execution entirely, bubbling a single, perfectly-pathed `MISSING_TYPE` error back to the UI natively to prevent confusing cascading failures.
* **Golden Match Context**: When exactly one structural candidate perfectly maps a discriminator, the Validator exclusively cascades that specific structural error context directly to the user, stripping away all noise generated by other parallel schemas.
* **Topological Array Pathing**: Instead of relying on explicit `$id` references or injected properties, array iteration paths are dynamically typed based on their compiler boundary constraints. If the array's `items` schema resolves to a topological table-backed entity (e.g., inheriting via a `family` macro tracked in the global DB catalog), the array locks paths and derives element indexes from their actual UUID paths (`array/widget-1/name`), natively enforcing database continuity. If evaluating isolated ad-hoc JSONB elements, strict numeric indexing is enforced natively (`array/1/name`) preventing synthetic payload manipulation.
---
@ -225,21 +249,16 @@ The Merger provides an automated, high-performance graph synchronization engine.
The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, designed to serve the exact shape of Punc responses directly via SQL.
### API Reference
* `jspg_query(schema_id text, filters jsonb) -> jsonb`: Compiles the JSON Schema AST of `schema_id` directly into pre-planned, nested multi-JOIN SQL execution trees. Processes `filters` structurally.
* `jspg_query(schema_id text, filter jsonb) -> jsonb`: Compiles the JSON Schema AST of `schema_id` directly into pre-planned, nested multi-JOIN SQL execution trees. Processes the `filter` structurally.
### Core Features
* **Caching Strategy (DashMap SQL Caching)**: The Queryer securely caches its compiled, static SQL string templates per schema permutation inside the `GLOBAL_JSPG` concurrent `DashMap`. This eliminates recursive AST schema crawling on consecutive requests. Furthermore, it evaluates the strings via Postgres SPI (Server Programming Interface) Prepared Statements, leveraging native database caching of execution plans for extreme performance.
* **Schema-to-SQL Compilation**: Compiles JSON Schema ASTs spanning deep arrays directly into static, pre-planned SQL multi-JOIN queries. This explicitly features the `Smart Merge` evaluation engine which natively translates properties through `type` inheritances, mapping JSON fields specifically to their physical database table aliases during translation.
* **Root Null-Stripping Optimization**: Unlike traditional nested document builders, the Queryer intelligently defers Postgres' natively recursive `jsonb_strip_nulls` execution to the absolute apex of the compiled query pipeline. The compiler organically layers millions of rapid `jsonb_build_object()` sub-query allocations instantly, wrapping them in a singular overarching pass. This strips all empty optionals uniformly before exiting the database, maximizing CPU throughput.
* **Dynamic Filtering**: Binds parameters natively through `cue.filters` objects. The queryer enforces a strict, structured, MongoDB-style operator syntax to map incoming JSON request constraints directly to their originating structural table columns. Filters support both flat path notation (e.g., `"contacts/is_primary": {...}`) and deeply nested recursive JSON structures (e.g., `{"contacts": {"is_primary": {...}}}`). The queryer recursively traverses and flattens these structures at AST compilation time.
* **Equality / Inequality**: `{"$eq": value}`, `{"$ne": value}` automatically map to `=` and `!=`.
* **Comparison**: `{"$gt": ...}`, `{"$gte": ...}`, `{"$lt": ...}`, `{"$lte": ...}` directly compile to Postgres comparison operators (`> `, `>=`, `<`, `<=`).
* **Array Inclusion**: `{"$in": [values]}`, `{"$nin": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks.
* **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches.
* **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`).
* **Polymorphic SQL Generation (`$family`)**: Compiles `$family` properties by analyzing the **Physical Database Variations**, *not* the schema descendants.
* **The Dot Convention**: When a schema requests `$family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition.
* **Dynamic Filter Execution**: Evaluates the structured `filter` payload and recursively traverses and flattens its paths at AST compilation time. It safely binds parameter constraints using standard operations (e.g., mapping `$eq` to `=`, `$of` to `IN`, `$gt` to `>`) and automatically casts values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`). Text matching naturally evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE`.
* **Polymorphic SQL Generation (`family`)**: Compiles `family` properties by analyzing the **Physical Database Variations**, *not* the schema descendants.
* **The Dot Convention**: When a schema requests `family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition.
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products.
* **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row.

File diff suppressed because one or more lines are too long

View File

@ -2,19 +2,23 @@
{
"description": "additionalProperties validates properties not matched by properties",
"database": {
"schemas": [
"types": [
{
"$id": "schema1",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "number"
"name": "schema1",
"schemas": {
"schema1": {
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "number"
}
},
"additionalProperties": {
"type": "boolean"
}
}
},
"additionalProperties": {
"type": "boolean"
}
}
]
@ -62,18 +66,22 @@
{
"description": "extensible: true with additionalProperties still validates structure",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"type": "string"
"name": "additionalProperties_1_0",
"schemas": {
"additionalProperties_1_0": {
"properties": {
"foo": {
"type": "string"
}
},
"extensible": true,
"additionalProperties": {
"type": "integer"
}
}
},
"extensible": true,
"additionalProperties": {
"type": "integer"
},
"$id": "additionalProperties_1_0"
}
}
]
},
@ -108,18 +116,22 @@
{
"description": "complex additionalProperties with object and array items",
"database": {
"schemas": [
"types": [
{
"$id": "schema3",
"properties": {
"type": {
"type": "string"
}
},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
"name": "schema3",
"schemas": {
"schema3": {
"properties": {
"type": {
"type": "string"
}
},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}

View File

@ -2,9 +2,12 @@
{
"description": "boolean schema 'true'",
"database": {
"schemas": [
"types": [
{
"$id": "booleanSchema_0_0"
"name": "booleanSchema_0_0",
"schemas": {
"booleanSchema_0_0": {}
}
}
]
},
@ -99,10 +102,14 @@
{
"description": "boolean schema 'false'",
"database": {
"schemas": [
"types": [
{
"not": {},
"$id": "booleanSchema_1_0"
"name": "booleanSchema_1_0",
"schemas": {
"booleanSchema_1_0": {
"not": {}
}
}
}
]
},

View File

@ -2,182 +2,373 @@
{
"description": "Multi-Paradigm Declarative Cases",
"database": {
"schemas": [
"types": [
{
"$id": "parallel_rules",
"type": "object",
"properties": {
"status": { "type": "string" },
"kind": { "type": "string" }
},
"cases": [
{
"when": { "properties": { "status": {"const": "unverified"} }, "required": ["status"] },
"then": {
"properties": {
"amount_1": {"type": "number"},
"amount_2": {"type": "number"}
"name": "parallel_rules",
"schemas": {
"parallel_rules": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"required": ["amount_1", "amount_2"]
}
},
{
"when": { "properties": { "kind": {"const": "credit"} }, "required": ["kind"] },
"then": {
"properties": {
"cvv": {"type": "number"}
},
"required": ["cvv"]
}
}
]
},
{
"$id": "mutually_exclusive",
"type": "object",
"properties": {
"type": { "type": "string" }
},
"cases": [
{
"when": { "properties": { "type": {"const": "A"} }, "required": ["type"] },
"then": {
"properties": { "field_a": {"type": "number"} },
"required": ["field_a"]
}
},
{
"when": { "properties": { "type": {"const": "B"} }, "required": ["type"] },
"then": {
"properties": { "field_b": {"type": "number"} },
"required": ["field_b"]
"kind": {
"type": "string"
}
},
"else": {
"properties": { "fallback_b": {"type": "number"} },
"required": ["fallback_b"]
}
}
]
},
{
"$id": "nested_fallbacks",
"type": "object",
"properties": {
"tier": { "type": "string" }
},
"cases": [
{
"when": { "properties": { "tier": {"const": "1"} }, "required": ["tier"] },
"then": {
"properties": { "basic": {"type": "number"} },
"required": ["basic"]
},
"else": {
"cases": [
{
"when": { "properties": { "tier": {"const": "2"} }, "required": ["tier"] },
"then": {
"properties": { "standard": {"type": "number"} },
"required": ["standard"]
"cases": [
{
"when": {
"properties": {
"status": {
"const": "unverified"
}
},
"else": {
"properties": { "premium": {"type": "number"} },
"required": ["premium"]
}
"required": [
"status"
]
},
"then": {
"properties": {
"amount_1": {
"type": "number"
},
"amount_2": {
"type": "number"
}
},
"required": [
"amount_1",
"amount_2"
]
}
]
}
},
{
"when": {
"properties": {
"kind": {
"const": "credit"
}
},
"required": [
"kind"
]
},
"then": {
"properties": {
"cvv": {
"type": "number"
}
},
"required": [
"cvv"
]
}
}
]
}
]
}
},
{
"$id": "missing_when",
"type": "object",
"cases": [
{
"else": {
"properties": { "unconditional": {"type": "number"} },
"required": ["unconditional"]
}
"name": "mutually_exclusive",
"schemas": {
"mutually_exclusive": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
},
"cases": [
{
"when": {
"properties": {
"type": {
"const": "A"
}
},
"required": [
"type"
]
},
"then": {
"properties": {
"field_a": {
"type": "number"
}
},
"required": [
"field_a"
]
}
},
{
"when": {
"properties": {
"type": {
"const": "B"
}
},
"required": [
"type"
]
},
"then": {
"properties": {
"field_b": {
"type": "number"
}
},
"required": [
"field_b"
]
},
"else": {
"properties": {
"fallback_b": {
"type": "number"
}
},
"required": [
"fallback_b"
]
}
}
]
}
]
}
},
{
"name": "nested_fallbacks",
"schemas": {
"nested_fallbacks": {
"type": "object",
"properties": {
"tier": {
"type": "string"
}
},
"cases": [
{
"when": {
"properties": {
"tier": {
"const": "1"
}
},
"required": [
"tier"
]
},
"then": {
"properties": {
"basic": {
"type": "number"
}
},
"required": [
"basic"
]
},
"else": {
"cases": [
{
"when": {
"properties": {
"tier": {
"const": "2"
}
},
"required": [
"tier"
]
},
"then": {
"properties": {
"standard": {
"type": "number"
}
},
"required": [
"standard"
]
},
"else": {
"properties": {
"premium": {
"type": "number"
}
},
"required": [
"premium"
]
}
}
]
}
}
]
}
}
},
{
"name": "missing_when",
"schemas": {
"missing_when": {
"type": "object",
"cases": [
{
"else": {
"properties": {
"unconditional": {
"type": "number"
}
},
"required": [
"unconditional"
]
}
}
]
}
}
}
]
},
"tests": [
{
"description": "Fires only the first rule successfully",
"data": { "status": "unverified", "amount_1": 1, "amount_2": 2 },
"data": {
"status": "unverified",
"amount_1": 1,
"amount_2": 2
},
"schema_id": "parallel_rules",
"action": "validate",
"expect": { "success": true }
"expect": {
"success": true
}
},
{
"description": "Fires both independent parallel rules flawlessly",
"data": { "status": "unverified", "kind": "credit", "amount_1": 1, "amount_2": 2, "cvv": 123 },
"data": {
"status": "unverified",
"kind": "credit",
"amount_1": 1,
"amount_2": 2,
"cvv": 123
},
"schema_id": "parallel_rules",
"action": "validate",
"expect": { "success": true }
"expect": {
"success": true
}
},
{
"description": "Catches errors triggered concurrently by multiple independent blocked rules",
"data": { "status": "unverified", "kind": "credit" },
"data": {
"status": "unverified",
"kind": "credit"
},
"schema_id": "parallel_rules",
"action": "validate",
"expect": {
"success": false,
"errors": [
{ "code": "REQUIRED_FIELD_MISSING", "details": { "path": "amount_1" } },
{ "code": "REQUIRED_FIELD_MISSING", "details": { "path": "amount_2" } },
{ "code": "REQUIRED_FIELD_MISSING", "details": { "path": "cvv" } }
{
"code": "REQUIRED_FIELD_MISSING",
"details": {
"path": "amount_1"
}
},
{
"code": "REQUIRED_FIELD_MISSING",
"details": {
"path": "amount_2"
}
},
{
"code": "REQUIRED_FIELD_MISSING",
"details": {
"path": "cvv"
}
}
]
}
},
{
"description": "STRICT_PROPERTY_VIOLATION throws if an un-triggered then property is submitted",
"data": { "status": "verified", "cvv": 123 },
"data": {
"status": "verified",
"cvv": 123
},
"schema_id": "parallel_rules",
"action": "validate",
"expect": {
"success": false,
"errors": [
{ "code": "STRICT_PROPERTY_VIOLATION", "details": { "path": "cvv" } }
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": {
"path": "cvv"
}
}
]
}
},
{
"description": "Successfully routes mutually exclusive properties seamlessly",
"data": { "type": "A", "field_a": 1, "fallback_b": 2 },
"data": {
"type": "A",
"field_a": 1,
"fallback_b": 2
},
"schema_id": "mutually_exclusive",
"action": "validate",
"expect": { "success": true }
"expect": {
"success": true
}
},
{
"description": "Nested fallbacks execute seamlessly",
"data": { "tier": "3", "premium": 1 },
"data": {
"tier": "3",
"premium": 1
},
"schema_id": "nested_fallbacks",
"action": "validate",
"expect": { "success": true }
"expect": {
"success": true
}
},
{
"description": "A case without a when executes its else indiscriminately",
"data": { "unconditional": 1 },
"data": {
"unconditional": 1
},
"schema_id": "missing_when",
"action": "validate",
"expect": { "success": true }
"expect": {
"success": true
}
},
{
"description": "A case without a when throws if else unconditionally requires field",
"data": { },
"data": {},
"schema_id": "missing_when",
"action": "validate",
"expect": {
"success": false,
"errors": [
{ "code": "REQUIRED_FIELD_MISSING", "details": { "path": "unconditional" } }
{
"code": "REQUIRED_FIELD_MISSING",
"details": {
"path": "unconditional"
}
}
]
}
}
]
}
]
]

View File

@ -2,10 +2,14 @@
{
"description": "const validation",
"database": {
"schemas": [
"types": [
{
"const": 2,
"$id": "const_0_0"
"name": "const_0_0",
"schemas": {
"const_0_0": {
"const": 2
}
}
}
]
},
@ -42,17 +46,21 @@
{
"description": "const with object",
"database": {
"schemas": [
"types": [
{
"const": {
"foo": "bar",
"baz": "bax"
},
"properties": {
"foo": {},
"baz": {}
},
"$id": "const_1_0"
"name": "const_1_0",
"schemas": {
"const_1_0": {
"const": {
"foo": "bar",
"baz": "bax"
},
"properties": {
"foo": {},
"baz": {}
}
}
}
}
]
},
@ -109,14 +117,18 @@
{
"description": "const with array",
"database": {
"schemas": [
"types": [
{
"const": [
{
"foo": "bar"
"name": "const_2_0",
"schemas": {
"const_2_0": {
"const": [
{
"foo": "bar"
}
]
}
],
"$id": "const_2_0"
}
}
]
},
@ -163,10 +175,14 @@
{
"description": "const with null",
"database": {
"schemas": [
"types": [
{
"const": null,
"$id": "const_3_0"
"name": "const_3_0",
"schemas": {
"const_3_0": {
"const": null
}
}
}
]
},
@ -194,10 +210,14 @@
{
"description": "const with false does not match 0",
"database": {
"schemas": [
"types": [
{
"const": false,
"$id": "const_4_0"
"name": "const_4_0",
"schemas": {
"const_4_0": {
"const": false
}
}
}
]
},
@ -234,10 +254,14 @@
{
"description": "const with true does not match 1",
"database": {
"schemas": [
"types": [
{
"const": true,
"$id": "const_5_0"
"name": "const_5_0",
"schemas": {
"const_5_0": {
"const": true
}
}
}
]
},
@ -274,12 +298,16 @@
{
"description": "const with [false] does not match [0]",
"database": {
"schemas": [
"types": [
{
"const": [
false
],
"$id": "const_6_0"
"name": "const_6_0",
"schemas": {
"const_6_0": {
"const": [
false
]
}
}
}
]
},
@ -322,12 +350,16 @@
{
"description": "const with [true] does not match [1]",
"database": {
"schemas": [
"types": [
{
"const": [
true
],
"$id": "const_7_0"
"name": "const_7_0",
"schemas": {
"const_7_0": {
"const": [
true
]
}
}
}
]
},
@ -370,12 +402,16 @@
{
"description": "const with {\"a\": false} does not match {\"a\": 0}",
"database": {
"schemas": [
"types": [
{
"const": {
"a": false
},
"$id": "const_8_0"
"name": "const_8_0",
"schemas": {
"const_8_0": {
"const": {
"a": false
}
}
}
}
]
},
@ -418,12 +454,16 @@
{
"description": "const with {\"a\": true} does not match {\"a\": 1}",
"database": {
"schemas": [
"types": [
{
"const": {
"a": true
},
"$id": "const_9_0"
"name": "const_9_0",
"schemas": {
"const_9_0": {
"const": {
"a": true
}
}
}
}
]
},
@ -466,10 +506,14 @@
{
"description": "const with 0 does not match other zero-like types",
"database": {
"schemas": [
"types": [
{
"const": 0,
"$id": "const_10_0"
"name": "const_10_0",
"schemas": {
"const_10_0": {
"const": 0
}
}
}
]
},
@ -533,10 +577,14 @@
{
"description": "const with 1 does not match true",
"database": {
"schemas": [
"types": [
{
"const": 1,
"$id": "const_11_0"
"name": "const_11_0",
"schemas": {
"const_11_0": {
"const": 1
}
}
}
]
},
@ -573,10 +621,14 @@
{
"description": "const with -2.0 matches integer and float types",
"database": {
"schemas": [
"types": [
{
"const": -2,
"$id": "const_12_0"
"name": "const_12_0",
"schemas": {
"const_12_0": {
"const": -2
}
}
}
]
},
@ -631,10 +683,14 @@
{
"description": "float and integers are equal up to 64-bit representation limits",
"database": {
"schemas": [
"types": [
{
"const": 9007199254740992,
"$id": "const_13_0"
"name": "const_13_0",
"schemas": {
"const_13_0": {
"const": 9007199254740992
}
}
}
]
},
@ -680,10 +736,14 @@
{
"description": "nul characters in strings",
"database": {
"schemas": [
"types": [
{
"const": "hello\u0000there",
"$id": "const_14_0"
"name": "const_14_0",
"schemas": {
"const_14_0": {
"const": "hello\u0000there"
}
}
}
]
},
@ -711,18 +771,22 @@
{
"description": "characters with the same visual representation but different codepoint",
"database": {
"schemas": [
"types": [
{
"const": "μ",
"$comment": "U+03BC",
"$id": "const_15_0"
"name": "const_15_0",
"schemas": {
"const_15_0": {
"const": "\u03bc",
"$comment": "U+03BC"
}
}
}
]
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "μ",
"data": "\u03bc",
"comment": "U+03BC",
"schema_id": "const_15_0",
"action": "validate",
@ -732,7 +796,7 @@
},
{
"description": "character looks the same but uses a different codepoint",
"data": "µ",
"data": "\u00b5",
"comment": "U+00B5",
"schema_id": "const_15_0",
"action": "validate",
@ -745,18 +809,22 @@
{
"description": "characters with the same visual representation, but different number of codepoints",
"database": {
"schemas": [
"types": [
{
"const": "ä",
"$comment": "U+00E4",
"$id": "const_16_0"
"name": "const_16_0",
"schemas": {
"const_16_0": {
"const": "\u00e4",
"$comment": "U+00E4"
}
}
}
]
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "ä",
"data": "\u00e4",
"comment": "U+00E4",
"schema_id": "const_16_0",
"action": "validate",
@ -766,7 +834,7 @@
},
{
"description": "character looks the same but uses combining marks",
"data": "ä",
"data": "a\u0308",
"comment": "a, U+0308",
"schema_id": "const_16_0",
"action": "validate",
@ -779,13 +847,17 @@
{
"description": "extensible: true allows extra properties in const object match",
"database": {
"schemas": [
"types": [
{
"const": {
"a": 1
},
"extensible": true,
"$id": "const_17_0"
"name": "const_17_0",
"schemas": {
"const_17_0": {
"const": {
"a": 1
},
"extensible": true
}
}
}
]
},

View File

@ -2,13 +2,17 @@
{
"description": "contains keyword validation",
"database": {
"schemas": [
"types": [
{
"contains": {
"minimum": 5
},
"items": true,
"$id": "contains_0_0"
"name": "contains_0_0",
"schemas": {
"contains_0_0": {
"contains": {
"minimum": 5
},
"items": true
}
}
}
]
},
@ -89,13 +93,17 @@
{
"description": "contains keyword with const keyword",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 5
},
"items": true,
"$id": "contains_1_0"
"name": "contains_1_0",
"schemas": {
"contains_1_0": {
"contains": {
"const": 5
},
"items": true
}
}
}
]
},
@ -146,10 +154,14 @@
{
"description": "contains keyword with boolean schema true",
"database": {
"schemas": [
"types": [
{
"contains": true,
"$id": "contains_2_0"
"name": "contains_2_0",
"schemas": {
"contains_2_0": {
"contains": true
}
}
}
]
},
@ -179,10 +191,14 @@
{
"description": "contains keyword with boolean schema false",
"database": {
"schemas": [
"types": [
{
"contains": false,
"$id": "contains_3_0"
"name": "contains_3_0",
"schemas": {
"contains_3_0": {
"contains": false
}
}
}
]
},
@ -221,15 +237,19 @@
{
"description": "items + contains",
"database": {
"schemas": [
"types": [
{
"items": {
"multipleOf": 2
},
"contains": {
"multipleOf": 3
},
"$id": "contains_4_0"
"name": "contains_4_0",
"schemas": {
"contains_4_0": {
"items": {
"multipleOf": 2
},
"contains": {
"multipleOf": 3
}
}
}
}
]
},
@ -289,13 +309,17 @@
{
"description": "contains with false if subschema",
"database": {
"schemas": [
"types": [
{
"contains": {
"if": false,
"else": true
},
"$id": "contains_5_0"
"name": "contains_5_0",
"schemas": {
"contains_5_0": {
"contains": {
"if": false,
"else": true
}
}
}
}
]
},
@ -325,12 +349,16 @@
{
"description": "contains with null instance elements",
"database": {
"schemas": [
"types": [
{
"contains": {
"type": "null"
},
"$id": "contains_6_0"
"name": "contains_6_0",
"schemas": {
"contains_6_0": {
"contains": {
"type": "null"
}
}
}
}
]
},
@ -351,13 +379,17 @@
{
"description": "extensible: true allows non-matching items in contains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"extensible": true,
"$id": "contains_7_0"
"name": "contains_7_0",
"schemas": {
"contains_7_0": {
"contains": {
"const": 1
},
"extensible": true
}
}
}
]
},
@ -379,12 +411,16 @@
{
"description": "strict by default: non-matching items in contains are invalid",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"$id": "contains_8_0"
"name": "contains_8_0",
"schemas": {
"contains_8_0": {
"contains": {
"const": 1
}
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "validation of string-encoded content based on media type",
"database": {
"schemas": [
"types": [
{
"contentMediaType": "application/json",
"$id": "content_0_0"
"name": "content_0_0",
"schemas": {
"content_0_0": {
"contentMediaType": "application/json"
}
}
}
]
},
@ -42,10 +46,14 @@
{
"description": "validation of binary string-encoding",
"database": {
"schemas": [
"types": [
{
"contentEncoding": "base64",
"$id": "content_1_0"
"name": "content_1_0",
"schemas": {
"content_1_0": {
"contentEncoding": "base64"
}
}
}
]
},
@ -82,11 +90,15 @@
{
"description": "validation of binary-encoded media type documents",
"database": {
"schemas": [
"types": [
{
"contentMediaType": "application/json",
"contentEncoding": "base64",
"$id": "content_2_0"
"name": "content_2_0",
"schemas": {
"content_2_0": {
"contentMediaType": "application/json",
"contentEncoding": "base64"
}
}
}
]
},
@ -132,25 +144,29 @@
{
"description": "validation of binary-encoded media type documents with schema",
"database": {
"schemas": [
"types": [
{
"contentMediaType": "application/json",
"contentEncoding": "base64",
"contentSchema": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string"
},
"boo": {
"type": "integer"
"name": "content_3_0",
"schemas": {
"content_3_0": {
"contentMediaType": "application/json",
"contentEncoding": "base64",
"contentSchema": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string"
},
"boo": {
"type": "integer"
}
}
}
}
},
"$id": "content_3_0"
}
}
]
},

View File

@ -15,9 +15,8 @@
"variations": [
"org"
],
"schemas": [
{
"$id": "full.org",
"schemas": {
"full.org": {
"type": "object",
"properties": {
"missing_users": {
@ -28,7 +27,7 @@
}
}
}
]
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
@ -42,13 +41,12 @@
"variations": [
"user"
],
"schemas": [
{
"$id": "full.user",
"schemas": {
"full.user": {
"type": "object",
"properties": {}
}
]
}
}
],
"relations": []
@ -84,9 +82,8 @@
"variations": [
"parent"
],
"schemas": [
{
"$id": "full.parent",
"schemas": {
"full.parent": {
"type": "object",
"properties": {
"children": {
@ -97,7 +94,7 @@
}
}
}
]
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
@ -111,13 +108,12 @@
"variations": [
"child"
],
"schemas": [
{
"$id": "full.child",
"schemas": {
"full.child": {
"type": "object",
"properties": {}
}
]
}
}
],
"relations": [
@ -167,9 +163,8 @@
"variations": [
"invoice"
],
"schemas": [
{
"$id": "full.invoice",
"schemas": {
"full.invoice": {
"type": "object",
"properties": {
"activities": {
@ -180,7 +175,7 @@
}
}
}
]
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
@ -194,13 +189,12 @@
"variations": [
"activity"
],
"schemas": [
{
"$id": "full.activity",
"schemas": {
"full.activity": {
"type": "object",
"properties": {}
}
]
}
}
],
"relations": [
@ -274,9 +268,8 @@
"variations": [
"actor"
],
"schemas": [
{
"$id": "full.actor",
"schemas": {
"full.actor": {
"type": "object",
"properties": {
"ambiguous_edge": {
@ -287,7 +280,7 @@
}
}
}
]
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
@ -301,13 +294,12 @@
"variations": [
"junction"
],
"schemas": [
{
"$id": "empty.junction",
"schemas": {
"empty.junction": {
"type": "object",
"properties": {}
}
]
}
}
],
"relations": [
@ -406,5 +398,542 @@
}
}
]
},
{
"description": "Schema Promotion Accuracy Test",
"database": {
"puncs": [],
"enums": [],
"relations": [
{
"id": "r1",
"type": "relation",
"constraint": "fk_person_email",
"source_type": "person",
"source_columns": [
"email_id"
],
"destination_type": "email_address",
"destination_columns": [
"id"
],
"prefix": "email"
},
{
"id": "r2",
"type": "relation",
"constraint": "fk_person_ad_hoc_bubble",
"source_type": "person",
"source_columns": [
"ad_hoc_bubble_id"
],
"destination_type": "some_bubble",
"destination_columns": [
"id"
],
"prefix": "ad_hoc_bubble"
},
{
"id": "r3",
"type": "relation",
"constraint": "fk_person_generic_bubble",
"source_type": "person",
"source_columns": [
"generic_bubble_id"
],
"destination_type": "some_bubble",
"destination_columns": [
"id"
],
"prefix": "generic_bubble"
},
{
"id": "r4",
"type": "relation",
"constraint": "fk_person_extended_relations",
"source_type": "contact",
"source_columns": [
"source_id"
],
"destination_type": "person",
"destination_columns": [
"id"
],
"prefix": "extended_relations"
},
{
"id": "r5",
"type": "relation",
"constraint": "fk_person_standard_relations",
"source_type": "contact",
"source_columns": [
"source_id_2"
],
"destination_type": "person",
"destination_columns": [
"id"
],
"prefix": "standard_relations"
},
{
"id": "r6",
"type": "relation",
"constraint": "fk_contact_target",
"source_type": "contact",
"source_columns": [
"target_id"
],
"destination_type": "email_address",
"destination_columns": [
"id"
],
"prefix": "target"
}
],
"types": [
{
"id": "t1",
"type": "type",
"name": "person",
"module": "core",
"source": "person",
"hierarchy": [
"person"
],
"variations": [
"person",
"student"
],
"schemas": {
"full.person": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"family": "email_address"
},
"generic_bubble": {
"type": "some_bubble"
},
"ad_hoc_bubble": {
"type": "some_bubble",
"properties": {
"extra_inline_feature": {
"type": "string"
}
}
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"standard_relations": {
"type": "array",
"items": {
"type": "contact"
}
},
"extended_relations": {
"type": "array",
"items": {
"type": "contact",
"properties": {
"target": {
"type": "email_address",
"properties": {
"extra_3rd_level": {
"type": "string"
}
}
}
}
}
}
}
},
"student.person": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"kind": {
"type": "string"
},
"school": {
"type": "string"
}
}
}
}
},
{
"id": "t2",
"type": "type",
"name": "email_address",
"module": "core",
"source": "email_address",
"hierarchy": [
"email_address"
],
"variations": [
"email_address"
],
"schemas": {
"light.email_address": {
"type": "object",
"properties": {
"address": {
"type": "string"
}
}
}
}
},
{
"id": "t3",
"type": "type",
"name": "contact",
"module": "core",
"source": "contact",
"hierarchy": [
"contact"
],
"variations": [
"contact"
],
"schemas": {
"full.contact": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
}
},
{
"id": "t4",
"type": "type",
"name": "some_bubble",
"module": "core",
"source": "some_bubble",
"hierarchy": [
"some_bubble"
],
"variations": [
"some_bubble"
],
"schemas": {
"some_bubble": {
"type": "object",
"properties": {
"bubble_prop": {
"type": "string"
}
}
}
}
}
]
},
"tests": [
{
"description": "Assert exact topological schema promotion paths",
"action": "compile",
"expect": {
"success": true,
"schemas": {
"full.contact": {},
"full.contact.filter": {},
"full.person": {},
"full.person.filter": {},
"full.person/ad_hoc_bubble": {},
"full.person/extended_relations": {},
"full.person/extended_relations/target": {},
"light.email_address": {},
"light.email_address.filter": {},
"some_bubble": {},
"some_bubble.filter": {},
"student.person": {},
"student.person.filter": {}
}
}
}
]
},
{
"description": "JSONB boundaries",
"database": {
"relations": [
{
"id": "33333333-3333-3333-3333-333333333333",
"type": "relation",
"constraint": "fk_invoice_line_invoice",
"source_type": "invoice_line",
"source_columns": [
"invoice_id"
],
"destination_type": "invoice",
"destination_columns": [
"id"
]
}
],
"types": [
{
"name": "entity",
"hierarchy": [
"entity"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived",
"created_at"
]
},
"field_types": {
"id": "uuid",
"archived": "boolean",
"created_at": "timestamptz",
"type": "text"
},
"schemas": {
"entity": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"created": {
"type": "boolean"
}
}
}
},
"fields": [
"id",
"type",
"archived",
"created_at"
],
"variations": [
"entity",
"invoice",
"invoice_line"
]
},
{
"name": "invoice",
"schemas": {
"invoice": {
"type": "entity",
"properties": {
"total": {
"type": "number"
},
"lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
},
"metadata_line": {
"type": "invoice_line"
},
"metadata_lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
},
"metadata_nested_line": {
"type": "object",
"properties": {
"line": {
"type": "invoice_line"
}
}
},
"metadata_nested_lines": {
"type": "object",
"properties": {
"lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
}
}
}
}
}
},
"hierarchy": [
"invoice",
"entity"
],
"fields": [
"id",
"type",
"total",
"metadata_line",
"metadata_lines",
"metadata_nested_line",
"metadata_nested_lines",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
],
"grouped_fields": {
"invoice": [
"id",
"type",
"total",
"metadata_line",
"metadata_lines",
"metadata_nested_line",
"metadata_nested_lines"
],
"entity": [
"id",
"type",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
]
},
"lookup_fields": [
"id"
],
"historical": true,
"relationship": false,
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"total": "numeric",
"metadata_line": "jsonb",
"metadata_lines": "jsonb",
"metadata_nested_line": "jsonb",
"metadata_nested_lines": "jsonb",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid"
},
"variations": [
"invoice"
]
},
{
"name": "invoice_line",
"schemas": {
"invoice_line": {
"type": "entity",
"properties": {
"invoice_id": {
"type": "string"
},
"price": {
"type": "number"
}
}
}
},
"hierarchy": [
"invoice_line",
"entity"
],
"fields": [
"id",
"type",
"invoice_id",
"price",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
],
"grouped_fields": {
"invoice_line": [
"id",
"type",
"invoice_id",
"price"
],
"entity": [
"id",
"type",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
]
},
"lookup_fields": [],
"historical": true,
"relationship": false,
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"invoice_id": "uuid",
"price": "numeric",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid"
},
"variations": [
"invoice_line"
]
}
]
},
"tests": [
{
"description": "Assert no JSONB paths promoted",
"action": "compile",
"expect": {
"success": true,
"schemas": {
"entity": {},
"entity.filter": {},
"invoice": {},
"invoice.filter": {},
"invoice_line": {},
"invoice_line.filter": {}
}
}
}
]
}
]

View File

@ -2,16 +2,20 @@
{
"description": "single dependency (required)",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema1",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
"name": "schema1",
"schemas": {
"schema1": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
}
}
}
]
},
@ -93,14 +97,18 @@
{
"description": "empty dependents",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema2",
"dependencies": {
"bar": []
},
"extensible": true
"name": "schema2",
"schemas": {
"schema2": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependencies": {
"bar": []
},
"extensible": true
}
}
}
]
},
@ -139,17 +147,21 @@
{
"description": "multiple dependents required",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema3",
"dependencies": {
"quux": [
"foo",
"bar"
]
},
"extensible": true
"name": "schema3",
"schemas": {
"schema3": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependencies": {
"quux": [
"foo",
"bar"
]
},
"extensible": true
}
}
}
]
},
@ -228,19 +240,23 @@
{
"description": "dependencies with escaped characters",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema4",
"dependencies": {
"foo\nbar": [
"foo\rbar"
],
"foo\"bar": [
"foo'bar"
]
},
"extensible": true
"name": "schema4",
"schemas": {
"schema4": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependencies": {
"foo\nbar": [
"foo\rbar"
],
"foo\"bar": [
"foo'bar"
]
},
"extensible": true
}
}
}
]
},
@ -297,16 +313,20 @@
{
"description": "extensible: true allows extra properties in dependentRequired",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema5",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
"name": "schema5",
"schemas": {
"schema5": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
}
}
}
]
},
@ -329,22 +349,26 @@
{
"description": "single dependency (schemas, STRICT)",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema1",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"name": "schema_schema1",
"schemas": {
"schema_schema1": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "integer"
},
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"type": "integer"
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
}
}
@ -451,27 +475,31 @@
{
"description": "single dependency (schemas, EXTENSIBLE)",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema2",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"name": "schema_schema2",
"schemas": {
"schema_schema2": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "integer"
},
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"type": "integer"
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
}
},
"extensible": true
}
},
"extensible": true
}
}
]
},
@ -492,17 +520,21 @@
{
"description": "boolean subschemas",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema3",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"foo": true,
"bar": false
"name": "schema_schema3",
"schemas": {
"schema_schema3": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"foo": true,
"bar": false
}
}
}
}
]
@ -556,26 +588,30 @@
{
"description": "dependencies with escaped characters",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema4",
"properties": {
"foo\tbar": true,
"foo'bar": true,
"a": true,
"b": true,
"c": true
},
"dependencies": {
"foo\tbar": {
"minProperties": 4,
"extensible": true
},
"foo'bar": {
"required": [
"foo\"bar"
]
"name": "schema_schema4",
"schemas": {
"schema_schema4": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo\tbar": true,
"foo'bar": true,
"a": true,
"b": true,
"c": true
},
"dependencies": {
"foo\tbar": {
"minProperties": 4,
"extensible": true
},
"foo'bar": {
"required": [
"foo\"bar"
]
}
}
}
}
}
@ -637,18 +673,22 @@
{
"description": "dependent subschema incompatible with root (STRICT)",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema5",
"properties": {
"foo": {},
"baz": true
},
"dependencies": {
"foo": {
"name": "schema_schema5",
"schemas": {
"schema_schema5": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"bar": {}
"foo": {},
"baz": true
},
"dependencies": {
"foo": {
"properties": {
"bar": {}
}
}
}
}
}
@ -711,23 +751,27 @@
{
"description": "dependent subschema incompatible with root (EXTENSIBLE)",
"database": {
"schemas": [
"types": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema6",
"properties": {
"foo": {},
"baz": true
},
"dependencies": {
"foo": {
"name": "schema_schema6",
"schemas": {
"schema_schema6": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"bar": {}
"foo": {},
"baz": true
},
"additionalProperties": false
"dependencies": {
"foo": {
"properties": {
"bar": {}
},
"additionalProperties": false
}
},
"extensible": true
}
},
"extensible": true
}
}
]
},

155
fixtures/dynamicType.json Normal file
View File

@ -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"
}
}
]
}
}
]
}
]

View File

@ -2,42 +2,46 @@
{
"description": "empty string is valid for all types (except const)",
"database": {
"schemas": [
"types": [
{
"properties": {
"obj": {
"type": "object"
},
"arr": {
"type": "array"
},
"str": {
"type": "string"
},
"int": {
"type": "integer"
},
"num": {
"type": "number"
},
"bool": {
"type": "boolean"
},
"nul": {
"type": "null"
},
"fmt": {
"type": "string",
"format": "uuid"
},
"con": {
"const": "value"
},
"con_empty": {
"const": ""
"name": "emptyString_0_0",
"schemas": {
"emptyString_0_0": {
"properties": {
"obj": {
"type": "object"
},
"arr": {
"type": "array"
},
"str": {
"type": "string"
},
"int": {
"type": "integer"
},
"num": {
"type": "number"
},
"bool": {
"type": "boolean"
},
"nul": {
"type": "null"
},
"fmt": {
"type": "string",
"format": "uuid"
},
"con": {
"const": "value"
},
"con_empty": {
"const": ""
}
}
}
},
"$id": "emptyString_0_0"
}
}
]
},
@ -142,7 +146,9 @@
"errors": [
{
"code": "CONST_VIOLATED",
"details": { "path": "con" }
"details": {
"path": "con"
}
}
]
}

View File

@ -2,14 +2,18 @@
{
"description": "simple enum validation",
"database": {
"schemas": [
"types": [
{
"enum": [
1,
2,
3
],
"$id": "enum_0_0"
"name": "enum_0_0",
"schemas": {
"enum_0_0": {
"enum": [
1,
2,
3
]
}
}
}
]
},
@ -37,21 +41,25 @@
{
"description": "heterogeneous enum validation",
"database": {
"schemas": [
"types": [
{
"enum": [
6,
"foo",
[],
true,
{
"foo": 12
"name": "enum_1_0",
"schemas": {
"enum_1_0": {
"enum": [
6,
"foo",
[],
true,
{
"foo": 12
}
],
"properties": {
"foo": {}
}
}
],
"properties": {
"foo": {}
},
"$id": "enum_1_0"
}
}
]
},
@ -113,13 +121,17 @@
{
"description": "heterogeneous enum-with-null validation",
"database": {
"schemas": [
"types": [
{
"enum": [
6,
null
],
"$id": "enum_2_0"
"name": "enum_2_0",
"schemas": {
"enum_2_0": {
"enum": [
6,
null
]
}
}
}
]
},
@ -156,25 +168,29 @@
{
"description": "enums in properties",
"database": {
"schemas": [
"types": [
{
"type": "object",
"properties": {
"foo": {
"enum": [
"foo"
]
},
"bar": {
"enum": [
"name": "enum_3_0",
"schemas": {
"enum_3_0": {
"type": "object",
"properties": {
"foo": {
"enum": [
"foo"
]
},
"bar": {
"enum": [
"bar"
]
}
},
"required": [
"bar"
]
}
},
"required": [
"bar"
],
"$id": "enum_3_0"
}
}
]
},
@ -251,13 +267,17 @@
{
"description": "enum with escaped characters",
"database": {
"schemas": [
"types": [
{
"enum": [
"foo\nbar",
"foo\rbar"
],
"$id": "enum_4_0"
"name": "enum_4_0",
"schemas": {
"enum_4_0": {
"enum": [
"foo\nbar",
"foo\rbar"
]
}
}
}
]
},
@ -294,12 +314,16 @@
{
"description": "enum with false does not match 0",
"database": {
"schemas": [
"types": [
{
"enum": [
false
],
"$id": "enum_5_0"
"name": "enum_5_0",
"schemas": {
"enum_5_0": {
"enum": [
false
]
}
}
}
]
},
@ -336,14 +360,18 @@
{
"description": "enum with [false] does not match [0]",
"database": {
"schemas": [
"types": [
{
"enum": [
[
false
]
],
"$id": "enum_6_0"
"name": "enum_6_0",
"schemas": {
"enum_6_0": {
"enum": [
[
false
]
]
}
}
}
]
},
@ -386,12 +414,16 @@
{
"description": "enum with true does not match 1",
"database": {
"schemas": [
"types": [
{
"enum": [
true
],
"$id": "enum_7_0"
"name": "enum_7_0",
"schemas": {
"enum_7_0": {
"enum": [
true
]
}
}
}
]
},
@ -428,14 +460,18 @@
{
"description": "enum with [true] does not match [1]",
"database": {
"schemas": [
"types": [
{
"enum": [
[
true
]
],
"$id": "enum_8_0"
"name": "enum_8_0",
"schemas": {
"enum_8_0": {
"enum": [
[
true
]
]
}
}
}
]
},
@ -478,12 +514,16 @@
{
"description": "enum with 0 does not match false",
"database": {
"schemas": [
"types": [
{
"enum": [
0
],
"$id": "enum_9_0"
"name": "enum_9_0",
"schemas": {
"enum_9_0": {
"enum": [
0
]
}
}
}
]
},
@ -520,14 +560,18 @@
{
"description": "enum with [0] does not match [false]",
"database": {
"schemas": [
"types": [
{
"enum": [
[
0
]
],
"$id": "enum_10_0"
"name": "enum_10_0",
"schemas": {
"enum_10_0": {
"enum": [
[
0
]
]
}
}
}
]
},
@ -570,12 +614,16 @@
{
"description": "enum with 1 does not match true",
"database": {
"schemas": [
"types": [
{
"enum": [
1
],
"$id": "enum_11_0"
"name": "enum_11_0",
"schemas": {
"enum_11_0": {
"enum": [
1
]
}
}
}
]
},
@ -612,14 +660,18 @@
{
"description": "enum with [1] does not match [true]",
"database": {
"schemas": [
"types": [
{
"enum": [
[
1
]
],
"$id": "enum_12_0"
"name": "enum_12_0",
"schemas": {
"enum_12_0": {
"enum": [
[
1
]
]
}
}
}
]
},
@ -662,12 +714,16 @@
{
"description": "nul characters in strings",
"database": {
"schemas": [
"types": [
{
"enum": [
"hello\u0000there"
],
"$id": "enum_13_0"
"name": "enum_13_0",
"schemas": {
"enum_13_0": {
"enum": [
"hello\u0000there"
]
}
}
}
]
},
@ -695,15 +751,19 @@
{
"description": "extensible: true allows extra properties in enum object match",
"database": {
"schemas": [
"types": [
{
"enum": [
{
"foo": 1
"name": "enum_14_0",
"schemas": {
"enum_14_0": {
"enum": [
{
"foo": 1
}
],
"extensible": true
}
],
"extensible": true,
"$id": "enum_14_0"
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "exclusiveMaximum validation",
"database": {
"schemas": [
"types": [
{
"exclusiveMaximum": 3,
"$id": "exclusiveMaximum_0_0"
"name": "exclusiveMaximum_0_0",
"schemas": {
"exclusiveMaximum_0_0": {
"exclusiveMaximum": 3
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "exclusiveMinimum validation",
"database": {
"schemas": [
"types": [
{
"exclusiveMinimum": 1.1,
"$id": "exclusiveMinimum_0_0"
"name": "exclusiveMinimum_0_0",
"schemas": {
"exclusiveMinimum_0_0": {
"exclusiveMinimum": 1.1
}
}
}
]
},

463
fixtures/filter.json Normal file
View File

@ -0,0 +1,463 @@
[
{
"description": "Filter Synthesis Object-Oriented Composition",
"database": {
"puncs": [],
"enums": [
{
"id": "enum1",
"name": "gender",
"module": "core",
"source": "gender",
"schemas": {
"gender": {
"type": "string",
"enum": [
"male",
"female",
"other"
]
}
}
}
],
"relations": [
{
"id": "rel1",
"type": "relation",
"constraint": "fk_person_billing_address",
"source_type": "person",
"source_columns": [
"billing_address_id"
],
"destination_type": "address",
"destination_columns": [
"id"
],
"prefix": "billing_address"
}
],
"types": [
{
"id": "type1",
"type": "type",
"name": "person",
"module": "core",
"source": "person",
"hierarchy": [
"person"
],
"variations": [
"person"
],
"schemas": {
"person": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"age": {
"type": "integer"
},
"billing_address": {
"type": "address"
},
"gender": {
"type": "gender"
},
"birth_date": {
"type": "string",
"format": "date-time"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ad_hoc": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
}
},
{
"id": "type2",
"type": "type",
"name": "address",
"module": "core",
"source": "address",
"hierarchy": [
"address"
],
"variations": [
"address"
],
"schemas": {
"address": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
}
}
}
},
{
"id": "type3",
"type": "type",
"name": "search",
"module": "core",
"source": "search",
"hierarchy": [
"search"
],
"variations": [
"search"
],
"schemas": {
"search": {
"type": "object",
"properties": {
"kind": {
"type": "string"
},
"name": {
"type": "string"
},
"filter": {
"type": "$kind.filter"
}
}
},
"condition": {
"type": "object",
"properties": {
"kind": {
"type": "string"
}
}
},
"string.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"string",
"null"
]
}
}
},
"integer.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"integer",
"null"
]
}
}
},
"date.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"string",
"null"
]
}
}
}
}
}
]
},
"tests": [
{
"description": "Assert filter generation map accurately represents strongly typed conditions natively.",
"action": "compile",
"expect": {
"success": true,
"schemas": {
"gender": {},
"gender.condition": {
"type": "condition",
"compiledPropertyNames": [
"$eq",
"$ne",
"$nof",
"$of",
"kind"
],
"properties": {
"$eq": {
"type": [
"gender",
"null"
]
},
"$ne": {
"type": [
"gender",
"null"
]
},
"$nof": {
"type": [
"array",
"null"
],
"items": {
"type": "gender"
}
},
"$of": {
"type": [
"array",
"null"
],
"items": {
"type": "gender"
}
}
}
},
"person": {},
"person.filter": {
"compiledPropertyNames": [
"$and",
"$or",
"ad_hoc",
"age",
"billing_address",
"birth_date",
"first_name",
"gender",
"tags"
],
"properties": {
"$and": {
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"ad_hoc",
"age",
"billing_address",
"birth_date",
"first_name",
"gender",
"tags"
],
"type": "person.filter"
},
"type": [
"array",
"null"
]
},
"$or": {
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"ad_hoc",
"age",
"billing_address",
"birth_date",
"first_name",
"gender",
"tags"
],
"type": "person.filter"
},
"type": [
"array",
"null"
]
},
"ad_hoc": {
"compiledPropertyNames": [
"foo"
],
"properties": {
"foo": {
"type": [
"string.condition",
"null"
]
}
},
"type": [
"object",
"null"
]
},
"age": {
"type": [
"integer.condition",
"null"
]
},
"billing_address": {
"type": [
"address.filter",
"null"
]
},
"birth_date": {
"type": [
"date.condition",
"null"
]
},
"first_name": {
"type": [
"string.condition",
"null"
]
},
"gender": {
"type": [
"gender.condition",
"null"
]
},
"tags": {
"type": [
"string.condition",
"null"
]
}
},
"type": "object"
},
"address": {},
"address.filter": {
"type": "object",
"compiledPropertyNames": [
"$and",
"$or",
"city"
],
"properties": {
"$and": {
"type": [
"array",
"null"
],
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"city"
],
"type": "address.filter"
}
},
"$or": {
"type": [
"array",
"null"
],
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"city"
],
"type": "address.filter"
}
},
"city": {
"type": [
"string.condition",
"null"
]
}
}
},
"condition": {},
"string.condition": {},
"integer.condition": {},
"date.condition": {},
"search": {},
"search.filter": {
"type": "object",
"compiledPropertyNames": [
"$and",
"$or",
"filter",
"kind",
"name"
],
"properties": {
"$and": {
"type": [
"array",
"null"
],
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"filter",
"kind",
"name"
],
"type": "search.filter"
}
},
"$or": {
"type": [
"array",
"null"
],
"items": {
"compiledPropertyNames": [
"$and",
"$or",
"filter",
"kind",
"name"
],
"type": "search.filter"
}
},
"filter": {
"type": [
"$kind.filter.filter",
"null"
]
},
"kind": {
"type": [
"string.condition",
"null"
]
},
"name": {
"type": [
"string.condition",
"null"
]
}
}
}
}
}
}
]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -5,15 +5,18 @@
"puncs": [
{
"name": "get_invoice",
"schemas": [
{
"$id": "get_invoice.response",
"schemas": {
"get_invoice.response": {
"oneOf": [
{ "type": "invoice" },
{ "type": "null" }
{
"type": "invoice"
},
{
"type": "null"
}
]
}
]
}
}
],
"enums": [],
@ -23,32 +26,64 @@
"type": "relation",
"constraint": "fk_attachment_attachable_entity",
"source_type": "attachment",
"source_columns": ["attachable_id", "attachable_type"],
"source_columns": [
"attachable_id",
"attachable_type"
],
"destination_type": "entity",
"destination_columns": ["id", "type"],
"destination_columns": [
"id",
"type"
],
"prefix": null
}
],
"types": [
{
"name": "entity",
"schemas": [
{
"$id": "entity",
"schemas": {
"entity": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"type": { "type": "string" },
"archived": { "type": "boolean" },
"created_at": { "type": "string", "format": "date-time" }
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"created_at": {
"type": "string",
"format": "date-time"
}
}
}
},
"hierarchy": [
"entity"
],
"variations": [
"entity",
"activity",
"invoice",
"attachment"
],
"fields": [
"id",
"type",
"archived",
"created_at"
],
"hierarchy": ["entity"],
"variations": ["entity", "activity", "invoice", "attachment"],
"fields": ["id", "type", "archived", "created_at"],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"]
"entity": [
"id",
"type",
"archived",
"created_at"
]
},
"field_types": {
"id": "uuid",
@ -63,21 +98,42 @@
},
{
"name": "activity",
"schemas": [
{
"$id": "activity",
"schemas": {
"activity": {
"type": "entity",
"properties": {
"start_date": { "type": "string", "format": "date-time" }
"start_date": {
"type": "string",
"format": "date-time"
}
}
}
},
"hierarchy": [
"activity",
"entity"
],
"variations": [
"activity",
"invoice"
],
"fields": [
"id",
"type",
"archived",
"created_at",
"start_date"
],
"hierarchy": ["activity", "entity"],
"variations": ["activity", "invoice"],
"fields": ["id", "type", "archived", "created_at", "start_date"],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"],
"activity": ["start_date"]
"entity": [
"id",
"type",
"archived",
"created_at"
],
"activity": [
"start_date"
]
},
"field_types": {
"id": "uuid",
@ -93,26 +149,51 @@
},
{
"name": "invoice",
"schemas": [
{
"$id": "invoice",
"schemas": {
"invoice": {
"type": "activity",
"properties": {
"status": { "type": "string" },
"status": {
"type": "string"
},
"attachments": {
"type": "array",
"items": { "type": "attachment" }
"items": {
"type": "attachment"
}
}
}
}
},
"hierarchy": [
"invoice",
"activity",
"entity"
],
"variations": [
"invoice"
],
"fields": [
"id",
"type",
"archived",
"created_at",
"start_date",
"status"
],
"hierarchy": ["invoice", "activity", "entity"],
"variations": ["invoice"],
"fields": ["id", "type", "archived", "created_at", "start_date", "status"],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"],
"activity": ["start_date"],
"invoice": ["status"]
"entity": [
"id",
"type",
"archived",
"created_at"
],
"activity": [
"start_date"
],
"invoice": [
"status"
]
},
"field_types": {
"id": "uuid",
@ -129,26 +210,66 @@
},
{
"name": "attachment",
"schemas": [
{
"$id": "attachment",
"schemas": {
"attachment": {
"type": "entity",
"properties": {
"name": { "type": "string" },
"attachable_id": { "type": "string", "format": "uuid" },
"attachable_type": { "type": "string" },
"kind": { "type": "string" },
"file": { "type": "string" },
"data": { "type": "object" }
"name": {
"type": "string"
},
"attachable_id": {
"type": "string",
"format": "uuid"
},
"attachable_type": {
"type": "string"
},
"kind": {
"type": "string"
},
"file": {
"type": "string"
},
"data": {
"type": "object"
}
}
}
},
"hierarchy": [
"attachment",
"entity"
],
"variations": [
"attachment"
],
"fields": [
"id",
"type",
"archived",
"created_at",
"attachable_id",
"attachable_type",
"kind",
"file",
"data",
"name"
],
"hierarchy": ["attachment", "entity"],
"variations": ["attachment"],
"fields": ["id", "type", "archived", "created_at", "attachable_id", "attachable_type", "kind", "file", "data", "name"],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"],
"attachment": ["attachable_id", "attachable_type", "kind", "file", "data", "name"]
"entity": [
"id",
"type",
"archived",
"created_at"
],
"attachment": [
"attachable_id",
"attachable_type",
"kind",
"file",
"data",
"name"
]
},
"field_types": {
"id": "uuid",

View File

@ -2,12 +2,16 @@
{
"description": "a schema given for items",
"database": {
"schemas": [
"types": [
{
"items": {
"type": "integer"
},
"$id": "items_0_0"
"name": "items_0_0",
"schemas": {
"items_0_0": {
"items": {
"type": "integer"
}
}
}
}
]
},
@ -65,10 +69,14 @@
{
"description": "items with boolean schema (true)",
"database": {
"schemas": [
"types": [
{
"items": true,
"$id": "items_1_0"
"name": "items_1_0",
"schemas": {
"items_1_0": {
"items": true
}
}
}
]
},
@ -100,10 +108,14 @@
{
"description": "items with boolean schema (false)",
"database": {
"schemas": [
"types": [
{
"items": false,
"$id": "items_2_0"
"name": "items_2_0",
"schemas": {
"items_2_0": {
"items": false
}
}
}
]
},
@ -135,42 +147,54 @@
{
"description": "items and subitems",
"database": {
"schemas": [
"types": [
{
"type": "array",
"items": false,
"prefixItems": [
{
"type": "item"
},
{
"type": "item"
},
{
"type": "item"
"name": "items_3_0",
"schemas": {
"items_3_0": {
"type": "array",
"items": false,
"prefixItems": [
{
"type": "item"
},
{
"type": "item"
},
{
"type": "item"
}
]
}
],
"$id": "items_3_0"
}
},
{
"$id": "item",
"type": "array",
"items": false,
"prefixItems": [
{
"type": "sub-item"
},
{
"type": "sub-item"
"name": "item",
"schemas": {
"item": {
"type": "array",
"items": false,
"prefixItems": [
{
"type": "sub-item"
},
{
"type": "sub-item"
}
]
}
]
}
},
{
"$id": "sub-item",
"type": "object",
"required": [
"foo"
]
"name": "sub-item",
"schemas": {
"sub-item": {
"type": "object",
"required": [
"foo"
]
}
}
}
]
},
@ -374,22 +398,26 @@
{
"description": "nested items",
"database": {
"schemas": [
"types": [
{
"type": "array",
"items": {
"type": "array",
"items": {
"name": "items_4_0",
"schemas": {
"items_4_0": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
}
},
"$id": "items_4_0"
}
}
]
},
@ -507,15 +535,19 @@
{
"description": "prefixItems with no additional items allowed",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{},
{},
{}
],
"items": false,
"$id": "items_5_0"
"name": "items_5_0",
"schemas": {
"items_5_0": {
"prefixItems": [
{},
{},
{}
],
"items": false
}
}
}
]
},
@ -584,21 +616,25 @@
{
"description": "items does not look in applicators, valid case",
"database": {
"schemas": [
"types": [
{
"allOf": [
{
"prefixItems": [
"name": "items_6_0",
"schemas": {
"items_6_0": {
"allOf": [
{
"minimum": 3
"prefixItems": [
{
"minimum": 3
}
]
}
]
],
"items": {
"minimum": 5
}
}
],
"items": {
"minimum": 5
},
"$id": "items_6_0"
}
}
]
},
@ -632,17 +668,21 @@
{
"description": "prefixItems validation adjusts the starting index for items",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "string"
"name": "items_7_0",
"schemas": {
"items_7_0": {
"prefixItems": [
{
"type": "string"
}
],
"items": {
"type": "integer"
}
}
],
"items": {
"type": "integer"
},
"$id": "items_7_0"
}
}
]
},
@ -677,13 +717,17 @@
{
"description": "items with heterogeneous array",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{}
],
"items": false,
"$id": "items_8_0"
"name": "items_8_0",
"schemas": {
"items_8_0": {
"prefixItems": [
{}
],
"items": false
}
}
}
]
},
@ -717,12 +761,16 @@
{
"description": "items with null instance elements",
"database": {
"schemas": [
"types": [
{
"items": {
"type": "null"
},
"$id": "items_9_0"
"name": "items_9_0",
"schemas": {
"items_9_0": {
"items": {
"type": "null"
}
}
}
}
]
},
@ -743,11 +791,15 @@
{
"description": "extensible: true allows extra items (when items is false)",
"database": {
"schemas": [
"types": [
{
"items": false,
"extensible": true,
"$id": "items_10_0"
"name": "items_10_0",
"schemas": {
"items_10_0": {
"items": false,
"extensible": true
}
}
}
]
},
@ -768,13 +820,17 @@
{
"description": "extensible: true allows extra properties for items",
"database": {
"schemas": [
"types": [
{
"items": {
"minimum": 5
},
"extensible": true,
"$id": "items_11_0"
"name": "items_11_0",
"schemas": {
"items_11_0": {
"items": {
"minimum": 5
},
"extensible": true
}
}
}
]
},
@ -807,11 +863,15 @@
{
"description": "array: simple extensible array",
"database": {
"schemas": [
"types": [
{
"type": "array",
"extensible": true,
"$id": "items_12_0"
"name": "items_12_0",
"schemas": {
"items_12_0": {
"type": "array",
"extensible": true
}
}
}
]
},
@ -842,11 +902,15 @@
{
"description": "array: strict array",
"database": {
"schemas": [
"types": [
{
"type": "array",
"extensible": false,
"$id": "items_13_0"
"name": "items_13_0",
"schemas": {
"items_13_0": {
"type": "array",
"extensible": false
}
}
}
]
},
@ -876,13 +940,17 @@
{
"description": "array: items extensible",
"database": {
"schemas": [
"types": [
{
"type": "array",
"items": {
"extensible": true
},
"$id": "items_14_0"
"name": "items_14_0",
"schemas": {
"items_14_0": {
"type": "array",
"items": {
"extensible": true
}
}
}
}
]
},
@ -914,14 +982,18 @@
{
"description": "array: items strict",
"database": {
"schemas": [
"types": [
{
"type": "array",
"items": {
"type": "object",
"extensible": false
},
"$id": "items_15_0"
"name": "items_15_0",
"schemas": {
"items_15_0": {
"type": "array",
"items": {
"type": "object",
"extensible": false
}
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "maxContains without contains is ignored",
"database": {
"schemas": [
"types": [
{
"maxContains": 1,
"extensible": true,
"$id": "maxContains_0_0"
"name": "maxContains_0_0",
"schemas": {
"maxContains_0_0": {
"maxContains": 1,
"extensible": true
}
}
}
]
},
@ -39,14 +43,18 @@
{
"description": "maxContains with contains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_1_0"
"name": "maxContains_1_0",
"schemas": {
"maxContains_1_0": {
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true
}
}
}
]
},
@ -113,14 +121,18 @@
{
"description": "maxContains with contains, value with a decimal",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_2_0"
"name": "maxContains_2_0",
"schemas": {
"maxContains_2_0": {
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true
}
}
}
]
},
@ -153,15 +165,19 @@
{
"description": "minContains < maxContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 1,
"maxContains": 3,
"extensible": true,
"$id": "maxContains_3_0"
"name": "maxContains_3_0",
"schemas": {
"maxContains_3_0": {
"contains": {
"const": 1
},
"minContains": 1,
"maxContains": 3,
"extensible": true
}
}
}
]
},
@ -206,14 +222,18 @@
{
"description": "extensible: true allows non-matching items in maxContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_4_0"
"name": "maxContains_4_0",
"schemas": {
"maxContains_4_0": {
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "maxItems validation",
"database": {
"schemas": [
"types": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_0_0"
"name": "maxItems_0_0",
"schemas": {
"maxItems_0_0": {
"maxItems": 2,
"extensible": true
}
}
}
]
},
@ -61,11 +65,15 @@
{
"description": "maxItems validation with a decimal",
"database": {
"schemas": [
"types": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_1_0"
"name": "maxItems_1_0",
"schemas": {
"maxItems_1_0": {
"maxItems": 2,
"extensible": true
}
}
}
]
},
@ -99,11 +107,15 @@
{
"description": "extensible: true allows extra items in maxItems (but counted)",
"database": {
"schemas": [
"types": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_2_0"
"name": "maxItems_2_0",
"schemas": {
"maxItems_2_0": {
"maxItems": 2,
"extensible": true
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "maxLength validation",
"database": {
"schemas": [
"types": [
{
"maxLength": 2,
"$id": "maxLength_0_0"
"name": "maxLength_0_0",
"schemas": {
"maxLength_0_0": {
"maxLength": 2
}
}
}
]
},
@ -48,7 +52,7 @@
},
{
"description": "two graphemes is long enough",
"data": "💩💩",
"data": "\ud83d\udca9\ud83d\udca9",
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
@ -60,10 +64,14 @@
{
"description": "maxLength validation with a decimal",
"database": {
"schemas": [
"types": [
{
"maxLength": 2,
"$id": "maxLength_1_0"
"name": "maxLength_1_0",
"schemas": {
"maxLength_1_0": {
"maxLength": 2
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "maxProperties validation",
"database": {
"schemas": [
"types": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_0_0"
"name": "maxProperties_0_0",
"schemas": {
"maxProperties_0_0": {
"maxProperties": 2,
"extensible": true
}
}
}
]
},
@ -83,11 +87,15 @@
{
"description": "maxProperties validation with a decimal",
"database": {
"schemas": [
"types": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_1_0"
"name": "maxProperties_1_0",
"schemas": {
"maxProperties_1_0": {
"maxProperties": 2,
"extensible": true
}
}
}
]
},
@ -121,11 +129,15 @@
{
"description": "maxProperties = 0 means the object is empty",
"database": {
"schemas": [
"types": [
{
"maxProperties": 0,
"extensible": true,
"$id": "maxProperties_2_0"
"name": "maxProperties_2_0",
"schemas": {
"maxProperties_2_0": {
"maxProperties": 0,
"extensible": true
}
}
}
]
},
@ -155,11 +167,15 @@
{
"description": "extensible: true allows extra properties in maxProperties (though maxProperties still counts them!)",
"database": {
"schemas": [
"types": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_3_0"
"name": "maxProperties_3_0",
"schemas": {
"maxProperties_3_0": {
"maxProperties": 2,
"extensible": true
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "maximum validation",
"database": {
"schemas": [
"types": [
{
"maximum": 3,
"$id": "maximum_0_0"
"name": "maximum_0_0",
"schemas": {
"maximum_0_0": {
"maximum": 3
}
}
}
]
},
@ -51,10 +55,14 @@
{
"description": "maximum validation with unsigned integer",
"database": {
"schemas": [
"types": [
{
"maximum": 300,
"$id": "maximum_1_0"
"name": "maximum_1_0",
"schemas": {
"maximum_1_0": {
"maximum": 300
}
}
}
]
},

View File

@ -2,23 +2,31 @@
{
"description": "merging: properties accumulate",
"database": {
"schemas": [
"types": [
{
"$id": "base_0",
"properties": {
"base_prop": {
"type": "string"
"name": "base_0",
"schemas": {
"base_0": {
"properties": {
"base_prop": {
"type": "string"
}
}
}
}
},
{
"type": "base_0",
"properties": {
"child_prop": {
"type": "string"
"name": "merge_0_0",
"schemas": {
"merge_0_0": {
"type": "base_0",
"properties": {
"child_prop": {
"type": "string"
}
}
}
},
"$id": "merge_0_0"
}
}
]
},
@ -48,7 +56,9 @@
"errors": [
{
"code": "INVALID_TYPE",
"details": { "path": "base_prop" }
"details": {
"path": "base_prop"
}
}
]
}
@ -58,29 +68,37 @@
{
"description": "merging: required fields accumulate",
"database": {
"schemas": [
"types": [
{
"$id": "base_1",
"properties": {
"a": {
"type": "string"
"name": "base_1",
"schemas": {
"base_1": {
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
}
},
"required": [
"a"
]
}
},
{
"type": "base_1",
"properties": {
"b": {
"type": "string"
"name": "merge_1_0",
"schemas": {
"merge_1_0": {
"type": "base_1",
"properties": {
"b": {
"type": "string"
}
},
"required": [
"b"
]
}
},
"required": [
"b"
],
"$id": "merge_1_0"
}
}
]
},
@ -109,7 +127,9 @@
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"details": { "path": "a" }
"details": {
"path": "a"
}
}
]
}
@ -126,7 +146,9 @@
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"details": { "path": "b" }
"details": {
"path": "b"
}
}
]
}
@ -136,36 +158,44 @@
{
"description": "merging: dependencies accumulate",
"database": {
"schemas": [
"types": [
{
"$id": "base_2",
"properties": {
"trigger": {
"type": "string"
},
"base_dep": {
"type": "string"
"name": "base_2",
"schemas": {
"base_2": {
"properties": {
"trigger": {
"type": "string"
},
"base_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"base_dep"
]
}
}
},
"dependencies": {
"trigger": [
"base_dep"
]
}
},
{
"type": "base_2",
"properties": {
"child_dep": {
"type": "string"
"name": "merge_2_0",
"schemas": {
"merge_2_0": {
"type": "base_2",
"properties": {
"child_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"child_dep"
]
}
}
},
"dependencies": {
"trigger": [
"child_dep"
]
},
"$id": "merge_2_0"
}
}
]
},
@ -196,7 +226,9 @@
"errors": [
{
"code": "DEPENDENCY_MISSING",
"details": { "path": "" }
"details": {
"path": ""
}
}
]
}
@ -214,7 +246,9 @@
"errors": [
{
"code": "DEPENDENCY_MISSING",
"details": { "path": "" }
"details": {
"path": ""
}
}
]
}
@ -224,33 +258,41 @@
{
"description": "merging: form and display do NOT merge",
"database": {
"schemas": [
"types": [
{
"$id": "base_3",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
"name": "base_3",
"schemas": {
"base_3": {
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
}
},
"form": [
"a",
"b"
]
}
},
"form": [
"a",
"b"
]
}
},
{
"type": "base_3",
"properties": {
"c": {
"type": "string"
"name": "merge_3_0",
"schemas": {
"merge_3_0": {
"type": "base_3",
"properties": {
"c": {
"type": "string"
}
},
"form": [
"c"
]
}
},
"form": [
"c"
],
"$id": "merge_3_0"
}
}
]
},

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,15 @@
{
"description": "minContains without contains is ignored",
"database": {
"schemas": [
"types": [
{
"minContains": 1,
"extensible": true,
"$id": "minContains_0_0"
"name": "minContains_0_0",
"schemas": {
"minContains_0_0": {
"minContains": 1,
"extensible": true
}
}
}
]
},
@ -36,14 +40,18 @@
{
"description": "minContains=1 with contains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true,
"$id": "minContains_1_0"
"name": "minContains_1_0",
"schemas": {
"minContains_1_0": {
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true
}
}
}
]
},
@ -108,14 +116,18 @@
{
"description": "minContains=2 with contains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true,
"$id": "minContains_2_0"
"name": "minContains_2_0",
"schemas": {
"minContains_2_0": {
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true
}
}
}
]
},
@ -195,14 +207,18 @@
{
"description": "minContains=2 with contains with a decimal value",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true,
"$id": "minContains_3_0"
"name": "minContains_3_0",
"schemas": {
"minContains_3_0": {
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true
}
}
}
]
},
@ -235,15 +251,19 @@
{
"description": "maxContains = minContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"maxContains": 2,
"minContains": 2,
"extensible": true,
"$id": "minContains_4_0"
"name": "minContains_4_0",
"schemas": {
"minContains_4_0": {
"contains": {
"const": 1
},
"maxContains": 2,
"minContains": 2,
"extensible": true
}
}
}
]
},
@ -298,15 +318,19 @@
{
"description": "maxContains < minContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"minContains": 3,
"extensible": true,
"$id": "minContains_5_0"
"name": "minContains_5_0",
"schemas": {
"minContains_5_0": {
"contains": {
"const": 1
},
"maxContains": 1,
"minContains": 3,
"extensible": true
}
}
}
]
},
@ -361,14 +385,18 @@
{
"description": "minContains = 0",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 0,
"extensible": true,
"$id": "minContains_6_0"
"name": "minContains_6_0",
"schemas": {
"minContains_6_0": {
"contains": {
"const": 1
},
"minContains": 0,
"extensible": true
}
}
}
]
},
@ -398,15 +426,19 @@
{
"description": "minContains = 0 with maxContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 0,
"maxContains": 1,
"extensible": true,
"$id": "minContains_7_0"
"name": "minContains_7_0",
"schemas": {
"minContains_7_0": {
"contains": {
"const": 1
},
"minContains": 0,
"maxContains": 1,
"extensible": true
}
}
}
]
},
@ -448,14 +480,18 @@
{
"description": "extensible: true allows non-matching items in minContains",
"database": {
"schemas": [
"types": [
{
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true,
"$id": "minContains_8_0"
"name": "minContains_8_0",
"schemas": {
"minContains_8_0": {
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "minItems validation",
"database": {
"schemas": [
"types": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_0_0"
"name": "minItems_0_0",
"schemas": {
"minItems_0_0": {
"minItems": 1,
"extensible": true
}
}
}
]
},
@ -57,11 +61,15 @@
{
"description": "minItems validation with a decimal",
"database": {
"schemas": [
"types": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_1_0"
"name": "minItems_1_0",
"schemas": {
"minItems_1_0": {
"minItems": 1,
"extensible": true
}
}
}
]
},
@ -92,11 +100,15 @@
{
"description": "extensible: true allows extra items in minItems",
"database": {
"schemas": [
"types": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_2_0"
"name": "minItems_2_0",
"schemas": {
"minItems_2_0": {
"minItems": 1,
"extensible": true
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "minLength validation",
"database": {
"schemas": [
"types": [
{
"minLength": 2,
"$id": "minLength_0_0"
"name": "minLength_0_0",
"schemas": {
"minLength_0_0": {
"minLength": 2
}
}
}
]
},
@ -48,7 +52,7 @@
},
{
"description": "one grapheme is not long enough",
"data": "💩",
"data": "\ud83d\udca9",
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
@ -60,10 +64,14 @@
{
"description": "minLength validation with a decimal",
"database": {
"schemas": [
"types": [
{
"minLength": 2,
"$id": "minLength_1_0"
"name": "minLength_1_0",
"schemas": {
"minLength_1_0": {
"minLength": 2
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "minProperties validation",
"database": {
"schemas": [
"types": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_0_0"
"name": "minProperties_0_0",
"schemas": {
"minProperties_0_0": {
"minProperties": 1,
"extensible": true
}
}
}
]
},
@ -75,11 +79,15 @@
{
"description": "minProperties validation with a decimal",
"database": {
"schemas": [
"types": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_1_0"
"name": "minProperties_1_0",
"schemas": {
"minProperties_1_0": {
"minProperties": 1,
"extensible": true
}
}
}
]
},
@ -110,11 +118,15 @@
{
"description": "extensible: true allows extra properties in minProperties",
"database": {
"schemas": [
"types": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_2_0"
"name": "minProperties_2_0",
"schemas": {
"minProperties_2_0": {
"minProperties": 1,
"extensible": true
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "minimum validation",
"database": {
"schemas": [
"types": [
{
"minimum": 1.1,
"$id": "minimum_0_0"
"name": "minimum_0_0",
"schemas": {
"minimum_0_0": {
"minimum": 1.1
}
}
}
]
},
@ -51,10 +55,14 @@
{
"description": "minimum validation with signed integer",
"database": {
"schemas": [
"types": [
{
"minimum": -2,
"$id": "minimum_1_0"
"name": "minimum_1_0",
"schemas": {
"minimum_1_0": {
"minimum": -2
}
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "by int",
"database": {
"schemas": [
"types": [
{
"multipleOf": 2,
"$id": "multipleOf_0_0"
"name": "multipleOf_0_0",
"schemas": {
"multipleOf_0_0": {
"multipleOf": 2
}
}
}
]
},
@ -42,10 +46,14 @@
{
"description": "by number",
"database": {
"schemas": [
"types": [
{
"multipleOf": 1.5,
"$id": "multipleOf_1_0"
"name": "multipleOf_1_0",
"schemas": {
"multipleOf_1_0": {
"multipleOf": 1.5
}
}
}
]
},
@ -82,10 +90,14 @@
{
"description": "by small number",
"database": {
"schemas": [
"types": [
{
"multipleOf": 0.0001,
"$id": "multipleOf_2_0"
"name": "multipleOf_2_0",
"schemas": {
"multipleOf_2_0": {
"multipleOf": 0.0001
}
}
}
]
},
@ -113,11 +125,15 @@
{
"description": "small multiple of large integer",
"database": {
"schemas": [
"types": [
{
"type": "integer",
"multipleOf": 1e-8,
"$id": "multipleOf_3_0"
"name": "multipleOf_3_0",
"schemas": {
"multipleOf_3_0": {
"type": "integer",
"multipleOf": 1e-08
}
}
}
]
},

View File

@ -2,12 +2,16 @@
{
"description": "not",
"database": {
"schemas": [
"types": [
{
"not": {
"type": "integer"
},
"$id": "not_0_0"
"name": "not_0_0",
"schemas": {
"not_0_0": {
"not": {
"type": "integer"
}
}
}
}
]
},
@ -35,15 +39,19 @@
{
"description": "not multiple types",
"database": {
"schemas": [
"types": [
{
"not": {
"type": [
"integer",
"boolean"
]
},
"$id": "not_1_0"
"name": "not_1_0",
"schemas": {
"not_1_0": {
"not": {
"type": [
"integer",
"boolean"
]
}
}
}
}
]
},
@ -80,18 +88,22 @@
{
"description": "not more complex schema",
"database": {
"schemas": [
"types": [
{
"not": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
"name": "not_2_0",
"schemas": {
"not_2_0": {
"not": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
},
"extensible": true
}
},
"extensible": true,
"$id": "not_2_0"
}
}
]
},
@ -132,14 +144,18 @@
{
"description": "forbidden property",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"not": {}
"name": "not_3_0",
"schemas": {
"not_3_0": {
"properties": {
"foo": {
"not": {}
}
}
}
},
"$id": "not_3_0"
}
}
]
},
@ -170,10 +186,14 @@
{
"description": "forbid everything with empty schema",
"database": {
"schemas": [
"types": [
{
"not": {},
"$id": "not_4_0"
"name": "not_4_0",
"schemas": {
"not_4_0": {
"not": {}
}
}
}
]
},
@ -268,10 +288,14 @@
{
"description": "forbid everything with boolean schema true",
"database": {
"schemas": [
"types": [
{
"not": true,
"$id": "not_5_0"
"name": "not_5_0",
"schemas": {
"not_5_0": {
"not": true
}
}
}
]
},
@ -366,11 +390,15 @@
{
"description": "allow everything with boolean schema false",
"database": {
"schemas": [
"types": [
{
"not": false,
"extensible": true,
"$id": "not_6_0"
"name": "not_6_0",
"schemas": {
"not_6_0": {
"not": false,
"extensible": true
}
}
}
]
},
@ -465,12 +493,16 @@
{
"description": "double negation",
"database": {
"schemas": [
"types": [
{
"not": {
"not": {}
},
"$id": "not_7_0"
"name": "not_7_0",
"schemas": {
"not_7_0": {
"not": {
"not": {}
}
}
}
}
]
},
@ -489,13 +521,17 @@
{
"description": "extensible: true allows extra properties in not",
"database": {
"schemas": [
"types": [
{
"not": {
"type": "integer"
},
"extensible": true,
"$id": "not_8_0"
"name": "not_8_0",
"schemas": {
"not_8_0": {
"not": {
"type": "integer"
},
"extensible": true
}
}
}
]
},
@ -516,12 +552,16 @@
{
"description": "extensible: false (default) forbids extra properties in not",
"database": {
"schemas": [
"types": [
{
"not": {
"type": "integer"
},
"$id": "not_9_0"
"name": "not_9_0",
"schemas": {
"not_9_0": {
"not": {
"type": "integer"
}
}
}
}
]
},
@ -542,18 +582,22 @@
{
"description": "property next to not (extensible: true)",
"database": {
"schemas": [
"types": [
{
"properties": {
"bar": {
"type": "string"
"name": "not_10_0",
"schemas": {
"not_10_0": {
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
},
"extensible": true
}
},
"not": {
"type": "integer"
},
"extensible": true,
"$id": "not_10_0"
}
}
]
},
@ -575,17 +619,21 @@
{
"description": "property next to not (extensible: false)",
"database": {
"schemas": [
"types": [
{
"properties": {
"bar": {
"type": "string"
"name": "not_11_0",
"schemas": {
"not_11_0": {
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
}
}
},
"not": {
"type": "integer"
},
"$id": "not_11_0"
}
}
]
},

View File

@ -2,17 +2,35 @@
{
"description": "Strict Inheritance",
"database": {
"schemas": [
"types": [
{
"$id": "parent_type",
"type": "object",
"properties": {"a": {"type": "integer"}},
"required": ["a"]
"name": "parent_type",
"schemas": {
"parent_type": {
"type": "object",
"properties": {
"a": {
"type": "integer"
}
},
"required": [
"a"
]
}
}
},
{
"$id": "child_type",
"type": "parent_type",
"properties": {"b": {"type": "integer"}}
"name": "child_type",
"schemas": {
"child_type": {
"type": "parent_type",
"properties": {
"b": {
"type": "integer"
}
}
}
}
}
]
},
@ -20,42 +38,71 @@
{
"description": "valid child inherits properties and strictness",
"schema_id": "child_type",
"data": {"a": 1, "b": 2},
"data": {
"a": 1,
"b": 2
},
"action": "validate",
"expect": {"success": true}
"expect": {
"success": true
}
},
{
"description": "missing inherited required property fails",
"schema_id": "child_type",
"data": {"b": 2},
"data": {
"b": 2
},
"action": "validate",
"expect": {"success": false}
"expect": {
"success": false
}
},
{
"description": "additional properties fail due to inherited strictness",
"schema_id": "child_type",
"data": {"a": 1, "b": 2, "c": 3},
"data": {
"a": 1,
"b": 2,
"c": 3
},
"action": "validate",
"expect": {"success": false}
"expect": {
"success": false
}
}
]
},
{
"description": "Local Shadowing (Composition & Proxies)",
"database": {
"schemas": [
"types": [
{
"$id": "budget",
"type": "object",
"properties": {
"max": {"type": "integer", "maximum": 100}
"name": "budget",
"schemas": {
"budget": {
"type": "object",
"properties": {
"max": {
"type": "integer",
"maximum": 100
}
}
}
}
},
{
"$id": "custom_budget",
"type": "budget",
"properties": {
"max": {"type": "integer", "maximum": 50}
"name": "custom_budget",
"schemas": {
"custom_budget": {
"type": "budget",
"properties": {
"max": {
"type": "integer",
"maximum": 50
}
}
}
}
}
]
@ -64,78 +111,61 @@
{
"description": "shadowing override applies (50 is locally allowed)",
"schema_id": "custom_budget",
"data": {"max": 40},
"data": {
"max": 40
},
"action": "validate",
"expect": {"success": true}
"expect": {
"success": true
}
},
{
"description": "shadowing override applies natively, rejecting 60 even though parent allowed 100",
"schema_id": "custom_budget",
"data": {"max": 60},
"data": {
"max": 60
},
"action": "validate",
"expect": {"success": false}
}
]
},
{
"description": "Inline id Resolution",
"database": {
"schemas": [
{
"$id": "api.request",
"type": "object",
"properties": {
"inline_obj": {
"$id": "inline_nested",
"type": "object",
"properties": {"foo": {"type": "string"}},
"required": ["foo"]
},
"proxy_obj": {
"type": "inline_nested"
}
}
"expect": {
"success": false
}
]
},
"tests": [
{
"description": "proxy resolves and validates to the inline component",
"schema_id": "api.request",
"data": {
"inline_obj": {"foo": "bar"},
"proxy_obj": {"foo": "baz"}
},
"action": "validate",
"expect": {"success": true}
},
{
"description": "proxy resolves and catches violation targeting inline component constraints",
"schema_id": "api.request",
"data": {
"inline_obj": {"foo": "bar"},
"proxy_obj": {}
},
"action": "validate",
"expect": {"success": false}
}
]
},
{
"description": "Primitive Array Shorthand (Optionality)",
"database": {
"schemas": [
"types": [
{
"$id": "invoice",
"type": "object",
"properties": {"amount": {"type": "integer"}},
"required": ["amount"]
"name": "invoice",
"schemas": {
"invoice": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
}
},
"required": [
"amount"
]
}
}
},
{
"$id": "request",
"type": "object",
"properties": {
"inv": {"type": ["invoice", "null"]}
"name": "request",
"schemas": {
"request": {
"type": "object",
"properties": {
"inv": {
"type": [
"invoice",
"null"
]
}
}
}
}
}
]
@ -144,24 +174,40 @@
{
"description": "valid object passes shorthand inheritance check",
"schema_id": "request",
"data": {"inv": {"amount": 5}},
"data": {
"inv": {
"amount": 5
}
},
"action": "validate",
"expect": {"success": true}
"expect": {
"success": true
}
},
{
"description": "null passes shorthand inheritance check",
"schema_id": "request",
"data": {"inv": null},
"data": {
"inv": null
},
"action": "validate",
"expect": {"success": true}
"expect": {
"success": true
}
},
{
"description": "invalid object fails inner constraints safely",
"schema_id": "request",
"data": {"inv": {"amount": "string"}},
"data": {
"inv": {
"amount": "string"
}
},
"action": "validate",
"expect": {"success": false}
"expect": {
"success": false
}
}
]
}
]
]

View File

@ -2,64 +2,68 @@
{
"description": "Hybrid Array Pathing",
"database": {
"schemas": [
"types": [
{
"$id": "hybrid_pathing",
"type": "object",
"properties": {
"primitives": {
"type": "array",
"items": {
"type": "string"
}
},
"ad_hoc_objects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"name": "hybrid_pathing",
"schemas": {
"hybrid_pathing": {
"type": "object",
"properties": {
"primitives": {
"type": "array",
"items": {
"type": "string"
}
},
"required": [
"name"
]
}
},
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"type": "number",
"minimum": 10
"ad_hoc_objects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
]
}
}
}
},
"deep_entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"nested": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"flag": {
"type": "boolean"
},
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"type": "number",
"minimum": 10
}
}
}
},
"deep_entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"nested": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"flag": {
"type": "boolean"
}
}
}
}
}
@ -123,7 +127,9 @@
"errors": [
{
"code": "INVALID_TYPE",
"details": { "path": "primitives/1" }
"details": {
"path": "primitives/1"
}
}
]
}
@ -147,11 +153,15 @@
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"details": { "path": "ad_hoc_objects/1/name" }
"details": {
"path": "ad_hoc_objects/1/name"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": { "path": "ad_hoc_objects/1/age" }
"details": {
"path": "ad_hoc_objects/1/age"
}
}
]
}
@ -177,7 +187,9 @@
"errors": [
{
"code": "MINIMUM_VIOLATED",
"details": { "path": "entities/entity-beta/value" }
"details": {
"path": "entities/entity-beta/value"
}
}
]
}
@ -208,7 +220,9 @@
"errors": [
{
"code": "INVALID_TYPE",
"details": { "path": "deep_entities/parent-omega/nested/child-beta/flag" }
"details": {
"path": "deep_entities/parent-omega/nested/child-beta/flag"
}
}
]
}
@ -216,74 +230,28 @@
]
},
{
"description": "Polymorphic Structure Pathing",
"description": "Ad-Hoc Union Pathing",
"database": {
"types": [
{
"name": "widget",
"variations": ["widget"],
"schemas": [
{
"$id": "widget",
"name": "ad_hoc_pathing",
"schemas": {
"ad_hoc_pathing": {
"type": "object",
"properties": {
"id": { "type": "string" },
"type": { "type": "string" }
}
},
{
"$id": "stock.widget",
"type": "widget",
"properties": {
"kind": { "type": "string" },
"amount": { "type": "integer" },
"details": {
"type": "object",
"properties": {
"nested_metric": { "type": "number" }
},
"required": ["nested_metric"]
}
},
"required": ["amount", "details"]
}
]
}
],
"schemas": [
{
"$id": "polymorphic_pathing",
"type": "object",
"properties": {
"metadata_bubbles": {
"type": "array",
"items": {
"oneOf": [
{
"$id": "contact_metadata",
"type": "object",
"properties": {
"type": { "const": "contact" },
"phone": { "type": "string" }
},
"required": ["phone"]
},
{
"$id": "invoice_metadata",
"type": "object",
"properties": {
"type": { "const": "invoice" },
"invoice_id": { "type": "integer" }
},
"required": ["invoice_id"]
"metadata_bubbles": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
}
]
}
},
"table_families": {
"type": "array",
"items": {
"$family": "widget"
}
}
}
}
@ -292,29 +260,112 @@
},
"tests": [
{
"description": "oneOf tags natively bubble specific schema paths into deep array roots",
"description": "oneOf arrays natively support disjoint primitive types dynamically mapped into topological paths natively",
"data": {
"metadata_bubbles": [
{ "type": "invoice", "invoice_id": 100 },
{ "type": "contact", "phone": 12345, "rogue_field": "x" }
100,
"metadata string",
true
]
},
"schema_id": "polymorphic_pathing",
"schema_id": "ad_hoc_pathing",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"details": { "path": "metadata_bubbles/1/phone" }
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": { "path": "metadata_bubbles/1/rogue_field" }
"code": "NO_ONEOF_MATCH",
"details": {
"path": "metadata_bubbles/2"
}
}
]
}
},
}
]
},
{
"description": "Polymorphic Family Pathing",
"database": {
"relations": [
{
"id": "11111111-1111-1111-1111-111111111111",
"type": "relation",
"constraint": "fk_family_pathing_table_families_widget",
"source_type": "widget",
"source_columns": ["family_pathing_id"],
"destination_type": "family_pathing",
"destination_columns": ["id"],
"prefix": "table_families"
}
],
"types": [
{
"name": "widget",
"hierarchy": ["widget"],
"variations": [
"widget"
],
"schemas": {
"widget": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"stock.widget": {
"type": "widget",
"properties": {
"kind": {
"type": "string"
},
"amount": {
"type": "integer"
},
"details": {
"type": "object",
"properties": {
"nested_metric": {
"type": "number"
}
},
"required": [
"nested_metric"
]
}
},
"required": [
"amount",
"details"
]
}
}
},
{
"name": "family_pathing",
"hierarchy": ["family_pathing"],
"schemas": {
"family_pathing": {
"type": "object",
"properties": {
"table_families": {
"type": "array",
"items": {
"family": "widget"
}
}
}
}
}
}
]
},
"tests": [
{
"description": "families mechanically map physical variants directly onto topological uuid array paths",
"data": {
@ -324,7 +375,9 @@
"type": "widget",
"kind": "stock",
"amount": 500,
"details": { "nested_metric": 42.0 }
"details": {
"nested_metric": 42.0
}
},
{
"id": "widget-2",
@ -332,32 +385,40 @@
"kind": "stock",
"amount": "not_an_int",
"details": {
"stray_child": true
"stray_child": true
},
"unexpected_root_prop": "hi"
}
]
},
"schema_id": "polymorphic_pathing",
"schema_id": "family_pathing",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"details": { "path": "table_families/widget-2/amount" }
"details": {
"path": "table_families/widget-2/amount"
}
},
{
"code": "REQUIRED_FIELD_MISSING",
"details": { "path": "table_families/widget-2/details/nested_metric" }
"details": {
"path": "table_families/widget-2/details/nested_metric"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": { "path": "table_families/widget-2/details/stray_child" }
"details": {
"path": "table_families/widget-2/details/stray_child"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": { "path": "table_families/widget-2/unexpected_root_prop" }
"details": {
"path": "table_families/widget-2/unexpected_root_prop"
}
}
]
}

View File

@ -2,10 +2,14 @@
{
"description": "pattern validation",
"database": {
"schemas": [
"types": [
{
"pattern": "^a*$",
"$id": "pattern_0_0"
"name": "pattern_0_0",
"schemas": {
"pattern_0_0": {
"pattern": "^a*$"
}
}
}
]
},
@ -87,10 +91,14 @@
{
"description": "pattern is not anchored",
"database": {
"schemas": [
"types": [
{
"pattern": "a+",
"$id": "pattern_1_0"
"name": "pattern_1_0",
"schemas": {
"pattern_1_0": {
"pattern": "a+"
}
}
}
]
},

View File

@ -2,15 +2,19 @@
{
"description": "patternProperties validates properties matching a regex",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"f.*o": {
"type": "integer"
"name": "patternProperties_0_0",
"schemas": {
"patternProperties_0_0": {
"patternProperties": {
"f.*o": {
"type": "integer"
}
},
"items": {}
}
},
"items": {},
"$id": "patternProperties_0_0"
}
}
]
},
@ -108,17 +112,21 @@
{
"description": "multiple simultaneous patternProperties are validated",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"a*": {
"type": "integer"
},
"aaa*": {
"maximum": 20
"name": "patternProperties_1_0",
"schemas": {
"patternProperties_1_0": {
"patternProperties": {
"a*": {
"type": "integer"
},
"aaa*": {
"maximum": 20
}
}
}
},
"$id": "patternProperties_1_0"
}
}
]
},
@ -196,18 +204,22 @@
{
"description": "regexes are not anchored by default and are case sensitive",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"[0-9]{2,}": {
"type": "boolean"
},
"X_": {
"type": "string"
"name": "patternProperties_2_0",
"schemas": {
"patternProperties_2_0": {
"patternProperties": {
"[0-9]{2,}": {
"type": "boolean"
},
"X_": {
"type": "string"
}
},
"extensible": true
}
},
"extensible": true,
"$id": "patternProperties_2_0"
}
}
]
},
@ -261,13 +273,17 @@
{
"description": "patternProperties with boolean schemas",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"f.*": true,
"b.*": false
},
"$id": "patternProperties_3_0"
"name": "patternProperties_3_0",
"schemas": {
"patternProperties_3_0": {
"patternProperties": {
"f.*": true,
"b.*": false
}
}
}
}
]
},
@ -331,14 +347,18 @@
{
"description": "patternProperties with null valued instance properties",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"^.*bar$": {
"type": "null"
"name": "patternProperties_4_0",
"schemas": {
"patternProperties_4_0": {
"patternProperties": {
"^.*bar$": {
"type": "null"
}
}
}
},
"$id": "patternProperties_4_0"
}
}
]
},
@ -359,15 +379,19 @@
{
"description": "extensible: true allows extra properties NOT matching pattern",
"database": {
"schemas": [
"types": [
{
"patternProperties": {
"f.*o": {
"type": "integer"
"name": "patternProperties_5_0",
"schemas": {
"patternProperties_5_0": {
"patternProperties": {
"f.*o": {
"type": "integer"
}
},
"extensible": true
}
},
"extensible": true,
"$id": "patternProperties_5_0"
}
}
]
},

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,21 @@
{
"description": "a schema given for prefixItems",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "integer"
},
{
"type": "string"
"name": "prefixItems_0_0",
"schemas": {
"prefixItems_0_0": {
"prefixItems": [
{
"type": "integer"
},
{
"type": "string"
}
]
}
],
"$id": "prefixItems_0_0"
}
}
]
},
@ -92,13 +96,17 @@
{
"description": "prefixItems with boolean schemas",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
true,
false
],
"$id": "prefixItems_1_0"
"name": "prefixItems_1_0",
"schemas": {
"prefixItems_1_0": {
"prefixItems": [
true,
false
]
}
}
}
]
},
@ -140,15 +148,19 @@
{
"description": "additional items are allowed by default",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "integer"
"name": "prefixItems_2_0",
"schemas": {
"prefixItems_2_0": {
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true
}
],
"extensible": true,
"$id": "prefixItems_2_0"
}
}
]
},
@ -171,14 +183,18 @@
{
"description": "prefixItems with null instance elements",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "null"
"name": "prefixItems_3_0",
"schemas": {
"prefixItems_3_0": {
"prefixItems": [
{
"type": "null"
}
]
}
],
"$id": "prefixItems_3_0"
}
}
]
},
@ -199,15 +215,19 @@
{
"description": "extensible: true allows extra items with prefixItems",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "integer"
"name": "prefixItems_4_0",
"schemas": {
"prefixItems_4_0": {
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true
}
],
"extensible": true,
"$id": "prefixItems_4_0"
}
}
]
},

View File

@ -2,10 +2,14 @@
{
"description": "integer type matches integers",
"database": {
"schemas": [
"types": [
{
"type": "integer",
"$id": "type_0_0"
"name": "type_0_0",
"schemas": {
"type_0_0": {
"type": "integer"
}
}
}
]
},
@ -96,10 +100,14 @@
{
"description": "number type matches numbers",
"database": {
"schemas": [
"types": [
{
"type": "number",
"$id": "type_1_0"
"name": "type_1_0",
"schemas": {
"type_1_0": {
"type": "number"
}
}
}
]
},
@ -190,10 +198,14 @@
{
"description": "string type matches strings",
"database": {
"schemas": [
"types": [
{
"type": "string",
"$id": "type_2_0"
"name": "type_2_0",
"schemas": {
"type_2_0": {
"type": "string"
}
}
}
]
},
@ -284,10 +296,14 @@
{
"description": "object type matches objects",
"database": {
"schemas": [
"types": [
{
"type": "object",
"$id": "type_3_0"
"name": "type_3_0",
"schemas": {
"type_3_0": {
"type": "object"
}
}
}
]
},
@ -360,10 +376,14 @@
{
"description": "array type matches arrays",
"database": {
"schemas": [
"types": [
{
"type": "array",
"$id": "type_4_0"
"name": "type_4_0",
"schemas": {
"type_4_0": {
"type": "array"
}
}
}
]
},
@ -436,10 +456,14 @@
{
"description": "boolean type matches booleans",
"database": {
"schemas": [
"types": [
{
"type": "boolean",
"$id": "type_5_0"
"name": "type_5_0",
"schemas": {
"type_5_0": {
"type": "boolean"
}
}
}
]
},
@ -539,10 +563,14 @@
{
"description": "null type matches only the null object",
"database": {
"schemas": [
"types": [
{
"type": "null",
"$id": "type_6_0"
"name": "type_6_0",
"schemas": {
"type_6_0": {
"type": "null"
}
}
}
]
},
@ -642,13 +670,17 @@
{
"description": "multiple types can be specified in an array",
"database": {
"schemas": [
"types": [
{
"type": [
"integer",
"string"
],
"$id": "type_7_0"
"name": "type_7_0",
"schemas": {
"type_7_0": {
"type": [
"integer",
"string"
]
}
}
}
]
},
@ -721,12 +753,16 @@
{
"description": "type as array with one item",
"database": {
"schemas": [
"types": [
{
"type": [
"string"
],
"$id": "type_8_0"
"name": "type_8_0",
"schemas": {
"type_8_0": {
"type": [
"string"
]
}
}
}
]
},
@ -754,14 +790,18 @@
{
"description": "type: array or object",
"database": {
"schemas": [
"types": [
{
"type": [
"array",
"object"
],
"items": {},
"$id": "type_9_0"
"name": "type_9_0",
"schemas": {
"type_9_0": {
"type": [
"array",
"object"
],
"items": {}
}
}
}
]
},
@ -820,15 +860,19 @@
{
"description": "type: array, object or null",
"database": {
"schemas": [
"types": [
{
"type": [
"array",
"object",
"null"
],
"items": {},
"$id": "type_10_0"
"name": "type_10_0",
"schemas": {
"type_10_0": {
"type": [
"array",
"object",
"null"
],
"items": {}
}
}
}
]
},
@ -887,11 +931,15 @@
{
"description": "extensible: true allows extra properties",
"database": {
"schemas": [
"types": [
{
"type": "object",
"extensible": true,
"$id": "type_11_0"
"name": "type_11_0",
"schemas": {
"type_11_0": {
"type": "object",
"extensible": true
}
}
}
]
},

View File

@ -2,17 +2,21 @@
{
"description": "object properties validation",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "string"
"name": "properties_0_0",
"schemas": {
"properties_0_0": {
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "string"
}
}
}
},
"$id": "properties_0_0"
}
}
]
},
@ -85,13 +89,17 @@
{
"description": "properties with boolean schema",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": true,
"bar": false
},
"$id": "properties_1_0"
"name": "properties_1_0",
"schemas": {
"properties_1_0": {
"properties": {
"foo": true,
"bar": false
}
}
}
}
]
},
@ -144,29 +152,33 @@
{
"description": "properties with escaped characters",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo\nbar": {
"type": "number"
},
"foo\"bar": {
"type": "number"
},
"foo\\bar": {
"type": "number"
},
"foo\rbar": {
"type": "number"
},
"foo\tbar": {
"type": "number"
},
"foo\fbar": {
"type": "number"
"name": "properties_2_0",
"schemas": {
"properties_2_0": {
"properties": {
"foo\nbar": {
"type": "number"
},
"foo\"bar": {
"type": "number"
},
"foo\\bar": {
"type": "number"
},
"foo\rbar": {
"type": "number"
},
"foo\tbar": {
"type": "number"
},
"foo\fbar": {
"type": "number"
}
}
}
},
"$id": "properties_2_0"
}
}
]
},
@ -208,14 +220,18 @@
{
"description": "properties with null valued instance properties",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"type": "null"
"name": "properties_3_0",
"schemas": {
"properties_3_0": {
"properties": {
"foo": {
"type": "null"
}
}
}
},
"$id": "properties_3_0"
}
}
]
},
@ -237,24 +253,28 @@
"description": "properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"database": {
"schemas": [
"types": [
{
"properties": {
"__proto__": {
"type": "number"
},
"toString": {
"name": "properties_4_0",
"schemas": {
"properties_4_0": {
"properties": {
"length": {
"type": "string"
"__proto__": {
"type": "number"
},
"toString": {
"properties": {
"length": {
"type": "string"
}
}
},
"constructor": {
"type": "number"
}
}
},
"constructor": {
"type": "number"
}
},
"$id": "properties_4_0"
}
}
]
},
@ -343,15 +363,19 @@
{
"description": "extensible: true allows extra properties",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"type": "integer"
"name": "properties_5_0",
"schemas": {
"properties_5_0": {
"properties": {
"foo": {
"type": "integer"
}
},
"extensible": true
}
},
"extensible": true,
"$id": "properties_5_0"
}
}
]
},
@ -373,14 +397,18 @@
{
"description": "strict by default: extra properties invalid",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {
"type": "string"
"name": "properties_6_0",
"schemas": {
"properties_6_0": {
"properties": {
"foo": {
"type": "string"
}
}
}
},
"$id": "properties_6_0"
}
}
]
},
@ -402,18 +430,22 @@
{
"description": "inheritance: nested object inherits strictness from strict parent",
"database": {
"schemas": [
"types": [
{
"properties": {
"nested": {
"name": "properties_7_0",
"schemas": {
"properties_7_0": {
"properties": {
"foo": {
"type": "string"
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"$id": "properties_7_0"
}
}
]
},
@ -437,19 +469,23 @@
{
"description": "override: nested object allows extra properties if extensible: true",
"database": {
"schemas": [
"types": [
{
"properties": {
"nested": {
"extensible": true,
"name": "properties_8_0",
"schemas": {
"properties_8_0": {
"properties": {
"foo": {
"type": "string"
"nested": {
"extensible": true,
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"$id": "properties_8_0"
}
}
]
},
@ -473,19 +509,23 @@
{
"description": "inheritance: nested object inherits looseness from loose parent",
"database": {
"schemas": [
"types": [
{
"extensible": true,
"properties": {
"nested": {
"name": "properties_9_0",
"schemas": {
"properties_9_0": {
"extensible": true,
"properties": {
"foo": {
"type": "string"
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"$id": "properties_9_0"
}
}
]
},
@ -509,20 +549,24 @@
{
"description": "override: nested object enforces strictness if extensible: false",
"database": {
"schemas": [
"types": [
{
"extensible": true,
"properties": {
"nested": {
"extensible": false,
"name": "properties_10_0",
"schemas": {
"properties_10_0": {
"extensible": true,
"properties": {
"foo": {
"type": "string"
"nested": {
"extensible": false,
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"$id": "properties_10_0"
}
}
]
},
@ -546,21 +590,25 @@
{
"description": "arrays: inline items inherit strictness from strict parent",
"database": {
"schemas": [
"types": [
{
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
"name": "properties_11_0",
"schemas": {
"properties_11_0": {
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
}
},
"$id": "properties_11_0"
}
}
]
},
@ -586,22 +634,26 @@
{
"description": "arrays: inline items inherit looseness from loose parent",
"database": {
"schemas": [
"types": [
{
"extensible": true,
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
"name": "properties_12_0",
"schemas": {
"properties_12_0": {
"extensible": true,
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
}
},
"$id": "properties_12_0"
}
}
]
},

View File

@ -2,13 +2,17 @@
{
"description": "propertyNames validation",
"database": {
"schemas": [
"types": [
{
"propertyNames": {
"maxLength": 3
},
"extensible": true,
"$id": "propertyNames_0_0"
"name": "propertyNames_0_0",
"schemas": {
"propertyNames_0_0": {
"propertyNames": {
"maxLength": 3
},
"extensible": true
}
}
}
]
},
@ -83,13 +87,17 @@
{
"description": "propertyNames validation with pattern",
"database": {
"schemas": [
"types": [
{
"propertyNames": {
"pattern": "^a+$"
},
"extensible": true,
"$id": "propertyNames_1_0"
"name": "propertyNames_1_0",
"schemas": {
"propertyNames_1_0": {
"propertyNames": {
"pattern": "^a+$"
},
"extensible": true
}
}
}
]
},
@ -132,11 +140,15 @@
{
"description": "propertyNames with boolean schema true",
"database": {
"schemas": [
"types": [
{
"propertyNames": true,
"extensible": true,
"$id": "propertyNames_2_0"
"name": "propertyNames_2_0",
"schemas": {
"propertyNames_2_0": {
"propertyNames": true,
"extensible": true
}
}
}
]
},
@ -166,11 +178,15 @@
{
"description": "propertyNames with boolean schema false",
"database": {
"schemas": [
"types": [
{
"propertyNames": false,
"extensible": true,
"$id": "propertyNames_3_0"
"name": "propertyNames_3_0",
"schemas": {
"propertyNames_3_0": {
"propertyNames": false,
"extensible": true
}
}
}
]
},
@ -200,13 +216,17 @@
{
"description": "propertyNames with const",
"database": {
"schemas": [
"types": [
{
"propertyNames": {
"const": "foo"
},
"extensible": true,
"$id": "propertyNames_4_0"
"name": "propertyNames_4_0",
"schemas": {
"propertyNames_4_0": {
"propertyNames": {
"const": "foo"
},
"extensible": true
}
}
}
]
},
@ -247,16 +267,20 @@
{
"description": "propertyNames with enum",
"database": {
"schemas": [
"types": [
{
"propertyNames": {
"enum": [
"foo",
"bar"
]
},
"extensible": true,
"$id": "propertyNames_5_0"
"name": "propertyNames_5_0",
"schemas": {
"propertyNames_5_0": {
"propertyNames": {
"enum": [
"foo",
"bar"
]
},
"extensible": true
}
}
}
]
},
@ -309,13 +333,17 @@
{
"description": "extensible: true allows extra properties (checked by propertyNames)",
"database": {
"schemas": [
"types": [
{
"propertyNames": {
"maxLength": 3
},
"extensible": true,
"$id": "propertyNames_6_0"
"name": "propertyNames_6_0",
"schemas": {
"propertyNames_6_0": {
"propertyNames": {
"maxLength": 3
},
"extensible": true
}
}
}
]
},

File diff suppressed because it is too large Load Diff

View File

@ -2,16 +2,20 @@
{
"description": "required validation",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {},
"bar": {}
},
"required": [
"foo"
],
"$id": "required_0_0"
"name": "required_0_0",
"schemas": {
"required_0_0": {
"properties": {
"foo": {},
"bar": {}
},
"required": [
"foo"
]
}
}
}
]
},
@ -88,12 +92,16 @@
{
"description": "required default validation",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {}
},
"$id": "required_1_0"
"name": "required_1_0",
"schemas": {
"required_1_0": {
"properties": {
"foo": {}
}
}
}
}
]
},
@ -112,13 +120,17 @@
{
"description": "required with empty array",
"database": {
"schemas": [
"types": [
{
"properties": {
"foo": {}
},
"required": [],
"$id": "required_2_0"
"name": "required_2_0",
"schemas": {
"required_2_0": {
"properties": {
"foo": {}
},
"required": []
}
}
}
]
},
@ -137,18 +149,22 @@
{
"description": "required with escaped characters",
"database": {
"schemas": [
"types": [
{
"required": [
"foo\nbar",
"foo\"bar",
"foo\\bar",
"foo\rbar",
"foo\tbar",
"foo\fbar"
],
"extensible": true,
"$id": "required_3_0"
"name": "required_3_0",
"schemas": {
"required_3_0": {
"required": [
"foo\nbar",
"foo\"bar",
"foo\\bar",
"foo\rbar",
"foo\tbar",
"foo\fbar"
],
"extensible": true
}
}
}
]
},
@ -187,15 +203,19 @@
"description": "required properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"database": {
"schemas": [
"types": [
{
"required": [
"__proto__",
"toString",
"constructor"
],
"extensible": true,
"$id": "required_4_0"
"name": "required_4_0",
"schemas": {
"required_4_0": {
"required": [
"__proto__",
"toString",
"constructor"
],
"extensible": true
}
}
}
]
},
@ -284,13 +304,17 @@
{
"description": "extensible: true allows extra properties in required",
"database": {
"schemas": [
"types": [
{
"required": [
"foo"
],
"extensible": true,
"$id": "required_5_0"
"name": "required_5_0",
"schemas": {
"required_5_0": {
"required": [
"foo"
],
"extensible": true
}
}
}
]
},

View File

@ -2,11 +2,15 @@
{
"description": "uniqueItems validation",
"database": {
"schemas": [
"types": [
{
"uniqueItems": true,
"extensible": true,
"$id": "uniqueItems_0_0"
"name": "uniqueItems_0_0",
"schemas": {
"uniqueItems_0_0": {
"uniqueItems": true,
"extensible": true
}
}
}
]
},
@ -469,19 +473,23 @@
{
"description": "uniqueItems with an array of items",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
"name": "uniqueItems_1_0",
"schemas": {
"uniqueItems_1_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"extensible": true
}
],
"uniqueItems": true,
"extensible": true,
"$id": "uniqueItems_1_0"
}
}
]
},
@ -595,19 +603,23 @@
{
"description": "uniqueItems with an array of items and additionalItems=false",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
"name": "uniqueItems_2_0",
"schemas": {
"uniqueItems_2_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"items": false
}
],
"uniqueItems": true,
"items": false,
"$id": "uniqueItems_2_0"
}
}
]
},
@ -678,11 +690,15 @@
{
"description": "uniqueItems=false validation",
"database": {
"schemas": [
"types": [
{
"uniqueItems": false,
"extensible": true,
"$id": "uniqueItems_3_0"
"name": "uniqueItems_3_0",
"schemas": {
"uniqueItems_3_0": {
"uniqueItems": false,
"extensible": true
}
}
}
]
},
@ -924,19 +940,23 @@
{
"description": "uniqueItems=false with an array of items",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
"name": "uniqueItems_4_0",
"schemas": {
"uniqueItems_4_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"extensible": true
}
],
"uniqueItems": false,
"extensible": true,
"$id": "uniqueItems_4_0"
}
}
]
},
@ -1050,19 +1070,23 @@
{
"description": "uniqueItems=false with an array of items and additionalItems=false",
"database": {
"schemas": [
"types": [
{
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
"name": "uniqueItems_5_0",
"schemas": {
"uniqueItems_5_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"items": false
}
],
"uniqueItems": false,
"items": false,
"$id": "uniqueItems_5_0"
}
}
]
},
@ -1133,11 +1157,15 @@
{
"description": "extensible: true allows extra items in uniqueItems",
"database": {
"schemas": [
"types": [
{
"uniqueItems": true,
"extensible": true,
"$id": "uniqueItems_6_0"
"name": "uniqueItems_6_0",
"schemas": {
"uniqueItems_6_0": {
"uniqueItems": true,
"extensible": true
}
}
}
]
},

45
out.txt

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,163 @@
use crate::database::schema::Schema;
use std::sync::Arc;
impl Schema {
#[allow(unused_variables)]
pub(crate) fn validate_identifier(
id: &str,
field_name: &str,
root_id: &str,
path: &str,
errors: &mut Vec<crate::drop::Error>,
) {
#[cfg(not(test))]
for c in id.chars() {
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' && c != '$' {
errors.push(crate::drop::Error {
code: "INVALID_IDENTIFIER".to_string(),
message: format!(
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.$]",
c, field_name, id
),
details: crate::drop::ErrorDetails {
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
},
});
return;
}
}
}
pub fn collect_schemas(
schema_arc: &Arc<Schema>,
root_id: &str,
path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
if t == "array" {
if let Some(items) = &schema_arc.obj.items {
if let Some(crate::database::object::SchemaTypeOrArray::Single(it)) = &items.obj.type_ {
if !crate::database::object::is_primitive_type(it) {
if items.obj.properties.is_some() || items.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
}
} else if !crate::database::object::is_primitive_type(t) {
Self::validate_identifier(t, "type", root_id, &path, errors);
// Is this an explicit inline ad-hoc composition?
if schema_arc.obj.properties.is_some() || schema_arc.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
if let Some(family) = &schema_arc.obj.family {
Self::validate_identifier(family, "family", root_id, &path, errors);
}
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
}
pub fn collect_child_schemas(
schema_arc: &Arc<Schema>,
root_id: &str,
path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(props) = &schema_arc.obj.properties {
for (k, v) in props.iter() {
let next_path = format!("{}/{}", path, k);
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
}
}
if let Some(pattern_props) = &schema_arc.obj.pattern_properties {
for (k, v) in pattern_props.iter() {
let next_path = format!("{}/{}", path, k);
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
}
}
let mut map_arr = |arr: &Vec<Arc<Schema>>, sub: &str| {
for (i, v) in arr.iter().enumerate() {
Self::collect_schemas(
v,
root_id,
format!("{}/{}/{}", path, sub, i),
to_insert,
errors,
);
}
};
if let Some(arr) = &schema_arc.obj.prefix_items {
map_arr(arr, "prefixItems");
}
if let Some(arr) = &schema_arc.obj.one_of {
map_arr(arr, "oneOf");
}
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
if let Some(v) = opt {
if pass_path {
// Arrays explicitly push their wrapper natively.
// 'items' becomes a transparent conduit, bypassing self-promotion and skipping the '/items' suffix.
Self::collect_child_schemas(v, root_id, path.clone(), to_insert, errors);
} else {
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
}
}
};
map_opt(
&schema_arc.obj.additional_properties,
false,
"additionalProperties",
);
map_opt(&schema_arc.obj.items, true, "items");
map_opt(&schema_arc.obj.not, false, "not");
map_opt(&schema_arc.obj.contains, false, "contains");
map_opt(&schema_arc.obj.property_names, false, "propertyNames");
if let Some(cases) = &schema_arc.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(when) = &c.when {
Self::collect_schemas(
when,
root_id,
format!("{}/cases/{}/when", path, i),
to_insert,
errors,
);
}
if let Some(then) = &c.then {
Self::collect_schemas(
then,
root_id,
format!("{}/cases/{}/then", path, i),
to_insert,
errors,
);
}
if let Some(else_) = &c.else_ {
Self::collect_schemas(
else_,
root_id,
format!("{}/cases/{}/else", path, i),
to_insert,
errors,
);
}
}
}
}
}

View File

@ -0,0 +1,87 @@
use crate::database::object::{SchemaObject, SchemaTypeOrArray};
use crate::database::schema::Schema;
use crate::database::r#enum::Enum;
use std::collections::BTreeMap;
use std::sync::Arc;
impl Enum {
pub fn compile_condition(&self) -> Schema {
let mut props = BTreeMap::new();
let enum_name = &self.name;
let mut eq_obj = SchemaObject::default();
eq_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
enum_name.clone(),
"null".to_string(),
]));
props.insert(
"$eq".to_string(),
Arc::new(Schema {
obj: eq_obj,
always_fail: false,
}),
);
let mut ne_obj = SchemaObject::default();
ne_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
enum_name.clone(),
"null".to_string(),
]));
props.insert(
"$ne".to_string(),
Arc::new(Schema {
obj: ne_obj,
always_fail: false,
}),
);
let mut of_obj = SchemaObject::default();
of_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"array".to_string(),
"null".to_string(),
]));
of_obj.items = Some(Arc::new(Schema {
obj: SchemaObject {
type_: Some(SchemaTypeOrArray::Single(enum_name.clone())),
..Default::default()
},
always_fail: false,
}));
props.insert(
"$of".to_string(),
Arc::new(Schema {
obj: of_obj,
always_fail: false,
}),
);
let mut nof_obj = SchemaObject::default();
nof_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"array".to_string(),
"null".to_string(),
]));
nof_obj.items = Some(Arc::new(Schema {
obj: SchemaObject {
type_: Some(SchemaTypeOrArray::Single(enum_name.clone())),
..Default::default()
},
always_fail: false,
}));
props.insert(
"$nof".to_string(),
Arc::new(Schema {
obj: nof_obj,
always_fail: false,
}),
);
let mut cond_obj = SchemaObject::default();
cond_obj.type_ = Some(SchemaTypeOrArray::Single("condition".to_string()));
cond_obj.properties = Some(props);
Schema {
obj: cond_obj,
always_fail: false,
}
}
}

View File

@ -0,0 +1,128 @@
use crate::database::schema::Schema;
impl Schema {
/// Dynamically infers and compiles all structural database relationships between this Schema
/// and its nested children. This functions recursively traverses the JSON Schema abstract syntax
/// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation
/// constraint paths directly onto the `compiled_edges` map in O(1) memory.
pub fn compile_edges(
&self,
db: &crate::database::Database,
root_id: &str,
path: &str,
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
errors: &mut Vec<crate::drop::Error>,
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
let mut schema_edges = std::collections::BTreeMap::new();
// Determine the physical Database Table Name this schema structurally represents
// Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person")
let mut parent_type_name = None;
if let Some(family) = &self.obj.family {
// 1. Explicit horizontal routing
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if path == root_id {
// 2. Root nodes trust their exact registry footprint
let base_type_name = path.split('.').next_back().unwrap_or(path).to_string();
if db.types.contains_key(&base_type_name) {
parent_type_name = Some(base_type_name);
}
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
// 3. Nested graphs trust their explicit struct pointer reference
if !crate::database::object::is_primitive_type(t) {
parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
}
}
if let Some(p_type) = parent_type_name {
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
if let Some(type_def) = db.types.get(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props {
let mut child_type_name = None;
let mut target_schema = prop_schema.clone();
let mut is_array = false;
// Structurally unpack the inner target entity if the object maps to an array list
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_
{
if t == "array" {
is_array = true;
if let Some(items) = &prop_schema.obj.items {
target_schema = items.clone();
}
}
}
// Determine the physical Postgres table backing the nested child schema recursively
if let Some(family) = &target_schema.obj.family {
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&target_schema.obj.type_
{
if !crate::database::object::is_primitive_type(t) {
child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
}
} else if let Some(arr) = &target_schema.obj.one_of {
if let Some(first) = arr.first() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &first.obj.type_
{
if !crate::database::object::is_primitive_type(t) {
child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
}
}
}
}
if let Some(c_type) = child_type_name {
// Skip edge compilation for JSONB columns — they store data inline, not relationally.
// The physical column type from field_types is the single source of truth.
if let Some(ft) = type_def
.field_types
.as_ref()
.and_then(|v| v.get(prop_name.as_str()))
.and_then(|v| v.as_str())
{
if ft == "jsonb" {
continue;
}
}
if db.types.contains_key(&c_type) {
// Ensure the child Schema's AST has accurately compiled its own physical property keys so we can
// inject them securely for Many-to-Many Twin Deduction disambiguation matching.
target_schema.compile(db, root_id, format!("{}/{}", path, prop_name), errors);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> =
compiled_target_props.keys().cloned().collect();
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
if let Some((relation, is_forward)) = db.resolve_relation(
&p_type,
&c_type,
prop_name,
Some(&keys_for_ambiguity),
is_array,
Some(root_id),
&format!("{}/{}", path, prop_name),
errors,
) {
schema_edges.insert(
prop_name.clone(),
crate::database::edge::Edge {
constraint: relation.constraint.clone(),
forward: is_forward,
},
);
}
}
}
}
}
}
}
schema_edges
}
}

View File

@ -0,0 +1,169 @@
use crate::database::Database;
use crate::database::object::{SchemaObject, SchemaTypeOrArray};
use crate::database::schema::Schema;
use std::collections::BTreeMap;
use std::sync::Arc;
impl Schema {
pub fn compile_filter(
&self,
_db: &Database,
root_id: &str,
_errors: &mut Vec<crate::drop::Error>,
) -> Option<Schema> {
if let Some(props) = self.obj.compiled_properties.get() {
let mut filter_props = BTreeMap::new();
for (key, child) in props {
let mut structural_filter = None;
let is_array = match &child.obj.type_ {
Some(SchemaTypeOrArray::Single(t)) => t == "array",
Some(SchemaTypeOrArray::Multiple(types)) => types.contains(&"array".to_string()),
None => false,
};
if is_array {
if let Some(items) = &child.obj.items {
if !items.is_proxy() {
structural_filter = items.compile_filter(_db, "", _errors);
}
}
} else if !child.is_proxy() {
structural_filter = child.compile_filter(_db, "", _errors);
}
if let Some(mut inline_schema) = structural_filter {
inline_schema.obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"object".to_string(),
"null".to_string(),
]));
filter_props.insert(
key.clone(),
Arc::new(inline_schema),
);
} else if let Some(mut filter_type) = Self::resolve_filter_type(child, _db) {
filter_type.push("null".to_string());
let mut child_obj = SchemaObject::default();
child_obj.type_ = Some(SchemaTypeOrArray::Multiple(filter_type));
filter_props.insert(
key.clone(),
Arc::new(Schema {
obj: child_obj,
always_fail: false,
}),
);
}
}
if !filter_props.is_empty() {
if !root_id.is_empty() {
let root_filter_type = format!("{}.filter", root_id);
let mut and_obj = SchemaObject::default();
and_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"array".to_string(),
"null".to_string(),
]));
and_obj.items = Some(Arc::new(Schema {
obj: SchemaObject {
type_: Some(SchemaTypeOrArray::Single(root_filter_type.clone())),
..Default::default()
},
always_fail: false,
}));
filter_props.insert(
"$and".to_string(),
Arc::new(Schema {
obj: and_obj,
always_fail: false,
}),
);
let mut or_obj = SchemaObject::default();
or_obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"array".to_string(),
"null".to_string(),
]));
or_obj.items = Some(Arc::new(Schema {
obj: SchemaObject {
type_: Some(SchemaTypeOrArray::Single(root_filter_type.clone())),
..Default::default()
},
always_fail: false,
}));
filter_props.insert(
"$or".to_string(),
Arc::new(Schema {
obj: or_obj,
always_fail: false,
}),
);
}
let mut wrapper_obj = SchemaObject::default();
// 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 {
obj: wrapper_obj,
always_fail: false,
});
}
}
None
}
fn resolve_filter_type(schema: &Arc<Schema>, db: &Database) -> Option<Vec<String>> {
if let Some(type_) = &schema.obj.type_ {
match type_ {
SchemaTypeOrArray::Single(t) => {
return Self::map_filter_string(t, schema, db);
}
SchemaTypeOrArray::Multiple(types) => {
for t in types {
if t != "null" {
return Self::map_filter_string(t, schema, db);
}
}
}
}
}
None
}
fn map_filter_string(t: &str, schema: &Arc<Schema>, db: &Database) -> Option<Vec<String>> {
match t {
"string" => {
if let Some(fmt) = &schema.obj.format {
if fmt == "date-time" {
return Some(vec!["date.condition".to_string()]);
}
}
Some(vec!["string.condition".to_string()])
}
"integer" => Some(vec!["integer.condition".to_string()]),
"number" => Some(vec!["number.condition".to_string()]),
"boolean" => Some(vec!["boolean.condition".to_string()]),
"object" => None, // Inline structures are ignored in Composed References
"array" => {
if let Some(items) = &schema.obj.items {
return Self::resolve_filter_type(items, db);
}
None
},
"null" => None,
custom => {
if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)])
} else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
Some(vec![format!("{}.filter", custom)])
}
}
}
}
}

176
src/database/compile/mod.rs Normal file
View File

@ -0,0 +1,176 @@
pub mod collection;
pub mod condition;
pub mod edges;
pub mod filter;
pub mod polymorphism;
use crate::database::schema::Schema;
impl Schema {
pub fn compile(
&self,
db: &crate::database::Database,
root_id: &str,
path: String,
errors: &mut Vec<crate::drop::Error>,
) {
if self.obj.compiled_properties.get().is_some() {
return;
}
if let Some(format_str) = &self.obj.format {
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
let _ = self
.obj
.compiled_format
.set(crate::database::object::CompiledFormat::Func(fmt.func));
}
}
if let Some(pattern_str) = &self.obj.pattern {
if let Ok(re) = regex::Regex::new(pattern_str) {
let _ = self
.obj
.compiled_pattern
.set(crate::database::object::CompiledRegex(re));
}
}
if let Some(pattern_props) = &self.obj.pattern_properties {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
let _ = self.obj.compiled_pattern_properties.set(compiled);
}
}
let mut props = std::collections::BTreeMap::new();
// 1. Resolve INHERITANCE dependencies first
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) && !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());
}
}
}
}
if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
let mut custom_type_count = 0;
for t in types {
if !crate::database::object::is_primitive_type(t) {
custom_type_count += 1;
}
}
if custom_type_count > 1 {
errors.push(crate::drop::Error {
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
message: format!(
"Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.",
types
),
details: crate::drop::ErrorDetails {
path: Some(path.clone()),
schema: Some(root_id.to_string()),
..Default::default()
}
});
}
for t in types {
if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') {
if let Some(parent) = db.schemas.get(t).cloned() {
parent.as_ref().compile(db, t, t.clone(), errors);
}
}
}
}
// 2. Add local properties
if let Some(local_props) = &self.obj.properties {
for (k, v) in local_props {
props.insert(k.clone(), v.clone());
}
}
// 3. Add cases conditionally-defined properties recursively
if let Some(cases) = &self.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(child) = &c.when {
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), errors);
}
if let Some(child) = &c.then {
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), errors);
if let Some(t_props) = child.obj.compiled_properties.get() {
props.extend(t_props.clone());
}
}
if let Some(child) = &c.else_ {
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), errors);
if let Some(e_props) = child.obj.compiled_properties.get() {
props.extend(e_props.clone());
}
}
}
}
// 4. Set the OnceLock!
let _ = self.obj.compiled_properties.set(props.clone());
let mut names: Vec<String> = props.keys().cloned().collect();
names.sort();
let _ = self.obj.compiled_property_names.set(names);
// 5. Compute Edges natively
let schema_edges = self.compile_edges(db, root_id, &path, &props, errors);
let _ = self.obj.compiled_edges.set(schema_edges);
// 5. Build our inline children properties recursively NOW! (Depth-first search)
if let Some(local_props) = &self.obj.properties {
for (k, child) in local_props {
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(items) = &self.obj.items {
items.compile(db, root_id, format!("{}/items", path), errors);
}
if let Some(pattern_props) = &self.obj.pattern_properties {
for (k, child) in pattern_props {
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(additional_props) = &self.obj.additional_properties {
additional_props.compile(
db,
root_id,
format!("{}/additionalProperties", path),
errors,
);
}
if let Some(one_of) = &self.obj.one_of {
for (i, child) in one_of.iter().enumerate() {
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), errors);
}
}
if let Some(arr) = &self.obj.prefix_items {
for (i, child) in arr.iter().enumerate() {
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors);
}
}
if let Some(child) = &self.obj.not {
child.compile(db, root_id, format!("{}/not", path), errors);
}
if let Some(child) = &self.obj.contains {
child.compile(db, root_id, format!("{}/contains", path), errors);
}
self.compile_polymorphism(db, root_id, &path, errors);
}
}

View File

@ -0,0 +1,156 @@
use crate::database::schema::Schema;
impl Schema {
pub fn compile_polymorphism(
&self,
db: &crate::database::Database,
root_id: &str,
path: &str,
errors: &mut Vec<crate::drop::Error>,
) {
let mut options = std::collections::BTreeMap::new();
let mut strategy = String::new();
if let Some(family) = &self.obj.family {
// Formalize the <Variant>.<Base> 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("")
.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.get(&target_id).is_some() {
options.insert(var.to_string(), (None, Some(target_id)));
}
}
} else {
// Scenario C: Single Table Inheritance (Horizontal)
strategy = "kind".to_string();
let suffix = format!(".{}", family_base);
for (id, schema) in &type_def.schemas {
if id.ends_with(&suffix) || id == &family_base {
if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) {
options.insert(kind_val, (None, Some(id.to_string())));
}
}
}
}
}
} else if let Some(one_of) = &self.obj.one_of {
let mut type_vals = std::collections::HashSet::new();
let mut kind_vals = std::collections::HashSet::new();
let mut disjoint_base = true;
let mut structural_types = std::collections::HashSet::new();
for c in one_of {
let mut child_id = String::new();
let mut child_is_primitive = false;
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) {
child_is_primitive = true;
structural_types.insert(t.clone());
} else {
child_id = t.clone();
structural_types.insert("object".to_string());
}
} else {
disjoint_base = false;
}
if !child_is_primitive {
if let Some(t_val) = c.obj.get_discriminator_value("type", &child_id) {
type_vals.insert(t_val);
}
if let Some(k_val) = c.obj.get_discriminator_value("kind", &child_id) {
kind_vals.insert(k_val);
}
}
}
if disjoint_base && structural_types.len() == one_of.len() {
strategy = "".to_string();
for (i, c) in one_of.iter().enumerate() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) {
options.insert(t.clone(), (Some(i), None));
} else {
options.insert("object".to_string(), (Some(i), None));
}
}
}
} else {
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
"type".to_string()
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
"kind".to_string()
} else {
"".to_string()
};
if strategy.is_empty() {
errors.push(crate::drop::Error {
code: "AMBIGUOUS_POLYMORPHISM".to_string(),
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."),
details: crate::drop::ErrorDetails {
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
}
});
return;
}
for (i, c) in one_of.iter().enumerate() {
let mut child_id = String::new();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
child_id = t.clone();
}
}
if let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) {
if options.contains_key(&val) {
errors.push(crate::drop::Error {
code: "POLYMORPHIC_COLLISION".to_string(),
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
details: crate::drop::ErrorDetails {
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
}
});
continue;
}
options.insert(val, (Some(i), None));
}
}
}
} else {
return;
}
if !options.is_empty() {
if !strategy.is_empty() {
let _ = self.obj.compiled_discriminator.set(strategy);
}
let _ = self.obj.compiled_options.set(options);
}
}
}

View File

@ -1,5 +1,6 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
@ -8,5 +9,6 @@ pub struct Enum {
pub module: String,
pub source: String,
pub values: Vec<String>,
pub schemas: Vec<Schema>,
#[serde(default)]
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
}

View File

@ -1,16 +1,15 @@
pub mod compile;
pub mod edge;
pub mod r#enum;
pub mod executors;
pub mod formats;
pub mod object;
pub mod page;
pub mod punc;
pub mod object;
pub mod relation;
pub mod schema;
pub mod r#type;
// External mock exports inside the executor sub-folder
use r#enum::Enum;
use executors::DatabaseExecutor;
@ -28,12 +27,15 @@ use std::collections::HashMap;
use std::sync::Arc;
use r#type::Type;
#[derive(serde::Serialize)]
pub struct Database {
pub enums: HashMap<String, Enum>,
pub types: HashMap<String, Type>,
pub puncs: HashMap<String, Punc>,
pub relations: HashMap<String, Relation>,
#[serde(skip)]
pub schemas: HashMap<String, Arc<Schema>>,
#[serde(skip)]
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
}
@ -60,10 +62,17 @@ impl Database {
db.enums.insert(def.name.clone(), def);
}
Err(e) => {
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
message: format!("Failed to parse database enum: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database enum '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -77,10 +86,17 @@ impl Database {
db.types.insert(def.name.clone(), def);
}
Err(e) => {
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
message: format!("Failed to parse database type: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database type '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -98,10 +114,17 @@ impl Database {
}
}
Err(e) => {
let constraint = item
.get("constraint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
message: format!("Failed to parse database relation: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database relation '{}': {}", constraint, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(constraint)),
..Default::default()
},
});
}
}
@ -115,33 +138,17 @@ impl Database {
db.puncs.insert(def.name.clone(), def);
}
Err(e) => {
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
message: format!("Failed to parse database punc: {}", e),
details: crate::drop::ErrorDetails::default(),
});
}
}
}
}
if let Some(arr) = val.get("schemas").and_then(|v| v.as_array()) {
for (i, item) in arr.iter().enumerate() {
match serde_json::from_value::<Schema>(item.clone()) {
Ok(mut schema) => {
let id = schema
.obj
.id
.clone()
.unwrap_or_else(|| format!("schema_{}", i));
schema.obj.id = Some(id.clone());
db.schemas.insert(id, Arc::new(schema));
}
Err(e) => {
errors.push(crate::drop::Error {
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
message: format!("Failed to parse database schema: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database punc '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -184,51 +191,186 @@ impl Database {
}
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
let mut harvested = Vec::new();
for schema_arc in self.schemas.values_mut() {
if let Some(s) = std::sync::Arc::get_mut(schema_arc) {
s.collect_schemas(None, &mut harvested, errors);
}
}
for (id, schema) in harvested {
self.schemas.insert(id, Arc::new(schema));
}
// Phase 1: Registration
self.collect_schemas(errors);
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
let mut visited = std::collections::HashSet::new();
for schema_arc in self.schemas.values() {
schema_arc.as_ref().compile(self, &mut visited, errors);
// Phase 2: 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 (_, 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 (_, 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);
}
}
// Phase 3: Synthesize Virtual Boundaries
let mut compile_ids = self.compile_filters(errors);
let mut condition_ids = self.compile_conditions();
compile_ids.append(&mut condition_ids);
// Phase 4: Compile Virtual Boundaries
// Now actively compile the newly injected schemas to lock all nested compose references natively
for (_, id) in compile_ids {
if let Some(schema_arc) = self.schemas.get(&id).cloned() {
let root_id = id.split('/').next().unwrap_or(&id);
schema_arc
.as_ref()
.compile(self, root_id, id.clone(), errors);
}
}
}
/// Synthesizes Composed Filter References for all table-backed boundaries.
fn compile_filters(&mut self, errors: &mut Vec<crate::drop::Error>) -> Vec<(String, String)> {
let mut filter_schemas = Vec::new();
for (type_name, type_def) in &self.types {
for (id, schema_arc) in &type_def.schemas {
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
let base_name = id.split('.').last().unwrap_or(id);
let is_table_backed = base_name == type_def.name;
if is_table_backed && !id.contains('/') {
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
filter_schemas.push((
type_name.clone(),
format!("{}.filter", id),
Arc::new(filter_schema),
));
}
}
}
}
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);
}
}
filter_ids
}
/// Synthesizes strong Enum Conditions mirroring the string.condition capabilities.
fn compile_conditions(&mut self) -> Vec<(String, String)> {
let mut enum_conditions = Vec::new();
for (enum_name, enum_def) in &self.enums {
let cond_schema = enum_def.compile_condition();
enum_conditions.push((
enum_name.clone(),
format!("{}.condition", enum_name),
Arc::new(cond_schema),
));
}
let mut condition_ids = Vec::new();
for (enum_name, id, cond_arc) in enum_conditions {
condition_ids.push((enum_name.clone(), id.clone()));
self.schemas.insert(id.clone(), cond_arc.clone());
if let Some(e) = self.enums.get_mut(&enum_name) {
e.schemas.insert(id.clone(), cond_arc.clone());
}
}
condition_ids
}
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {
let mut to_insert = Vec::new();
let mut type_insert = Vec::new();
let mut punc_insert = Vec::new();
let mut enum_insert = Vec::new();
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
// Validate every node recursively via string filters natively!
for type_def in self.types.values() {
for mut schema in type_def.schemas.clone() {
schema.collect_schemas(None, &mut to_insert, errors);
}
}
for punc_def in self.puncs.values() {
for mut schema in punc_def.schemas.clone() {
schema.collect_schemas(None, &mut to_insert, errors);
}
}
for enum_def in self.enums.values() {
for mut schema in enum_def.schemas.clone() {
schema.collect_schemas(None, &mut to_insert, errors);
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,
id,
id.clone(),
&mut local_insert,
errors,
);
for entry in &local_insert {
type_insert.push((type_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
}
}
}
for (id, schema) in to_insert {
self.schemas.insert(id, Arc::new(schema));
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,
id,
id.clone(),
&mut local_insert,
errors,
);
for entry in &local_insert {
punc_insert.push((punc_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
}
}
}
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,
id,
id.clone(),
&mut local_insert,
errors,
);
for entry in &local_insert {
enum_insert.push((enum_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
}
}
}
// 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);
}
}
}
/// Inspects the Postgres pg_constraint relations catalog to securely identify
/// the precise Foreign Key connecting a parent and child hierarchy path.
pub fn resolve_relation<'a>(
@ -260,10 +402,10 @@ impl Database {
all_rels.sort_by(|a, b| a.constraint.cmp(&b.constraint));
for rel in all_rels {
let mut is_forward =
p_def.hierarchy.contains(&rel.source_type) && c_def.hierarchy.contains(&rel.destination_type);
let is_reverse =
p_def.hierarchy.contains(&rel.destination_type) && c_def.hierarchy.contains(&rel.source_type);
let mut is_forward = p_def.hierarchy.contains(&rel.source_type)
&& c_def.hierarchy.contains(&rel.destination_type);
let is_reverse = p_def.hierarchy.contains(&rel.destination_type)
&& c_def.hierarchy.contains(&rel.source_type);
// Structural Cardinality Filtration:
// If the schema requires a collection (Array), it is mathematically impossible for a pure
@ -285,7 +427,7 @@ impl Database {
// Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() {
let mut details = crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
..Default::default()
};
if let Some(sid) = schema_id {
@ -384,7 +526,7 @@ impl Database {
// and forces a clean structural error for the architect.
if !resolved {
let mut details = crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
context: serde_json::to_value(&matching_rels).ok(),
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
..Default::default()

View File

@ -1,9 +1,9 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::database::schema::Schema;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Case {
@ -19,9 +19,6 @@ pub struct Case {
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -40,7 +37,7 @@ pub struct SchemaObject {
#[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")]
#[serde(rename = "family")]
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
@ -157,12 +154,15 @@ pub struct SchemaObject {
#[serde(skip_serializing_if = "Option::is_none")]
pub extensible: Option<bool>,
#[serde(rename = "compiledProperties")]
// Contains ALL structural fields perfectly flattened from the ENTIRE Database inheritance tree (e.g. `entity` fields like `id`) as well as local fields hidden inside conditional `cases` blocks.
// This JSON exported array gives clients absolute deterministic visibility to O(1) validation and masking bounds without duplicating structural memory.
#[serde(rename = "compiledPropertyNames")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_property_names: OnceLock<Vec<String>>,
// Internal structural representation caching active AST Node maps. Unlike the Go framework counterpart, the JSPG implementation DOES natively include ALL ancestral inheritance boundary schemas because it compiles locally against the raw database graph.
#[serde(skip)]
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
@ -176,7 +176,7 @@ pub struct SchemaObject {
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_options: OnceLock<BTreeMap<String, String>>,
pub compiled_options: OnceLock<BTreeMap<String, (Option<usize>, Option<String>)>>,
#[serde(rename = "compiledEdges")]
#[serde(skip_deserializing)]
@ -275,93 +275,49 @@ pub fn is_primitive_type(t: &str) -> bool {
}
impl SchemaObject {
pub fn identifier(&self) -> Option<String> {
if let Some(id) = &self.id {
return Some(id.split('.').next_back().unwrap_or("").to_string());
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
return Some(t.split('.').next_back().unwrap_or("").to_string());
}
}
None
}
pub fn get_discriminator_value(&self, dim: &str) -> Option<String> {
pub fn get_discriminator_value(&self, dim: &str, schema_id: &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(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
let base = schema_id.split('/').last().unwrap_or(schema_id);
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(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !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());
}
}
let base = schema_id.split('/').last().unwrap_or(schema_id);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
None
}
pub fn requires_uuid_path(&self, db: &crate::database::Database) -> bool {
// 1. Explicitly defines "id" either directly or via inheritance/extension?
if self
.compiled_properties
.get()
.map_or(false, |p| p.contains_key("id"))
{
return true;
}
// 2. Implicit table-backed rule: Does its family boundary map directly to the global database catalog?
if let Some(family) = &self.family {
let base = family.split('.').next_back().unwrap_or(family);
if db.types.contains_key(base) {
return true;
}
}
false
}
}

View File

@ -1,6 +1,7 @@
use crate::database::page::Page;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
@ -14,7 +15,8 @@ pub struct Punc {
pub public: bool,
pub form: bool,
pub get: Option<String>,
pub save: Option<String>,
pub page: Option<Page>,
#[serde(default)]
pub schemas: Vec<Schema>,
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
}

View File

@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Relation {
pub id: String,
pub r#type: String,
pub constraint: String,
pub source_type: String,
pub source_columns: Vec<String>,

View File

@ -1,7 +1,7 @@
use crate::database::object::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
use crate::database::object::*;
#[derive(Debug, Clone, Serialize, Default)]
pub struct Schema {
#[serde(flatten)]
@ -23,507 +23,23 @@ impl std::ops::DerefMut for Schema {
}
impl Schema {
pub fn compile(
&self,
db: &crate::database::Database,
visited: &mut std::collections::HashSet<String>,
errors: &mut Vec<crate::drop::Error>,
) {
if self.obj.compiled_properties.get().is_some() {
return;
}
if let Some(id) = &self.obj.id {
if !visited.insert(id.clone()) {
return; // Break cyclical resolution
}
}
if let Some(format_str) = &self.obj.format {
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
let _ = self
.obj
.compiled_format
.set(crate::database::object::CompiledFormat::Func(fmt.func));
}
}
if let Some(pattern_str) = &self.obj.pattern {
if let Ok(re) = regex::Regex::new(pattern_str) {
let _ = self
.obj
.compiled_pattern
.set(crate::database::object::CompiledRegex(re));
}
}
if let Some(pattern_props) = &self.obj.pattern_properties {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
let _ = self.obj.compiled_pattern_properties.set(compiled);
}
}
let mut props = std::collections::BTreeMap::new();
// 1. Resolve INHERITANCE dependencies first
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.as_ref().compile(db, visited, errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
}
}
}
}
if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
let mut custom_type_count = 0;
for t in types {
if !crate::database::object::is_primitive_type(t) {
custom_type_count += 1;
}
}
if custom_type_count > 1 {
errors.push(crate::drop::Error {
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
message: format!(
"Schema '{}' attempts to extend multiple custom object pointers in its type array. Use 'oneOf' for polymorphism and tagged unions.",
self.obj.identifier().unwrap_or("unknown".to_string())
),
details: crate::drop::ErrorDetails {
path: self.obj.identifier().unwrap_or("unknown".to_string()),
..Default::default()
}
});
}
for t in types {
if !crate::database::object::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.as_ref().compile(db, visited, errors);
}
}
}
}
// 2. Add local properties
if let Some(local_props) = &self.obj.properties {
for (k, v) in local_props {
props.insert(k.clone(), v.clone());
}
}
// 3. Set the OnceLock!
let _ = self.obj.compiled_properties.set(props.clone());
let mut names: Vec<String> = props.keys().cloned().collect();
names.sort();
let _ = self.obj.compiled_property_names.set(names);
// 4. Compute Edges natively
let schema_edges = self.compile_edges(db, visited, &props, errors);
let _ = self.obj.compiled_edges.set(schema_edges);
// 5. Build our inline children properties recursively NOW! (Depth-first search)
if let Some(local_props) = &self.obj.properties {
for child in local_props.values() {
child.compile(db, visited, errors);
}
}
if let Some(items) = &self.obj.items {
items.compile(db, visited, errors);
}
if let Some(pattern_props) = &self.obj.pattern_properties {
for child in pattern_props.values() {
child.compile(db, visited, errors);
}
}
if let Some(additional_props) = &self.obj.additional_properties {
additional_props.compile(db, visited, errors);
}
if let Some(one_of) = &self.obj.one_of {
for child in one_of {
child.compile(db, visited, errors);
}
}
if let Some(arr) = &self.obj.prefix_items {
for child in arr {
child.compile(db, visited, errors);
}
}
if let Some(child) = &self.obj.not {
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.contains {
child.compile(db, visited, errors);
}
if let Some(cases) = &self.obj.cases {
for c in cases {
if let Some(child) = &c.when {
child.compile(db, visited, errors);
}
if let Some(child) = &c.then {
child.compile(db, visited, errors);
}
if let Some(child) = &c.else_ {
child.compile(db, visited, errors);
}
}
}
self.compile_polymorphism(db, errors);
if let Some(id) = &self.obj.id {
visited.remove(id);
}
}
/// Dynamically infers and compiles all structural database relationships between this Schema
/// and its nested children. This functions recursively traverses the JSON Schema abstract syntax
/// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation
/// constraint paths directly onto the `compiled_edges` map in O(1) memory.
pub fn compile_edges(
&self,
db: &crate::database::Database,
visited: &mut std::collections::HashSet<String>,
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
errors: &mut Vec<crate::drop::Error>,
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
let mut schema_edges = std::collections::BTreeMap::new();
// Determine the physical Database Table Name this schema structurally represents
// Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person")
let mut parent_type_name = None;
if let Some(family) = &self.obj.family {
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(identifier) = self.obj.identifier() {
parent_type_name = Some(
identifier
.split('.')
.next_back()
.unwrap_or(&identifier)
.to_string(),
);
}
if let Some(p_type) = parent_type_name {
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
if db.types.contains_key(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props {
let mut child_type_name = None;
let mut target_schema = prop_schema.clone();
let mut is_array = false;
// Structurally unpack the inner target entity if the object maps to an array list
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_
{
if t == "array" {
is_array = true;
if let Some(items) = &prop_schema.obj.items {
target_schema = items.clone();
}
}
}
// Determine the physical Postgres table backing the nested child schema recursively
if let Some(family) = &target_schema.obj.family {
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(ref_id) = target_schema.obj.identifier() {
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
} else if let Some(arr) = &target_schema.obj.one_of {
if let Some(first) = arr.first() {
if let Some(ref_id) = first.obj.identifier() {
child_type_name =
Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
}
}
}
if let Some(c_type) = child_type_name {
if db.types.contains_key(&c_type) {
// Ensure the child Schema's AST has accurately compiled its own physical property keys so we can
// inject them securely for Many-to-Many Twin Deduction disambiguation matching.
target_schema.compile(db, visited, errors);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> =
compiled_target_props.keys().cloned().collect();
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
if let Some((relation, is_forward)) = db.resolve_relation(
&p_type,
&c_type,
prop_name,
Some(&keys_for_ambiguity),
is_array,
self.id.as_deref(),
&format!("/{}", prop_name),
errors,
) {
schema_edges.insert(
prop_name.clone(),
crate::database::edge::Edge {
constraint: relation.constraint.clone(),
forward: is_forward,
},
);
}
}
}
}
}
}
}
schema_edges
}
pub fn compile_polymorphism(
&self,
db: &crate::database::Database,
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 suffix = format!(".{}", family_base);
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
if id.ends_with(&suffix) || id == &family_base {
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::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
target_id = Some(t.clone());
}
}
}
if let Some(tid) = target_id {
options.insert(val, tid);
}
}
}
} else {
return;
}
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))]
for c in id.chars() {
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' {
errors.push(crate::drop::Error {
code: "INVALID_IDENTIFIER".to_string(),
message: format!(
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]",
c, field_name, id
),
details: crate::drop::ErrorDetails::default(),
});
return;
}
}
}
pub fn collect_schemas(
&mut self,
tracking_path: Option<String>,
to_insert: &mut Vec<(String, Schema)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(id) = &self.obj.id {
Self::validate_identifier(id, "$id", errors);
to_insert.push((id.clone(), self.clone()));
}
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
Self::validate_identifier(t, "type", errors);
}
}
if let Some(family) = &self.obj.family {
Self::validate_identifier(family, "$family", errors);
}
// Is this schema an inline ad-hoc composition?
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
if self.obj.id.is_none() && self.obj.properties.is_some() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if let Some(ref path) = tracking_path {
to_insert.push((path.clone(), self.clone()));
}
}
}
}
// Provide the path origin to children natively, prioritizing the explicit `$id` boundary if one exists
let origin_path = self.obj.id.clone().or(tracking_path);
self.collect_child_schemas(origin_path, to_insert, errors);
}
pub fn collect_child_schemas(
&mut self,
origin_path: Option<String>,
to_insert: &mut Vec<(String, Schema)>,
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(props) = &mut self.obj.properties {
for (k, v) in props.iter_mut() {
let mut inner = (**v).clone();
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
inner.collect_schemas(next_path, to_insert, errors);
*v = Arc::new(inner);
}
}
if let Some(pattern_props) = &mut self.obj.pattern_properties {
for (k, v) in pattern_props.iter_mut() {
let mut inner = (**v).clone();
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
inner.collect_schemas(next_path, to_insert, errors);
*v = Arc::new(inner);
}
}
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*v = Arc::new(inner);
}
};
if let Some(arr) = &mut self.obj.prefix_items {
map_arr(arr);
}
if let Some(arr) = &mut self.obj.one_of {
map_arr(arr);
}
let mut map_opt = |opt: &mut Option<Arc<Schema>>, pass_path: bool| {
if let Some(v) = opt {
let mut inner = (**v).clone();
let next = if pass_path { origin_path.clone() } else { None };
inner.collect_schemas(next, to_insert, errors);
*v = Arc::new(inner);
}
};
map_opt(&mut self.obj.additional_properties, false);
// `items` absolutely must inherit the EXACT property path assigned to the Array wrapper!
// This allows nested Arrays enclosing bare Entity structs to correctly register as the boundary mapping.
map_opt(&mut self.obj.items, true);
map_opt(&mut self.obj.not, false);
map_opt(&mut self.obj.contains, false);
map_opt(&mut self.obj.property_names, false);
if let Some(cases) = &mut self.obj.cases {
for c in cases.iter_mut() {
if let Some(when) = &mut c.when {
let mut inner = (**when).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*when = Arc::new(inner);
}
if let Some(then) = &mut c.then {
let mut inner = (**then).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*then = Arc::new(inner);
}
if let Some(else_) = &mut c.else_ {
let mut inner = (**else_).clone();
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*else_ = Arc::new(inner);
}
}
}
/// 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()
}
}

View File

@ -2,6 +2,7 @@ use std::collections::HashSet;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -38,5 +39,5 @@ pub struct Type {
pub default_fields: Vec<String>,
pub field_types: Option<Value>,
#[serde(default)]
pub schemas: Vec<Schema>,
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
}

View File

@ -66,7 +66,8 @@ pub struct Error {
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ErrorDetails {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]

View File

@ -30,7 +30,7 @@ fn jspg_failure() -> JsonB {
code: "ENGINE_NOT_INITIALIZED".to_string(),
message: "JSPG extension has not been initialized via jspg_setup".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: None,
schema: None,
@ -72,7 +72,7 @@ pub fn jspg_merge(schema_id: &str, data: JsonB) -> JsonB {
}
#[cfg_attr(not(test), pg_extern)]
pub fn jspg_query(schema_id: &str, filters: Option<JsonB>) -> JsonB {
pub fn jspg_query(schema_id: &str, filter: Option<JsonB>) -> JsonB {
let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
@ -82,7 +82,7 @@ pub fn jspg_query(schema_id: &str, filters: Option<JsonB>) -> JsonB {
Some(engine) => {
let drop = engine
.queryer
.query(schema_id, filters.as_ref().map(|f| &f.0));
.query(schema_id, filter.as_ref().map(|f| &f.0));
JsonB(serde_json::to_value(drop).unwrap())
}
None => jspg_failure(),
@ -109,7 +109,7 @@ pub fn jspg_validate(schema_id: &str, instance: JsonB) -> JsonB {
}
#[cfg_attr(not(test), pg_extern)]
pub fn jspg_schemas() -> JsonB {
pub fn jspg_database() -> JsonB {
let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
@ -117,9 +117,9 @@ pub fn jspg_schemas() -> JsonB {
match engine_opt {
Some(engine) => {
let schemas_json = serde_json::to_value(&engine.database.schemas)
let database_json = serde_json::to_value(&engine.database)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let drop = crate::drop::Drop::success_with_val(schemas_json);
let drop = crate::drop::Drop::success_with_val(database_json);
JsonB(serde_json::to_value(drop).unwrap())
}
None => jspg_failure(),

View File

@ -25,13 +25,13 @@ impl Merger {
let mut notifications_queue = Vec::new();
let target_schema = match self.db.schemas.get(schema_id) {
Some(s) => Arc::clone(s),
Some(s) => Arc::clone(&s),
None => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "MERGE_FAILED".to_string(),
message: format!("Unknown schema_id: {}", schema_id),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: Some(data),
schema: None,
@ -76,7 +76,7 @@ impl Merger {
code: final_code,
message: final_message,
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: final_cause,
context: None,
schema: None,
@ -92,7 +92,7 @@ impl Merger {
code: "MERGE_FAILED".to_string(),
message: format!("Executor Error in pre-ordered notify: {:?}", e),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: None,
schema: None,
@ -142,22 +142,51 @@ impl Merger {
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::clone(target_schema);
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.schemas.get(target_id)
{
schema = target_schema.clone();
} else {
return Err(format!(
"Polymorphic mapped target '{}' not found in database registry",
target_id
));
}
} else if let Some(idx) = idx_opt {
if let Some(target_schema) = schema
.obj
.one_of
.as_ref()
.and_then(|options| options.get(*idx))
{
schema = Arc::clone(target_schema);
} else {
return Err(format!(
"Polymorphic index target '{}' not found in local oneOf array",
idx
));
}
} else {
return Err(format!("Polymorphic mapped target '{}' not found in database registry", target_id));
return Err(format!("Polymorphic mapped target has no path"));
}
} else {
return Err(format!("Polymorphic discriminator {}='{}' matched no compiled options", disc, v));
return Err(format!(
"Polymorphic discriminator {}='{}' matched no compiled options",
disc, v
));
}
} else {
return Err(format!("Polymorphic merging failed: missing required discriminator '{}'", disc));
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()),
}
}
@ -215,7 +244,7 @@ impl Merger {
for (k, v) in obj {
// Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables
if k == "id" || k == "type" || k == "created" {
entity_fields.insert(k.clone(), v.clone());
entity_fields.insert(k, v);
continue;
}
@ -234,18 +263,18 @@ impl Merger {
_ => "field", // Malformed edge data?
};
if typeof_v == "object" {
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
entity_objects.insert(k, (v, prop_schema.clone()));
} else if typeof_v == "array" {
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
entity_arrays.insert(k, (v, prop_schema.clone()));
} else {
entity_fields.insert(k.clone(), v.clone());
entity_fields.insert(k, v);
}
} else {
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
entity_fields.insert(k.clone(), v.clone());
entity_fields.insert(k, v);
}
} else if type_def.fields.contains(&k) {
entity_fields.insert(k.clone(), v.clone());
entity_fields.insert(k, v);
}
}
@ -524,7 +553,7 @@ impl Merger {
entity_change_kind = Some("create".to_string());
let mut new_fields = changes.clone();
let mut new_fields = changes;
new_fields.insert("id".to_string(), id_val);
new_fields.insert("type".to_string(), Value::String(type_name.to_string()));
new_fields.insert("created_by".to_string(), Value::String(user_id.to_string()));
@ -564,7 +593,7 @@ impl Merger {
Some("update".to_string())
};
let mut new_fields = changes.clone();
let mut new_fields = changes;
new_fields.insert(
"id".to_string(),
entity_fetched.as_ref().unwrap().get("id").unwrap().clone(),

View File

@ -18,6 +18,7 @@ pub struct Node<'a> {
pub depth: usize,
pub ast_path: String,
pub is_polymorphic_branch: bool,
pub schema_id: Option<String>,
}
impl<'a> Compiler<'a> {
@ -27,9 +28,10 @@ impl<'a> Compiler<'a> {
.db
.schemas
.get(schema_id)
.cloned()
.ok_or_else(|| format!("Schema not found: {}", schema_id))?;
let target_schema = std::sync::Arc::clone(schema);
let target_schema = schema;
let mut compiler = Compiler {
db: &self.db,
@ -47,6 +49,7 @@ impl<'a> Compiler<'a> {
depth: 0,
ast_path: String::new(),
is_polymorphic_branch: false,
schema_id: Some(schema_id.to_string()),
};
let (sql, _) = compiler.compile_node(node)?;
@ -66,17 +69,31 @@ impl<'a> Compiler<'a> {
}
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// 1. Array of DB Entities (`type` or `$family` pointing to a table limit)
if let Some(items) = &node.schema.obj.items {
let mut resolved_type = None;
if let Some(family_target) = items.obj.family.as_ref() {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name);
} else if let Some(base_type_name) = items.obj.identifier() {
resolved_type = self.db.types.get(&base_type_name);
if let Some(sid) = &node.schema_id {
resolved_type = self
.db
.types
.get(&sid.split('.').next_back().unwrap_or(sid).to_string());
}
if resolved_type.is_none() {
if let Some(family_target) = items.obj.family.as_ref() {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name);
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &items.obj.type_
{
if !crate::database::object::is_primitive_type(t) {
resolved_type = self
.db
.types
.get(&t.split('.').next_back().unwrap_or(t).to_string());
}
}
}
if let Some(type_def) = resolved_type {
@ -105,16 +122,28 @@ impl<'a> Compiler<'a> {
// Determine if this schema represents a Database Entity
let mut resolved_type = None;
if let Some(family_target) = node.schema.obj.family.as_ref() {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name);
} else if let Some(base_type_name) = node.schema.obj.identifier() {
if let Some(sid) = &node.schema_id {
let base_type_name = sid.split('.').next_back().unwrap_or(sid).to_string();
resolved_type = self.db.types.get(&base_type_name);
}
if resolved_type.is_none() {
if let Some(family_target) = node.schema.obj.family.as_ref() {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name);
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&node.schema.obj.type_
{
if !crate::database::object::is_primitive_type(t) {
let base_type_name = t.split('.').next_back().unwrap_or(t).to_string();
resolved_type = self.db.types.get(&base_type_name);
}
}
}
if let Some(type_def) = resolved_type {
return self.compile_entity(type_def, node.clone(), false);
}
@ -123,9 +152,10 @@ 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.schemas.get(t) {
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
let mut ref_node = node.clone();
ref_node.schema = Arc::clone(target_schema);
ref_node.schema = target_schema.clone();
ref_node.schema_id = Some(t.clone());
return self.compile_node(ref_node);
}
return Err(format!("Unresolved schema type pointer: {}", t));
@ -133,17 +163,21 @@ impl<'a> Compiler<'a> {
}
// 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::object::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);
if let Some(options) = node.schema.obj.compiled_options.get() {
if options.len() == 1 {
let (_, target_opt) = options.values().next().unwrap();
if let Some(target_id) = target_opt {
let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.type_ = Some(crate::database::object::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);
}
}
return self.compile_one_of(node);
}
}
return self.compile_one_of(node);
}
// Just an inline object definition?
@ -171,27 +205,27 @@ impl<'a> Compiler<'a> {
let (table_aliases, from_clauses) = self.compile_from_clause(r#type);
let jsonb_obj_sql = 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 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);
case_node.parent_type = Some(r#type);
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
let (case_sql, _) = self.compile_one_of(case_node)?;
case_sql
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);
case_node.parent_type = Some(r#type);
let (case_sql, _) = self.compile_one_of(case_node)?;
case_sql
} else {
let select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
let select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
};
// 3. Build WHERE clauses
@ -249,14 +283,21 @@ impl<'a> Compiler<'a> {
Ok((combined, "object".to_string()))
}
fn compile_one_of(
&mut self,
node: Node<'a>,
) -> Result<(String, String), String> {
fn compile_one_of(&mut self, 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 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!("{}_{}", prop, disc)
@ -264,36 +305,74 @@ impl<'a> Compiler<'a> {
disc.to_string()
};
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 = Arc::clone(target_schema);
child_node.is_polymorphic_branch = true;
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.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());
child_node.is_polymorphic_branch = true;
let val_sql = if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
let aliases = aliases_arc.as_ref();
let p_type = node.parent_type.unwrap();
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
if select_args.is_empty() {
let val_sql =
if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
let aliases = aliases_arc.as_ref();
let p_type = node.parent_type.unwrap();
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
} else {
let (sql, _) = self.compile_node(child_node)?;
sql
};
}
} else {
let (sql, _) = self.compile_node(child_node)?;
sql
};
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",
node.parent_alias, type_col, disc_val, val_sql
));
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",
node.parent_alias, type_col, disc_val, val_sql
));
}
} else if let Some(idx) = idx_opt {
if let Some(target_schema) = node
.schema
.obj
.one_of
.as_ref()
.and_then(|options| options.get(*idx))
{
let mut child_node = node.clone();
child_node.schema = Arc::clone(target_schema);
child_node.is_polymorphic_branch = true;
let val_sql =
if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
let aliases = aliases_arc.as_ref();
let p_type = node.parent_type.unwrap();
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
} else {
let (sql, _) = self.compile_node(child_node)?;
sql
};
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",
node.parent_alias, type_col, disc_val, val_sql
));
}
}
}
if case_statements.is_empty() {
return Ok(("NULL".to_string(), "string".to_string()));
}
@ -339,14 +418,14 @@ impl<'a> Compiler<'a> {
let mut select_args = Vec::new();
let grouped_fields = r#type.grouped_fields.as_ref().and_then(|v| v.as_object());
let default_props = std::collections::BTreeMap::new();
let merged_props = node.schema.obj.compiled_properties.get().unwrap_or(&default_props);
let mut sorted_keys: Vec<&String> = merged_props.keys().collect();
sorted_keys.sort();
for prop_key in sorted_keys {
let prop_schema = &merged_props[prop_key];
let merged_props = node
.schema
.obj
.compiled_properties
.get()
.unwrap_or(&default_props);
for (prop_key, prop_schema) in merged_props {
let is_object_or_array = match &prop_schema.obj.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(s)) => {
s == "object" || s == "array"
@ -396,6 +475,15 @@ impl<'a> Compiler<'a> {
}
}
if let Some(ft) = r#type.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type) = ft.get(prop_key).and_then(|v| v.as_str()) {
if pg_type == "json" || pg_type == "jsonb" {
select_args.push(format!("'{}', {}.{}", prop_key, owner_alias, prop_key));
continue;
}
}
}
let child_node = Node {
schema: std::sync::Arc::clone(prop_schema),
parent_alias: owner_alias.clone(),
@ -410,6 +498,7 @@ impl<'a> Compiler<'a> {
format!("{}/{}", node.ast_path, prop_key)
},
is_polymorphic_branch: false,
schema_id: None,
};
let (val_sql, val_type) = self.compile_node(child_node)?;
@ -449,7 +538,7 @@ impl<'a> Compiler<'a> {
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
let start_len = where_clauses.len();
self.compile_relation_conditions(
r#type,
@ -491,8 +580,12 @@ impl<'a> Compiler<'a> {
.unwrap_or(family_target)
.to_string(),
);
} else if let Some(lookup_key) = prop_schema.obj.identifier() {
bound_type_name = Some(lookup_key);
} else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_
{
if !crate::database::object::is_primitive_type(t) {
bound_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
}
}
if let Some(type_name) = bound_type_name {
@ -625,8 +718,8 @@ impl<'a> Compiler<'a> {
let param_index = i + 1;
let p_val = format!("${}#>>'{{}}'", param_index);
if op == "$in" || op == "$nin" {
let sql_op = if op == "$in" { "IN" } else { "NOT IN" };
if op == "$of" || op == "$nof" {
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
let subquery = format!(
"(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))",
cast, p_val

View File

@ -21,9 +21,9 @@ impl Queryer {
pub fn query(
&self,
schema_id: &str,
filters: Option<&serde_json::Value>,
filter: Option<&serde_json::Value>,
) -> crate::drop::Drop {
let filters_map = filters.and_then(|f| f.as_object());
let filters_map = filter.and_then(|f| f.as_object());
// 1. Process filters into structured $op keys and linear values
let (filter_keys, args) = match self.parse_filter_entries(filters_map) {
@ -33,9 +33,9 @@ impl Queryer {
code: "FILTER_PARSE_FAILED".to_string(),
message: msg.clone(),
details: crate::drop::ErrorDetails {
path: "".to_string(), // filters apply to the root query
path: None, // filters apply to the root query
cause: Some(msg),
context: filters.cloned(),
context: filter.cloned(),
schema: Some(schema_id.to_string()),
},
}]);
@ -138,7 +138,7 @@ impl Queryer {
code: "QUERY_COMPILATION_FAILED".to_string(),
message: e.clone(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(e),
context: None,
schema: Some(schema_id.to_string()),
@ -165,7 +165,7 @@ impl Queryer {
code: "QUERY_FAILED".to_string(),
message: format!("Expected array from generic query, got: {:?}", other),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(format!("Expected array, got {}", other)),
context: Some(serde_json::json!([sql])),
schema: Some(schema_id.to_string()),
@ -175,7 +175,7 @@ impl Queryer {
code: "QUERY_FAILED".to_string(),
message: format!("SPI error in queryer: {}", e),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(format!("SPI error in queryer: {}", e)),
context: Some(serde_json::json!([sql])),
schema: Some(schema_id.to_string()),

View File

@ -533,6 +533,12 @@ fn test_unique_items_6_1() {
crate::tests::runner::run_test_case(&path, 6, 1).unwrap();
}
#[test]
fn test_filter_0_0() {
let path = format!("{}/fixtures/filter.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 0).unwrap();
}
#[test]
fn test_min_items_0_0() {
let path = format!("{}/fixtures/minItems.json", env!("CARGO_MANIFEST_DIR"));
@ -1241,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"));
@ -1451,6 +1487,18 @@ fn test_queryer_0_12() {
crate::tests::runner::run_test_case(&path, 0, 12).unwrap();
}
#[test]
fn test_queryer_0_13() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}
#[test]
fn test_queryer_0_14() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
}
#[test]
fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
@ -3558,9 +3606,9 @@ fn test_paths_1_0() {
}
#[test]
fn test_paths_1_1() {
fn test_paths_2_0() {
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 1, 1).unwrap();
crate::tests::runner::run_test_case(&path, 2, 0).unwrap();
}
#[test]
@ -3701,6 +3749,18 @@ fn test_database_4_0() {
crate::tests::runner::run_test_case(&path, 4, 0).unwrap();
}
#[test]
fn test_database_5_0() {
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 5, 0).unwrap();
}
#[test]
fn test_database_6_0() {
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
}
#[test]
fn test_cases_0_0() {
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));
@ -7740,21 +7800,9 @@ fn test_object_types_2_1() {
}
#[test]
fn test_object_types_3_0() {
fn test_object_types_2_2() {
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 3, 0).unwrap();
}
#[test]
fn test_object_types_3_1() {
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 3, 1).unwrap();
}
#[test]
fn test_object_types_3_2() {
let path = format!("{}/fixtures/objectTypes.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 3, 2).unwrap();
crate::tests::runner::run_test_case(&path, 2, 2).unwrap();
}
#[test]
@ -8110,3 +8158,15 @@ fn test_merger_0_12() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 12).unwrap();
}
#[test]
fn test_merger_0_13() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}
#[test]
fn test_merger_0_14() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
}

View File

@ -18,7 +18,7 @@ fn test_library_api() {
"errors": [{
"code": "ENGINE_NOT_INITIALIZED",
"message": "JSPG extension has not been initialized via jspg_setup",
"details": { "path": "" }
"details": {}
}]
})
);
@ -44,27 +44,30 @@ fn test_library_api() {
"name": "source_schema",
"variations": ["source_schema"],
"hierarchy": ["source_schema", "entity"],
"schemas": [{
"$id": "source_schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"target": { "type": "target_schema" }
},
"required": ["name"]
}]
"schemas": {
"source_schema": {
"type": "object",
"properties": {
"type": { "type": "string" },
"name": { "type": "string" },
"target": { "type": "target_schema" }
},
"required": ["name"]
}
}
},
{
"name": "target_schema",
"variations": ["target_schema"],
"hierarchy": ["target_schema", "entity"],
"schemas": [{
"$id": "target_schema",
"type": "object",
"properties": {
"value": { "type": "number" }
"schemas": {
"target_schema": {
"type": "object",
"properties": {
"value": { "type": "number" }
}
}
}]
}
}
]
});
@ -78,39 +81,144 @@ fn test_library_api() {
})
);
// 3. Validate jspg_schemas
let schemas_drop = jspg_schemas();
// 3. Validate jspg_database mapping natively!
let db_drop = jspg_database();
assert_eq!(
schemas_drop.0,
db_drop.0,
json!({
"type": "drop",
"response": {
"source_schema": {
"$id": "source_schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"target": {
"type": "target_schema",
"compiledProperties": ["value"]
}
},
"required": ["name"],
"compiledProperties": ["name", "target"],
"compiledEdges": {
"target": {
"constraint": "fk_test_target",
"forward": true
}
"enums": {},
"puncs": {},
"relations": {
"fk_test_target": {
"id": "11111111-1111-1111-1111-111111111111",
"type": "relation",
"constraint": "fk_test_target",
"destination_columns": ["id"],
"destination_type": "target_schema",
"prefix": "target",
"source_columns": ["target_id"],
"source_type": "source_schema"
}
},
"target_schema": {
"$id": "target_schema",
"type": "object",
"properties": {
"value": { "type": "number" }
"types": {
"source_schema": {
"default_fields": [],
"field_types": null,
"fields": [],
"grouped_fields": null,
"hierarchy": ["source_schema", "entity"],
"historical": false,
"id": "",
"longevity": null,
"lookup_fields": [],
"module": "",
"name": "source_schema",
"notify": false,
"null_fields": [],
"ownable": false,
"relationship": false,
"schemas": {
"source_schema": {
"compiledEdges": {
"target": {
"constraint": "fk_test_target",
"forward": true
}
},
"compiledPropertyNames": ["name", "target", "type"],
"properties": {
"name": { "type": "string" },
"target": {
"compiledPropertyNames": ["value"],
"type": "target_schema"
},
"type": { "type": "string" }
},
"required": ["name"],
"type": "object"
},
"source_schema.filter": {
"compiledPropertyNames": ["$and", "$or", "name", "target", "type"],
"properties": {
"$and": {
"type": ["array", "null"],
"items": {
"compiledPropertyNames": ["$and", "$or", "name", "target", "type"],
"type": "source_schema.filter"
}
},
"$or": {
"type": ["array", "null"],
"items": {
"compiledPropertyNames": ["$and", "$or", "name", "target", "type"],
"type": "source_schema.filter"
}
},
"name": { "type": ["string.condition", "null"] },
"target": { "type": ["target_schema.filter", "null"] },
"type": { "type": ["string.condition", "null"] }
},
"type": "object"
}
},
"sensitive": false,
"source": "",
"type": "",
"variations": ["source_schema"]
},
"compiledProperties": ["value"]
"target_schema": {
"default_fields": [],
"field_types": null,
"fields": [],
"grouped_fields": null,
"hierarchy": ["target_schema", "entity"],
"historical": false,
"id": "",
"longevity": null,
"lookup_fields": [],
"module": "",
"name": "target_schema",
"notify": false,
"null_fields": [],
"ownable": false,
"relationship": false,
"schemas": {
"target_schema": {
"compiledPropertyNames": ["value"],
"properties": {
"value": { "type": "number" }
},
"type": "object"
},
"target_schema.filter": {
"compiledPropertyNames": ["$and", "$or", "value"],
"properties": {
"$and": {
"type": ["array", "null"],
"items": {
"compiledPropertyNames": ["$and", "$or", "value"],
"type": "target_schema.filter"
}
},
"$or": {
"type": ["array", "null"],
"items": {
"compiledPropertyNames": ["$and", "$or", "value"],
"type": "target_schema.filter"
}
},
"value": { "type": ["number.condition", "null"] }
},
"type": "object"
}
},
"sensitive": false,
"source": "",
"type": "",
"variations": ["target_schema"]
}
}
}
})

View File

@ -86,7 +86,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
let error_messages: Vec<String> = drop
.errors
.iter()
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path, e.message))
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path.as_deref().unwrap_or("/"), e.message))
.collect();
failures.push(format!(
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}",

View File

@ -17,7 +17,7 @@ pub struct Case {
// For Query
#[serde(default)]
pub filters: Option<serde_json::Value>,
pub filter: Option<serde_json::Value>,
// For Merge & Validate
#[serde(default)]
@ -49,7 +49,13 @@ impl Case {
Err(d) => d.clone(),
};
expect.assert_drop(&result)
expect.assert_drop(&result)?;
if let Ok(db) = db_res {
expect.assert_schemas(db)?;
}
Ok(())
}
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> {
@ -58,12 +64,6 @@ impl Case {
let validator = Validator::new(db);
let schema_id = &self.schema_id;
if !validator.db.schemas.contains_key(schema_id) {
return Err(format!(
"Missing Schema: Cannot find schema ID '{}'",
schema_id
));
}
let test_data = self.data.clone().unwrap_or(Value::Null);
let result = validator.validate(schema_id, &test_data);
@ -116,7 +116,7 @@ impl Case {
use crate::queryer::Queryer;
let queryer = Queryer::new(db.clone());
let result = queryer.query(&self.schema_id, self.filters.as_ref());
let result = queryer.query(&self.schema_id, self.filter.as_ref());
let return_val = if let Some(expect) = &self.expect {
if let Err(e) = expect.assert_drop(&result) {

View File

@ -1,6 +1,7 @@
pub mod pattern;
pub mod sql;
pub mod drop;
pub mod schema;
use serde::Deserialize;
@ -18,4 +19,6 @@ pub struct Expect {
pub errors: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub sql: Option<Vec<SqlExpectation>>,
#[serde(default)]
pub schemas: Option<std::collections::HashMap<String, serde_json::Value>>,
}

View File

@ -0,0 +1,53 @@
use super::Expect;
use std::sync::Arc;
impl Expect {
pub fn assert_schemas(&self, db: &Arc<crate::database::Database>) -> Result<(), String> {
if let Some(expected_map) = &self.schemas {
// Collect actual schemas and sort
let mut actual: Vec<String> = Vec::new();
for type_def in db.types.values() {
actual.extend(type_def.schemas.keys().cloned());
}
for punc_def in db.puncs.values() {
actual.extend(punc_def.schemas.keys().cloned());
}
for enum_def in db.enums.values() {
actual.extend(enum_def.schemas.keys().cloned());
}
actual.sort();
// Collect expected schemas and sort
let mut expected: Vec<String> = expected_map.keys().cloned().collect();
expected.sort();
if actual != expected {
return Err(format!(
"Schema Promotion Mismatch!\nExpected Schemas ({}):\n{:#?}\n\nActual Promoted Schemas ({}):\n{:#?}",
expected.len(),
expected,
actual.len(),
actual
));
}
for (key, expected_val) in expected_map {
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 actual_ast = db.schemas.get(key).cloned().unwrap();
let actual_val = serde_json::to_value(actual_ast).unwrap();
if actual_val != *expected_val {
return Err(format!(
"Detailed Schema Match Failure for '{}'!\n\nExpected:\n{}\n\nActual:\n{}",
key,
serde_json::to_string_pretty(expected_val).unwrap(),
serde_json::to_string_pretty(&actual_val).unwrap()
));
}
}
}
Ok(())
}
}

View File

@ -15,6 +15,7 @@ pub struct ValidationContext<'a> {
pub extensible: bool,
pub reporter: bool,
pub overrides: HashSet<String>,
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<String>,
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,
)
}

View File

@ -23,10 +23,6 @@ impl Validator {
Self { db }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.db.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val
&& s.is_empty()
@ -46,11 +42,13 @@ impl Validator {
}
pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop {
if let Some(schema) = self.db.schemas.get(schema_id) {
let schema_opt = self.db.schemas.get(schema_id);
if let Some(schema) = schema_opt {
let ctx = ValidationContext::new(
&self.db,
schema,
schema,
&schema,
&schema,
instance,
HashSet::new(),
false,
@ -68,7 +66,7 @@ impl Validator {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails {
path: e.path,
path: Some(e.path),
cause: None,
context: None,
schema: None,
@ -82,7 +80,7 @@ impl Validator {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails {
path: e.path,
path: Some(e.path),
cause: None,
context: None,
schema: None,
@ -94,7 +92,7 @@ impl Validator {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
details: crate::drop::ErrorDetails {
path: "/".to_string(),
path: Some("/".to_string()),
cause: None,
context: None,
schema: None,

View File

@ -57,6 +57,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
self.extensible,
false,
Some(self.instance),
);
let check = derived.validate()?;
@ -93,9 +94,12 @@ impl<'a> ValidationContext<'a> {
if i < len {
if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
let is_topological = sub_schema.obj.requires_uuid_path(self.db);
if is_topological {
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
}
let derived = self.derive(
@ -105,6 +109,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
self.extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
@ -116,12 +121,15 @@ impl<'a> ValidationContext<'a> {
}
if let Some(ref items_schema) = self.schema.items {
let is_topological = items_schema.obj.requires_uuid_path(self.db);
for i in validation_index..len {
if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
if is_topological {
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
}
let derived = self.derive(
@ -131,6 +139,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
self.extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);

View File

@ -24,9 +24,6 @@ impl<'a> ValidationContext<'a> {
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if key == "type" || key == "kind" {
continue; // Reserved keywords implicitly allowed
}
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),

View File

@ -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);
}

View File

@ -13,13 +13,23 @@ impl<'a> ValidationContext<'a> {
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(obj) = current.as_object() {
let mut schema_identifier = None;
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.schema.type_ {
if !crate::database::object::is_primitive_type(t) {
schema_identifier = Some(t.clone());
}
}
// Entity implicit type validation
if let Some(schema_identifier) = self.schema.identifier() {
if let Some(ref schema_identifier_str) = schema_identifier {
// We decompose identity string routing inherently
let expected_type = schema_identifier.split('.').last().unwrap_or(&schema_identifier);
let expected_type = schema_identifier_str
.split('.')
.last()
.unwrap_or(schema_identifier_str);
// Check if the identifier represents a registered global database entity boundary mathematically
if let Some(type_def) = self.db.types.get(expected_type) {
if let Some(type_def) = self.db.types.get(expected_type).filter(|t| !t.variations.is_empty()) {
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if type_def.variations.contains(type_str) {
@ -40,21 +50,33 @@ impl<'a> ValidationContext<'a> {
// Because it's a global entity target, the payload must structurally provide a discriminator natively
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: format!("Schema mechanically requires type discrimination '{}'", expected_type),
message: format!(
"Schema mechanically requires type discrimination '{}'",
expected_type
),
path: self.path.clone(), // Empty boundary
});
}
// If the target mathematically declares a horizontal structural STI variation natively
if schema_identifier.contains('.') {
if obj.get("kind").is_none() {
result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination".to_string(),
path: self.path.clone(),
});
} else {
result.evaluated_keys.insert("kind".to_string());
if schema_identifier_str.contains('.') {
let requires_kind = self
.schema
.compiled_properties
.get()
.map_or(false, |p| p.contains_key("kind"));
if requires_kind {
if obj.get("kind").is_none() {
result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination"
.to_string(),
path: self.path.clone(),
});
} else {
result.evaluated_keys.insert("kind".to_string());
}
}
}
} else {
@ -62,20 +84,20 @@ impl<'a> ValidationContext<'a> {
// Because they lack manual type property descriptors, we natively shield "type" and "kind" keys from
// triggering additionalProperty violations natively IF they precisely correspond to their fast-path boundaries
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if type_str == expected_type {
result.evaluated_keys.insert("type".to_string());
}
}
if let Some(type_str) = type_val.as_str() {
if type_str == expected_type {
result.evaluated_keys.insert("type".to_string());
}
}
}
if let Some(kind_val) = obj.get("kind") {
if let Some((kind_str, _)) = schema_identifier.rsplit_once('.') {
if let Some(actual_kind) = kind_val.as_str() {
if actual_kind == kind_str {
result.evaluated_keys.insert("kind".to_string());
}
if let Some((kind_str, _)) = schema_identifier_str.rsplit_once('.') {
if let Some(actual_kind) = kind_val.as_str() {
if actual_kind == kind_str {
result.evaluated_keys.insert("kind".to_string());
}
}
}
}
}
}
}
@ -155,7 +177,9 @@ impl<'a> ValidationContext<'a> {
if let Some(child_instance) = obj.get(key) {
let new_path = self.join_path(key);
let is_ref = match &sub_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
!crate::database::object::is_primitive_type(t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
@ -167,11 +191,10 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
@ -184,7 +207,9 @@ impl<'a> ValidationContext<'a> {
if compiled_re.0.is_match(key) {
let new_path = self.join_path(key);
let is_ref = match &sub_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
!crate::database::object::is_primitive_type(t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
@ -196,6 +221,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
@ -213,7 +239,8 @@ impl<'a> ValidationContext<'a> {
{
locally_matched = true;
}
if !locally_matched && let Some(compiled_pp) = self.schema.compiled_pattern_properties.get()
if !locally_matched
&& let Some(compiled_pp) = self.schema.compiled_pattern_properties.get()
{
for (compiled_re, _) in compiled_pp {
if compiled_re.0.is_match(key) {
@ -226,7 +253,9 @@ impl<'a> ValidationContext<'a> {
if !locally_matched {
let new_path = self.join_path(key);
let is_ref = match &additional_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
!crate::database::object::is_primitive_type(t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
@ -238,6 +267,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);

View File

@ -21,7 +21,7 @@ impl<'a> ValidationContext<'a> {
if conflicts {
result.errors.push(ValidationError {
code: "INVALID_SCHEMA".to_string(),
message: "$family must be used exclusively without other constraints".to_string(),
message: "family must be used exclusively without other constraints".to_string(),
path: self.path.to_string(),
});
return Ok(false);
@ -30,77 +30,34 @@ impl<'a> ValidationContext<'a> {
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);
}
return self.execute_polymorph(options, result);
} else {
result.errors.push(ValidationError {
code: "UNCOMPILED_FAMILY".to_string(),
message: "Encountered family block that could not be mapped to deterministic options during db schema compilation.".to_string(),
path: self.path.to_string(),
});
return Ok(false);
}
}
Ok(true)
}
pub(crate) fn validate_one_of(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if let Some(one_of) = &self.schema.one_of {
if self.schema.one_of.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);
}
}
// Native Draft2020-12 oneOf Evaluation Fallback
let mut valid_count = 0;
let mut final_successful_result = None;
let mut failed_candidates = Vec::new();
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() {
valid_count += 1;
final_successful_result = Some(sub_res.clone());
} else {
failed_candidates.push(sub_res);
}
}
}
if valid_count == 1 {
if let Some(successful_res) = final_successful_result {
result.merge(successful_res);
}
return Ok(true);
} else if valid_count == 0 {
result.errors.push(ValidationError {
code: "NO_ONEOF_MATCH".to_string(),
message: "Payload matches none of the required candidate sub-schemas natively".to_string(),
path: self.path.to_string(),
});
if let Some(first) = failed_candidates.first() {
let mut shared_errors = first.errors.clone();
for sub_res in failed_candidates.iter().skip(1) {
shared_errors.retain(|e1| {
sub_res.errors.iter().any(|e2| e1.code == e2.code && e1.path == e2.path)
});
}
for e in shared_errors {
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
result.errors.push(e);
}
}
}
return Ok(false);
return self.execute_polymorph(options, result);
} else {
result.errors.push(ValidationError {
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
message: "Matches multiple polymorphic candidates inextricably natively".to_string(),
result.errors.push(ValidationError {
code: "UNCOMPILED_ONEOF".to_string(),
message: "Encountered oneOf block that could not be mapped to deterministic compiled options natively.".to_string(),
path: self.path.to_string(),
});
return Ok(false);
return Ok(false);
}
}
Ok(true)
@ -108,118 +65,115 @@ impl<'a> ValidationContext<'a> {
pub(crate) fn execute_polymorph(
&self,
disc: &str,
options: &std::collections::BTreeMap<String, String>,
options: &std::collections::BTreeMap<String, (Option<usize>, Option<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());
let instance_val = if let Some(disc) = self.schema.compiled_discriminator.get() {
let val = self
.instance
.as_object()
.and_then(|o| o.get(disc))
.and_then(|t| t.as_str());
if val.is_some() {
result.evaluated_keys.insert(disc.to_string());
}
val.map(|s| s.to_string())
} else {
match self.instance {
serde_json::Value::Null => Some("null".to_string()),
serde_json::Value::Bool(_) => Some("boolean".to_string()),
serde_json::Value::Number(n) => {
if n.is_i64() || n.is_u64() {
Some("integer".to_string())
} else {
Some("number".to_string())
}
}
serde_json::Value::String(_) => Some("string".to_string()),
serde_json::Value::Array(_) => Some("array".to_string()),
serde_json::Value::Object(_) => Some("object".to_string()),
}
};
if let Some(val) = instance_val {
result.evaluated_keys.insert(disc.to_string());
if let Some(target_id) = options.get(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.schemas.get(target_id) {
let derived = self.derive_for_schema(target_schema.as_ref(), false);
let sub_res = derived.validate()?;
let is_valid = sub_res.is_valid();
result.merge(sub_res);
return Ok(is_valid);
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);
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(),
});
} else if let Some(idx) = idx_opt {
if let Some(target_schema) = self
.schema
.one_of
.as_ref()
.and_then(|options| options.get(*idx))
{
let derived = self.derive_for_schema(target_schema.as_ref(), 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 index target '{}' does not exist in the local oneOf array",
idx
),
path: self.path.to_string(),
});
return Ok(false);
}
} else {
return Ok(false);
}
} else {
}
} else {
let disc_msg = if let Some(d) = self.schema.compiled_discriminator.get() {
format!("discriminator {}='{}'", d, val)
} else {
format!("structural JSON base primitive '{}'", val)
};
result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(),
message: format!("Missing '{}' discriminator. Unable to resolve polymorphic boundaries", disc),
code: if self.schema.family.is_some() {
"NO_FAMILY_MATCH".to_string()
} else {
"NO_ONEOF_MATCH".to_string()
},
message: format!(
"Payload matched no candidate boundaries based on its {}",
disc_msg
),
path: self.path.to_string(),
});
return Ok(false);
}
}
pub(crate) fn validate_type_inheritance(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
// 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.schemas.get(&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 {
} else {
if let Some(d) = self.schema.compiled_discriminator.get() {
result.errors.push(ValidationError {
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
code: "MISSING_TYPE".to_string(),
message: format!(
"Inherited entity pointer '{}' was not found in schema registry",
t
"Missing explicit '{}' discriminator. Unable to resolve polymorphic boundaries",
d
),
path: self.path.to_string(),
});
}
return Ok(false);
}
Ok(true)
}
}

138
src/validator/rules/type.rs Normal file
View File

@ -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<bool, ValidationError> {
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)
}
}

109
test_output.txt Normal file
View File

@ -0,0 +1,109 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s
Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386)
running 1 test
test tests::test_filter_0_0 ... FAILED
failures:
---- tests::test_filter_0_0 stdout ----
TEST COMPILE ERROR FOR 'Assert filter generation map accurately represents strongly typed conditions natively.': Detailed Schema Match Failure for 'gender.condition'!
Expected:
{
"compiledPropertyNames": [
"$eq",
"$ne",
"$nof",
"$of"
],
"properties": {
"$eq": {
"type": [
"gender",
"null"
]
},
"$ne": {
"type": [
"gender",
"null"
]
},
"$nof": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
},
"$of": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
}
Actual:
{
"compiledPropertyNames": [
"$eq",
"$ne",
"$nof",
"$of",
"kind"
],
"properties": {
"$eq": {
"type": [
"gender",
"null"
]
},
"$ne": {
"type": [
"gender",
"null"
]
},
"$nof": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
},
"$of": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
}
},
"type": "condition"
}
thread 'tests::test_filter_0_0' (118346550) panicked at src/tests/fixtures.rs:539:54:
called `Result::unwrap()` on an `Err` value: "[Filter Synthesis Object-Oriented Composition] Compile Test 'Assert filter generation map accurately represents strongly typed conditions natively.' failed. Error: Detailed Schema Match Failure for 'gender.condition'!\n\nExpected:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"object\"\n}\n\nActual:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\",\n \"kind\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"condition\"\n}"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::test_filter_0_0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1362 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`

View File

@ -1 +1 @@
1.0.109
1.0.132