Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9387152859 | |||
| 4ab1b210ae | |||
| 7c8df22709 | |||
| e4286ac6a9 | |||
| 4411ac82f7 | |||
| 9b763a23c3 | |||
| ae4d83acd1 | |||
| ee8c9086ef | |||
| 8730a828c6 | |||
| 776a442374 | |||
| 5c1779651c | |||
| 6c047e326d | |||
| 7876567ae7 | |||
| 06f6a587de | |||
| 29d8dfb608 | |||
| 5b36ecf06c | |||
| 76467a6fed | |||
| 930d0513cd | |||
| cad651dbd8 | |||
| ea9ac8469c | |||
| ebcdb661fa | |||
| c893e29c59 | |||
| 7523431007 | |||
| dd98bfac9e | |||
| 2f3a1d16b7 | |||
| e86fe5cc4e | |||
| 93b0a70718 | |||
| 9c24f1af8f | |||
| f9cf1f837a | |||
| 796df7763c |
@ -4,9 +4,11 @@ description: jspg work preparation
|
||||
|
||||
This workflow will get you up-to-speed on the JSPG custom json-schema-based cargo pgrx postgres validation extension. Everything you read will be in the jspg directory/project.
|
||||
|
||||
Read over this entire workflow and commit to every section of work in a task list, so that you don't stop half way through before reviewing all of the directories and files mentioned. Do not ask for confirmation after generating this task list and proceed through all sections in your list.
|
||||
Read over this entire workflow and commit to every section of work in a fresh task list (DO THIS FIRST), so that you don't stop half way through before reviewing all of the directories and files mentioned. Do not ask for confirmation after generating this task list and proceed through all sections in your list.
|
||||
|
||||
Please analyze the files and directories and do not use cat, find, or the terminal to discover or read in any of these files. Analyze every file mentioned. If a directory is mentioned or a /*, please analyze the directory, every single file at its root, and recursively analyze every subdirectory and every single file in every subdirectory to capture not just critical files, but the entirety of what is requested. I state again, DO NOT just review a cherry picking of files in any folder or wildcard specified. Review 100% of all files discovered recursively!
|
||||
Please analyze the files and directories and do not use cat, find, or the terminal AT ALL to discover or read in any of these files! USE YOUR TOOLS ONLY. Analyze every file mentioned. If a directory is mentioned or a /*, please analyze the directory, every single file at its root, and recursively analyze every subdirectory and every single file in every subdirectory to capture not just critical files, but the entirety of what is requested. I state again, DO NOT just review a cherry picking of files in any folder or wildcard specified. Review 100% of all files discovered recursively!
|
||||
|
||||
Do not make any code changes. Just focus on your task list and reading files!
|
||||
|
||||
Section 1: Various Documentation
|
||||
|
||||
@ -48,7 +50,6 @@ Now, review some punc type and enum source in the api project with api/ these fi
|
||||
- api/punc/sql/tables.sql
|
||||
- api/punc/sql/domains.sql
|
||||
- api/punc/sql/indexes.sql
|
||||
- api/punc/sql/functions/entity.sql
|
||||
- api/punc/sql/functions/puncs.sql
|
||||
- api/punc/sql/puncs/entity.sql
|
||||
- api/punc/sql/puncs/persons.sql
|
||||
|
||||
182
GEMINI.md
182
GEMINI.md
@ -10,7 +10,7 @@ JSPG operates by deeply integrating the JSON Schema Draft 2020-12 specification
|
||||
* **Queryer**: Compile JSON Schemas into static, cached SQL SPI `SELECT` plans for fetching full entities or isolated ad-hoc object boundaries.
|
||||
|
||||
### 🎯 Goals
|
||||
1. **Draft 2020-12 Compliance**: Attempt to adhere to the official 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.
|
||||
@ -20,7 +20,7 @@ JSPG operates by deeply integrating the JSON Schema Draft 2020-12 specification
|
||||
To support high-throughput operations while allowing for runtime updates (e.g., during hot-reloading), JSPG uses an **Atomic Swap** pattern:
|
||||
1. **Parser Phase**: Schema JSONs are parsed into ordered `Schema` structs.
|
||||
2. **Compiler Phase**: The database iterates all parsed schemas and pre-computes native optimization maps (Descendants Map, Depths Map, Variations Map).
|
||||
3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling.
|
||||
3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `type` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling.
|
||||
4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates.
|
||||
|
||||
### Global API Reference
|
||||
@ -32,7 +32,138 @@ These functions operate on the global `GLOBAL_JSPG` engine instance and provide
|
||||
|
||||
---
|
||||
|
||||
## 2. Validator
|
||||
## 2. Schema Modeling (Punc Developer Guide)
|
||||
|
||||
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.
|
||||
|
||||
### Types of Types
|
||||
* **Table-Backed (Entity Types)**: Primarily defined in root type schemas. These represent physical Postgres tables.
|
||||
* They absolutely **require** an `$id`.
|
||||
* 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`).
|
||||
* **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.
|
||||
* 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.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
* **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:
|
||||
```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.
|
||||
|
||||
### 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"`).
|
||||
* **Single Inheritance**: Setting `"type": "user"` acts exactly like an `extends` keyword. The schema borrows all fields and constraints from the `user` identity. During `jspg_setup`, the compiler recursively crawls the dependencies to map the physical Postgres table, permanently mapping its type restriction to `"object"` under the hood so JSON standards remain unbroken.
|
||||
* **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.
|
||||
|
||||
### Polymorphism (`$family` and `oneOf`)
|
||||
Polymorphism is how an object boundary can dynamically take on entirely different shapes based on the payload provided at runtime.
|
||||
* **`$family` (Target-Based Polymorphism)**: An explicit Punc compiler macro instructing the database compiler to dynamically search its internal `db.descendants` registry and find all physical schemas that mathematically resolve to the target.
|
||||
* *Across Tables (Vertical)*: If `$family: entity` is requested, the payload's `type` field acts as the discriminator, dynamically routing to standard variations like `organization` or `person` spanning multiple Postgres tables.
|
||||
* *Single Table (Horizontal)*: If `$family: widget` is requested, the router explicitly evaluates the Dot Convention dynamically. If the payload possesses `"type": "widget"` and `"kind": "stock"`, the router mathematically resolves to the string `"stock.widget"` and routes exclusively to that explicit `JSPG` schema.
|
||||
* **`oneOf` (Strict Tagged Unions)**: A hardcoded array of JSON Schema candidate options. Punc strictly bans mathematical "Union of Sets" evaluation. Every `oneOf` candidate item MUST either be a pure primitive (`{ "type": "null" }`) or a user-defined Object Pointer providing a specific discriminator (e.g., `{ "type": "invoice_metadata" }`). This ensures validations remain pure $O(1)$ fast-paths and allows the Dart generator to emit pristine `sealed classes`.
|
||||
|
||||
### Conditionals (`cases`)
|
||||
Standard JSON Schema forces developers to write deeply nested `allOf` -> `if` -> `properties` blocks just to execute conditional branching. **JSPG completely abandons `allOf` and this practice.** For declarative business logic and structural mutations conditionally based upon property bounds, use the top-level `cases` array.
|
||||
|
||||
It evaluates as an **Independent Declarative Rules Engine**. Every `Case` block within the array is evaluated independently in parallel. For a given rule, if the `when` condition evaluates to true, its `then` schema is executed. If it evaluates to false, its `else` schema is executed (if present). To maintain strict standard JSON Schema compatibility internally, the `when` block utilizes pure JSON Schema `properties` definitions (e.g. `enum`, `const`) rather than injecting unstandardized MongoDB operators. Because `when`, `then`, and `else` are themselves standard schemas, they natively support nested `cases` to handle mutually exclusive `else if` architectures.
|
||||
|
||||
```json
|
||||
{
|
||||
"$id": "save_external_account",
|
||||
"cases": [
|
||||
{
|
||||
"when": {
|
||||
"properties": {
|
||||
"status": { "const": "unverified" }
|
||||
},
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["amount_1", "amount_2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"properties": { "kind": { "const": "credit" } },
|
||||
"required": ["kind"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["details"]
|
||||
},
|
||||
"else": {
|
||||
"cases": [
|
||||
{
|
||||
"when": { "properties": { "kind": { "const": "checking" } }, "required": ["kind"] },
|
||||
"then": { "required": ["routing_number"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Strict by Default & Extensibility
|
||||
* **Strictness**: By default, any property not explicitly defined in the schema causes a validation error (effectively enforcing `additionalProperties: false` globally).
|
||||
* **Extensibility (`extensible: true`)**: To allow a free-for-all of undefined properties, schemas must explicitly declare `"extensible": true`.
|
||||
* **Structured Additional Properties**: If `additionalProperties: {...}` is defined as a schema, arbitrary keys are allowed so long as their values match the defined type constraint.
|
||||
* **Inheritance Boundaries**: Strictness resets when crossing non-primitive `type` boundaries. A schema extending a strict parent remains strict unless it explicitly overrides with `"extensible": true`.
|
||||
|
||||
### 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".
|
||||
|
||||
---
|
||||
|
||||
## 3. Database
|
||||
|
||||
The Database module manages the core execution graphs and structural compilation of the Postgres environment.
|
||||
|
||||
### Relational Edge Resolution
|
||||
When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. To guarantee deterministic SQL generation, it utilizes a strict, multi-step algebraic resolution process applied during the `OnceLock` Compilation phase:
|
||||
|
||||
1. **Graph Locality Boundary**: Before evaluating constraints, the engine ensures the parent and child types do not belong strictly to the same inheritance lineage (e.g., `invoice` -> `activity`). Structural inheritance edges are handled natively by the payload merger, so relational edge discovery is intentionally bypassed.
|
||||
2. **Structural Cardinality Filtration**: If the JSON Schema requires an Array collection (`{"type": "array"}`), JSPG mathematically rejects pure scalar Forward constraints (where the parent holds a single UUID pointer), logically narrowing the possibilities to Reverse (1:N) or Junction (M:M) constraints.
|
||||
3. **Exact Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) directly matches the name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected.
|
||||
4. **Ambiguity Elimination (M:M Twin Deduction)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` or `role`), the compiler inspects the actual compiled child JSON schema AST. If it observes the child natively consumes one of the prefixes as an explicit outbound property (e.g. `contact` explicitly defining `{ "target": ... }`), it considers that arrow "used up". It mathematically deduces that its exact twin providing reverse ownership (`"source"`) MUST be the inbound link mapping from the parent.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 4. Validator
|
||||
|
||||
The Validator provides strict, schema-driven evaluation for the "Punc" architecture.
|
||||
|
||||
@ -43,35 +174,13 @@ 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.
|
||||
|
||||
#### A. Polymorphism & Referencing (`$ref`, `$family`, and Native Types)
|
||||
* **Native Type Discrimination (`variations`)**: Schemas defined inside a Postgres `type` are Entities. The validator securely and implicitly manages their `"type"` property. If an entity inherits from `user`, incoming JSON can safely define `{"type": "person"}` without errors, thanks to `compiled_variations` inheritance.
|
||||
* **Structural Inheritance & Viral Infection (`$ref`)**: `$ref` is used exclusively for structural inheritance, *never* for union creation. A Punc request schema that `$ref`s an Entity virally inherits all physical database polymorphism rules for that target.
|
||||
* **Shape Polymorphism (`$family`)**: Auto-expands polymorphic API lists based on an abstract **Descendants Graph**. If `{"$family": "widget"}` is used, the Validator dynamically identifies *every* schema in the registry that `$ref`s `widget` (e.g., `stock.widget`, `task.widget`) and evaluates the JSON against all of them.
|
||||
* **Strict Matches & Depth Heuristic**: Polymorphic structures MUST match exactly **one** schema permutation. If multiple inherited struct permutations pass, JSPG applies the **Depth Heuristic Tie-Breaker**, selecting the candidate deepest in the inheritance tree.
|
||||
|
||||
#### B. Dot-Notation Schema Resolution & Database Mapping
|
||||
* **The Dot Convention**: When a schema represents a specific variation or shape of an underlying physical database `Type` (e.g., a "summary" of a "person"), its `$id` must adhere to a dot-notation suffix convention (e.g., `summary.person` or `full.person`).
|
||||
* **Entity Resolution**: The framework (Validator, Queryer, Merger) dynamically determines the backing PostgreSQL table structure by splitting the schema's `$id` (or `$ref`) by `.` and extracting the **last segment** (`next_back()`). If the last segment matches a known Database Type (like `person`), the framework natively applies that table's inheritance rules, variations, and physical foreign keys to the schema graph, regardless of the prefix.
|
||||
|
||||
#### C. Strict by Default & Extensibility
|
||||
* **Strictness**: By default, any property not explicitly defined in the schema causes a validation error (effectively enforcing `additionalProperties: false` globally).
|
||||
* **Extensibility (`extensible: true`)**: To allow a free-for-all of undefined properties, schemas must explicitly declare `"extensible": true`.
|
||||
* **Structured Additional Properties**: If `additionalProperties: {...}` is defined as a schema, arbitrary keys are allowed so long as their values match the defined type constraint.
|
||||
* **Inheritance Boundaries**: Strictness resets when crossing `$ref` boundaries. A schema extending a strict parent remains strict unless it explicitly overrides with `"extensible": true`.
|
||||
|
||||
#### D. Implicit Keyword Shadowing
|
||||
* **Inheritance (`$ref` + properties)**: Unlike standard JSON Schema, when a schema uses `$ref` alongside local properties, JSPG implements **Smart Merge**. Local constraints natively take precedence over (shadow) inherited constraints for the same keyword.
|
||||
* *Example*: If `entity` has `type: {const: "entity"}`, but `person` defines `type: {const: "person"}`, the local `person` const cleanly overrides the inherited one.
|
||||
* **Composition (`allOf`)**: When evaluating `allOf`, standard intersection rules apply seamlessly. No shadowing occurs, meaning all constraints from all branches must pass.
|
||||
|
||||
#### E. 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".
|
||||
* **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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 3. Merger
|
||||
## 5. Merger
|
||||
|
||||
The Merger provides an automated, high-performance graph synchronization engine. It orchestrates the complex mapping of nested JSON objects into normalized Postgres relational tables, honoring all inheritance and graph constraints.
|
||||
|
||||
@ -96,7 +205,7 @@ The Merger provides an automated, high-performance graph synchronization engine.
|
||||
|
||||
---
|
||||
|
||||
## 4. Queryer
|
||||
## 6. Queryer
|
||||
|
||||
The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, designed to serve the exact shape of Punc responses directly via SQL.
|
||||
|
||||
@ -106,8 +215,9 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
|
||||
### 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 `allOf` and `$ref` inheritances, mapping JSON fields specifically to their physical database table aliases during translation.
|
||||
* **Dynamic Filtering**: Binds parameters natively through `cue.filters` objects. The queryer enforces a strict, structured, MongoDB-style operator syntax to map incoming JSON request paths directly to their originating structural table columns.
|
||||
* **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.
|
||||
@ -118,13 +228,9 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
|
||||
* **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 `JOIN`s for each variation.
|
||||
* **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.
|
||||
|
||||
### 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.
|
||||
|
||||
## 5. Testing & Execution Architecture
|
||||
## 7. Testing & Execution Architecture
|
||||
|
||||
JSPG implements a strict separation of concerns to bypass the need to boot a full PostgreSQL cluster for unit and integration testing. Because `pgrx::spi::Spi` directly links to PostgreSQL C-headers, building the library with `cargo test` on macOS natively normally results in fatal `dyld` crashes.
|
||||
|
||||
|
||||
58
LOOKUP_VERIFICATION.md
Normal file
58
LOOKUP_VERIFICATION.md
Normal file
@ -0,0 +1,58 @@
|
||||
# The Postgres Partial Index Claiming Pattern
|
||||
|
||||
This document outlines the architectural strategy for securely handling the deduplication, claiming, and verification of sensitive unique identifiers (like email addresses or phone numbers) strictly through PostgreSQL without requiring "magical" logic in the JSPG `Merger`.
|
||||
|
||||
## The Denial of Service (DoS) Squatter Problem
|
||||
|
||||
If you enforce a standard `UNIQUE` constraint on an email address table:
|
||||
1. Malicious User A signs up and adds `jeff.bezos@amazon.com` to their account but never verifies it.
|
||||
2. The real Jeff Bezos signs up.
|
||||
3. The Database blocks Jeff because the unique string already exists.
|
||||
|
||||
The squatter has effectively locked the legitimate owner out of the system.
|
||||
|
||||
## The Anti-Patterns
|
||||
|
||||
1. **Global Entity Flags**: Adding a global `verified` boolean to the root `entity` table forces unrelated objects (like Widgets, Invoices, Orders) to carry verification logic that doesn't belong to them.
|
||||
2. **Magical Merger Logic**: Making JSPG's `Merger` aware of a specific `verified` field breaks its pure structural translation model. The Merger shouldn't need hardcoded conditional logic to know if it's allowed to update an unverified row.
|
||||
|
||||
## The Solution: Postgres Partial Unique Indexes
|
||||
|
||||
The holy grail is to defer all claiming logic natively to the database engine using a **Partial Unique Index**.
|
||||
|
||||
```sql
|
||||
-- Remove any existing global unique constraint on address first
|
||||
CREATE UNIQUE INDEX lk_email_address_verified
|
||||
ON email_address (address)
|
||||
WHERE verified_at IS NOT NULL;
|
||||
```
|
||||
|
||||
### How the Lifecycle Works Natively
|
||||
|
||||
1. **Unverified Squatters (Isolated Rows):**
|
||||
A hundred different users can send `{ "address": "jeff.bezos@amazon.com" }` through the `save_person` Punc. Because the Punc isolates them and doesn't allow setting the `verified_at` property natively (enforced by the JSON schema), the JSPG Merger inserts `NULL`.
|
||||
Postgres permits all 100 `INSERT` commands to succeed because the Partial Index **ignores** rows where `verified_at IS NULL`. Every user gets their own isolated, unverified row acting as a placeholder on their contact edge.
|
||||
|
||||
2. **The Verification Race (The Claim):**
|
||||
The real Jeff clicks his magic verification link. The backend securely executes a specific verification Punc that runs:
|
||||
`UPDATE email_address SET verified_at = now() WHERE id = <jeff's-real-uuid>`
|
||||
|
||||
3. **The Lockout:**
|
||||
Because Jeff's row now strictly satisfies `verified_at IS NOT NULL`, that exact row enters the Partial Unique Index.
|
||||
If any of the other 99 squatters *ever* click their fake verification links (or if a new user tries to verify that same email), PostgreSQL hits the index and violently throws a **Unique Constraint Violation**, flawlessly blocking them. The winner has permanently claimed the slot across the entire environment!
|
||||
|
||||
### Periodic Cleanup
|
||||
|
||||
Since unverified rows are allowed to accumulate without colliding, a simple Postgres `pg_cron` job or backend worker can sweep the table nightly to prune abandoned claims and preserve storage:
|
||||
|
||||
```sql
|
||||
DELETE FROM email_address
|
||||
WHERE verified_at IS NULL
|
||||
AND created_at < NOW() - INTERVAL '24 hours';
|
||||
```
|
||||
|
||||
### Why this is the Ultimate Architecture
|
||||
|
||||
* The **JSPG Merger** remains mathematically pure. It doesn't know what `verified_at` is; it simply respects the database's structural limits (`O(1)` pure translation).
|
||||
* **Row-Level Security (RLS)** naturally blocks users from seeing or claiming each other's unverified rows.
|
||||
* You offload complex race-condition tracking entirely to the C-level PostgreSQL B-Tree indexing engine, guaranteeing absolute cluster-wide atomicity.
|
||||
0
agreego.sql
Normal file
0
agreego.sql
Normal file
@ -1,677 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "allOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "allOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allOf",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "allOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch second",
|
||||
"data": {
|
||||
"foo": "baz"
|
||||
},
|
||||
"schema_id": "allOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch first",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "allOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "wrong type",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": "quux"
|
||||
},
|
||||
"schema_id": "allOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with base schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
},
|
||||
"baz": {},
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"baz": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "allOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"bar": 2,
|
||||
"baz": null
|
||||
},
|
||||
"schema_id": "allOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"baz": null
|
||||
},
|
||||
"schema_id": "allOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch first allOf",
|
||||
"data": {
|
||||
"bar": 2,
|
||||
"baz": null
|
||||
},
|
||||
"schema_id": "allOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch second allOf",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "allOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch both",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "allOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf simple types",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"maximum": 30
|
||||
},
|
||||
{
|
||||
"minimum": 20
|
||||
}
|
||||
],
|
||||
"$id": "allOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": 25,
|
||||
"schema_id": "allOf_2_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "mismatch one",
|
||||
"data": 35,
|
||||
"schema_id": "allOf_2_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"$id": "allOf_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"schema_id": "allOf_3_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, some false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"$id": "allOf_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "allOf_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"$id": "allOf_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "allOf_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with one empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"schema_id": "allOf_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with two empty schemas",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"schema_id": "allOf_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the first empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"$id": "allOf_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"schema_id": "allOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "allOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the last empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"schema_id": "allOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "allOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested allOf, to check validation semantics",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "allOf_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"schema_id": "allOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"schema_id": "allOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "extensible: true allows extra properties in allOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"extensible": true,
|
||||
"$id": "allOf_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra property is valid",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": 2,
|
||||
"qux": 3
|
||||
},
|
||||
"schema_id": "allOf_12_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "strict by default with allOf properties",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"const": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$id": "allOf_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "validates merged properties",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "allOf_13_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "fails on extra property z explicitly",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"z": 3
|
||||
},
|
||||
"schema_id": "allOf_13_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with nested extensible: true (partial looseness)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"extensible": true,
|
||||
"properties": {
|
||||
"bar": {
|
||||
"const": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$id": "allOf_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extensible subschema doesn't make root extensible if root is strict",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"z": 3
|
||||
},
|
||||
"schema_id": "allOf_14_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "strictness: allOf composition with strict refs",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "partA"
|
||||
},
|
||||
{
|
||||
"$ref": "partB"
|
||||
}
|
||||
],
|
||||
"$id": "allOf_15_0"
|
||||
},
|
||||
{
|
||||
"$id": "partA",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "partB",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "merged instance is valid",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"name": "Me"
|
||||
},
|
||||
"schema_id": "allOf_15_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "extra property is invalid (root is strict)",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"name": "Me",
|
||||
"extra": 1
|
||||
},
|
||||
"schema_id": "allOf_15_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "partA mismatch is invalid",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "Me"
|
||||
},
|
||||
"schema_id": "allOf_15_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
183
fixtures/cases.json
Normal file
183
fixtures/cases.json
Normal file
@ -0,0 +1,183 @@
|
||||
[
|
||||
{
|
||||
"description": "Multi-Paradigm Declarative Cases",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$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"}
|
||||
},
|
||||
"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"]
|
||||
},
|
||||
"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"]
|
||||
},
|
||||
"else": {
|
||||
"properties": { "premium": {"type": "number"} },
|
||||
"required": ["premium"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$id": "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 },
|
||||
"schema_id": "parallel_rules",
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Fires both independent parallel rules flawlessly",
|
||||
"data": { "status": "unverified", "kind": "credit", "amount_1": 1, "amount_2": 2, "cvv": 123 },
|
||||
"schema_id": "parallel_rules",
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Catches errors triggered concurrently by multiple independent blocked rules",
|
||||
"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" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "STRICT_PROPERTY_VIOLATION throws if an un-triggered then property is submitted",
|
||||
"data": { "status": "verified", "cvv": 123 },
|
||||
"schema_id": "parallel_rules",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{ "code": "STRICT_PROPERTY_VIOLATION", "details": { "path": "cvv" } }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Successfully routes mutually exclusive properties seamlessly",
|
||||
"data": { "type": "A", "field_a": 1, "fallback_b": 2 },
|
||||
"schema_id": "mutually_exclusive",
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Nested fallbacks execute seamlessly",
|
||||
"data": { "tier": "3", "premium": 1 },
|
||||
"schema_id": "nested_fallbacks",
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "A case without a when executes its else indiscriminately",
|
||||
"data": { "unconditional": 1 },
|
||||
"schema_id": "missing_when",
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "A case without a when throws if else unconditionally requires field",
|
||||
"data": { },
|
||||
"schema_id": "missing_when",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{ "code": "REQUIRED_FIELD_MISSING", "details": { "path": "unconditional" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
410
fixtures/database.json
Normal file
410
fixtures/database.json
Normal file
@ -0,0 +1,410 @@
|
||||
[
|
||||
{
|
||||
"description": "Edge missing - 0 relations",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "type",
|
||||
"name": "org",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"org"
|
||||
],
|
||||
"variations": [
|
||||
"org"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.org",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"missing_users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "full.user"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "user",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"user"
|
||||
],
|
||||
"variations": [
|
||||
"user"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.user",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "throws EDGE_MISSING when 0 relations exist between org and user",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "EDGE_MISSING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Edge missing - array cardinality rejection",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "type",
|
||||
"name": "parent",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"parent"
|
||||
],
|
||||
"variations": [
|
||||
"parent"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.parent",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "full.child"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "child",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"child"
|
||||
],
|
||||
"variations": [
|
||||
"child"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.child",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "relation",
|
||||
"constraint": "fk_parent_child",
|
||||
"source_type": "parent",
|
||||
"source_columns": [
|
||||
"child_id"
|
||||
],
|
||||
"destination_type": "child",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "throws EDGE_MISSING because a Forward scaler edge cannot mathematically fulfill an Array collection",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "EDGE_MISSING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ambiguous type relations - multiple unprefixed relations",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "type",
|
||||
"name": "invoice",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"invoice"
|
||||
],
|
||||
"variations": [
|
||||
"invoice"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.invoice",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"activities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "full.activity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "activity",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"activity"
|
||||
],
|
||||
"variations": [
|
||||
"activity"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.activity",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "relation",
|
||||
"constraint": "fk_activity_invoice_1",
|
||||
"source_type": "activity",
|
||||
"source_columns": [
|
||||
"invoice_id_1"
|
||||
],
|
||||
"destination_type": "invoice",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "44444444-4444-4444-4444-444444444444",
|
||||
"type": "relation",
|
||||
"constraint": "fk_activity_invoice_2",
|
||||
"source_type": "activity",
|
||||
"source_columns": [
|
||||
"invoice_id_2"
|
||||
],
|
||||
"destination_type": "invoice",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "throws AMBIGUOUS_TYPE_RELATIONS when fallback encounters multiple naked constraints",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "AMBIGUOUS_TYPE_RELATIONS",
|
||||
"details": {
|
||||
"cause": "Multiple conflicting constraints found matching prefixes",
|
||||
"context": [
|
||||
{
|
||||
"constraint": "fk_activity_invoice_1"
|
||||
},
|
||||
{
|
||||
"constraint": "fk_activity_invoice_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ambiguous type relations - M:M twin deduction failure",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "type",
|
||||
"name": "actor",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"actor"
|
||||
],
|
||||
"variations": [
|
||||
"actor"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "full.actor",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ambiguous_edge": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "empty.junction"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "junction",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"junction"
|
||||
],
|
||||
"variations": [
|
||||
"junction"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "empty.junction",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "relation",
|
||||
"constraint": "fk_junction_source_actor",
|
||||
"source_type": "junction",
|
||||
"source_columns": [
|
||||
"source_id"
|
||||
],
|
||||
"destination_type": "actor",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
],
|
||||
"prefix": "source"
|
||||
},
|
||||
{
|
||||
"id": "44444444-4444-4444-4444-444444444444",
|
||||
"type": "relation",
|
||||
"constraint": "fk_junction_target_actor",
|
||||
"source_type": "junction",
|
||||
"source_columns": [
|
||||
"target_id"
|
||||
],
|
||||
"destination_type": "actor",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
],
|
||||
"prefix": "target"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "throws AMBIGUOUS_TYPE_RELATIONS because child doesn't explicitly expose 'source' or 'target' for twin deduction",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "AMBIGUOUS_TYPE_RELATIONS",
|
||||
"details": {
|
||||
"cause": "Multiple conflicting constraints found matching prefixes",
|
||||
"context": [
|
||||
{
|
||||
"constraint": "fk_junction_source_actor"
|
||||
},
|
||||
{
|
||||
"constraint": "fk_junction_target_actor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Database type parse failed",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": [
|
||||
"must",
|
||||
"be",
|
||||
"string",
|
||||
"to",
|
||||
"fail"
|
||||
],
|
||||
"type": "type",
|
||||
"name": "failure",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": [
|
||||
"failure"
|
||||
],
|
||||
"variations": [
|
||||
"failure"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "throws DATABASE_TYPE_PARSE_FAILED when metadata completely fails Serde typing",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "DATABASE_TYPE_PARSE_FAILED"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -142,7 +142,7 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "CONST_VIOLATED",
|
||||
"path": "con"
|
||||
"details": { "path": "con" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,217 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "Entity families via pure $ref graph",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": [
|
||||
"entity",
|
||||
"organization",
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.entity",
|
||||
"$ref": "entity"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"variations": [
|
||||
"organization",
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "organization",
|
||||
"$ref": "entity",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": [
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"$ref": "organization",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.person",
|
||||
"$ref": "light.entity"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"puncs": [
|
||||
{
|
||||
"name": "get_entities",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "get_entities.response",
|
||||
"$family": "entity"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_light_entities",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "get_light_entities.response",
|
||||
"$family": "light.entity"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Family matches base entity",
|
||||
"schema_id": "get_entities.response",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "entity"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Family matches descendant person",
|
||||
"schema_id": "get_entities.response",
|
||||
"data": {
|
||||
"id": "2",
|
||||
"type": "person",
|
||||
"name": "ACME",
|
||||
"first_name": "John"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Graph family matches light.entity",
|
||||
"schema_id": "get_light_entities.response",
|
||||
"data": {
|
||||
"id": "3",
|
||||
"type": "entity"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Graph family matches light.person (because it $refs light.entity)",
|
||||
"schema_id": "get_light_entities.response",
|
||||
"data": {
|
||||
"id": "4",
|
||||
"type": "person"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Graph family excludes organization (missing light. schema that $refs light.entity)",
|
||||
"schema_id": "get_light_entities.response",
|
||||
"data": {
|
||||
"id": "5",
|
||||
"type": "organization",
|
||||
"name": "ACME"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "FAMILY_MISMATCH",
|
||||
"path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ad-hoc non-entity families (using normal json-schema object structures)",
|
||||
"database": {
|
||||
"puncs": [
|
||||
{
|
||||
"name": "get_widgets",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "widget",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"widget_type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "special_widget",
|
||||
"$ref": "widget",
|
||||
"properties": {
|
||||
"special_feature": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "get_widgets.response",
|
||||
"$family": "widget"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Ad-hoc family matches strictly by shape (no magic variations for base schemas)",
|
||||
"schema_id": "get_widgets.response",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"widget_type": "special",
|
||||
"special_feature": "yes"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,594 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "ignore if without then or else",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"const": 0
|
||||
},
|
||||
"$id": "if-then-else_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone if",
|
||||
"data": 0,
|
||||
"schema_id": "if-then-else_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone if",
|
||||
"data": "hello",
|
||||
"schema_id": "if-then-else_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ignore then without if",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"then": {
|
||||
"const": 0
|
||||
},
|
||||
"$id": "if-then-else_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone then",
|
||||
"data": 0,
|
||||
"schema_id": "if-then-else_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone then",
|
||||
"data": "hello",
|
||||
"schema_id": "if-then-else_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ignore else without if",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"else": {
|
||||
"const": 0
|
||||
},
|
||||
"$id": "if-then-else_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone else",
|
||||
"data": 0,
|
||||
"schema_id": "if-then-else_2_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone else",
|
||||
"data": "hello",
|
||||
"schema_id": "if-then-else_2_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if and then without else",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"then": {
|
||||
"minimum": -10
|
||||
},
|
||||
"$id": "if-then-else_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid through then",
|
||||
"data": -1,
|
||||
"schema_id": "if-then-else_3_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"schema_id": "if-then-else_3_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid when if test fails",
|
||||
"data": 3,
|
||||
"schema_id": "if-then-else_3_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if and else without then",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
},
|
||||
"$id": "if-then-else_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when if test passes",
|
||||
"data": -1,
|
||||
"schema_id": "if-then-else_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"schema_id": "if-then-else_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"schema_id": "if-then-else_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "validate against correct branch, then vs else",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"then": {
|
||||
"minimum": -10
|
||||
},
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
},
|
||||
"$id": "if-then-else_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid through then",
|
||||
"data": -1,
|
||||
"schema_id": "if-then-else_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"schema_id": "if-then-else_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"schema_id": "if-then-else_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"schema_id": "if-then-else_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "non-interference across combined schemas",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"then": {
|
||||
"minimum": -10
|
||||
}
|
||||
},
|
||||
{
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"$id": "if-then-else_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid, but would have been invalid through then",
|
||||
"data": -100,
|
||||
"schema_id": "if-then-else_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "valid, but would have been invalid through else",
|
||||
"data": 3,
|
||||
"schema_id": "if-then-else_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if with boolean schema true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": true,
|
||||
"then": {
|
||||
"const": "then"
|
||||
},
|
||||
"else": {
|
||||
"const": "else"
|
||||
},
|
||||
"$id": "if-then-else_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "boolean schema true in if always chooses the then path (valid)",
|
||||
"data": "then",
|
||||
"schema_id": "if-then-else_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "boolean schema true in if always chooses the then path (invalid)",
|
||||
"data": "else",
|
||||
"schema_id": "if-then-else_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if with boolean schema false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": false,
|
||||
"then": {
|
||||
"const": "then"
|
||||
},
|
||||
"else": {
|
||||
"const": "else"
|
||||
},
|
||||
"$id": "if-then-else_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "boolean schema false in if always chooses the else path (invalid)",
|
||||
"data": "then",
|
||||
"schema_id": "if-then-else_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "boolean schema false in if always chooses the else path (valid)",
|
||||
"data": "else",
|
||||
"schema_id": "if-then-else_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if appears at the end when serialized (keyword processing sequence)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"then": {
|
||||
"const": "yes"
|
||||
},
|
||||
"else": {
|
||||
"const": "other"
|
||||
},
|
||||
"if": {
|
||||
"maxLength": 4
|
||||
},
|
||||
"$id": "if-then-else_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "yes redirects to then and passes",
|
||||
"data": "yes",
|
||||
"schema_id": "if-then-else_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "other redirects to else and passes",
|
||||
"data": "other",
|
||||
"schema_id": "if-then-else_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "no redirects to then and fails",
|
||||
"data": "no",
|
||||
"schema_id": "if-then-else_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "invalid redirects to else and fails",
|
||||
"data": "invalid",
|
||||
"schema_id": "if-then-else_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "then: false fails when condition matches",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"const": 1
|
||||
},
|
||||
"then": false,
|
||||
"$id": "if-then-else_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "matches if → then=false → invalid",
|
||||
"data": 1,
|
||||
"schema_id": "if-then-else_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "does not match if → then ignored → valid",
|
||||
"data": 2,
|
||||
"schema_id": "if-then-else_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "else: false fails when condition does not match",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"const": 1
|
||||
},
|
||||
"else": false,
|
||||
"$id": "if-then-else_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "matches if → else ignored → valid",
|
||||
"data": 1,
|
||||
"schema_id": "if-then-else_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "does not match if → else executes → invalid",
|
||||
"data": 2,
|
||||
"schema_id": "if-then-else_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "extensible: true allows extra properties in if-then-else",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"foo": {
|
||||
"const": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"bar": {
|
||||
"const": 2
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
"extensible": true,
|
||||
"$id": "if-then-else_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra property is valid (matches if and then)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"extra": "prop"
|
||||
},
|
||||
"schema_id": "if-then-else_12_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "strict by default with if-then properties",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"foo": {
|
||||
"const": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"bar": {
|
||||
"const": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"$id": "if-then-else_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid match (foo + bar)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "if-then-else_13_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "fails on extra property z explicitly",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"z": 3
|
||||
},
|
||||
"schema_id": "if-then-else_13_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
219
fixtures/invoice.json
Normal file
219
fixtures/invoice.json
Normal file
@ -0,0 +1,219 @@
|
||||
[
|
||||
{
|
||||
"description": "Invoice Attachment Reproducer",
|
||||
"database": {
|
||||
"puncs": [
|
||||
{
|
||||
"name": "get_invoice",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "get_invoice.response",
|
||||
"oneOf": [
|
||||
{ "type": "invoice" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"enums": [],
|
||||
"relations": [
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000001",
|
||||
"type": "relation",
|
||||
"constraint": "fk_attachment_attachable_entity",
|
||||
"source_type": "attachment",
|
||||
"source_columns": ["attachable_id", "attachable_type"],
|
||||
"destination_type": "entity",
|
||||
"destination_columns": ["id", "type"],
|
||||
"prefix": null
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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"],
|
||||
"grouped_fields": {
|
||||
"entity": ["id", "type", "archived", "created_at"]
|
||||
},
|
||||
"field_types": {
|
||||
"id": "uuid",
|
||||
"type": "text",
|
||||
"archived": "boolean",
|
||||
"created_at": "timestamptz"
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"historical": false,
|
||||
"notify": false,
|
||||
"relationship": false
|
||||
},
|
||||
{
|
||||
"name": "activity",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "activity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"start_date": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"]
|
||||
},
|
||||
"field_types": {
|
||||
"id": "uuid",
|
||||
"type": "text",
|
||||
"archived": "boolean",
|
||||
"created_at": "timestamptz",
|
||||
"start_date": "timestamptz"
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"historical": false,
|
||||
"notify": false,
|
||||
"relationship": false
|
||||
},
|
||||
{
|
||||
"name": "invoice",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "invoice",
|
||||
"type": "activity",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": { "type": "attachment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"]
|
||||
},
|
||||
"field_types": {
|
||||
"id": "uuid",
|
||||
"type": "text",
|
||||
"archived": "boolean",
|
||||
"created_at": "timestamptz",
|
||||
"start_date": "timestamptz",
|
||||
"status": "text"
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"historical": false,
|
||||
"notify": false,
|
||||
"relationship": false
|
||||
},
|
||||
{
|
||||
"name": "attachment",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "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" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"]
|
||||
},
|
||||
"field_types": {
|
||||
"id": "uuid",
|
||||
"type": "text",
|
||||
"archived": "boolean",
|
||||
"created_at": "timestamptz",
|
||||
"attachable_id": "uuid",
|
||||
"attachable_type": "text",
|
||||
"kind": "text",
|
||||
"file": "text",
|
||||
"data": "jsonb",
|
||||
"name": "text"
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"historical": false,
|
||||
"notify": false,
|
||||
"relationship": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Invoice with an empty array of attachments",
|
||||
"schema_id": "get_invoice.response",
|
||||
"data": {
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "invoice",
|
||||
"archived": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"status": "draft",
|
||||
"attachments": []
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Invoice with a valid attachment with null data",
|
||||
"schema_id": "get_invoice.response",
|
||||
"data": {
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"type": "invoice",
|
||||
"archived": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"status": "draft",
|
||||
"attachments": [
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "attachment",
|
||||
"archived": false,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"name": "receipt",
|
||||
"attachable_id": "11111111-1111-1111-1111-111111111111",
|
||||
"attachable_type": "invoice",
|
||||
"kind": "document",
|
||||
"file": "path/to/doc.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -141,13 +141,13 @@
|
||||
"items": false,
|
||||
"prefixItems": [
|
||||
{
|
||||
"$ref": "item"
|
||||
"type": "item"
|
||||
},
|
||||
{
|
||||
"$ref": "item"
|
||||
"type": "item"
|
||||
},
|
||||
{
|
||||
"$ref": "item"
|
||||
"type": "item"
|
||||
}
|
||||
],
|
||||
"$id": "items_3_0"
|
||||
@ -158,10 +158,10 @@
|
||||
"items": false,
|
||||
"prefixItems": [
|
||||
{
|
||||
"$ref": "sub-item"
|
||||
"type": "sub-item"
|
||||
},
|
||||
{
|
||||
"$ref": "sub-item"
|
||||
"type": "sub-item"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "base_0",
|
||||
"type": "base_0",
|
||||
"properties": {
|
||||
"child_prop": {
|
||||
"type": "string"
|
||||
@ -47,8 +47,8 @@
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "TYPE_MISMATCH",
|
||||
"path": "base_prop"
|
||||
"code": "INVALID_TYPE",
|
||||
"details": { "path": "base_prop" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -71,7 +71,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "base_1",
|
||||
"type": "base_1",
|
||||
"properties": {
|
||||
"b": {
|
||||
"type": "string"
|
||||
@ -109,7 +109,7 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "a"
|
||||
"details": { "path": "a" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -126,7 +126,7 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "b"
|
||||
"details": { "path": "b" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -154,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "base_2",
|
||||
"type": "base_2",
|
||||
"properties": {
|
||||
"child_dep": {
|
||||
"type": "string"
|
||||
@ -195,8 +195,8 @@
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "DEPENDENCY_FAILED",
|
||||
"path": "base_dep"
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"details": { "path": "" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -213,8 +213,8 @@
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "DEPENDENCY_FAILED",
|
||||
"path": "child_dep"
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"details": { "path": "" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -241,7 +241,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "base_3",
|
||||
"type": "base_3",
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "relation",
|
||||
"constraint": "fk_order_customer",
|
||||
"constraint": "fk_order_customer_person",
|
||||
"source_type": "order",
|
||||
"source_columns": [
|
||||
"customer_id"
|
||||
@ -41,8 +41,7 @@
|
||||
"destination_type": "order",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
],
|
||||
"prefix": "lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "44444444-4444-4444-4444-444444444444",
|
||||
@ -75,6 +74,20 @@
|
||||
"type"
|
||||
],
|
||||
"prefix": "target"
|
||||
},
|
||||
{
|
||||
"id": "66666666-6666-6666-6666-666666666666",
|
||||
"type": "relation",
|
||||
"constraint": "fk_entity_organization",
|
||||
"source_type": "entity",
|
||||
"source_columns": [
|
||||
"organization_id"
|
||||
],
|
||||
"destination_type": "organization",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
],
|
||||
"prefix": null
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
@ -152,7 +165,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "organization",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
@ -200,7 +213,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "user",
|
||||
"$ref": "organization",
|
||||
"type": "organization",
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
@ -249,7 +262,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"$ref": "user",
|
||||
"type": "user",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
@ -269,20 +282,31 @@
|
||||
"contacts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "contact",
|
||||
"type": "contact",
|
||||
"properties": {
|
||||
"target": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "phone_number"
|
||||
"type": "phone_number"
|
||||
},
|
||||
{
|
||||
"$ref": "email_address"
|
||||
"type": "email_address"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_addresses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "contact",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "email_address"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,7 +376,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "order",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "number"
|
||||
@ -361,12 +385,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"customer": {
|
||||
"$ref": "person"
|
||||
"type": "person"
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "order_line"
|
||||
"type": "order_line"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -416,7 +440,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "order_line",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"order_id": {
|
||||
"type": "string"
|
||||
@ -525,7 +549,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "relationship",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
@ -595,7 +619,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "contact",
|
||||
"$ref": "relationship",
|
||||
"type": "relationship",
|
||||
"properties": {
|
||||
"is_primary": {
|
||||
"type": "boolean"
|
||||
@ -653,7 +677,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "phone_number",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"number": {
|
||||
"type": "string"
|
||||
@ -712,7 +736,7 @@
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "email_address",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
@ -748,7 +772,7 @@
|
||||
},
|
||||
{
|
||||
"$id": "attachment",
|
||||
"$ref": "entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"flags": {
|
||||
"type": "array",
|
||||
@ -757,10 +781,10 @@
|
||||
}
|
||||
},
|
||||
"type_metadata": {
|
||||
"$ref": "type_metadata"
|
||||
"type": "type_metadata"
|
||||
},
|
||||
"other_metadata": {
|
||||
"$ref": "other_metadata"
|
||||
"type": "other_metadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1145,7 +1169,71 @@
|
||||
" },",
|
||||
" \"old\":{",
|
||||
" \"contact_id\":\"old-contact\"",
|
||||
" }",
|
||||
" },",
|
||||
" \"replaces\":\"33333333-3333-3333-3333-333333333333\"",
|
||||
" }')"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Replace existing person with id and no changes (lookup)",
|
||||
"action": "merge",
|
||||
"data": {
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "person",
|
||||
"first_name": "LookupFirst",
|
||||
"last_name": "LookupLast",
|
||||
"date_of_birth": "1990-01-01T00:00:00Z",
|
||||
"pronouns": "they/them"
|
||||
},
|
||||
"mocks": [
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "person",
|
||||
"first_name": "LookupFirst",
|
||||
"last_name": "LookupLast",
|
||||
"date_of_birth": "1990-01-01T00:00:00Z",
|
||||
"pronouns": "they/them",
|
||||
"contact_id": "old-contact"
|
||||
}
|
||||
],
|
||||
"schema_id": "person",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
|
||||
"FROM agreego.\"person\" t1",
|
||||
"LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id",
|
||||
"LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id",
|
||||
"LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id",
|
||||
"WHERE",
|
||||
" t1.id = '33333333-3333-3333-3333-333333333333'",
|
||||
" OR (",
|
||||
" \"first_name\" = 'LookupFirst'",
|
||||
" AND \"last_name\" = 'LookupLast'",
|
||||
" AND \"date_of_birth\" = '1990-01-01T00:00:00Z'",
|
||||
" AND \"pronouns\" = 'they/them'",
|
||||
" )"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"contact_id\":\"old-contact\",",
|
||||
" \"date_of_birth\":\"1990-01-01T00:00:00Z\",",
|
||||
" \"first_name\":\"LookupFirst\",",
|
||||
" \"id\":\"22222222-2222-2222-2222-222222222222\",",
|
||||
" \"last_name\":\"LookupLast\",",
|
||||
" \"modified_at\":\"2026-03-10T00:00:00Z\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"pronouns\":\"they/them\",",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"replaces\":\"33333333-3333-3333-3333-333333333333\"",
|
||||
" }')"
|
||||
]
|
||||
]
|
||||
@ -1770,16 +1858,18 @@
|
||||
"type": "contact",
|
||||
"is_primary": false,
|
||||
"target": {
|
||||
"type": "phone_number",
|
||||
"number": "555-0002"
|
||||
"type": "email_address",
|
||||
"address": "test@example.com"
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
"email_addresses": [
|
||||
{
|
||||
"type": "contact",
|
||||
"is_primary": false,
|
||||
"target": {
|
||||
"type": "email_address",
|
||||
"address": "test@example.com"
|
||||
"address": "test2@example.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1871,7 +1961,10 @@
|
||||
" modified_by",
|
||||
") VALUES (",
|
||||
" NULL,",
|
||||
" '{\"number\":\"555-0001\",\"type\":\"phone_number\"}',",
|
||||
" '{",
|
||||
" \"number\":\"555-0001\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" }',",
|
||||
" '{{uuid:phone1_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
@ -1942,115 +2035,6 @@
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"type\"",
|
||||
") VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:phone2_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'phone_number'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"phone_number\" (",
|
||||
" \"number\"",
|
||||
") VALUES (",
|
||||
" '555-0002'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
") VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"number\":\"555-0002\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" }',",
|
||||
" '{{uuid:phone2_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"type\"",
|
||||
") VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:contact2_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'contact'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"relationship\" (",
|
||||
" \"source_id\",",
|
||||
" \"source_type\",",
|
||||
" \"target_id\",",
|
||||
" \"target_type\"",
|
||||
") VALUES (",
|
||||
" '{{uuid:person_id}}',",
|
||||
" 'person',",
|
||||
" '{{uuid:phone2_id}}',",
|
||||
" 'phone_number'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"contact\" (",
|
||||
" \"is_primary\"",
|
||||
") VALUES (",
|
||||
" false",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
") VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
||||
" \"target_type\":\"phone_number\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }',",
|
||||
" '{{uuid:contact2_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
@ -2108,7 +2092,7 @@
|
||||
") VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:contact3_id}}',",
|
||||
" '{{uuid:contact2_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'contact'",
|
||||
@ -2153,6 +2137,115 @@
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }',",
|
||||
" '{{uuid:contact2_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"type\"",
|
||||
") VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:email2_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'email_address'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"email_address\" (",
|
||||
" \"address\"",
|
||||
") VALUES (",
|
||||
" 'test2@example.com'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
") VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"address\":\"test2@example.com\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" }',",
|
||||
" '{{uuid:email2_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"type\"",
|
||||
") VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:contact3_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'contact'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"relationship\" (",
|
||||
" \"source_id\",",
|
||||
" \"source_type\",",
|
||||
" \"target_id\",",
|
||||
" \"target_type\"",
|
||||
") VALUES (",
|
||||
" '{{uuid:person_id}}',",
|
||||
" 'person',",
|
||||
" '{{uuid:email2_id}}',",
|
||||
" 'email_address'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"contact\" (",
|
||||
" \"is_primary\"",
|
||||
") VALUES (",
|
||||
" false",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
") VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }',",
|
||||
" '{{uuid:contact3_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
@ -2185,16 +2278,16 @@
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"first_name\":\"Relation\",",
|
||||
" \"id\":\"{{uuid:person_id}}\",",
|
||||
" \"last_name\":\"Test\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"first_name\":\"Relation\",",
|
||||
" \"id\":\"{{uuid:person_id}}\",",
|
||||
" \"last_name\":\"Test\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"first_name\":\"Relation\",",
|
||||
" \"last_name\":\"Test\",",
|
||||
@ -2204,19 +2297,19 @@
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact1_id}}\",",
|
||||
" \"is_primary\":true,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:phone1_id}}\",",
|
||||
" \"target_type\":\"phone_number\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact1_id}}\",",
|
||||
" \"is_primary\":true,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:phone1_id}}\",",
|
||||
" \"target_type\":\"phone_number\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"is_primary\":true,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
@ -2229,15 +2322,15 @@
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:phone1_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"number\":\"555-0001\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" },",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:phone1_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"number\":\"555-0001\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"number\":\"555-0001\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
@ -2246,87 +2339,87 @@
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact2_id}}\",",
|
||||
" \"is_primary\":false,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
||||
" \"target_type\":\"phone_number\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
||||
" \"target_type\":\"phone_number\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }",
|
||||
" }')"
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact2_id}}\",",
|
||||
" \"is_primary\":false,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }",
|
||||
"}')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:phone2_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"number\":\"555-0002\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"number\":\"555-0002\",",
|
||||
" \"type\":\"phone_number\"",
|
||||
" }",
|
||||
" }')"
|
||||
" \"complete\":{",
|
||||
" \"address\":\"test@example.com\",",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:email1_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"address\":\"test@example.com\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" }",
|
||||
"}')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact3_id}}\",",
|
||||
" \"is_primary\":false,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }",
|
||||
" }')"
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:contact3_id}}\",",
|
||||
" \"is_primary\":false,",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"is_primary\":false,",
|
||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||
" \"source_type\":\"person\",",
|
||||
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||
" \"target_type\":\"email_address\",",
|
||||
" \"type\":\"contact\"",
|
||||
" }",
|
||||
"}')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"address\":\"test@example.com\",",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:email1_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"address\":\"test@example.com\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" }",
|
||||
" }')"
|
||||
" \"complete\":{",
|
||||
" \"address\":\"test2@example.com\",",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:email2_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"address\":\"test2@example.com\",",
|
||||
" \"type\":\"email_address\"",
|
||||
" }",
|
||||
"}')"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
167
fixtures/objectTypes.json
Normal file
167
fixtures/objectTypes.json
Normal file
@ -0,0 +1,167 @@
|
||||
[
|
||||
{
|
||||
"description": "Strict Inheritance",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "parent_type",
|
||||
"type": "object",
|
||||
"properties": {"a": {"type": "integer"}},
|
||||
"required": ["a"]
|
||||
},
|
||||
{
|
||||
"$id": "child_type",
|
||||
"type": "parent_type",
|
||||
"properties": {"b": {"type": "integer"}}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid child inherits properties and strictness",
|
||||
"schema_id": "child_type",
|
||||
"data": {"a": 1, "b": 2},
|
||||
"action": "validate",
|
||||
"expect": {"success": true}
|
||||
},
|
||||
{
|
||||
"description": "missing inherited required property fails",
|
||||
"schema_id": "child_type",
|
||||
"data": {"b": 2},
|
||||
"action": "validate",
|
||||
"expect": {"success": false}
|
||||
},
|
||||
{
|
||||
"description": "additional properties fail due to inherited strictness",
|
||||
"schema_id": "child_type",
|
||||
"data": {"a": 1, "b": 2, "c": 3},
|
||||
"action": "validate",
|
||||
"expect": {"success": false}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Local Shadowing (Composition & Proxies)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "budget",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max": {"type": "integer", "maximum": 100}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "custom_budget",
|
||||
"type": "budget",
|
||||
"properties": {
|
||||
"max": {"type": "integer", "maximum": 50}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shadowing override applies (50 is locally allowed)",
|
||||
"schema_id": "custom_budget",
|
||||
"data": {"max": 40},
|
||||
"action": "validate",
|
||||
"expect": {"success": true}
|
||||
},
|
||||
{
|
||||
"description": "shadowing override applies natively, rejecting 60 even though parent allowed 100",
|
||||
"schema_id": "custom_budget",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"$id": "invoice",
|
||||
"type": "object",
|
||||
"properties": {"amount": {"type": "integer"}},
|
||||
"required": ["amount"]
|
||||
},
|
||||
{
|
||||
"$id": "request",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inv": {"type": ["invoice", "null"]}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid object passes shorthand inheritance check",
|
||||
"schema_id": "request",
|
||||
"data": {"inv": {"amount": 5}},
|
||||
"action": "validate",
|
||||
"expect": {"success": true}
|
||||
},
|
||||
{
|
||||
"description": "null passes shorthand inheritance check",
|
||||
"schema_id": "request",
|
||||
"data": {"inv": null},
|
||||
"action": "validate",
|
||||
"expect": {"success": true}
|
||||
},
|
||||
{
|
||||
"description": "invalid object fails inner constraints safely",
|
||||
"schema_id": "request",
|
||||
"data": {"inv": {"amount": "string"}},
|
||||
"action": "validate",
|
||||
"expect": {"success": false}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,670 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "oneOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"minimum": 2
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid",
|
||||
"data": 1,
|
||||
"schema_id": "oneOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": 2.5,
|
||||
"schema_id": "oneOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": 3,
|
||||
"schema_id": "oneOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": 1.5,
|
||||
"schema_id": "oneOf_0_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with base schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"minLength": 2
|
||||
},
|
||||
{
|
||||
"maxLength": 4
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": 3,
|
||||
"schema_id": "oneOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "one oneOf valid",
|
||||
"data": "foobar",
|
||||
"schema_id": "oneOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_1_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, all true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"$id": "oneOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_2_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, one true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"$id": "oneOf_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_3_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, more than one true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
],
|
||||
"$id": "oneOf_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, all false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"$id": "oneOf_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf complex types",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid (complex)",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": "baz"
|
||||
},
|
||||
"schema_id": "oneOf_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": 2,
|
||||
"bar": "quux"
|
||||
},
|
||||
"schema_id": "oneOf_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
],
|
||||
"$id": "oneOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "one valid - valid",
|
||||
"data": "foo",
|
||||
"schema_id": "oneOf_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": 123,
|
||||
"schema_id": "oneOf_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with required",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
"baz": true
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"foo",
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "both invalid - invalid",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "first valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "second valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"baz": 3
|
||||
},
|
||||
"schema_id": "oneOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"schema_id": "oneOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "extra property invalid (strict)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"extra": 3
|
||||
},
|
||||
"schema_id": "oneOf_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with required (extensible)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "object",
|
||||
"extensible": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"foo",
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"foo",
|
||||
"baz"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "both invalid - invalid",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "first valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"schema_id": "oneOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "second valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"baz": 3
|
||||
},
|
||||
"schema_id": "oneOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"schema_id": "oneOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "extra properties are valid (extensible)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"extra": "value"
|
||||
},
|
||||
"schema_id": "oneOf_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with missing optional property",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": true,
|
||||
"baz": true
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": true
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid",
|
||||
"data": {
|
||||
"bar": 8
|
||||
},
|
||||
"schema_id": "oneOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": {
|
||||
"foo": "foo"
|
||||
},
|
||||
"schema_id": "oneOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": {
|
||||
"foo": "foo",
|
||||
"bar": 8
|
||||
},
|
||||
"schema_id": "oneOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": {
|
||||
"baz": "quux"
|
||||
},
|
||||
"schema_id": "oneOf_10_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested oneOf, to check validation semantics",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"schema_id": "oneOf_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"schema_id": "oneOf_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "extensible: true allows extra properties in oneOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"extensible": true,
|
||||
"$id": "oneOf_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra property is valid (matches first option)",
|
||||
"data": {
|
||||
"bar": 2,
|
||||
"extra": "prop"
|
||||
},
|
||||
"schema_id": "oneOf_12_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -123,7 +123,7 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"path": "primitives/1"
|
||||
"details": { "path": "primitives/1" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -147,7 +147,11 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "ad_hoc_objects/1/name"
|
||||
"details": { "path": "ad_hoc_objects/1/name" }
|
||||
},
|
||||
{
|
||||
"code": "STRICT_PROPERTY_VIOLATION",
|
||||
"details": { "path": "ad_hoc_objects/1/age" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -173,7 +177,7 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"path": "entities/entity-beta/value"
|
||||
"details": { "path": "entities/entity-beta/value" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -204,7 +208,156 @@
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"path": "deep_entities/parent-omega/nested/child-beta/flag"
|
||||
"details": { "path": "deep_entities/parent-omega/nested/child-beta/flag" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Polymorphic Structure Pathing",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "widget",
|
||||
"variations": ["widget"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "widget",
|
||||
"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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"table_families": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$family": "widget"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "oneOf tags natively bubble specific schema paths into deep array roots",
|
||||
"data": {
|
||||
"metadata_bubbles": [
|
||||
{ "type": "invoice", "invoice_id": 100 },
|
||||
{ "type": "contact", "phone": 12345, "rogue_field": "x" }
|
||||
]
|
||||
},
|
||||
"schema_id": "polymorphic_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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "families mechanically map physical variants directly onto topological uuid array paths",
|
||||
"data": {
|
||||
"table_families": [
|
||||
{
|
||||
"id": "widget-1",
|
||||
"type": "widget",
|
||||
"kind": "stock",
|
||||
"amount": 500,
|
||||
"details": { "nested_metric": 42.0 }
|
||||
},
|
||||
{
|
||||
"id": "widget-2",
|
||||
"type": "widget",
|
||||
"kind": "stock",
|
||||
"amount": "not_an_int",
|
||||
"details": {
|
||||
"stray_child": true
|
||||
},
|
||||
"unexpected_root_prop": "hi"
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema_id": "polymorphic_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"details": { "path": "table_families/widget-2/amount" }
|
||||
},
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"details": { "path": "table_families/widget-2/details/nested_metric" }
|
||||
},
|
||||
{
|
||||
"code": "STRICT_PROPERTY_VIOLATION",
|
||||
"details": { "path": "table_families/widget-2/details/stray_child" }
|
||||
},
|
||||
{
|
||||
"code": "STRICT_PROPERTY_VIOLATION",
|
||||
"details": { "path": "table_families/widget-2/unexpected_root_prop" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
479
fixtures/polymorphism.json
Normal file
479
fixtures/polymorphism.json
Normal file
@ -0,0 +1,479 @@
|
||||
[
|
||||
{
|
||||
"description": "Vertical $family Routing (Across Tables)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": ["entity", "organization", "person", "bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" },
|
||||
"id": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"variations": ["organization", "person"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "organization",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": ["person"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"type": "organization",
|
||||
"properties": {
|
||||
"first_name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bot",
|
||||
"variations": ["bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "bot",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"model": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "family_entity",
|
||||
"$family": "entity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Matches base entity",
|
||||
"schema_id": "family_entity",
|
||||
"data": { "type": "entity", "id": "1" },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Matches descendant person natively",
|
||||
"schema_id": "family_entity",
|
||||
"data": { "type": "person", "id": "2", "first_name": "Bob" },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Missing type fails out immediately",
|
||||
"schema_id": "family_entity",
|
||||
"data": { "id": "3", "first_name": "Bob" },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "MISSING_TYPE", "details": { "path": "" } } ] }
|
||||
},
|
||||
{
|
||||
"description": "Alias matching failure",
|
||||
"schema_id": "family_entity",
|
||||
"data": { "type": "alien", "id": "4" },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "NO_FAMILY_MATCH", "details": { "path": "" } } ] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Matrix $family Routing (Vertical + Horizontal Intersections)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": ["entity", "organization", "person", "bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.entity",
|
||||
"type": "entity",
|
||||
"properties": {
|
||||
"kind": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"variations": ["organization", "person"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "organization",
|
||||
"type": "entity"
|
||||
},
|
||||
{
|
||||
"$id": "light.organization",
|
||||
"type": "light.entity"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": ["person"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"type": "organization"
|
||||
},
|
||||
{
|
||||
"$id": "light.person",
|
||||
"type": "light.organization"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bot",
|
||||
"variations": ["bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "bot",
|
||||
"type": "entity"
|
||||
},
|
||||
{
|
||||
"$id": "light.bot",
|
||||
"type": "light.entity"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "family_light_org",
|
||||
"$family": "light.organization"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Matches light.organization exact matrix target",
|
||||
"schema_id": "family_light_org",
|
||||
"data": { "type": "organization", "kind": "light" },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Matches descendant light.person through matrix evaluation",
|
||||
"schema_id": "family_light_org",
|
||||
"data": { "type": "person", "kind": "light" },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Structurally fails to route bot (bot is not descendant of organization)",
|
||||
"schema_id": "family_light_org",
|
||||
"data": { "type": "bot", "kind": "light" },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "NO_FAMILY_MATCH", "details": { "path": "" } } ] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Horizontal $family Routing (Virtual Variations)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "widget",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "stock.widget",
|
||||
"type": "widget",
|
||||
"properties": {
|
||||
"kind": { "type": "string" },
|
||||
"amount": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "super_stock.widget",
|
||||
"type": "stock.widget",
|
||||
"properties": {
|
||||
"super_amount": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "family_widget",
|
||||
"$family": "widget"
|
||||
},
|
||||
{
|
||||
"$id": "family_stock_widget",
|
||||
"$family": "stock.widget"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Base widget resolves stock widget horizontally",
|
||||
"schema_id": "family_widget",
|
||||
"data": { "type": "widget", "kind": "stock", "amount": 5 },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Base widget resolves nested super stock widget natively via descendants crawl",
|
||||
"schema_id": "family_widget",
|
||||
"data": { "type": "widget", "kind": "super_stock", "amount": 5, "super_amount": 10 },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "stock.widget explicit family resolves nested super stock via fast path",
|
||||
"schema_id": "family_stock_widget",
|
||||
"data": { "type": "widget", "kind": "super_stock", "amount": 5, "super_amount": 10 },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "stock.widget fails when presented an invalid payload constraint",
|
||||
"schema_id": "family_stock_widget",
|
||||
"data": { "type": "widget", "kind": "super_stock", "amount": 5, "super_amount": "not_an_int" },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "INVALID_TYPE", "details": { "path": "super_amount" } } ] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Strict oneOf Punc Pointers (Tagged Unions)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": ["entity", "person", "bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": ["person"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"type": "entity"
|
||||
},
|
||||
{
|
||||
"$id": "full.person",
|
||||
"type": "person",
|
||||
"properties": {
|
||||
"kind": { "type": "string" },
|
||||
"age": { "type": "integer" }
|
||||
},
|
||||
"required": ["age"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bot",
|
||||
"variations": ["bot"],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "bot",
|
||||
"type": "entity"
|
||||
},
|
||||
{
|
||||
"$id": "full.bot",
|
||||
"type": "bot",
|
||||
"properties": {
|
||||
"kind": { "type": "string" },
|
||||
"version": { "type": "string" }
|
||||
},
|
||||
"required": ["version"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "oneOf_union",
|
||||
"oneOf": [
|
||||
{ "type": "full.person" },
|
||||
{ "type": "full.bot" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Throws MISSING_TYPE if discriminator matches neither",
|
||||
"schema_id": "oneOf_union",
|
||||
"data": { "kind": "full", "age": 5 },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "MISSING_TYPE", "details": { "path": "" } } ] }
|
||||
},
|
||||
{
|
||||
"description": "Golden match throws pure structural error exclusively on person",
|
||||
"schema_id": "oneOf_union",
|
||||
"data": { "type": "person", "kind": "full", "age": "five" },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "INVALID_TYPE", "details": { "path": "age" } } ] }
|
||||
},
|
||||
{
|
||||
"description": "Golden matches perfectly",
|
||||
"schema_id": "oneOf_union",
|
||||
"data": { "type": "person", "kind": "full", "age": 5 },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Fails nicely with NO_ONEOF_MATCH",
|
||||
"schema_id": "oneOf_union",
|
||||
"data": { "type": "alien", "kind": "full", "age": 5 },
|
||||
"action": "validate",
|
||||
"expect": { "success": false, "errors": [ { "code": "NO_ONEOF_MATCH", "details": { "path": "" } } ] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "JSONB Field Bubble oneOf Discrimination (Promoted IDs)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "metadata",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "string" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "invoice.metadata",
|
||||
"type": "metadata",
|
||||
"properties": {
|
||||
"invoice_id": { "type": "integer" }
|
||||
},
|
||||
"required": ["invoice_id"]
|
||||
},
|
||||
{
|
||||
"$id": "payment.metadata",
|
||||
"type": "metadata",
|
||||
"properties": {
|
||||
"payment_id": { "type": "integer" }
|
||||
},
|
||||
"required": ["payment_id"]
|
||||
},
|
||||
{
|
||||
"$id": "oneOf_bubble",
|
||||
"oneOf": [
|
||||
{ "type": "invoice.metadata" },
|
||||
{ "type": "payment.metadata" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Extracts golden match natively from explicit JSONB type discriminator",
|
||||
"schema_id": "oneOf_bubble",
|
||||
"data": { "type": "invoice.metadata", "invoice_id": "nan" },
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [ { "code": "INVALID_TYPE", "details": { "path": "invoice_id" } } ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Valid payload succeeds perfectly in JSONB universe",
|
||||
"schema_id": "oneOf_bubble",
|
||||
"data": { "type": "payment.metadata", "payment_id": 123 },
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Standard JSON Schema oneOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "oneOf_scalars",
|
||||
"oneOf": [
|
||||
{ "type": "integer" },
|
||||
{ "minimum": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"$id": "oneOf_dedupe",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shared": { "type": "integer" }
|
||||
},
|
||||
"required": ["shared"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shared": { "type": "integer" },
|
||||
"extra": { "type": "string" }
|
||||
},
|
||||
"required": ["shared", "extra"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Valid exclusively against first scalar choice",
|
||||
"schema_id": "oneOf_scalars",
|
||||
"data": 1,
|
||||
"action": "validate",
|
||||
"expect": { "success": true }
|
||||
},
|
||||
{
|
||||
"description": "Fails mathematically if matches both schemas natively",
|
||||
"schema_id": "oneOf_scalars",
|
||||
"data": 3,
|
||||
"action": "validate",
|
||||
"expect": { "success": false }
|
||||
},
|
||||
{
|
||||
"description": "Deduper merges shared errors dynamically exactly like JSON Schema",
|
||||
"schema_id": "oneOf_dedupe",
|
||||
"data": { "shared": "nan" },
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{ "code": "NO_ONEOF_MATCH", "details": { "path": "" } },
|
||||
{ "code": "INVALID_TYPE", "details": { "path": "shared" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,929 +0,0 @@
|
||||
[
|
||||
{
|
||||
"description": "nested refs",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "c_212",
|
||||
"$id": "ref_4_0"
|
||||
},
|
||||
{
|
||||
"$id": "a_212",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"$id": "b_212",
|
||||
"$ref": "a_212"
|
||||
},
|
||||
{
|
||||
"$id": "c_212",
|
||||
"$ref": "b_212"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "nested ref valid",
|
||||
"data": 5,
|
||||
"schema_id": "ref_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "nested ref invalid",
|
||||
"data": "a",
|
||||
"schema_id": "ref_4_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ref applies alongside sibling keywords",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"$ref": "reffed_248",
|
||||
"maxItems": 2
|
||||
}
|
||||
},
|
||||
"$id": "ref_5_0"
|
||||
},
|
||||
{
|
||||
"$id": "reffed_248",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "ref valid, maxItems valid",
|
||||
"data": {
|
||||
"foo": []
|
||||
},
|
||||
"schema_id": "ref_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "ref valid, maxItems invalid",
|
||||
"data": {
|
||||
"foo": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"schema_id": "ref_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "ref invalid",
|
||||
"data": {
|
||||
"foo": "string"
|
||||
},
|
||||
"schema_id": "ref_5_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "property named $ref that is not a reference",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "ref_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "property named $ref valid",
|
||||
"data": {
|
||||
"$ref": "a"
|
||||
},
|
||||
"schema_id": "ref_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "property named $ref invalid",
|
||||
"data": {
|
||||
"$ref": 2
|
||||
},
|
||||
"schema_id": "ref_6_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "property named $ref, containing an actual $ref",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"$ref": {
|
||||
"$ref": "is-string_344"
|
||||
}
|
||||
},
|
||||
"$id": "ref_7_0"
|
||||
},
|
||||
{
|
||||
"$id": "is-string_344",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "property named $ref valid",
|
||||
"data": {
|
||||
"$ref": "a"
|
||||
},
|
||||
"schema_id": "ref_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "property named $ref invalid",
|
||||
"data": {
|
||||
"$ref": 2
|
||||
},
|
||||
"schema_id": "ref_7_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "$ref to boolean schema true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "bool_378",
|
||||
"$id": "ref_8_0"
|
||||
},
|
||||
{
|
||||
"$id": "bool_378",
|
||||
"extensible": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"schema_id": "ref_8_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "$ref to boolean schema false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "bool_400",
|
||||
"$id": "ref_9_0"
|
||||
},
|
||||
{
|
||||
"$id": "bool_400",
|
||||
"extensible": false,
|
||||
"not": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"schema_id": "ref_9_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "refs with quote",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo\"bar": {
|
||||
"$ref": "foo%22bar_550"
|
||||
}
|
||||
},
|
||||
"$id": "ref_11_0"
|
||||
},
|
||||
{
|
||||
"$id": "foo%22bar_550",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "object with numbers is valid",
|
||||
"data": {
|
||||
"foo\"bar": 1
|
||||
},
|
||||
"schema_id": "ref_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "object with strings is invalid",
|
||||
"data": {
|
||||
"foo\"bar": "1"
|
||||
},
|
||||
"schema_id": "ref_11_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "$ref boundary resets to loose",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "target_1465",
|
||||
"$id": "ref_35_0"
|
||||
},
|
||||
{
|
||||
"$id": "target_1465",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra property in ref target is invalid (strict by default)",
|
||||
"data": {
|
||||
"foo": "bar",
|
||||
"extra": 1
|
||||
},
|
||||
"schema_id": "ref_35_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "$ref target can enforce strictness",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "target_1496",
|
||||
"$id": "ref_36_0"
|
||||
},
|
||||
{
|
||||
"$id": "target_1496",
|
||||
"extensible": false,
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra property in ref target is invalid",
|
||||
"data": {
|
||||
"foo": "bar",
|
||||
"extra": 1
|
||||
},
|
||||
"schema_id": "ref_36_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "strictness: boundary reset at $ref",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"extensible": true,
|
||||
"properties": {
|
||||
"inline_child": {
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ref_child": {
|
||||
"$ref": "strict_node_1544"
|
||||
},
|
||||
"extensible_ref_child": {
|
||||
"$ref": "extensible_node_1551"
|
||||
}
|
||||
},
|
||||
"$id": "ref_37_0"
|
||||
},
|
||||
{
|
||||
"$id": "strict_node_1544",
|
||||
"properties": {
|
||||
"b": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "extensible_node_1551",
|
||||
"extensible": true,
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "inline child inherits looseness",
|
||||
"data": {
|
||||
"inline_child": {
|
||||
"a": 1,
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
"schema_id": "ref_37_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "ref child resets to strict (default)",
|
||||
"data": {
|
||||
"ref_child": {
|
||||
"b": 1,
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
"schema_id": "ref_37_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "ref child with explicit extensible=true is loose",
|
||||
"data": {
|
||||
"extensible_ref_child": {
|
||||
"c": 1,
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
"schema_id": "ref_37_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "arrays: ref items inherit strictness (reset at boundary)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "strict_node_1614"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$id": "ref_38_0"
|
||||
},
|
||||
{
|
||||
"$id": "strict_node_1614",
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "ref item with extra property is invalid (strict by default)",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"a": 1,
|
||||
"extra": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema_id": "ref_38_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "implicit keyword shadowing",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$ref": "parent_1648",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "child"
|
||||
},
|
||||
"age": {
|
||||
"minimum": 15
|
||||
}
|
||||
},
|
||||
"$id": "ref_39_0"
|
||||
},
|
||||
{
|
||||
"$id": "parent_1648",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "parent"
|
||||
},
|
||||
"age": {
|
||||
"minimum": 10,
|
||||
"maximum": 20
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"age"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "child type overrides parent type",
|
||||
"data": {
|
||||
"type": "child",
|
||||
"age": 15
|
||||
},
|
||||
"schema_id": "ref_39_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "parent type is now invalid (shadowed)",
|
||||
"data": {
|
||||
"type": "parent",
|
||||
"age": 15
|
||||
},
|
||||
"schema_id": "ref_39_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "child min age (15) is enforced",
|
||||
"data": {
|
||||
"type": "child",
|
||||
"age": 12
|
||||
},
|
||||
"schema_id": "ref_39_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "parent max age (20) is shadowed (replaced) by child definition",
|
||||
"data": {
|
||||
"type": "child",
|
||||
"age": 21
|
||||
},
|
||||
"schema_id": "ref_39_0",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Entities extending entities (Physical Birth)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": [
|
||||
"entity",
|
||||
"organization",
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"variations": [
|
||||
"organization",
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "organization",
|
||||
"$ref": "entity",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": [
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"$ref": "organization",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"puncs": [
|
||||
{
|
||||
"name": "save_org",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "save_org.request",
|
||||
"$ref": "organization"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Valid person against organization schema (implicit type allowance from physical hierarchy)",
|
||||
"schema_id": "save_org.request",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "person",
|
||||
"name": "ACME"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Valid organization against organization schema",
|
||||
"schema_id": "save_org.request",
|
||||
"data": {
|
||||
"id": "2",
|
||||
"type": "organization",
|
||||
"name": "ACME"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Invalid entity against organization schema (ancestor not allowed)",
|
||||
"schema_id": "save_org.request",
|
||||
"data": {
|
||||
"id": "3",
|
||||
"type": "entity"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "TYPE_MISMATCH",
|
||||
"path": "type"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Viral Infection: Ad-hocs inheriting entity boundaries via $ref",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
"variations": [
|
||||
"entity",
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "entity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "person",
|
||||
"variations": [
|
||||
"person"
|
||||
],
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "person",
|
||||
"$ref": "entity",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.person",
|
||||
"$ref": "entity",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"puncs": [
|
||||
{
|
||||
"name": "save_person_light",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "save_person_light.request",
|
||||
"$ref": "light.person",
|
||||
"properties": {
|
||||
"extra_request_field": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Valid person against ad-hoc request schema (request virally inherited person variations)",
|
||||
"schema_id": "save_person_light.request",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "person",
|
||||
"first_name": "John",
|
||||
"extra_request_field": "test"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Invalid entity against ad-hoc request schema (viral inheritance enforces person boundary)",
|
||||
"schema_id": "save_person_light.request",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "entity",
|
||||
"first_name": "John"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "TYPE_MISMATCH",
|
||||
"path": "type"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ad-hocs extending ad-hocs (No type property)",
|
||||
"database": {
|
||||
"puncs": [
|
||||
{
|
||||
"name": "save_address",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "address",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "us_address",
|
||||
"$ref": "address",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"zip": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "save_address.request",
|
||||
"$ref": "us_address"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Valid us_address",
|
||||
"schema_id": "save_address.request",
|
||||
"data": {
|
||||
"street": "123 Main",
|
||||
"city": "Anytown",
|
||||
"state": "CA",
|
||||
"zip": "12345"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Invalid base address against us_address",
|
||||
"schema_id": "save_address.request",
|
||||
"data": {
|
||||
"street": "123 Main",
|
||||
"city": "Anytown"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Ad-hocs extending ad-hocs (with string type property, no magic)",
|
||||
"database": {
|
||||
"puncs": [
|
||||
{
|
||||
"name": "save_config",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "config_base",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "config_base"
|
||||
},
|
||||
"setting": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "config_advanced",
|
||||
"$ref": "config_base",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "config_advanced"
|
||||
},
|
||||
"advanced_setting": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "save_config.request",
|
||||
"$ref": "config_base"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Valid config_base against config_base",
|
||||
"schema_id": "save_config.request",
|
||||
"data": {
|
||||
"type": "config_base",
|
||||
"setting": "on"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Invalid config_advanced against config_base (no type magic, const is strictly 'config_base')",
|
||||
"schema_id": "save_config.request",
|
||||
"data": {
|
||||
"type": "config_advanced",
|
||||
"setting": "on",
|
||||
"advanced_setting": "off"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2
flows
2
flows
Submodule flows updated: a7b0f5dc4d...4d61e13e00
@ -44,8 +44,8 @@ impl MockExecutor {
|
||||
|
||||
#[cfg(test)]
|
||||
impl DatabaseExecutor for MockExecutor {
|
||||
fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> {
|
||||
println!("DEBUG SQL QUERY: {}", sql);
|
||||
fn query(&self, sql: &str, _args: Option<Vec<Value>>) -> Result<Value, String> {
|
||||
println!("JSPG_SQL: {}", sql);
|
||||
MOCK_STATE.with(|state| {
|
||||
let mut s = state.borrow_mut();
|
||||
s.captured_queries.push(sql.to_string());
|
||||
@ -65,8 +65,8 @@ impl DatabaseExecutor for MockExecutor {
|
||||
})
|
||||
}
|
||||
|
||||
fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> {
|
||||
println!("DEBUG SQL EXECUTE: {}", sql);
|
||||
fn execute(&self, sql: &str, _args: Option<Vec<Value>>) -> Result<(), String> {
|
||||
println!("JSPG_SQL: {}", sql);
|
||||
MOCK_STATE.with(|state| {
|
||||
let mut s = state.borrow_mut();
|
||||
s.captured_queries.push(sql.to_string());
|
||||
@ -170,7 +170,7 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
|
||||
.unwrap_or("")
|
||||
.trim_matches('"');
|
||||
let right = part[eq_idx + 1..].trim().trim_matches('\'');
|
||||
|
||||
|
||||
let mock_val_str = match mock_obj.get(left) {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
@ -189,12 +189,12 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
|
||||
.last()
|
||||
.unwrap_or("")
|
||||
.trim_matches('"');
|
||||
|
||||
|
||||
let mock_val_str = match mock_obj.get(left) {
|
||||
Some(Value::Null) => "null".to_string(),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
|
||||
|
||||
if mock_val_str != "null" {
|
||||
branch_matches = false;
|
||||
break;
|
||||
|
||||
@ -9,10 +9,10 @@ use serde_json::Value;
|
||||
/// without a live Postgres SPI connection.
|
||||
pub trait DatabaseExecutor: Send + Sync {
|
||||
/// Executes a query expecting a single JSONB return, representing rows.
|
||||
fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String>;
|
||||
fn query(&self, sql: &str, args: Option<Vec<Value>>) -> Result<Value, String>;
|
||||
|
||||
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
|
||||
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String>;
|
||||
fn execute(&self, sql: &str, args: Option<Vec<Value>>) -> Result<(), String>;
|
||||
|
||||
/// Returns the current authenticated user's ID
|
||||
fn auth_user_id(&self) -> Result<String, String>;
|
||||
|
||||
@ -67,21 +67,17 @@ impl SpiExecutor {
|
||||
}
|
||||
|
||||
impl DatabaseExecutor for SpiExecutor {
|
||||
fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String> {
|
||||
let mut json_args = Vec::new();
|
||||
fn query(&self, sql: &str, args: Option<Vec<Value>>) -> Result<Value, String> {
|
||||
let mut args_with_oid: Vec<pgrx::datum::DatumWithOid> = Vec::new();
|
||||
if let Some(params) = args {
|
||||
for val in params {
|
||||
json_args.push(pgrx::JsonB(val.clone()));
|
||||
}
|
||||
for j_val in json_args.into_iter() {
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(j_val));
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(pgrx::JsonB(val)));
|
||||
}
|
||||
}
|
||||
|
||||
pgrx::debug1!("JSPG_SQL: {}", sql);
|
||||
self.transact(|| {
|
||||
Spi::connect(|client| {
|
||||
pgrx::notice!("JSPG_SQL: {}", sql);
|
||||
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Ok(tup_table) => {
|
||||
let mut results = Vec::new();
|
||||
@ -98,21 +94,17 @@ impl DatabaseExecutor for SpiExecutor {
|
||||
})
|
||||
}
|
||||
|
||||
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String> {
|
||||
let mut json_args = Vec::new();
|
||||
fn execute(&self, sql: &str, args: Option<Vec<Value>>) -> Result<(), String> {
|
||||
let mut args_with_oid: Vec<pgrx::datum::DatumWithOid> = Vec::new();
|
||||
if let Some(params) = args {
|
||||
for val in params {
|
||||
json_args.push(pgrx::JsonB(val.clone()));
|
||||
}
|
||||
for j_val in json_args.into_iter() {
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(j_val));
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(pgrx::JsonB(val)));
|
||||
}
|
||||
}
|
||||
|
||||
pgrx::debug1!("JSPG_SQL: {}", sql);
|
||||
self.transact(|| {
|
||||
Spi::connect_mut(|client| {
|
||||
pgrx::notice!("JSPG_SQL: {}", sql);
|
||||
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("SPI Execution Failure: {}", e)),
|
||||
|
||||
@ -38,7 +38,7 @@ pub struct Database {
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(val: &serde_json::Value) -> Result<Self, crate::drop::Drop> {
|
||||
pub fn new(val: &serde_json::Value) -> (Self, crate::drop::Drop) {
|
||||
let mut db = Self {
|
||||
enums: HashMap::new(),
|
||||
types: HashMap::new(),
|
||||
@ -53,18 +53,38 @@ impl Database {
|
||||
executor: Box::new(MockExecutor::new()),
|
||||
};
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) {
|
||||
for item in arr {
|
||||
if let Ok(def) = serde_json::from_value::<Enum>(item.clone()) {
|
||||
db.enums.insert(def.name.clone(), def);
|
||||
match serde_json::from_value::<Enum>(item.clone()) {
|
||||
Ok(def) => {
|
||||
db.enums.insert(def.name.clone(), def);
|
||||
}
|
||||
Err(e) => {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arr) = val.get("types").and_then(|v| v.as_array()) {
|
||||
for item in arr {
|
||||
if let Ok(def) = serde_json::from_value::<Type>(item.clone()) {
|
||||
db.types.insert(def.name.clone(), def);
|
||||
match serde_json::from_value::<Type>(item.clone()) {
|
||||
Ok(def) => {
|
||||
db.types.insert(def.name.clone(), def);
|
||||
}
|
||||
Err(e) => {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,16 +100,11 @@ impl Database {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
errors.push(crate::drop::Error {
|
||||
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
||||
message: format!("Failed to parse database relation: {}", e),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
cause: None,
|
||||
context: None,
|
||||
schema: None,
|
||||
},
|
||||
}]));
|
||||
details: crate::drop::ErrorDetails::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -97,28 +112,51 @@ impl Database {
|
||||
|
||||
if let Some(arr) = val.get("puncs").and_then(|v| v.as_array()) {
|
||||
for item in arr {
|
||||
if let Ok(def) = serde_json::from_value::<Punc>(item.clone()) {
|
||||
db.puncs.insert(def.name.clone(), def);
|
||||
match serde_json::from_value::<Punc>(item.clone()) {
|
||||
Ok(def) => {
|
||||
db.puncs.insert(def.name.clone(), def);
|
||||
}
|
||||
Err(e) => {
|
||||
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() {
|
||||
if let Ok(mut schema) = serde_json::from_value::<Schema>(item.clone()) {
|
||||
let id = schema
|
||||
.obj
|
||||
.id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("schema_{}", i));
|
||||
schema.obj.id = Some(id.clone());
|
||||
db.schemas.insert(id, schema);
|
||||
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, 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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.compile()?;
|
||||
Ok(db)
|
||||
db.compile(&mut errors);
|
||||
let drop = if errors.is_empty() {
|
||||
crate::drop::Drop::success()
|
||||
} else {
|
||||
crate::drop::Drop::with_errors(errors)
|
||||
};
|
||||
(db, drop)
|
||||
}
|
||||
|
||||
/// Override the default executor for unit testing
|
||||
@ -128,12 +166,12 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Executes a query expecting a single JSONB array return, representing rows.
|
||||
pub fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String> {
|
||||
pub fn query(&self, sql: &str, args: Option<Vec<Value>>) -> Result<Value, String> {
|
||||
self.executor.query(sql, args)
|
||||
}
|
||||
|
||||
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
|
||||
pub fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String> {
|
||||
pub fn execute(&self, sql: &str, args: Option<Vec<Value>>) -> Result<(), String> {
|
||||
self.executor.execute(sql, args)
|
||||
}
|
||||
|
||||
@ -147,68 +185,48 @@ impl Database {
|
||||
self.executor.timestamp()
|
||||
}
|
||||
|
||||
pub fn compile(&mut self) -> Result<(), crate::drop::Drop> {
|
||||
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||
let mut harvested = Vec::new();
|
||||
for schema in self.schemas.values_mut() {
|
||||
if let Err(msg) = schema.collect_schemas(None, &mut harvested) {
|
||||
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
code: "SCHEMA_VALIDATION_FAILED".to_string(),
|
||||
message: msg,
|
||||
details: crate::drop::ErrorDetails { path: "".to_string(), cause: None, context: None, schema: None },
|
||||
}]));
|
||||
}
|
||||
schema.collect_schemas(None, &mut harvested, errors);
|
||||
}
|
||||
self.schemas.extend(harvested);
|
||||
|
||||
if let Err(msg) = self.collect_schemas() {
|
||||
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
code: "SCHEMA_VALIDATION_FAILED".to_string(),
|
||||
message: msg,
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
cause: None,
|
||||
context: None,
|
||||
schema: None,
|
||||
},
|
||||
}]));
|
||||
}
|
||||
self.collect_schemas(errors);
|
||||
self.collect_depths();
|
||||
self.collect_descendants();
|
||||
|
||||
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
for schema in self.schemas.values() {
|
||||
schema.compile(self, &mut visited);
|
||||
schema.compile(self, &mut visited, errors);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_schemas(&mut self) -> Result<(), String> {
|
||||
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||
let mut to_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)?;
|
||||
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)?;
|
||||
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)?;
|
||||
schema.collect_schemas(None, &mut to_insert, errors);
|
||||
}
|
||||
}
|
||||
|
||||
for (id, schema) in to_insert {
|
||||
self.schemas.insert(id, schema);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_depths(&mut self) {
|
||||
@ -224,12 +242,14 @@ impl Database {
|
||||
if !visited.insert(current_id.clone()) {
|
||||
break; // Cycle detected
|
||||
}
|
||||
if let Some(ref_str) = &schema.obj.r#ref {
|
||||
current_id = ref_str.clone();
|
||||
depth += 1;
|
||||
} else {
|
||||
break;
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
current_id = t.clone();
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
depths.insert(id, depth);
|
||||
}
|
||||
@ -239,27 +259,25 @@ impl Database {
|
||||
fn collect_descendants(&mut self) {
|
||||
let mut direct_refs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for (id, schema) in &self.schemas {
|
||||
if let Some(ref_str) = &schema.obj.r#ref {
|
||||
direct_refs
|
||||
.entry(ref_str.clone())
|
||||
.or_default()
|
||||
.push(id.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
direct_refs
|
||||
.entry(t.clone())
|
||||
.or_default()
|
||||
.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache generic descendants for $family runtime lookups
|
||||
// Cache exhaustive descendants matrix for generic $family string lookups natively
|
||||
let mut descendants = HashMap::new();
|
||||
for (id, schema) in &self.schemas {
|
||||
if let Some(family_target) = &schema.obj.family {
|
||||
let mut desc_set = HashSet::new();
|
||||
Self::collect_descendants_recursively(family_target, &direct_refs, &mut desc_set);
|
||||
let mut desc_vec: Vec<String> = desc_set.into_iter().collect();
|
||||
desc_vec.sort();
|
||||
for id in self.schemas.keys() {
|
||||
let mut desc_set = HashSet::new();
|
||||
Self::collect_descendants_recursively(id, &direct_refs, &mut desc_set);
|
||||
let mut desc_vec: Vec<String> = desc_set.into_iter().collect();
|
||||
desc_vec.sort();
|
||||
|
||||
// By placing all descendants directly onto the ID mapped location of the Family declaration,
|
||||
// we can lookup descendants natively in ValidationContext without AST replacement overrides.
|
||||
descendants.insert(id.clone(), desc_vec);
|
||||
}
|
||||
descendants.insert(id.clone(), desc_vec);
|
||||
}
|
||||
self.descendants = descendants;
|
||||
}
|
||||
|
||||
@ -33,15 +33,30 @@ where
|
||||
Ok(Some(v))
|
||||
}
|
||||
|
||||
pub fn is_primitive_type(t: &str) -> bool {
|
||||
matches!(
|
||||
t,
|
||||
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Case {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub when: Option<Arc<Schema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
}
|
||||
|
||||
#[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(rename = "$ref")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub r#ref: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@ -150,24 +165,14 @@ pub struct SchemaObject {
|
||||
pub exclusive_maximum: Option<f64>,
|
||||
|
||||
// Combining Keywords
|
||||
#[serde(rename = "allOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub all_of: Option<Vec<Arc<Schema>>>,
|
||||
pub cases: Option<Vec<Case>>,
|
||||
#[serde(rename = "oneOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub one_of: Option<Vec<Arc<Schema>>>,
|
||||
#[serde(rename = "not")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub not: Option<Arc<Schema>>,
|
||||
#[serde(rename = "if")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub if_: Option<Arc<Schema>>,
|
||||
#[serde(rename = "then")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then_: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
|
||||
// Custom Vocabularies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@ -255,6 +260,7 @@ impl Schema {
|
||||
&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;
|
||||
@ -299,35 +305,45 @@ impl Schema {
|
||||
let mut props = std::collections::BTreeMap::new();
|
||||
|
||||
// 1. Resolve INHERITANCE dependencies first
|
||||
if let Some(ref_id) = &self.obj.r#ref {
|
||||
if let Some(parent) = db.schemas.get(ref_id) {
|
||||
parent.compile(db, visited);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.compile(db, visited, errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(all_of) = &self.obj.all_of {
|
||||
for ao in all_of {
|
||||
ao.compile(db, visited);
|
||||
if let Some(ao_props) = ao.obj.compiled_properties.get() {
|
||||
props.extend(ao_props.clone());
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
|
||||
let mut custom_type_count = 0;
|
||||
for t in types {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
custom_type_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(then_schema) = &self.obj.then_ {
|
||||
then_schema.compile(db, visited);
|
||||
if let Some(t_props) = then_schema.obj.compiled_properties.get() {
|
||||
props.extend(t_props.clone());
|
||||
if 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()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(else_schema) = &self.obj.else_ {
|
||||
else_schema.compile(db, visited);
|
||||
if let Some(e_props) = else_schema.obj.compiled_properties.get() {
|
||||
props.extend(e_props.clone());
|
||||
for t in types {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,47 +361,54 @@ impl Schema {
|
||||
let _ = self.obj.compiled_property_names.set(names);
|
||||
|
||||
// 4. Compute Edges natively
|
||||
let schema_edges = self.compile_edges(db, visited, &props);
|
||||
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);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(items) = &self.obj.items {
|
||||
items.compile(db, visited);
|
||||
items.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(pattern_props) = &self.obj.pattern_properties {
|
||||
for child in pattern_props.values() {
|
||||
child.compile(db, visited);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(additional_props) = &self.obj.additional_properties {
|
||||
additional_props.compile(db, visited);
|
||||
additional_props.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(one_of) = &self.obj.one_of {
|
||||
for child in one_of {
|
||||
child.compile(db, visited);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(arr) = &self.obj.prefix_items {
|
||||
for child in arr {
|
||||
child.compile(db, visited);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
if let Some(child) = &self.obj.not {
|
||||
child.compile(db, visited);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &self.obj.contains {
|
||||
child.compile(db, visited);
|
||||
child.compile(db, visited, errors);
|
||||
}
|
||||
if let Some(child) = &self.obj.property_names {
|
||||
child.compile(db, visited);
|
||||
}
|
||||
if let Some(child) = &self.obj.if_ {
|
||||
child.compile(db, visited);
|
||||
if let Some(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = &self.obj.id {
|
||||
@ -394,57 +417,71 @@ impl Schema {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn validate_identifier(id: &str, field_name: &str) -> Result<(), String> {
|
||||
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 != '.' {
|
||||
return Err(format!("Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]", c, field_name, id));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn collect_schemas(
|
||||
&mut self,
|
||||
tracking_path: Option<String>,
|
||||
to_insert: &mut Vec<(String, Schema)>,
|
||||
) -> Result<(), String> {
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
if let Some(id) = &self.obj.id {
|
||||
Self::validate_identifier(id, "$id")?;
|
||||
Self::validate_identifier(id, "$id", errors);
|
||||
to_insert.push((id.clone(), self.clone()));
|
||||
}
|
||||
if let Some(r#ref) = &self.obj.r#ref {
|
||||
Self::validate_identifier(r#ref, "$ref")?;
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
Self::validate_identifier(t, "type", errors);
|
||||
}
|
||||
}
|
||||
if let Some(family) = &self.obj.family {
|
||||
Self::validate_identifier(family, "$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.r#ref.is_some() && self.obj.properties.is_some() {
|
||||
if let Some(ref path) = tracking_path {
|
||||
to_insert.push((path.clone(), self.clone()));
|
||||
if self.obj.id.is_none() && self.obj.properties.is_some() {
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::schema::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)?;
|
||||
Ok(())
|
||||
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)>,
|
||||
) -> Result<(), String> {
|
||||
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)?;
|
||||
inner.collect_schemas(next_path, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
}
|
||||
}
|
||||
@ -453,101 +490,150 @@ impl Schema {
|
||||
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)?;
|
||||
inner.collect_schemas(next_path, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
}
|
||||
}
|
||||
|
||||
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| -> Result<(), String> {
|
||||
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)?;
|
||||
inner.collect_schemas(origin_path.clone(), to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Some(arr) = &mut self.obj.prefix_items { map_arr(arr)?; }
|
||||
if let Some(arr) = &mut self.obj.all_of { map_arr(arr)?; }
|
||||
if let Some(arr) = &mut self.obj.one_of { map_arr(arr)?; }
|
||||
if let Some(arr) = &mut self.obj.prefix_items {
|
||||
map_arr(arr);
|
||||
}
|
||||
|
||||
let mut map_opt = |opt: &mut Option<Arc<Schema>>, pass_path: bool| -> Result<(), String> {
|
||||
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)?;
|
||||
inner.collect_schemas(next, to_insert, errors);
|
||||
*v = Arc::new(inner);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
map_opt(&mut self.obj.additional_properties, false)?;
|
||||
|
||||
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)?;
|
||||
map_opt(&mut self.obj.if_, false)?;
|
||||
map_opt(&mut self.obj.then_, false)?;
|
||||
map_opt(&mut self.obj.else_, false)?;
|
||||
map_opt(&mut self.obj.items, true);
|
||||
|
||||
Ok(())
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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::schema::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);
|
||||
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);
|
||||
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) {
|
||||
target_schema.compile(db, visited);
|
||||
// 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();
|
||||
if let Some((relation, is_forward)) =
|
||||
resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity))
|
||||
{
|
||||
|
||||
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
|
||||
if let Some((relation, is_forward)) = resolve_relation(
|
||||
db,
|
||||
&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 {
|
||||
@ -566,15 +652,22 @@ impl Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
||||
/// the precise Foreign Key connecting a parent and child hierarchy path.
|
||||
pub(crate) fn resolve_relation<'a>(
|
||||
db: &'a crate::database::Database,
|
||||
parent_type: &str,
|
||||
child_type: &str,
|
||||
prop_name: &str,
|
||||
relative_keys: Option<&Vec<String>>,
|
||||
is_array: bool,
|
||||
schema_id: Option<&str>,
|
||||
path: &str,
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) -> Option<(&'a crate::database::relation::Relation, bool)> {
|
||||
// Enforce graph locality by ensuring we don't accidentally crawl to pure structural entity boundaries
|
||||
if parent_type == "entity" && child_type == "entity" {
|
||||
return None;
|
||||
return None;
|
||||
}
|
||||
|
||||
let p_def = db.types.get(parent_type)?;
|
||||
@ -583,11 +676,25 @@ pub(crate) fn resolve_relation<'a>(
|
||||
let mut matching_rels = Vec::new();
|
||||
let mut directions = Vec::new();
|
||||
|
||||
for rel in db.relations.values() {
|
||||
let is_forward = p_def.hierarchy.contains(&rel.source_type)
|
||||
&& c_def.hierarchy.contains(&rel.destination_type);
|
||||
let is_reverse = p_def.hierarchy.contains(&rel.destination_type)
|
||||
&& c_def.hierarchy.contains(&rel.source_type);
|
||||
// Scour the complete catalog for any Edge matching the inheritance scope of the two objects
|
||||
// This automatically binds polymorphic structures (e.g. recognizing a relationship targeting User
|
||||
// also natively binds instances specifically typed as Person).
|
||||
let mut all_rels: Vec<&crate::database::relation::Relation> = db.relations.values().collect();
|
||||
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);
|
||||
|
||||
// Structural Cardinality Filtration:
|
||||
// If the schema requires a collection (Array), it is mathematically impossible for a pure
|
||||
// Forward scalar edge (where the parent holds exactly one UUID pointer) to fulfill a One-to-Many request.
|
||||
// Thus, if it's an array, we fully reject pure Forward edges and only accept Reverse edges (or Junction edges).
|
||||
if is_array && is_forward && !is_reverse {
|
||||
is_forward = false;
|
||||
}
|
||||
|
||||
if is_forward {
|
||||
matching_rels.push(rel);
|
||||
@ -598,10 +705,28 @@ pub(crate) fn resolve_relation<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(sid) = schema_id {
|
||||
details.schema = Some(sid.to_string());
|
||||
}
|
||||
|
||||
errors.push(crate::drop::Error {
|
||||
code: "EDGE_MISSING".to_string(),
|
||||
message: format!(
|
||||
"No database relation exists between '{}' and '{}' for property '{}'",
|
||||
parent_type, child_type, prop_name
|
||||
),
|
||||
details,
|
||||
});
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ideal State: The objects only share a solitary structural relation, resolving ambiguity instantly.
|
||||
if matching_rels.len() == 1 {
|
||||
return Some((matching_rels[0], directions[0]));
|
||||
}
|
||||
@ -609,6 +734,8 @@ pub(crate) fn resolve_relation<'a>(
|
||||
let mut chosen_idx = 0;
|
||||
let mut resolved = false;
|
||||
|
||||
// Exact Prefix Disambiguation: Determine if the database specifically names this constraint
|
||||
// directly mapping to the JSON Schema property name (e.g., `fk_{child}_{property_name}`)
|
||||
for (i, rel) in matching_rels.iter().enumerate() {
|
||||
if let Some(prefix) = &rel.prefix {
|
||||
if prop_name.starts_with(prefix)
|
||||
@ -622,21 +749,84 @@ pub(crate) fn resolve_relation<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
// Complex Subgraph Resolution: The database contains multiple equally explicit foreign key constraints
|
||||
// linking these objects (such as pointing to `source` and `target` in Many-to-Many junction models).
|
||||
if !resolved && relative_keys.is_some() {
|
||||
// Twin Deduction Pass 1: We inspect the exact properties structurally defined inside the compiled payload
|
||||
// to observe which explicit relation arrow the child payload natively consumes.
|
||||
let keys = relative_keys.unwrap();
|
||||
let mut missing_prefix_ids = Vec::new();
|
||||
let mut consumed_rel_idx = None;
|
||||
for (i, rel) in matching_rels.iter().enumerate() {
|
||||
if let Some(prefix) = &rel.prefix {
|
||||
if !keys.contains(prefix) {
|
||||
missing_prefix_ids.push(i);
|
||||
if keys.contains(prefix) {
|
||||
consumed_rel_idx = Some(i);
|
||||
break; // Found the routing edge explicitly consumed by the schema payload
|
||||
}
|
||||
}
|
||||
}
|
||||
if missing_prefix_ids.len() == 1 {
|
||||
chosen_idx = missing_prefix_ids[0];
|
||||
|
||||
// Twin Deduction Pass 2: Knowing which arrow points outbound, we can mathematically deduce its twin
|
||||
// providing the reverse ownership on the same junction boundary must be the incoming Edge to the parent.
|
||||
if let Some(used_idx) = consumed_rel_idx {
|
||||
let used_rel = matching_rels[used_idx];
|
||||
let mut twin_ids = Vec::new();
|
||||
for (i, rel) in matching_rels.iter().enumerate() {
|
||||
if i != used_idx
|
||||
&& rel.source_type == used_rel.source_type
|
||||
&& rel.destination_type == used_rel.destination_type
|
||||
&& rel.prefix.is_some()
|
||||
{
|
||||
twin_ids.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if twin_ids.len() == 1 {
|
||||
chosen_idx = twin_ids[0];
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implicit Base Fallback: If no complex explicit paths resolve, but exactly one relation
|
||||
// sits entirely naked (without a constraint prefix), it must be the core structural parent ownership.
|
||||
if !resolved {
|
||||
let mut null_prefix_ids = Vec::new();
|
||||
for (i, rel) in matching_rels.iter().enumerate() {
|
||||
if rel.prefix.is_none() {
|
||||
null_prefix_ids.push(i);
|
||||
}
|
||||
}
|
||||
if null_prefix_ids.len() == 1 {
|
||||
chosen_idx = null_prefix_ids[0];
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we exhausted all mathematical deduction pathways and STILL cannot isolate a single edge,
|
||||
// we must abort rather than silently guessing. Returning None prevents arbitrary SQL generation
|
||||
// and forces a clean structural error for the architect.
|
||||
if !resolved {
|
||||
let mut details = crate::drop::ErrorDetails {
|
||||
path: path.to_string(),
|
||||
context: serde_json::to_value(&matching_rels).ok(),
|
||||
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(sid) = schema_id {
|
||||
details.schema = Some(sid.to_string());
|
||||
}
|
||||
|
||||
errors.push(crate::drop::Error {
|
||||
code: "AMBIGUOUS_TYPE_RELATIONS".to_string(),
|
||||
message: format!(
|
||||
"Ambiguous database relation between '{}' and '{}' for property '{}'",
|
||||
parent_type, child_type, prop_name
|
||||
),
|
||||
details,
|
||||
});
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((matching_rels[chosen_idx], directions[chosen_idx]))
|
||||
}
|
||||
|
||||
@ -675,13 +865,9 @@ impl<'de> Deserialize<'de> for Schema {
|
||||
&& obj.format.is_none()
|
||||
&& obj.enum_.is_none()
|
||||
&& obj.const_.is_none()
|
||||
&& obj.all_of.is_none()
|
||||
&& obj.cases.is_none()
|
||||
&& obj.one_of.is_none()
|
||||
&& obj.not.is_none()
|
||||
&& obj.if_.is_none()
|
||||
&& obj.then_.is_none()
|
||||
&& obj.else_.is_none()
|
||||
&& obj.r#ref.is_none()
|
||||
&& obj.family.is_none();
|
||||
|
||||
if is_empty && obj.extensible.is_none() {
|
||||
@ -697,11 +883,15 @@ impl<'de> Deserialize<'de> for Schema {
|
||||
|
||||
impl SchemaObject {
|
||||
pub fn identifier(&self) -> Option<String> {
|
||||
if let Some(lookup_key) = self.id.as_ref().or(self.r#ref.as_ref()) {
|
||||
Some(lookup_key.split('.').next_back().unwrap_or("").to_string())
|
||||
} else {
|
||||
None
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ pub struct Error {
|
||||
pub details: ErrorDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct ErrorDetails {
|
||||
pub path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
19
src/jspg.rs
19
src/jspg.rs
@ -12,18 +12,21 @@ pub struct Jspg {
|
||||
}
|
||||
|
||||
impl Jspg {
|
||||
pub fn new(database_val: &serde_json::Value) -> Result<Self, crate::drop::Drop> {
|
||||
let database_instance = Database::new(database_val)?;
|
||||
pub fn new(database_val: &serde_json::Value) -> (Self, crate::drop::Drop) {
|
||||
let (database_instance, drop) = Database::new(database_val);
|
||||
let database = Arc::new(database_instance);
|
||||
let validator = Validator::new(database.clone());
|
||||
let queryer = Queryer::new(database.clone());
|
||||
let merger = Merger::new(database.clone());
|
||||
|
||||
Ok(Self {
|
||||
database,
|
||||
validator,
|
||||
queryer,
|
||||
merger,
|
||||
})
|
||||
(
|
||||
Self {
|
||||
database,
|
||||
validator,
|
||||
queryer,
|
||||
merger,
|
||||
},
|
||||
drop,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
21
src/lib.rs
21
src/lib.rs
@ -42,21 +42,16 @@ fn jspg_failure() -> JsonB {
|
||||
|
||||
#[cfg_attr(not(test), pg_extern(strict))]
|
||||
pub fn jspg_setup(database: JsonB) -> JsonB {
|
||||
match crate::jspg::Jspg::new(&database.0) {
|
||||
Ok(new_jspg) => {
|
||||
let new_arc = Arc::new(new_jspg);
|
||||
let (new_jspg, drop) = crate::jspg::Jspg::new(&database.0);
|
||||
let new_arc = Arc::new(new_jspg);
|
||||
|
||||
// 3. ATOMIC SWAP
|
||||
{
|
||||
let mut lock = GLOBAL_JSPG.write().unwrap();
|
||||
*lock = Some(new_arc);
|
||||
}
|
||||
|
||||
let drop = crate::drop::Drop::success();
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
Err(drop) => JsonB(serde_json::to_value(drop).unwrap()),
|
||||
// 3. ATOMIC SWAP
|
||||
{
|
||||
let mut lock = GLOBAL_JSPG.write().unwrap();
|
||||
*lock = Some(new_arc);
|
||||
}
|
||||
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), pg_extern)]
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
|
||||
pub mod cache;
|
||||
|
||||
use crate::database::r#type::Type;
|
||||
use crate::database::Database;
|
||||
use crate::database::r#type::Type;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -25,22 +25,22 @@ impl Merger {
|
||||
let mut notifications_queue = Vec::new();
|
||||
|
||||
let target_schema = match self.db.schemas.get(schema_id) {
|
||||
Some(s) => Arc::new(s.clone()),
|
||||
None => {
|
||||
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
code: "MERGE_FAILED".to_string(),
|
||||
message: format!("Unknown schema_id: {}", schema_id),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
cause: None,
|
||||
context: Some(data),
|
||||
schema: None,
|
||||
},
|
||||
}]);
|
||||
}
|
||||
Some(s) => Arc::new(s.clone()),
|
||||
None => {
|
||||
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||
code: "MERGE_FAILED".to_string(),
|
||||
message: format!("Unknown schema_id: {}", schema_id),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
cause: None,
|
||||
context: Some(data),
|
||||
schema: None,
|
||||
},
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
let result = self.merge_internal(target_schema, data.clone(), &mut notifications_queue);
|
||||
let result = self.merge_internal(target_schema, data, &mut notifications_queue);
|
||||
|
||||
let val_resolved = match result {
|
||||
Ok(val) => val,
|
||||
@ -50,18 +50,24 @@ impl Merger {
|
||||
let mut final_cause = None;
|
||||
|
||||
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) {
|
||||
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) = (map.get("error"), map.get("code")) {
|
||||
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) =
|
||||
(map.get("error"), map.get("code"))
|
||||
{
|
||||
final_message = e_msg.clone();
|
||||
final_code = e_code.clone();
|
||||
let mut cause_parts = Vec::new();
|
||||
if let Some(Value::String(d)) = map.get("detail") {
|
||||
if !d.is_empty() { cause_parts.push(d.clone()); }
|
||||
if !d.is_empty() {
|
||||
cause_parts.push(d.clone());
|
||||
}
|
||||
}
|
||||
if let Some(Value::String(h)) = map.get("hint") {
|
||||
if !h.is_empty() { cause_parts.push(h.clone()); }
|
||||
if !h.is_empty() {
|
||||
cause_parts.push(h.clone());
|
||||
}
|
||||
}
|
||||
if !cause_parts.is_empty() {
|
||||
final_cause = Some(cause_parts.join("\n"));
|
||||
final_cause = Some(cause_parts.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,7 +78,7 @@ impl Merger {
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
cause: final_cause,
|
||||
context: Some(data),
|
||||
context: None,
|
||||
schema: None,
|
||||
},
|
||||
}]);
|
||||
@ -144,11 +150,11 @@ impl Merger {
|
||||
) -> Result<Value, String> {
|
||||
let mut item_schema = schema.clone();
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if t == "array" {
|
||||
if let Some(items_def) = &schema.obj.items {
|
||||
item_schema = items_def.clone();
|
||||
}
|
||||
if t == "array" {
|
||||
if let Some(items_def) = &schema.obj.items {
|
||||
item_schema = items_def.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut resolved_items = Vec::new();
|
||||
@ -178,8 +184,8 @@ impl Merger {
|
||||
};
|
||||
|
||||
let compiled_props = match schema.obj.compiled_properties.get() {
|
||||
Some(props) => props,
|
||||
None => return Err("Schema has no compiled properties for merging".to_string()),
|
||||
Some(props) => props,
|
||||
None => return Err("Schema has no compiled properties for merging".to_string()),
|
||||
};
|
||||
|
||||
let mut entity_fields = serde_json::Map::new();
|
||||
@ -189,37 +195,37 @@ 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());
|
||||
continue;
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(prop_schema) = compiled_props.get(&k) {
|
||||
let mut is_edge = false;
|
||||
if let Some(edges) = schema.obj.compiled_edges.get() {
|
||||
if edges.contains_key(&k) {
|
||||
is_edge = true;
|
||||
}
|
||||
}
|
||||
|
||||
if is_edge {
|
||||
let typeof_v = match &v {
|
||||
Value::Object(_) => "object",
|
||||
Value::Array(_) => "array",
|
||||
_ => "field", // Malformed edge data?
|
||||
};
|
||||
if typeof_v == "object" {
|
||||
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
} else if typeof_v == "array" {
|
||||
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
} else {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else {
|
||||
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else if type_def.fields.contains(&k) {
|
||||
let mut is_edge = false;
|
||||
if let Some(edges) = schema.obj.compiled_edges.get() {
|
||||
if edges.contains_key(&k) {
|
||||
is_edge = true;
|
||||
}
|
||||
}
|
||||
|
||||
if is_edge {
|
||||
let typeof_v = match &v {
|
||||
Value::Object(_) => "object",
|
||||
Value::Array(_) => "array",
|
||||
_ => "field", // Malformed edge data?
|
||||
};
|
||||
if typeof_v == "object" {
|
||||
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
} else if typeof_v == "array" {
|
||||
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||
} else {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else {
|
||||
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
} else if type_def.fields.contains(&k) {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,13 +234,15 @@ impl Merger {
|
||||
|
||||
let mut entity_change_kind = None;
|
||||
let mut entity_fetched = None;
|
||||
let mut entity_replaces = None;
|
||||
|
||||
if !type_def.relationship {
|
||||
let (fields, kind, fetched) =
|
||||
self.stage_entity(entity_fields.clone(), type_def, &user_id, ×tamp)?;
|
||||
let (fields, kind, fetched, replaces) =
|
||||
self.stage_entity(entity_fields, type_def, &user_id, ×tamp)?;
|
||||
entity_fields = fields;
|
||||
entity_change_kind = kind;
|
||||
entity_fetched = fetched;
|
||||
entity_replaces = replaces;
|
||||
}
|
||||
|
||||
let mut entity_response = serde_json::Map::new();
|
||||
@ -251,12 +259,10 @@ impl Merger {
|
||||
};
|
||||
|
||||
if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
|
||||
println!("Compiled Edges keys for relation {}: {:?}", relation_name, compiled_edges.keys().collect::<Vec<_>>());
|
||||
if let Some(edge) = compiled_edges.get(&relation_name) {
|
||||
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
|
||||
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
||||
let parent_is_source = edge.forward;
|
||||
|
||||
|
||||
if parent_is_source {
|
||||
if !relative.contains_key("organization_id") {
|
||||
if let Some(org_id) = entity_fields.get("organization_id") {
|
||||
@ -264,15 +270,16 @@ impl Merger {
|
||||
}
|
||||
}
|
||||
|
||||
let mut merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? {
|
||||
let mut merged_relative = match self.merge_internal(
|
||||
rel_schema.clone(),
|
||||
Value::Object(relative),
|
||||
notifications,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
merged_relative.insert(
|
||||
"type".to_string(),
|
||||
Value::String(relative_type_name),
|
||||
);
|
||||
merged_relative.insert("type".to_string(), Value::String(relative_type_name));
|
||||
|
||||
Self::apply_entity_relation(
|
||||
&mut entity_fields,
|
||||
@ -295,7 +302,11 @@ impl Merger {
|
||||
&entity_fields,
|
||||
);
|
||||
|
||||
let merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? {
|
||||
let merged_relative = match self.merge_internal(
|
||||
rel_schema.clone(),
|
||||
Value::Object(relative),
|
||||
notifications,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
};
|
||||
@ -308,11 +319,12 @@ impl Merger {
|
||||
}
|
||||
|
||||
if type_def.relationship {
|
||||
let (fields, kind, fetched) =
|
||||
self.stage_entity(entity_fields.clone(), type_def, &user_id, ×tamp)?;
|
||||
let (fields, kind, fetched, replaces) =
|
||||
self.stage_entity(entity_fields, type_def, &user_id, ×tamp)?;
|
||||
entity_fields = fields;
|
||||
entity_change_kind = kind;
|
||||
entity_fetched = fetched;
|
||||
entity_replaces = replaces;
|
||||
}
|
||||
|
||||
self.merge_entity_fields(
|
||||
@ -357,19 +369,24 @@ impl Merger {
|
||||
);
|
||||
|
||||
let mut item_schema = rel_schema.clone();
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &rel_schema.obj.type_ {
|
||||
if t == "array" {
|
||||
if let Some(items_def) = &rel_schema.obj.items {
|
||||
item_schema = items_def.clone();
|
||||
}
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
|
||||
&rel_schema.obj.type_
|
||||
{
|
||||
if t == "array" {
|
||||
if let Some(items_def) = &rel_schema.obj.items {
|
||||
item_schema = items_def.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let merged_relative =
|
||||
match self.merge_internal(item_schema, Value::Object(relative_item), notifications)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
};
|
||||
let merged_relative = match self.merge_internal(
|
||||
item_schema,
|
||||
Value::Object(relative_item),
|
||||
notifications,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
relative_responses.push(Value::Object(merged_relative));
|
||||
}
|
||||
@ -388,6 +405,7 @@ impl Merger {
|
||||
entity_change_kind.as_deref(),
|
||||
&user_id,
|
||||
×tamp,
|
||||
entity_replaces.as_deref(),
|
||||
)?;
|
||||
|
||||
if let Some(sql) = notify_sql {
|
||||
@ -419,6 +437,7 @@ impl Merger {
|
||||
serde_json::Map<String, Value>,
|
||||
Option<String>,
|
||||
Option<serde_json::Map<String, Value>>,
|
||||
Option<String>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
@ -428,8 +447,8 @@ impl Merger {
|
||||
// An anchor is STRICTLY a struct containing merely an `id` and `type`.
|
||||
// We aggressively bypass Database SPI `SELECT` fetches because there are no primitive
|
||||
// mutations to apply to the row. PostgreSQL inherently protects relationships via Foreign Keys downstream.
|
||||
let is_anchor = entity_fields.len() == 2
|
||||
&& entity_fields.contains_key("id")
|
||||
let is_anchor = entity_fields.len() == 2
|
||||
&& entity_fields.contains_key("id")
|
||||
&& entity_fields.contains_key("type");
|
||||
|
||||
let has_valid_id = entity_fields
|
||||
@ -438,11 +457,22 @@ impl Merger {
|
||||
.map_or(false, |s| !s.is_empty());
|
||||
|
||||
if is_anchor && has_valid_id {
|
||||
return Ok((entity_fields, None, None));
|
||||
return Ok((entity_fields, None, None, None));
|
||||
}
|
||||
|
||||
let entity_fetched = self.fetch_entity(&entity_fields, type_def)?;
|
||||
|
||||
let mut replaces_id = None;
|
||||
if let Some(ref fetched_row) = entity_fetched {
|
||||
let provided_id = entity_fields.get("id").and_then(|v| v.as_str());
|
||||
let fetched_id = fetched_row.get("id").and_then(|v| v.as_str());
|
||||
if let (Some(pid), Some(fid)) = (provided_id, fetched_id) {
|
||||
if !pid.is_empty() && pid != fid {
|
||||
replaces_id = Some(pid.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let system_keys = vec![
|
||||
"id".to_string(),
|
||||
"type".to_string(),
|
||||
@ -492,7 +522,7 @@ impl Merger {
|
||||
);
|
||||
|
||||
entity_fields = new_fields;
|
||||
} else if changes.is_empty() {
|
||||
} else if changes.is_empty() && replaces_id.is_none() {
|
||||
let mut new_fields = serde_json::Map::new();
|
||||
new_fields.insert(
|
||||
"id".to_string(),
|
||||
@ -508,6 +538,8 @@ impl Merger {
|
||||
.unwrap_or(false);
|
||||
entity_change_kind = if is_archived {
|
||||
Some("delete".to_string())
|
||||
} else if changes.is_empty() && replaces_id.is_some() {
|
||||
Some("replace".to_string())
|
||||
} else {
|
||||
Some("update".to_string())
|
||||
};
|
||||
@ -530,7 +562,12 @@ impl Merger {
|
||||
entity_fields = new_fields;
|
||||
}
|
||||
|
||||
Ok((entity_fields, entity_change_kind, entity_fetched))
|
||||
Ok((
|
||||
entity_fields,
|
||||
entity_change_kind,
|
||||
entity_fetched,
|
||||
replaces_id,
|
||||
))
|
||||
}
|
||||
|
||||
fn fetch_entity(
|
||||
@ -717,9 +754,7 @@ impl Merger {
|
||||
columns.join(", "),
|
||||
values.join(", ")
|
||||
);
|
||||
self
|
||||
.db
|
||||
.execute(&sql, None)?;
|
||||
self.db.execute(&sql, None)?;
|
||||
} else if change_kind == "update" || change_kind == "delete" {
|
||||
entity_pairs.remove("id");
|
||||
entity_pairs.remove("type");
|
||||
@ -751,9 +786,7 @@ impl Merger {
|
||||
set_clauses.join(", "),
|
||||
Self::quote_literal(&Value::String(id_str.to_string()))
|
||||
);
|
||||
self
|
||||
.db
|
||||
.execute(&sql, None)?;
|
||||
self.db.execute(&sql, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -768,6 +801,7 @@ impl Merger {
|
||||
entity_change_kind: Option<&str>,
|
||||
user_id: &str,
|
||||
timestamp: &str,
|
||||
replaces_id: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let change_kind = match entity_change_kind {
|
||||
Some(k) => k,
|
||||
@ -779,9 +813,9 @@ impl Merger {
|
||||
|
||||
let mut old_vals = serde_json::Map::new();
|
||||
let mut new_vals = serde_json::Map::new();
|
||||
let is_update = change_kind == "update" || change_kind == "delete";
|
||||
let exists = change_kind == "update" || change_kind == "delete" || change_kind == "replace";
|
||||
|
||||
if !is_update {
|
||||
if !exists {
|
||||
let system_keys = vec![
|
||||
"id".to_string(),
|
||||
"created_by".to_string(),
|
||||
@ -818,7 +852,7 @@ impl Merger {
|
||||
}
|
||||
|
||||
let mut complete = entity_fields.clone();
|
||||
if is_update {
|
||||
if exists {
|
||||
if let Some(fetched) = entity_fetched {
|
||||
let mut temp = fetched.clone();
|
||||
for (k, v) in entity_fields {
|
||||
@ -838,13 +872,17 @@ impl Merger {
|
||||
let mut notification = serde_json::Map::new();
|
||||
notification.insert("complete".to_string(), Value::Object(complete));
|
||||
notification.insert("new".to_string(), new_val_obj.clone());
|
||||
|
||||
|
||||
if old_val_obj != Value::Null {
|
||||
notification.insert("old".to_string(), old_val_obj.clone());
|
||||
notification.insert("old".to_string(), old_val_obj.clone());
|
||||
}
|
||||
|
||||
if let Some(rep) = replaces_id {
|
||||
notification.insert("replaces".to_string(), Value::String(rep.to_string()));
|
||||
}
|
||||
|
||||
let mut notify_sql = None;
|
||||
if type_obj.historical {
|
||||
if type_obj.historical && change_kind != "replace" {
|
||||
let change_sql = format!(
|
||||
"INSERT INTO agreego.change (\"old\", \"new\", entity_id, id, kind, modified_at, modified_by) VALUES ({}, {}, {}, {}, {}, {}, {})",
|
||||
Self::quote_literal(&old_val_obj),
|
||||
|
||||
@ -47,7 +47,7 @@ impl<'a> Compiler<'a> {
|
||||
};
|
||||
|
||||
let (sql, _) = compiler.compile_node(node)?;
|
||||
Ok(sql)
|
||||
Ok(format!("(SELECT jsonb_strip_nulls({}))", sql))
|
||||
}
|
||||
|
||||
/// Recursively walks the schema AST emitting native PostgreSQL jsonb mapping
|
||||
@ -63,11 +63,14 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
// 1. Array of DB Entities (`$ref` or `$family` pointing to a table limit)
|
||||
// 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);
|
||||
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);
|
||||
@ -89,7 +92,10 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
// 3. Fallback for root execution of standalone non-entity arrays
|
||||
Err("Cannot compile a root array without a valid entity reference or table mapped via `items`.".to_string())
|
||||
Err(
|
||||
"Cannot compile a root array without a valid entity reference or table mapped via `items`."
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
@ -106,45 +112,42 @@ impl<'a> Compiler<'a> {
|
||||
return self.compile_entity(type_def, node.clone(), false);
|
||||
}
|
||||
|
||||
// Handle Direct Refs
|
||||
if let Some(ref_id) = &node.schema.obj.r#ref {
|
||||
// If it's just an ad-hoc struct ref, we should resolve it
|
||||
if let Some(target_schema) = self.db.schemas.get(ref_id) {
|
||||
let mut ref_node = node.clone();
|
||||
ref_node.schema = std::sync::Arc::new(target_schema.clone());
|
||||
return self.compile_node(ref_node);
|
||||
// Handle Direct Refs via type pointer
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
|
||||
if !crate::database::schema::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) {
|
||||
let mut ref_node = node.clone();
|
||||
ref_node.schema = std::sync::Arc::new(target_schema.clone());
|
||||
return self.compile_node(ref_node);
|
||||
}
|
||||
return Err(format!("Unresolved schema type pointer: {}", t));
|
||||
}
|
||||
return Err(format!("Unresolved $ref: {}", ref_id));
|
||||
}
|
||||
// Handle $family Polymorphism fallbacks for relations
|
||||
if let Some(family_target) = &node.schema.obj.family {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target)
|
||||
.to_string();
|
||||
|
||||
if let Some(type_def) = self.db.types.get(&base_type_name) {
|
||||
if type_def.variations.len() == 1 {
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.r#ref = Some(family_target.clone());
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
return self.compile_node(bypass_node);
|
||||
}
|
||||
|
||||
let mut sorted_variations: Vec<String> = type_def.variations.iter().cloned().collect();
|
||||
sorted_variations.sort();
|
||||
|
||||
let mut family_schemas = Vec::new();
|
||||
for variation in &sorted_variations {
|
||||
let mut ref_schema = crate::database::schema::Schema::default();
|
||||
ref_schema.obj.r#ref = Some(variation.clone());
|
||||
family_schemas.push(std::sync::Arc::new(ref_schema));
|
||||
}
|
||||
|
||||
return self.compile_one_of(&family_schemas, node);
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
all_targets.extend(descendants.clone());
|
||||
}
|
||||
|
||||
if all_targets.len() == 1 {
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(all_targets[0].clone()));
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
return self.compile_node(bypass_node);
|
||||
}
|
||||
|
||||
all_targets.sort();
|
||||
let mut family_schemas = Vec::new();
|
||||
for variation in &all_targets {
|
||||
let mut ref_schema = crate::database::schema::Schema::default();
|
||||
ref_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(variation.clone()));
|
||||
family_schemas.push(std::sync::Arc::new(ref_schema));
|
||||
}
|
||||
|
||||
return self.compile_one_of(&family_schemas, node);
|
||||
}
|
||||
|
||||
// Handle oneOf Polymorphism fallbacks for relations
|
||||
@ -224,49 +227,62 @@ impl<'a> Compiler<'a> {
|
||||
let mut select_args = Vec::new();
|
||||
|
||||
if let Some(family_target) = node.schema.obj.family.as_ref() {
|
||||
let base_type_name = family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target)
|
||||
.to_string();
|
||||
let family_prefix = family_target.rfind('.').map(|idx| &family_target[..idx]);
|
||||
|
||||
if let Some(fam_type_def) = self.db.types.get(&base_type_name) {
|
||||
if fam_type_def.variations.len() == 1 {
|
||||
let mut bypass_schema = crate::database::schema::Schema::default();
|
||||
bypass_schema.obj.r#ref = Some(family_target.clone());
|
||||
bypass_schema.compile(self.db, &mut std::collections::HashSet::new());
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
all_targets.extend(descendants.clone());
|
||||
}
|
||||
|
||||
// Filter targets to EXACTLY match the family_target prefix
|
||||
let mut final_targets = Vec::new();
|
||||
for target in all_targets {
|
||||
let target_prefix = target.rfind('.').map(|idx| &target[..idx]);
|
||||
if target_prefix == family_prefix {
|
||||
final_targets.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
final_targets.sort();
|
||||
final_targets.dedup();
|
||||
|
||||
if final_targets.len() == 1 {
|
||||
let variation = &final_targets[0];
|
||||
if let Some(target_schema) = self.db.schemas.get(variation) {
|
||||
let mut bypass_node = node.clone();
|
||||
bypass_node.schema = std::sync::Arc::new(bypass_schema);
|
||||
bypass_node.schema = std::sync::Arc::new(target_schema.clone());
|
||||
|
||||
let mut bypassed_args = self.compile_select_clause(r#type, table_aliases, bypass_node)?;
|
||||
select_args.append(&mut bypassed_args);
|
||||
} else {
|
||||
let mut family_schemas = Vec::new();
|
||||
let mut sorted_fam_variations: Vec<String> =
|
||||
fam_type_def.variations.iter().cloned().collect();
|
||||
sorted_fam_variations.sort();
|
||||
|
||||
for variation in &sorted_fam_variations {
|
||||
let mut ref_schema = crate::database::schema::Schema::default();
|
||||
ref_schema.obj.r#ref = Some(variation.clone());
|
||||
ref_schema.compile(self.db, &mut std::collections::HashSet::new());
|
||||
family_schemas.push(std::sync::Arc::new(ref_schema));
|
||||
}
|
||||
|
||||
let base_alias = table_aliases
|
||||
.get(&r#type.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| node.parent_alias.to_string());
|
||||
select_args.push(format!("'id', {}.id", base_alias));
|
||||
let mut case_node = node.clone();
|
||||
case_node.parent_alias = base_alias.clone();
|
||||
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
|
||||
case_node.parent_type_aliases = Some(arc_aliases);
|
||||
|
||||
let (case_sql, _) = self.compile_one_of(&family_schemas, case_node)?;
|
||||
select_args.push(format!("'type', {}", case_sql));
|
||||
return Err(format!("Could not find schema for variation {}", variation));
|
||||
}
|
||||
} else {
|
||||
let mut family_schemas = Vec::new();
|
||||
|
||||
for variation in &final_targets {
|
||||
if let Some(target_schema) = self.db.schemas.get(variation) {
|
||||
family_schemas.push(std::sync::Arc::new(target_schema.clone()));
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Could not find schema metadata for variation {}",
|
||||
variation
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let base_alias = table_aliases
|
||||
.get(&r#type.name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| node.parent_alias.to_string());
|
||||
select_args.push(format!("'id', {}.id", base_alias));
|
||||
let mut case_node = node.clone();
|
||||
case_node.parent_alias = base_alias.clone();
|
||||
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
|
||||
case_node.parent_type_aliases = Some(arc_aliases);
|
||||
|
||||
let (case_sql, _) = self.compile_one_of(&family_schemas, case_node)?;
|
||||
select_args.push(format!("'type', {}", case_sql));
|
||||
}
|
||||
} else if let Some(one_of) = &node.schema.obj.one_of {
|
||||
let base_alias = table_aliases
|
||||
@ -328,10 +344,7 @@ impl<'a> Compiler<'a> {
|
||||
};
|
||||
|
||||
for option_schema in schemas {
|
||||
if let Some(ref_id) = &option_schema.obj.r#ref {
|
||||
// Find the physical type this ref maps to
|
||||
let base_type_name = ref_id.split('.').next_back().unwrap_or("").to_string();
|
||||
|
||||
if let Some(base_type_name) = option_schema.obj.identifier() {
|
||||
// Generate the nested SQL for this specific target type
|
||||
let mut child_node = node.clone();
|
||||
child_node.schema = std::sync::Arc::clone(option_schema);
|
||||
@ -405,7 +418,14 @@ impl<'a> Compiler<'a> {
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let is_primitive = prop_schema.obj.r#ref.is_none()
|
||||
let is_custom_object_pointer = match &prop_schema.obj.type_ {
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => {
|
||||
!crate::database::schema::is_primitive_type(s)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let is_primitive = !is_custom_object_pointer
|
||||
&& !is_object_or_array
|
||||
&& prop_schema.obj.family.is_none()
|
||||
&& prop_schema.obj.one_of.is_none();
|
||||
@ -452,7 +472,6 @@ impl<'a> Compiler<'a> {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
let (val_sql, val_type) = self.compile_node(child_node)?;
|
||||
|
||||
if val_type != "abort" {
|
||||
@ -515,7 +534,13 @@ impl<'a> Compiler<'a> {
|
||||
// Determine if the property schema resolves to a physical Database Entity
|
||||
let mut bound_type_name = None;
|
||||
if let Some(family_target) = prop_schema.obj.family.as_ref() {
|
||||
bound_type_name = Some(family_target.split('.').next_back().unwrap_or(family_target).to_string());
|
||||
bound_type_name = Some(
|
||||
family_target
|
||||
.split('.')
|
||||
.next_back()
|
||||
.unwrap_or(family_target)
|
||||
.to_string(),
|
||||
);
|
||||
} else if let Some(lookup_key) = prop_schema.obj.identifier() {
|
||||
bound_type_name = Some(lookup_key);
|
||||
}
|
||||
@ -536,7 +561,10 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
if let Some(col) = poly_col {
|
||||
if let Some(alias) = type_aliases.get(table_to_alias).or_else(|| type_aliases.get(&node.parent_alias)) {
|
||||
if let Some(alias) = type_aliases
|
||||
.get(table_to_alias)
|
||||
.or_else(|| type_aliases.get(&node.parent_alias))
|
||||
{
|
||||
where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name));
|
||||
}
|
||||
}
|
||||
@ -710,8 +738,6 @@ impl<'a> Compiler<'a> {
|
||||
) -> Result<(), String> {
|
||||
if let Some(prop_ref) = &node.property_name {
|
||||
let prop = prop_ref.as_str();
|
||||
println!("DEBUG: Eval prop: {}", prop);
|
||||
|
||||
let mut parent_relation_alias = node.parent_alias.clone();
|
||||
let mut child_relation_alias = base_alias.to_string();
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ impl Queryer {
|
||||
};
|
||||
|
||||
// 3. Execute via Database Executor
|
||||
self.execute_sql(schema_id, &sql, &args)
|
||||
self.execute_sql(schema_id, &sql, args)
|
||||
}
|
||||
|
||||
fn extract_filters(
|
||||
@ -151,7 +151,7 @@ impl Queryer {
|
||||
&self,
|
||||
schema_id: &str,
|
||||
sql: &str,
|
||||
args: &[serde_json::Value],
|
||||
args: Vec<serde_json::Value>,
|
||||
) -> crate::drop::Drop {
|
||||
match self.db.query(sql, Some(args)) {
|
||||
Ok(serde_json::Value::Array(table)) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -42,19 +42,21 @@ fn test_library_api() {
|
||||
"types": [
|
||||
{
|
||||
"name": "source_schema",
|
||||
"variations": ["source_schema"],
|
||||
"hierarchy": ["source_schema", "entity"],
|
||||
"schemas": [{
|
||||
"$id": "source_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"target": { "$ref": "target_schema" }
|
||||
"target": { "type": "target_schema" }
|
||||
},
|
||||
"required": ["name"]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "target_schema",
|
||||
"variations": ["target_schema"],
|
||||
"hierarchy": ["target_schema", "entity"],
|
||||
"schemas": [{
|
||||
"$id": "target_schema",
|
||||
@ -89,7 +91,7 @@ fn test_library_api() {
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"target": {
|
||||
"$ref": "target_schema",
|
||||
"type": "target_schema",
|
||||
"compiledProperties": ["value"]
|
||||
}
|
||||
},
|
||||
@ -115,7 +117,7 @@ fn test_library_api() {
|
||||
);
|
||||
|
||||
// 4. Validate Happy Path
|
||||
let happy_drop = jspg_validate("source_schema", JsonB(json!({"name": "Neo"})));
|
||||
let happy_drop = jspg_validate("source_schema", JsonB(json!({"type": "source_schema", "name": "Neo"})));
|
||||
assert_eq!(
|
||||
happy_drop.0,
|
||||
json!({
|
||||
@ -125,7 +127,7 @@ fn test_library_api() {
|
||||
);
|
||||
|
||||
// 5. Validate Unhappy Path
|
||||
let unhappy_drop = jspg_validate("source_schema", JsonB(json!({"wrong": "data"})));
|
||||
let unhappy_drop = jspg_validate("source_schema", JsonB(json!({"type": "source_schema", "wrong": "data"})));
|
||||
assert_eq!(
|
||||
unhappy_drop.0,
|
||||
json!({
|
||||
|
||||
@ -14,7 +14,7 @@ where
|
||||
}
|
||||
|
||||
// Type alias for easier reading
|
||||
type CompiledSuite = Arc<Vec<(Suite, Arc<crate::database::Database>)>>;
|
||||
type CompiledSuite = Arc<Vec<(Suite, Arc<Result<Arc<crate::database::Database>, crate::drop::Drop>>)>>;
|
||||
|
||||
// Global cache mapping filename -> Vector of (Parsed JSON suite, Compiled Database)
|
||||
static CACHE: OnceLock<RwLock<HashMap<String, CompiledSuite>>> = OnceLock::new();
|
||||
@ -42,20 +42,13 @@ fn get_cached_file(path: &str) -> CompiledSuite {
|
||||
|
||||
let mut compiled_suites = Vec::new();
|
||||
for suite in suites {
|
||||
let db_result = crate::database::Database::new(&suite.database);
|
||||
if let Err(drop) = db_result {
|
||||
let error_messages: Vec<String> = drop
|
||||
.errors
|
||||
.into_iter()
|
||||
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path, e.message))
|
||||
.collect();
|
||||
panic!(
|
||||
"System Setup Compilation failed for {}:\n{}",
|
||||
path,
|
||||
error_messages.join("\n")
|
||||
);
|
||||
}
|
||||
compiled_suites.push((suite, Arc::new(db_result.unwrap())));
|
||||
let (db, drop) = crate::database::Database::new(&suite.database);
|
||||
let compiled_db = if drop.errors.is_empty() {
|
||||
Ok(Arc::new(db))
|
||||
} else {
|
||||
Err(drop)
|
||||
};
|
||||
compiled_suites.push((suite, Arc::new(compiled_db)));
|
||||
}
|
||||
|
||||
let new_data = Arc::new(compiled_suites);
|
||||
@ -85,11 +78,36 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
|
||||
let test = &group.tests[case_idx];
|
||||
let mut failures = Vec::<String>::new();
|
||||
|
||||
// For validate/merge/query, if setup failed we must structurally fail this test
|
||||
let db_unwrapped = if test.action.as_str() != "compile" {
|
||||
match &**db {
|
||||
Ok(valid_db) => Some(valid_db.clone()),
|
||||
Err(drop) => {
|
||||
let error_messages: Vec<String> = drop
|
||||
.errors
|
||||
.iter()
|
||||
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path, e.message))
|
||||
.collect();
|
||||
failures.push(format!(
|
||||
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}",
|
||||
group.description, test.action, test.description, error_messages.join("\n")
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !failures.is_empty() {
|
||||
return Err(failures.join("\n"));
|
||||
}
|
||||
|
||||
// 4. Run Tests
|
||||
|
||||
match test.action.as_str() {
|
||||
"compile" => {
|
||||
let result = test.run_compile(db.clone());
|
||||
let result = test.run_compile(db);
|
||||
if let Err(e) = result {
|
||||
println!("TEST COMPILE ERROR FOR '{}': {}", test.description, e);
|
||||
failures.push(format!(
|
||||
@ -99,7 +117,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
|
||||
}
|
||||
}
|
||||
"validate" => {
|
||||
let result = test.run_validate(db.clone());
|
||||
let result = test.run_validate(db_unwrapped.unwrap());
|
||||
if let Err(e) = result {
|
||||
println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e);
|
||||
failures.push(format!(
|
||||
@ -109,7 +127,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
|
||||
}
|
||||
}
|
||||
"merge" => {
|
||||
let result = test.run_merge(db.clone());
|
||||
let result = test.run_merge(db_unwrapped.unwrap());
|
||||
if let Err(e) = result {
|
||||
println!("TEST MERGE ERROR FOR '{}': {}", test.description, e);
|
||||
failures.push(format!(
|
||||
@ -119,7 +137,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
|
||||
}
|
||||
}
|
||||
"query" => {
|
||||
let result = test.run_query(db.clone());
|
||||
let result = test.run_query(db_unwrapped.unwrap());
|
||||
if let Err(e) = result {
|
||||
println!("TEST QUERY ERROR FOR '{}': {}", test.description, e);
|
||||
failures.push(format!(
|
||||
|
||||
@ -35,21 +35,21 @@ fn default_action() -> String {
|
||||
}
|
||||
|
||||
impl Case {
|
||||
pub fn run_compile(&self, _db: Arc<Database>) -> Result<(), String> {
|
||||
let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false);
|
||||
pub fn run_compile(
|
||||
&self,
|
||||
db_res: &Result<Arc<Database>, crate::drop::Drop>,
|
||||
) -> Result<(), String> {
|
||||
let expect = match &self.expect {
|
||||
Some(e) => e,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// We assume db has already been setup and compiled successfully by runner.rs's `jspg_setup`
|
||||
// We just need to check if there are compilation errors vs expected success
|
||||
let got_success = true; // Setup ensures success unless setup fails, which runner handles
|
||||
let result = match db_res {
|
||||
Ok(_) => crate::drop::Drop::success(),
|
||||
Err(d) => d.clone(),
|
||||
};
|
||||
|
||||
if expected_success != got_success {
|
||||
return Err(format!(
|
||||
"Expected success: {}, Got: {}",
|
||||
expected_success, got_success
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
expect.assert_drop(&result)
|
||||
}
|
||||
|
||||
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> {
|
||||
@ -57,8 +57,6 @@ impl Case {
|
||||
|
||||
let validator = Validator::new(db);
|
||||
|
||||
let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false);
|
||||
|
||||
let schema_id = &self.schema_id;
|
||||
if !validator.db.schemas.contains_key(schema_id) {
|
||||
return Err(format!(
|
||||
@ -70,19 +68,8 @@ impl Case {
|
||||
let test_data = self.data.clone().unwrap_or(Value::Null);
|
||||
let result = validator.validate(schema_id, &test_data);
|
||||
|
||||
let got_valid = result.errors.is_empty();
|
||||
|
||||
if got_valid != expected_success {
|
||||
let error_msg = if result.errors.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
format!("{:?}", result.errors)
|
||||
};
|
||||
|
||||
return Err(format!(
|
||||
"Expected: {}, Got: {}. Errors: {}",
|
||||
expected_success, got_valid, error_msg
|
||||
));
|
||||
if let Some(expect) = &self.expect {
|
||||
expect.assert_drop(&result)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -101,24 +88,16 @@ impl Case {
|
||||
let test_data = self.data.clone().unwrap_or(Value::Null);
|
||||
let result = merger.merge(&self.schema_id, test_data);
|
||||
|
||||
let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false);
|
||||
let got_success = result.errors.is_empty();
|
||||
|
||||
let error_msg = if result.errors.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
format!("{:?}", result.errors)
|
||||
};
|
||||
|
||||
let return_val = if expected_success != got_success {
|
||||
Err(format!(
|
||||
"Merge Expected: {}, Got: {}. Errors: {}",
|
||||
expected_success, got_success, error_msg
|
||||
))
|
||||
} else if let Some(expect) = &self.expect {
|
||||
let queries = db.executor.get_queries();
|
||||
expect.assert_pattern(&queries)?;
|
||||
expect.assert_sql(&queries)
|
||||
let return_val = if let Some(expect) = &self.expect {
|
||||
if let Err(e) = expect.assert_drop(&result) {
|
||||
Err(format!("Merge {}", e))
|
||||
} else if result.errors.is_empty() {
|
||||
// Only assert SQL if merge succeeded
|
||||
let queries = db.executor.get_queries();
|
||||
expect.assert_pattern(&queries).and_then(|_| expect.assert_sql(&queries))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
@ -139,24 +118,15 @@ impl Case {
|
||||
|
||||
let result = queryer.query(&self.schema_id, self.filters.as_ref());
|
||||
|
||||
let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false);
|
||||
let got_success = result.errors.is_empty();
|
||||
|
||||
let error_msg = if result.errors.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
format!("{:?}", result.errors)
|
||||
};
|
||||
|
||||
let return_val = if expected_success != got_success {
|
||||
Err(format!(
|
||||
"Query Expected: {}, Got: {}. Errors: {}",
|
||||
expected_success, got_success, error_msg
|
||||
))
|
||||
} else if let Some(expect) = &self.expect {
|
||||
let queries = db.executor.get_queries();
|
||||
expect.assert_pattern(&queries)?;
|
||||
expect.assert_sql(&queries)
|
||||
let return_val = if let Some(expect) = &self.expect {
|
||||
if let Err(e) = expect.assert_drop(&result) {
|
||||
Err(format!("Query {}", e))
|
||||
} else if result.errors.is_empty() {
|
||||
let queries = db.executor.get_queries();
|
||||
expect.assert_pattern(&queries).and_then(|_| expect.assert_sql(&queries))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
88
src/tests/types/expect/drop.rs
Normal file
88
src/tests/types/expect/drop.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use super::Expect;
|
||||
|
||||
impl Expect {
|
||||
pub fn assert_drop(&self, drop: &crate::drop::Drop) -> Result<(), String> {
|
||||
let got_success = drop.errors.is_empty();
|
||||
|
||||
if self.success != got_success {
|
||||
let mut err_msg = format!("Expected success: {}, Got: {}.", self.success, got_success);
|
||||
if !drop.errors.is_empty() {
|
||||
err_msg.push_str(&format!(" Actual Errors: {:?}", drop.errors));
|
||||
}
|
||||
return Err(err_msg);
|
||||
}
|
||||
|
||||
if !self.success {
|
||||
if let Some(expected_errors) = &self.errors {
|
||||
let actual_values: Vec<serde_json::Value> = drop.errors
|
||||
.iter()
|
||||
.map(|e| serde_json::to_value(e).unwrap())
|
||||
.collect();
|
||||
|
||||
if expected_errors.len() != actual_values.len() {
|
||||
return Err(format!(
|
||||
"Expected {} errors, but got {}.\nExpected subset: {:?}\nActual full errors: {:?}",
|
||||
expected_errors.len(),
|
||||
actual_values.len(),
|
||||
expected_errors,
|
||||
drop.errors
|
||||
));
|
||||
}
|
||||
|
||||
for (i, expected_val) in expected_errors.iter().enumerate() {
|
||||
let mut matched = false;
|
||||
|
||||
for actual_val in &actual_values {
|
||||
if subset_match(expected_val, actual_val) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return Err(format!(
|
||||
"Expected error {} was not found in actual errors.\nExpected subset: {}\nActual full errors: {:?}",
|
||||
i,
|
||||
serde_json::to_string_pretty(expected_val).unwrap(),
|
||||
drop.errors,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if `expected` is a structural subset of `actual`
|
||||
fn subset_match(expected: &serde_json::Value, actual: &serde_json::Value) -> bool {
|
||||
match (expected, actual) {
|
||||
(serde_json::Value::Object(exp_map), serde_json::Value::Object(act_map)) => {
|
||||
for (k, v) in exp_map {
|
||||
if let Some(act_v) = act_map.get(k) {
|
||||
if !subset_match(v, act_v) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
(serde_json::Value::Array(exp_arr), serde_json::Value::Array(act_arr)) => {
|
||||
// Basic check: array sizes and elements must match exactly in order
|
||||
if exp_arr.len() != act_arr.len() {
|
||||
return false;
|
||||
}
|
||||
for (e, a) in exp_arr.iter().zip(act_arr.iter()) {
|
||||
if !subset_match(e, a) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
// For primitives, exact match
|
||||
(e, a) => e == a,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod pattern;
|
||||
pub mod sql;
|
||||
pub mod drop;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
45
src/validator/rules/cases.rs
Normal file
45
src/validator/rules/cases.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_cases(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(cases) = &self.schema.cases {
|
||||
for case in cases {
|
||||
if let Some(ref when_schema) = case.when {
|
||||
let derived_when = self.derive_for_schema(when_schema, true);
|
||||
let when_res = derived_when.validate()?;
|
||||
|
||||
// Evaluates all cases independently.
|
||||
if when_res.is_valid() {
|
||||
result
|
||||
.evaluated_keys
|
||||
.extend(when_res.evaluated_keys.clone());
|
||||
result
|
||||
.evaluated_indices
|
||||
.extend(when_res.evaluated_indices.clone());
|
||||
|
||||
if let Some(ref then_schema) = case.then {
|
||||
let derived_then = self.derive_for_schema(then_schema, true);
|
||||
result.merge(derived_then.validate()?);
|
||||
}
|
||||
} else {
|
||||
if let Some(ref else_schema) = case.else_ {
|
||||
let derived_else = self.derive_for_schema(else_schema, true);
|
||||
result.merge(derived_else.validate()?);
|
||||
}
|
||||
}
|
||||
} else if let Some(ref else_schema) = case.else_ {
|
||||
// A rule with a missing `when` fires the `else` indiscriminately
|
||||
let derived_else = self.derive_for_schema(else_schema, true);
|
||||
result.merge(derived_else.validate()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_combinators(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(ref all_of) = self.schema.all_of {
|
||||
for sub in all_of {
|
||||
let derived = self.derive_for_schema(sub, true);
|
||||
let res = derived.validate()?;
|
||||
result.merge(res);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref one_of) = self.schema.one_of {
|
||||
let mut passed_candidates: Vec<(Option<String>, usize, ValidationResult)> = Vec::new();
|
||||
|
||||
for sub in one_of {
|
||||
let derived = self.derive_for_schema(sub, true);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
let child_id = sub.id.clone();
|
||||
let depth = child_id
|
||||
.as_ref()
|
||||
.and_then(|id| self.db.depths.get(id).copied())
|
||||
.unwrap_or(0);
|
||||
passed_candidates.push((child_id, depth, sub_res));
|
||||
}
|
||||
}
|
||||
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().2);
|
||||
} else if passed_candidates.is_empty() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NO_ONEOF_MATCH".to_string(),
|
||||
message: "Matches none of oneOf schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
// Apply depth heuristic tie-breaker
|
||||
let mut best_depth: Option<usize> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut best_res = None;
|
||||
|
||||
for (_, depth, res) in passed_candidates.into_iter() {
|
||||
if let Some(current_best) = best_depth {
|
||||
if depth > current_best {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
ambiguous = false;
|
||||
} else if depth == current_best {
|
||||
ambiguous = true;
|
||||
}
|
||||
} else {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
}
|
||||
}
|
||||
|
||||
if !ambiguous {
|
||||
if let Some(res) = best_res {
|
||||
result.merge(res);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_ONEOF_MATCH".to_string(),
|
||||
message: "Matches multiple oneOf schemas without a clear depth winner".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref not_schema) = self.schema.not {
|
||||
let derived = self.derive_for_schema(not_schema, true);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NOT_VIOLATED".to_string(),
|
||||
message: "Matched 'not' schema".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@ -3,30 +3,14 @@ use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_conditionals(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(ref if_schema) = self.schema.if_ {
|
||||
let derived_if = self.derive_for_schema(if_schema, true);
|
||||
let if_res = derived_if.validate()?;
|
||||
|
||||
result.evaluated_keys.extend(if_res.evaluated_keys.clone());
|
||||
result
|
||||
.evaluated_indices
|
||||
.extend(if_res.evaluated_indices.clone());
|
||||
|
||||
if if_res.is_valid() {
|
||||
if let Some(ref then_schema) = self.schema.then_ {
|
||||
let derived_then = self.derive_for_schema(then_schema, true);
|
||||
result.merge(derived_then.validate()?);
|
||||
}
|
||||
} else if let Some(ref else_schema) = self.schema.else_ {
|
||||
let derived_else = self.derive_for_schema(else_schema, true);
|
||||
result.merge(derived_else.validate()?);
|
||||
pub(crate) fn validate_extensible(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
|
||||
if self.extensible {
|
||||
if let Some(obj) = self.instance.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = self.instance.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -40,6 +24,9 @@ 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(),
|
||||
@ -3,10 +3,11 @@ use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
pub mod array;
|
||||
pub mod combinators;
|
||||
pub mod conditionals;
|
||||
pub mod cases;
|
||||
pub mod core;
|
||||
pub mod extensible;
|
||||
pub mod format;
|
||||
pub mod not;
|
||||
pub mod numeric;
|
||||
pub mod object;
|
||||
pub mod polymorphism;
|
||||
@ -27,7 +28,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !self.validate_family(&mut result)? {
|
||||
return Ok(result);
|
||||
}
|
||||
if !self.validate_refs(&mut result)? {
|
||||
if !self.validate_type_inheritance(&mut result)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@ -42,8 +43,11 @@ impl<'a> ValidationContext<'a> {
|
||||
self.validate_array(&mut result)?;
|
||||
|
||||
// Multipliers & Conditionals
|
||||
self.validate_combinators(&mut result)?;
|
||||
self.validate_conditionals(&mut result)?;
|
||||
if !self.validate_one_of(&mut result)? {
|
||||
return Ok(result);
|
||||
}
|
||||
self.validate_not(&mut result)?;
|
||||
self.validate_cases(&mut result)?;
|
||||
|
||||
// State Tracking
|
||||
self.validate_extensible(&mut result)?;
|
||||
@ -77,15 +81,4 @@ impl<'a> ValidationContext<'a> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_extensible(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
|
||||
if self.extensible {
|
||||
if let Some(obj) = self.instance.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
} else if let Some(arr) = self.instance.as_array() {
|
||||
result.evaluated_indices.extend(0..arr.len());
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
24
src/validator/rules/not.rs
Normal file
24
src/validator/rules/not.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_not(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
if let Some(ref not_schema) = self.schema.not {
|
||||
let derived = self.derive_for_schema(not_schema, true);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NOT_VIOLATED".to_string(),
|
||||
message: "Matched 'not' schema".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@ -15,32 +15,68 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(obj) = current.as_object() {
|
||||
// Entity implicit type validation
|
||||
if let Some(schema_identifier) = self.schema.identifier() {
|
||||
// Kick in if the data object has a type field
|
||||
if let Some(type_val) = obj.get("type")
|
||||
&& let Some(type_str) = type_val.as_str()
|
||||
{
|
||||
// Check if the identifier is a global type name
|
||||
if let Some(type_def) = self.db.types.get(&schema_identifier) {
|
||||
// Ensure the instance type is a variation of the global type
|
||||
if type_def.variations.contains(type_str) {
|
||||
// Ensure it passes strict mode
|
||||
result.evaluated_keys.insert("type".to_string());
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors
|
||||
message: format!(
|
||||
"Type '{}' is not a valid descendant for this entity bound schema",
|
||||
type_str
|
||||
),
|
||||
path: self.join_path("type"),
|
||||
});
|
||||
// We decompose identity string routing inherently
|
||||
let expected_type = schema_identifier.split('.').last().unwrap_or(&schema_identifier);
|
||||
|
||||
// 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_val) = obj.get("type") {
|
||||
if let Some(type_str) = type_val.as_str() {
|
||||
if type_def.variations.contains(type_str) {
|
||||
// The instance is validly declaring a known structural descent
|
||||
result.evaluated_keys.insert("type".to_string());
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively
|
||||
message: format!(
|
||||
"Type '{}' is not a valid descendant for this entity bound schema",
|
||||
type_str
|
||||
),
|
||||
path: self.join_path("type"),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ad-Hoc schemas natively use strict schema discriminator strings instead of variation inheritance
|
||||
if type_str == schema_identifier.as_str() {
|
||||
result.evaluated_keys.insert("type".to_string());
|
||||
// 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),
|
||||
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());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it isn't registered globally, it might be a nested Ad-Hoc candidate running via O(1) union routers.
|
||||
// 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(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,11 +103,19 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(ref req) = self.schema.required {
|
||||
for field in req {
|
||||
if !obj.contains_key(field) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||
message: format!("Missing {}", field),
|
||||
path: self.join_path(field),
|
||||
});
|
||||
if field == "type" {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
message: "Missing type discriminator".to_string(),
|
||||
path: self.join_path(field),
|
||||
});
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||
message: format!("Missing {}", field),
|
||||
path: self.join_path(field),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,7 +154,10 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
if let Some(child_instance) = obj.get(key) {
|
||||
let new_path = self.join_path(key);
|
||||
let is_ref = sub_schema.r#ref.is_some();
|
||||
let is_ref = match &sub_schema.type_ {
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
|
||||
_ => false,
|
||||
};
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
@ -121,21 +168,9 @@ impl<'a> ValidationContext<'a> {
|
||||
next_extensible,
|
||||
false,
|
||||
);
|
||||
let mut item_res = derived.validate()?;
|
||||
let item_res = derived.validate()?;
|
||||
|
||||
|
||||
// Entity Bound Implicit Type Interception
|
||||
if key == "type"
|
||||
&& let Some(schema_bound) = sub_schema.identifier()
|
||||
{
|
||||
if let Some(type_def) = self.db.types.get(&schema_bound)
|
||||
&& let Some(instance_type) = child_instance.as_str()
|
||||
&& type_def.variations.contains(instance_type)
|
||||
{
|
||||
item_res
|
||||
.errors
|
||||
.retain(|e| e.code != "CONST_VIOLATED" && e.code != "ENUM_VIOLATED");
|
||||
}
|
||||
}
|
||||
|
||||
result.merge(item_res);
|
||||
result.evaluated_keys.insert(key.to_string());
|
||||
@ -148,7 +183,10 @@ impl<'a> ValidationContext<'a> {
|
||||
for (key, child_instance) in obj {
|
||||
if compiled_re.0.is_match(key) {
|
||||
let new_path = self.join_path(key);
|
||||
let is_ref = sub_schema.r#ref.is_some();
|
||||
let is_ref = match &sub_schema.type_ {
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
|
||||
_ => false,
|
||||
};
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
@ -187,7 +225,10 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
if !locally_matched {
|
||||
let new_path = self.join_path(key);
|
||||
let is_ref = additional_schema.r#ref.is_some();
|
||||
let is_ref = match &additional_schema.type_ {
|
||||
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
|
||||
_ => false,
|
||||
};
|
||||
let next_extensible = if is_ref { false } else { self.extensible };
|
||||
|
||||
let derived = self.derive(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::database::schema::Schema;
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -13,9 +14,8 @@ impl<'a> ValidationContext<'a> {
|
||||
|| self.schema.required.is_some()
|
||||
|| self.schema.additional_properties.is_some()
|
||||
|| self.schema.items.is_some()
|
||||
|| self.schema.r#ref.is_some()
|
||||
|| self.schema.cases.is_some()
|
||||
|| self.schema.one_of.is_some()
|
||||
|| self.schema.all_of.is_some()
|
||||
|| self.schema.enum_.is_some()
|
||||
|| self.schema.const_.is_some();
|
||||
|
||||
@ -25,105 +25,325 @@ impl<'a> ValidationContext<'a> {
|
||||
message: "$family must be used exclusively without other constraints".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
// Short-circuit: the schema formulation is broken
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(family_target) = &self.schema.family {
|
||||
// The descendants map is keyed by the schema's own $id, not the target string.
|
||||
if let Some(schema_id) = &self.schema.id
|
||||
&& let Some(descendants) = self.db.descendants.get(schema_id)
|
||||
{
|
||||
// Validate against all descendants simulating strict oneOf logic
|
||||
let mut passed_candidates: Vec<(String, usize, ValidationResult)> = Vec::new();
|
||||
|
||||
// The target itself is also an implicitly valid candidate
|
||||
let mut all_targets = vec![family_target.clone()];
|
||||
all_targets.extend(descendants.clone());
|
||||
|
||||
for child_id in &all_targets {
|
||||
if let Some(child_schema) = self.db.schemas.get(child_id) {
|
||||
let derived = self.derive(
|
||||
child_schema,
|
||||
self.instance,
|
||||
&self.path,
|
||||
self.overrides.clone(),
|
||||
self.extensible,
|
||||
self.reporter, // Inherit parent reporter flag, do not bypass strictness!
|
||||
);
|
||||
|
||||
// Explicitly run validate_scoped to accurately test candidates with strictness checks enabled
|
||||
let res = derived.validate_scoped()?;
|
||||
|
||||
if res.is_valid() {
|
||||
let depth = self.db.depths.get(child_id).copied().unwrap_or(0);
|
||||
passed_candidates.push((child_id.clone(), depth, res));
|
||||
}
|
||||
}
|
||||
if let Some(descendants) = self.db.descendants.get(family_target) {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
// Add the target base schema itself
|
||||
if let Some(base_schema) = self.db.schemas.get(family_target) {
|
||||
candidates.push(base_schema);
|
||||
}
|
||||
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().2);
|
||||
} else if passed_candidates.is_empty() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "NO_FAMILY_MATCH".to_string(),
|
||||
message: format!(
|
||||
"Payload did not match any descendants of family '{}'",
|
||||
family_target
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
} else {
|
||||
// Apply depth heuristic tie-breaker
|
||||
let mut best_depth: Option<usize> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut best_res = None;
|
||||
|
||||
for (_, depth, res) in passed_candidates.into_iter() {
|
||||
if let Some(current_best) = best_depth {
|
||||
if depth > current_best {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
ambiguous = false; // Broke the tie
|
||||
} else if depth == current_best {
|
||||
ambiguous = true; // Tie at the highest level
|
||||
}
|
||||
} else {
|
||||
best_depth = Some(depth);
|
||||
best_res = Some(res);
|
||||
}
|
||||
// Add all descendants
|
||||
for child_id in descendants {
|
||||
if let Some(child_schema) = self.db.schemas.get(child_id) {
|
||||
candidates.push(child_schema);
|
||||
}
|
||||
}
|
||||
|
||||
// Use prefix from family string (e.g. `light.`)
|
||||
let prefix = family_target
|
||||
.rsplit_once('.')
|
||||
.map(|(p, _)| format!("{}.", p))
|
||||
.unwrap_or_default();
|
||||
|
||||
if !ambiguous {
|
||||
if let Some(res) = best_res {
|
||||
result.merge(res);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_FAMILY_MATCH".to_string(),
|
||||
message: format!(
|
||||
"Payload matched multiple descendants of family '{}' without a clear depth winner",
|
||||
family_target
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
if !self.validate_polymorph(&candidates, Some(&prefix), result)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_refs(
|
||||
|
||||
pub(crate) fn validate_one_of(
|
||||
&self,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
// 1. Core $ref logic relies on the fast O(1) map to allow cycles and proper nesting
|
||||
if let Some(ref_str) = &self.schema.r#ref {
|
||||
if let Some(global_schema) = self.db.schemas.get(ref_str) {
|
||||
if let Some(ref one_of) = self.schema.one_of {
|
||||
let mut candidates = Vec::new();
|
||||
for schema in one_of {
|
||||
candidates.push(schema.as_ref());
|
||||
}
|
||||
if !self.validate_polymorph(&candidates, None, result)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_polymorph(
|
||||
&self,
|
||||
candidates: &[&Schema],
|
||||
family_prefix: Option<&str>,
|
||||
result: &mut ValidationResult,
|
||||
) -> Result<bool, ValidationError> {
|
||||
let mut passed_candidates: Vec<(Option<String>, ValidationResult)> = Vec::new();
|
||||
let mut failed_candidates: Vec<ValidationResult> = Vec::new();
|
||||
|
||||
// 1. O(1) Fast-Path Router & Extractor
|
||||
let instance_type = self.instance.as_object().and_then(|o| o.get("type")).and_then(|t| t.as_str());
|
||||
let instance_kind = self.instance.as_object().and_then(|o| o.get("kind")).and_then(|k| k.as_str());
|
||||
|
||||
let mut viable_candidates = Vec::new();
|
||||
|
||||
for sub in candidates {
|
||||
let _child_id = sub.identifier().unwrap_or_default();
|
||||
let mut can_match = true;
|
||||
|
||||
if let Some(t) = instance_type {
|
||||
// Fast Path 1: Pure Ad-Hoc Match (schema identifier == type)
|
||||
// If it matches exactly, it's our golden candidate. Make all others non-viable manually?
|
||||
// Wait, we loop through all and filter down. If exact match is found, we should ideally break and use ONLY that.
|
||||
// Let's implement the logic safely.
|
||||
|
||||
let mut exact_match_found = false;
|
||||
|
||||
if let Some(schema_id) = &sub.id {
|
||||
// Compute Vertical Exact Target (e.g. "person" or "light.person")
|
||||
let exact_target = if let Some(prefix) = family_prefix {
|
||||
format!("{}{}", prefix, t)
|
||||
} else {
|
||||
t.to_string()
|
||||
};
|
||||
|
||||
// Fast Path 1 & 2: Vertical Exact Match
|
||||
if schema_id == &exact_target {
|
||||
if instance_kind.is_none() {
|
||||
exact_match_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fast Path 3: Horizontal Sibling Match (kind + . + type)
|
||||
if let Some(k) = instance_kind {
|
||||
let sibling_target = format!("{}.{}", k, t);
|
||||
if schema_id == &sibling_target {
|
||||
exact_match_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if exact_match_found {
|
||||
// We found an exact literal structural identity match!
|
||||
// Wipe the existing viable_candidates and only yield this guy!
|
||||
viable_candidates.clear();
|
||||
viable_candidates.push(*sub);
|
||||
break;
|
||||
}
|
||||
|
||||
// Fast Path 4: Vertical Inheritance Fallback (Physical DB constraint)
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t_ptr)) = &sub.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t_ptr) {
|
||||
if let Some(base_type) = t_ptr.split('.').last() {
|
||||
if let Some(type_def) = self.db.types.get(base_type) {
|
||||
if !type_def.variations.contains(&t.to_string()) {
|
||||
can_match = false;
|
||||
}
|
||||
} else {
|
||||
if t_ptr != t {
|
||||
can_match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fast Path 5: Explicit Schema JSON `const` values check
|
||||
if can_match {
|
||||
if let Some(props) = &sub.properties {
|
||||
if let Some(type_prop) = props.get("type") {
|
||||
if let Some(const_val) = &type_prop.const_ {
|
||||
if let Some(const_str) = const_val.as_str() {
|
||||
if const_str != t {
|
||||
can_match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_match {
|
||||
viable_candidates.push(*sub);
|
||||
}
|
||||
}
|
||||
|
||||
println!("DEBUG VIABLE: {:?}", viable_candidates.iter().map(|s| s.id.clone()).collect::<Vec<_>>());
|
||||
// 2. Evaluate Viable Candidates
|
||||
// 2. Evaluate Viable Candidates
|
||||
// Composition validation is natively handled directly via type compilation.
|
||||
// The deprecated allOf JSON structure is no longer supported nor traversed.
|
||||
for sub in viable_candidates.clone() {
|
||||
let derived = self.derive_for_schema(sub, false);
|
||||
let sub_res = derived.validate()?;
|
||||
if sub_res.is_valid() {
|
||||
passed_candidates.push((sub.id.clone(), sub_res));
|
||||
} else {
|
||||
failed_candidates.push(sub_res);
|
||||
}
|
||||
}
|
||||
for f in &failed_candidates {
|
||||
println!(" - Failed candidate errors: {:?}", f.errors.iter().map(|e| e.code.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
if passed_candidates.len() == 1 {
|
||||
result.merge(passed_candidates.pop().unwrap().1);
|
||||
} else if passed_candidates.is_empty() {
|
||||
// 3. Discriminator Pathing (Failure Analytics)
|
||||
let type_path = self.join_path("type");
|
||||
|
||||
if instance_type.is_some() {
|
||||
// Filter to candidates that didn't explicitly throw a CONST violation on `type`
|
||||
let mut genuinely_failed = Vec::new();
|
||||
for res in &failed_candidates {
|
||||
let rejected_type = res.errors.iter().any(|e| {
|
||||
(e.code == "CONST_VIOLATED" || e.code == "ENUM_VIOLATED") && e.path == type_path
|
||||
});
|
||||
if !rejected_type {
|
||||
genuinely_failed.push(res.clone());
|
||||
}
|
||||
}
|
||||
|
||||
println!("DEBUG genuinely_failed len: {}", genuinely_failed.len());
|
||||
|
||||
if genuinely_failed.len() == 1 {
|
||||
// Golden Type Match (1 candidate was structurally possible but failed property validation)
|
||||
let sub_res = genuinely_failed.pop().unwrap();
|
||||
result.errors.extend(sub_res.errors);
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Pure Ad-Hoc Union
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
|
||||
message: "Payload matches none of the required candidate sub-schemas".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
for sub_res in &failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys.clone());
|
||||
}
|
||||
println!("DEBUG ELSE NO_FAMILY_MATCH RUNNING. Genuinely Failed len: {}", genuinely_failed.len());
|
||||
if viable_candidates.is_empty() {
|
||||
if let Some(obj) = self.instance.as_object() {
|
||||
result.evaluated_keys.extend(obj.keys().cloned());
|
||||
}
|
||||
}
|
||||
for sub_res in genuinely_failed {
|
||||
for e in sub_res.errors {
|
||||
if !result.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) {
|
||||
result.errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
// Instance missing type
|
||||
// Instance missing type
|
||||
let expects_type = viable_candidates.iter().any(|c| {
|
||||
c.compiled_property_names.get().map_or(false, |props| props.contains(&"type".to_string()))
|
||||
});
|
||||
|
||||
if expects_type {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
message: "Missing type discriminator. Unable to resolve polymorphic boundaries".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
for sub_res in failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
}
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Pure Ad-Hoc Union
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() { "NO_FAMILY_MATCH".to_string() } else { "NO_ONEOF_MATCH".to_string() },
|
||||
message: "Payload matches none of the required candidate sub-schemas".to_string(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sub_res in failed_candidates {
|
||||
result.evaluated_keys.extend(sub_res.evaluated_keys);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "AMBIGUOUS_POLYMORPHIC_MATCH".to_string(),
|
||||
message: "Matches multiple polymorphic candidates inextricably".to_string(),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
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::schema::SchemaTypeOrArray::Single(t)) => {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
custom_types.push(t.clone());
|
||||
}
|
||||
}
|
||||
Some(crate::database::schema::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::schema::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()));
|
||||
@ -135,16 +355,16 @@ impl<'a> ValidationContext<'a> {
|
||||
&self.path,
|
||||
new_overrides,
|
||||
self.extensible,
|
||||
true,
|
||||
true, // Reporter mode
|
||||
);
|
||||
shadow.root = global_schema;
|
||||
result.merge(shadow.validate()?);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REF_RESOLUTION_FAILED".to_string(),
|
||||
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
|
||||
message: format!(
|
||||
"Reference pointer to '{}' was not found in schema registry",
|
||||
ref_str
|
||||
"Inherited entity pointer '{}' was not found in schema registry",
|
||||
t
|
||||
),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user