Compare commits

...

99 Commits

Author SHA1 Message Date
d6deaa0b0f version: 1.0.59 2026-03-13 01:17:50 -04:00
6a275e1d90 stems fixes and tests 2026-03-13 01:17:27 -04:00
797a0a5460 version: 1.0.58 2026-03-12 19:32:42 -04:00
cfcb259eab final queryer and merger test suite installed; docs fully up to date 2026-03-12 18:37:50 -04:00
f666e608da more tests progress 2026-03-12 18:24:13 -04:00
732034bbc7 more tests progress 2026-03-12 17:46:38 -04:00
5b183a1aba test cleanup 2026-03-11 23:19:51 -04:00
be1367930d test cleanups passing 2026-03-11 21:55:37 -04:00
44ba3e0e18 test cleanup 2026-03-11 21:11:31 -04:00
e692fc52ee all tests passing again 2026-03-11 20:38:53 -04:00
641f7b5d92 all tests passing again 2026-03-11 18:38:37 -04:00
c007d7d479 test checkpoint 2026-03-11 17:46:08 -04:00
2c74d0a1a6 passing all tests 2026-03-11 17:26:45 -04:00
44be75f5d4 queryer merger test progress 2026-03-11 05:18:01 -04:00
1c08a8f2b8 query test progress 2026-03-10 18:25:29 -04:00
bb263190f6 version: 1.0.57 2026-03-04 18:05:42 -05:00
737c0b04ac version: 1.0.56 2026-03-04 17:36:55 -05:00
2d1fca599b a few tweaks 2026-03-04 17:36:42 -05:00
8d0369c672 doc update 2026-03-04 01:07:42 -05:00
566b599512 all jspg tests now passing 2026-03-04 01:02:32 -05:00
e7f20e2cb6 significantly simplified the validator and work continues on query 2026-03-03 17:58:31 -05:00
3898c43742 validator refactor progress 2026-03-03 00:13:37 -05:00
e14f53e7d9 validator reorg 2026-02-26 19:17:13 -05:00
960a99034a version: 1.0.55 2026-02-26 15:47:49 -05:00
81388149e8 added keyword to jspg 2026-02-26 15:47:43 -05:00
b8b3f7a501 version: 1.0.54 2026-02-26 15:46:09 -05:00
bc5489b1ea added keyword to jspg 2026-02-26 15:46:01 -05:00
7b55277116 flow update 2026-02-25 13:22:27 -05:00
ed636b05a4 flow update 2026-02-24 18:00:20 -05:00
2aec2da2fd version: 1.0.53 2026-02-19 20:14:34 -05:00
ad78896f72 library test suite for drop validation, fixed drop return structures 2026-02-19 20:14:21 -05:00
55b93d9957 version: 1.0.52 2026-02-19 18:20:18 -05:00
7ec6e09ae0 added agent workflow, added back in a structured version of additionalProperties 2026-02-19 18:20:06 -05:00
9d9c6d2c06 version: 1.0.51 2026-02-18 13:53:23 -05:00
12e952fa94 flow update 2026-02-18 13:53:15 -05:00
776a912098 version: 1.0.50 2026-02-18 13:46:57 -05:00
612188a54b version: 1.0.49 2026-02-18 13:46:00 -05:00
29c5160b49 jspg masking system installed 2026-02-18 13:45:40 -05:00
944675d669 jspg doc update 2026-02-18 01:40:25 -05:00
53a40d1099 jspg performance optimizations 2026-02-18 01:39:00 -05:00
e55977c11b jspg cleanup 2026-02-17 22:41:08 -05:00
8e50d4852d alpha jspg 2026-02-17 22:40:22 -05:00
623c34c0bc jspg progress 2026-02-17 21:46:10 -05:00
32ed463df8 jspg progress 2026-02-17 17:41:54 -05:00
6e06b6fdc2 version: 1.0.48 2025-11-18 18:56:56 -05:00
61735646ca version: 1.0.47 2025-11-18 18:55:48 -05:00
54c34b2848 upgrade to pg18 2025-11-18 18:55:39 -05:00
0f912c12b2 version: 1.0.46 2025-11-18 17:03:23 -05:00
b225afdd1b new version 2025-11-18 17:03:16 -05:00
f0bd32450d version: 1.0.45 2025-11-18 16:48:13 -05:00
bb17f153de version: 1.0.44 2025-11-18 16:17:12 -05:00
ec8bfad390 version: 1.0.43 2025-11-18 16:08:57 -05:00
8a1b13b139 upgraded all dependencies 2025-11-18 16:08:43 -05:00
469dd0519b version: 1.0.42 2025-10-10 17:36:08 -04:00
4b6ea6536c added type family support 2025-10-10 17:35:57 -04:00
d8a924c662 version: 1.0.41 2025-10-08 12:38:38 -04:00
f3d157ebcb bringing back type constants for validation via new overrides vocabulary 2025-10-08 12:38:26 -04:00
44cde90c3d jspg union fixes 2025-10-07 20:43:23 -04:00
9ddc899411 version: 1.0.40 2025-10-02 18:15:19 -04:00
a8d726ec73 unevaluatedProperties now cascade infinitely down their leaf when strict validation mode is on 2025-10-02 18:15:07 -04:00
6b6647f2d6 version: 1.0.39 2025-09-30 20:44:35 -04:00
d301d5fab9 types at root not strict 2025-09-30 20:44:17 -04:00
61511b595d added flow commands for testing validator vs jspg 2025-09-30 20:29:13 -04:00
c7ae975275 version: 1.0.38 2025-09-30 20:19:51 -04:00
aa58082cd7 boon test suite itself passing 2025-09-30 20:19:41 -04:00
491fb3a3e3 docs updated 2025-09-30 20:01:49 -04:00
fc939d84ee version: 1.0.37 2025-09-30 19:56:46 -04:00
d6b34c99bb jspg additional properties bug squashed 2025-09-30 19:56:34 -04:00
cc04f38c14 boon now included 2025-09-30 01:10:58 -04:00
c9b1245a57 version: 1.0.36 2025-09-12 23:00:53 -04:00
3d770b0831 fixed type mismatch checking to not fail fast and work through nested data 2025-09-12 22:59:27 -04:00
3fdbf60396 minor reorg no release 2025-09-12 15:43:20 -04:00
6610b069db version: 1.0.35 2025-09-12 01:02:45 -04:00
bb84f9aa73 implemented type match checking for types on schema id instead of type const 2025-09-12 01:02:32 -04:00
704770051c version: 1.0.34 2025-09-01 22:58:10 -04:00
88c77deede punc request and response moved to punc schemas 2025-09-01 22:58:01 -04:00
0184c244d9 version: 1.0.33 2025-08-27 03:30:25 -04:00
e40de2eb12 more improvements to ref tracking in json schemas and tests 2025-08-27 03:30:15 -04:00
5e55786e3e version: 1.0.32 2025-08-21 20:18:44 -04:00
6520413069 jspg updates for punc-v2 2025-08-21 20:18:32 -04:00
b97879ff61 version: 1.0.31 2025-07-08 07:27:14 -04:00
ea0b139f87 upgraded rust and pgrx versions 2025-07-08 07:27:05 -04:00
dccaa0a46e version: 1.0.30 2025-07-04 04:23:15 -04:00
441597e604 need to allow empty strings when a string property has a format 2025-07-04 04:23:06 -04:00
710598752f version: 1.0.29 2025-06-17 18:55:27 -04:00
5fbf64bac5 serializing ErrorKind directly to drop error cause 2025-06-17 18:55:16 -04:00
2dd17f0b37 version: 1.0.28 2025-06-12 22:27:59 -04:00
cbda45e610 fixed conditional errors with false schemas and unevaluatedProperties 2025-06-12 22:27:49 -04:00
1085964c17 version: 1.0.27 2025-06-12 17:07:37 -04:00
65971d9b93 splitting up errorkind paths to produce multiple drop errors 2025-06-12 17:07:28 -04:00
d938058d34 version: 1.0.26 2025-06-12 00:59:44 -04:00
69ab6165bb improvements to error handling again 2025-06-12 00:59:33 -04:00
03beada825 version: 1.0.25 2025-06-11 20:28:46 -04:00
efdd7528cc switched strict validation from additionalProperties to unevaluatedProperties to catch conditional properties automatically in verification 2025-06-11 20:28:39 -04:00
59395a33ac version: 1.0.24 2025-06-11 19:38:56 -04:00
92c0a6fc0b even more jspg improved error handling, missing some codes before 2025-06-11 19:38:46 -04:00
7f66a4a35a no-op 2025-06-10 16:01:58 -04:00
d37aadb0dd version: 1.0.23 2025-06-09 18:09:33 -04:00
d0ccc47d97 added strict validation option 2025-06-09 18:09:15 -04:00
99 changed files with 39182 additions and 1425 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

63
.agent/workflows/jspg.md Normal file
View File

@ -0,0 +1,63 @@
---
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.
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!
Section 1: Various Documentation
- GEMINI.md at the root (JSPG doc)
- api/punc/GEMINI.md
- api/punc/generator/GEMINI.md
Section 2: Flow file for cmd interface
- flow at the root
Section 3: Source
- src/*
Section 4: Test Fixtures
Just review some of the *.json files in fixtures/ at the root
- fixtures/allOf.json
- fixtures/ref.json
- fixtures/contains.json
- fixtures/queryer.json
- fixtures/merger.json
- fixtures/merge.json
Section 5: Build
- build.rs
Section 6: Cargo TOML
- Cargo.toml
Section 7: Some PUNC Syntax
Now, review some punc type and enum source in the api project with api/ these files:
- 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
- api/punc/sql/puncs/puncs.sql
- api/punc/sql/puncs/job.sql
- api/contact/sql/tables.sql
- api/contact/sql/domains.sql
- api/contact/sql/indexes.sql
- api/contact/sql/domains.sql
- api/contact/sql/puncs/contacts.sql
Now you are ready to help me work on this extension.

3
.geminiignore Normal file
View File

@ -0,0 +1,3 @@
/target/
/package/
.env

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
/target
/package
.env
.env
/src/tests.rs
/pgrx-develop

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "flows"]
path = flows
url = git@gitea-ssh.thoughtpatterns.ai:cellular/flows.git
[submodule "tests/fixtures/JSON-Schema-Test-Suite"]
path = tests/fixtures/JSON-Schema-Test-Suite
url = git@github.com:json-schema-org/JSON-Schema-Test-Suite.git

1549
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,35 @@
[package]
name = "jspg"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
pgrx = "0.14.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
pgrx = "0.16.1"
serde = { version = "1.0.228", features = ["derive", "rc"] }
serde_json = "1.0.149"
lazy_static = "1.5.0"
boon = "0.6.1"
once_cell = "1.21.3"
ahash = "0.8.12"
regex = "1.12.3"
regex-syntax = "0.8.9"
url = "2.5.8"
fluent-uri = "0.3.2"
idna = "1.1.0"
percent-encoding = "2.3.2"
uuid = { version = "1.20.0", features = ["v4", "serde"] }
chrono = { version = "0.4.43", features = ["serde"] }
json-pointer = "0.3.4"
indexmap = { version = "2.13.0", features = ["serde"] }
moka = { version = "0.12.14", features = ["sync"] }
xxhash-rust = { version = "0.8.15", features = ["xxh64"] }
dashmap = "6.1.0"
[dev-dependencies]
pgrx-tests = "0.14.0"
pgrx-tests = "0.16.1"
[build-dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
[lib]
crate-type = ["cdylib", "lib"]
@ -21,7 +39,8 @@ name = "pgrx_embed_jspg"
path = "src/bin/pgrx_embed.rs"
[features]
pg17 = ["pgrx/pg17", "pgrx-tests/pg17" ]
default = ["pg18"]
pg18 = ["pgrx/pg18", "pgrx-tests/pg18" ]
# Local feature flag used by `cargo pgrx test`
pg_test = []
@ -34,3 +53,6 @@ panic = "unwind"
opt-level = 3
lto = "fat"
codegen-units = 1
[package.metadata.jspg]
target_draft = "draft2020-12"

136
GEMINI.md Normal file
View File

@ -0,0 +1,136 @@
# JSPG: JSON Schema Postgres
**JSPG** is a high-performance PostgreSQL extension written in Rust (using `pgrx`) that transforms Postgres into a pre-compiled Semantic Engine. It serves as the core engine for the "Punc" architecture, where the database is the single source of truth for all data models, API contracts, validations, and reactive queries.
## 1. Overview & Architecture
JSPG operates by deeply integrating the JSON Schema Draft 2020-12 specification directly into the Postgres session lifecycle. It is built around three core pillars:
* **Validator**: In-memory, near-instant JSON structural validation and type polymorphism routing.
* **Merger**: Automatically traverse and UPSERT deeply nested JSON graphs into normalized relational tables.
* **Queryer**: Compile JSON Schemas into static, cached SQL SPI `SELECT` plans for fetching full entities or isolated "Stems".
### 🎯 Goals
1. **Draft 2020-12 Compliance**: Attempt to adhere to the official JSON Schema Draft 2020-12 specification.
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.
5. **Reactive Beats**: Provide natively generated "Stems" (isolated payload fragments) for dynamic websocket reactivity.
### Concurrency & Threading ("Immutable Graphs")
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 Validator**: The `Validator` struct immutably owns the `Database` registry and all its global maps. Schemas themselves are completely frozen; `$ref` strings are resolved dynamically at runtime using pre-computed O(1) maps.
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.
---
## 2. Validator
The Validator provides strict, schema-driven evaluation for the "Punc" architecture.
### API Reference
* `jspg_setup(database jsonb) -> jsonb`: Loads and compiles the entire registry (types, enums, puncs, relations) atomically.
* `mask_json_schema(schema_id text, instance jsonb) -> jsonb`: Validates and prunes unknown properties dynamically, returning masked data.
* `jspg_validate(schema_id text, instance jsonb) -> jsonb`: Returns boolean-like success or structured errors.
* `jspg_teardown() -> jsonb`: Clears the current session's schema cache.
### Custom Features & Deviations
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, JSPG evaluates the JSON against every schema that `$ref`s widget.
* **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".
---
## 3. Merger
The Merger provides an automated, high-performance graph synchronization engine via the `jspg_merge(cue JSONB)` API. It orchestrates the complex mapping of nested JSON objects into normalized Postgres relational tables, honoring all inheritance and graph constraints.
### Core Features
* **Caching Strategy**: The Merger leverages the `Validator`'s in-memory `Database` registry to instantly resolve Foreign Key mapping graphs. It additionally utilizes the concurrent `GLOBAL_JSPG` application memory (`DashMap`) to cache statically constructed SQL `SELECT` strings used during deduplication (`lk_`) and difference tracking calculations.
* **Deep Graph Merging**: The Merger walks arbitrary levels of deeply nested JSON schemas (e.g. tracking an `order`, its `customer`, and an array of its `lines`). It intelligently discovers the correct parent-to-child or child-to-parent Foreign Keys stored in the registry and automatically maps the UUIDs across the relationships during UPSERT.
* **Prefix Foreign Key Matching**: Handles scenario where multiple relations point to the same table by using database Foreign Key constraint prefixes (`fk_`). For example, if a schema has `shipping_address` and `billing_address`, the merger resolves against `fk_shipping_address_entity` vs `fk_billing_address_entity` automatically to correctly route object properties.
* **Dynamic Deduplication & Lookups**: If a nested object is provided without an `id`, the Merger utilizes Postgres `lk_` index constraints defined in the schema registry (e.g. `lk_person` mapped to `first_name` and `last_name`). It dynamically queries these unique matching constraints to discover the correct UUID to perform an UPDATE, preventing data duplication.
* **Hierarchical Table Inheritance**: The Punc system uses distributed table inheritance (e.g. `person` inherits `user` inherits `organization` inherits `entity`). The Merger splits the incoming JSON payload and performs atomic row updates across *all* relevant tables in the lineage map.
* **The Archive Paradigm**: Data is never deleted in the Punc system. The Merger securely enforces referential integrity by toggling the `archived` Boolean flag on the base `entity` table rather than issuing SQL `DELETE` commands.
* **Change Tracking & Reactivity**: The Merger diffs the incoming JSON against the existing database row (utilizing static, `DashMap`-cached `lk_` SELECT string templates). Every detected change is recorded into the `agreego.change` audit table, tracking the user mapping. It then natively uses `pg_notify` to broadcast a completely flat row-level diff out to the Go WebSocket server for O(1) routing.
* **Many-to-Many Graph Edge Management**: Operates seamlessly with the global `agreego.relationship` table, allowing the system to represent and merge arbitrary reified M:M relationships directionally between any two entities.
* **Sparse Updates**: Empty JSON strings `""` are directly bound as explicit SQL `NULL` directives to clear data, whilst omitted (missing) properties skip UPDATE execution entirely, ensuring partial UI submissions do not wipe out sibling fields.
* **Unified Return Structure**: To eliminate UI hydration race conditions and multi-user duplication, `jspg_merge` explicitly strips the response graph and returns only the root `{ "id": "uuid" }` (or an array of IDs for list insertions). External APIs can then explicitly call read APIs to fetch the resulting graph, while the UI relies 100% implicitly on the flat `pg_notify` pipeline for reactive state synchronization.
* **Decoupled SQL Generation**: Because Writes (INSERT/UPDATE) are inherently highly dynamic based on partial payload structures, the Merger generates raw SQL strings dynamically per execution without caching, guaranteeing a minimal memory footprint while scaling optimally.
---
## 4. Queryer
The Queryer transforms Postgres into a pre-compiled Semantic Query Engine via the `jspg_query(schema_id text, cue jsonb)` API, designed to serve the exact shape of Punc responses directly via SQL.
### 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.
* **Equality / Inequality**: `{"$eq": value}`, `{"$ne": value}` automatically map to `=` and `!=`.
* **Comparison**: `{"$gt": ...}`, `{"$gte": ...}`, `{"$lt": ...}`, `{"$lte": ...}` directly compile to Postgres comparison operators (`> `, `>=`, `<`, `<=`).
* **Array Inclusion**: `{"$in": [values]}`, `{"$nin": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks.
* **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches.
* **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`).
### 4. The Stem Engine
Rather than over-fetching heavy Entity payloads and trimming them, Punc Framework Websockets depend on isolated subgraphs defined as **Stems**.
A `Stem` is **not a JSON Pointer** or a physical path string (like `/properties/contacts/items/phone_number`). It is simply a declaration of an **Entity Type boundary** that exists somewhere within the compiled JSON Schema graph.
Because `pg_notify` (Beats) fire rigidly from physical Postgres tables (e.g. `{"type": "phone_number"}`), the Go Framework only ever needs to know: "Does the schema `with_contacts.person` contain the `phone_number` Entity anywhere inside its tree?"
* **Initialization:** During startup (`jspg_stems()`), the database crawls all Schemas and maps out every physical Entity Type it references. It builds a flat dictionary of `Schema ID -> [Entity Types]` (e.g. `with_contacts.person -> ["person", "contact", "phone_number", "email_address"]`).
* **Identifier Prioritization**: When determining if a nested object boundary is an Entity, JSPG natively prioritizes defined `$id` tags over `$ref` inheritance pointers to prevent polymorphic boundaries from devolving into their generic base classes.
* **Cyclical Deduplication**: Because Punc relationships often reference back on themselves via deeply nested classes, the Stem Engine applies intelligent path deduplication. If the active `current_path` already ends with the target entity string, it traverses the inheritance properties without appending the entity to the stem path again, eliminating infinite powerset loops.
* **Relationship Path Squashing:** When calculating nested string paths structurally to discover these boundaries, JSPG intentionally **omits** properties natively named `target` or `source` if they belong to a native database `relationship` table override. This ensures paths like `phone_numbers/contact/target` correctly register their beat resolution pattern as `phone_numbers/contact/phone_number`.
* **The Go Router**: The Golang Punc framework uses this exact mapping to register WebSocket Beat frequencies exclusively on the Entity types discovered.
* **The Queryer Execution**: When the Go framework asks JSPG to hydrate a partial `phone_number` stem for the `with_contacts.person` schema, instead of jumping through string paths, the SQL Compiler simply reaches into the Schema's AST using the `phone_number` Type string, pulls out exactly that entity's mapping rules, and returns a fully correlated `SELECT` block! This natively handles nested array properties injected via `oneOf` or array references efficiently bypassing runtime powerset expansion.
* **Performance:** These Stem execution structures are fully statically compiled via SPI and map perfectly to `O(1)` real-time routing logic on the application tier.
## 5. 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.
To solve this, JSPG introduces the `DatabaseExecutor` trait inside `src/database/executors/`:
* **`SpiExecutor` (`pgrx.rs`)**: The production evaluator that is conditionally compiled (`#[cfg(not(test))]`). It unwraps standard `pgrx::spi` connections to the database.
* **`MockExecutor` (`mock.rs`)**: The testing evaluator that is conditionally compiled (`#[cfg(test)]`). It absorbs SQL calls and captures parameter bindings in memory without executing them.
### Universal Test Harness (`src/tests/`)
JSPG abandons the standard `cargo pgrx test` model in favor of native OS testing for a >1000x speed increase (`~0.05s` execution).
1. **JSON Fixtures**: All core interactions are defined abstractly as JSON arrays in `fixtures/`. Each file contains suites of `TestCase` objects with an `action` flag (`compile`, `validate`, `merge`, `query`).
2. **`build.rs` Generator**: The build script traverses the JSON fixtures, extracts their structural identities, and generates standard `#[test]` blocks into `src/tests/fixtures.rs`.
3. **Modular Test Dispatcher**: The `src/tests/types/` module deserializes the abstract JSON test payloads into `Suite`, `Case`, and `Expect` data structures.
* The `compile` action natively asserts the exact output shape of `jspg_stems`, allowing structural and relationship mapping logic to be tested purely through JSON without writing brute-force manual tests in Rust.
4. **Unit Context Execution**: When `cargo test` executes, the runner iterates the JSON payloads. Because the tests run natively inside the module via `#cfg(test)`, the Rust compiler globally erases `pgrx` C-linkage, instantiates the `MockExecutor`, and allows for pure structural evaluation of complex database logic completely in memory in parallel.

0
agreego.sql Normal file
View File

98
build.rs Normal file
View File

@ -0,0 +1,98 @@
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn to_safe_identifier(name: &str) -> String {
let mut safe = String::new();
for (i, c) in name.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
safe.push('_');
}
safe.push(c.to_ascii_lowercase());
} else if c == '-' || c == '.' {
safe.push('_');
} else {
safe.push(c);
}
}
safe
}
fn main() {
println!("cargo:rerun-if-changed=fixtures");
println!("cargo:rerun-if-changed=Cargo.toml");
// File: src/tests/fixtures.rs for standard #[test] integration
let std_dest_path = Path::new("src/tests/fixtures.rs");
let mut std_file = File::create(std_dest_path).unwrap();
// Walk tests/fixtures directly
let fixtures_path = "fixtures";
if Path::new(fixtures_path).exists() {
for entry in fs::read_dir(fixtures_path).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().unwrap_or_default() == "json" {
let file_name = path.file_stem().unwrap().to_str().unwrap();
// Parse the JSON file to find blocks
let file = File::open(&path).unwrap();
let val: serde_json::Value = serde_json::from_reader(file).unwrap();
if let Some(arr) = val.as_array() {
for (i, item) in arr.iter().enumerate() {
// Enforce test suite structure
let group = item.as_object().expect("Test suite must be an object");
// Validate required suite fields
if !group.contains_key("description")
|| !group.contains_key("database")
|| !group.contains_key("tests")
{
panic!(
"File {} index {} is missing required suite fields (description, database, tests)",
file_name, i
);
}
// Validate required test case fields
let tests = group
.get("tests")
.unwrap()
.as_array()
.expect("Tests must be an array");
let safe_filename = to_safe_identifier(file_name);
for (t_idx, test) in tests.iter().enumerate() {
let t_obj = test.as_object().expect("Test case must be an object");
if !t_obj.contains_key("description") {
panic!(
"File {} suite {} test {} is missing required case fields (description)",
file_name, i, t_idx
);
}
// Use deterministic names: test_{filename}_{suite_idx}_{test_idx}
let fn_name = format!("test_{}_{}_{}", safe_filename, i, t_idx);
// Write to src/tests/fixtures.rs (Std Test)
write!(
std_file,
r#"
#[test]
fn {}() {{
let path = format!("{{}}/fixtures/{}.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, {}, {}).unwrap();
}}
"#,
fn_name, file_name, i, t_idx
)
.unwrap();
}
}
}
}
}
}
}

View File

@ -0,0 +1,176 @@
[
{
"description": "additionalProperties validates properties not matched by properties",
"database": {
"schemas": [
{
"$id": "schema1",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "number"
}
},
"additionalProperties": {
"type": "boolean"
}
}
]
},
"tests": [
{
"description": "defined properties are valid",
"data": {
"foo": "value",
"bar": 123
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "additional property matching schema is valid",
"data": {
"foo": "value",
"is_active": true,
"hidden": false
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "additional property not matching schema is invalid",
"data": {
"foo": "value",
"is_active": 1
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true with additionalProperties still validates structure",
"database": {
"schemas": [
{
"properties": {
"foo": {
"type": "string"
}
},
"extensible": true,
"additionalProperties": {
"type": "integer"
},
"$id": "additionalProperties_1_0"
}
]
},
"tests": [
{
"description": "additional property matching schema is valid",
"data": {
"foo": "hello",
"count": 5,
"age": 42
},
"schema_id": "additionalProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "additional property not matching schema is invalid despite extensible: true",
"data": {
"foo": "hello",
"count": "five"
},
"schema_id": "additionalProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "complex additionalProperties with object and array items",
"database": {
"schemas": [
{
"$id": "schema3",
"properties": {
"type": {
"type": "string"
}
},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
]
},
"tests": [
{
"description": "valid array of strings",
"data": {
"type": "my_type",
"group_a": [
"field1",
"field2"
],
"group_b": [
"field3"
]
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid array of integers",
"data": {
"type": "my_type",
"group_a": [
1,
2
]
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "invalid non-array type",
"data": {
"type": "my_type",
"group_a": "field1"
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

677
fixtures/allOf.json Normal file
View File

@ -0,0 +1,677 @@
[
{
"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
}
}
]
}
]

197
fixtures/booleanSchema.json Normal file
View File

@ -0,0 +1,197 @@
[
{
"description": "boolean schema 'true'",
"database": {
"schemas": [
{
"$id": "booleanSchema_0_0"
}
]
},
"tests": [
{
"description": "number is valid",
"data": 1,
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "string is valid",
"data": "foo",
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boolean true is valid",
"data": true,
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boolean false is valid",
"data": false,
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "null is valid",
"data": null,
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object is valid",
"data": {
"foo": "bar"
},
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array is valid",
"data": [
"foo"
],
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty array is valid",
"data": [],
"schema_id": "booleanSchema_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "boolean schema 'false'",
"database": {
"schemas": [
{
"not": {},
"$id": "booleanSchema_1_0"
}
]
},
"tests": [
{
"description": "number is invalid",
"data": 1,
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "string is invalid",
"data": "foo",
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean true is invalid",
"data": true,
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean false is invalid",
"data": false,
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is invalid",
"data": null,
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is invalid",
"data": {},
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "array is invalid",
"data": [
"foo"
],
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "booleanSchema_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

818
fixtures/const.json Normal file
View File

@ -0,0 +1,818 @@
[
{
"description": "const validation",
"database": {
"schemas": [
{
"const": 2,
"$id": "const_0_0"
}
]
},
"tests": [
{
"description": "same value is valid",
"data": 2,
"schema_id": "const_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "another value is invalid",
"data": 5,
"schema_id": "const_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "another type is invalid",
"data": "a",
"schema_id": "const_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with object",
"database": {
"schemas": [
{
"const": {
"foo": "bar",
"baz": "bax"
},
"properties": {
"foo": {},
"baz": {}
},
"$id": "const_1_0"
}
]
},
"tests": [
{
"description": "same object is valid",
"data": {
"foo": "bar",
"baz": "bax"
},
"schema_id": "const_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "same object with different property order is valid",
"data": {
"baz": "bax",
"foo": "bar"
},
"schema_id": "const_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "another object is invalid",
"data": {
"foo": "bar"
},
"schema_id": "const_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "another type is invalid",
"data": [
1,
2
],
"schema_id": "const_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with array",
"database": {
"schemas": [
{
"const": [
{
"foo": "bar"
}
],
"$id": "const_2_0"
}
]
},
"tests": [
{
"description": "same array is valid",
"data": [
{
"foo": "bar"
}
],
"schema_id": "const_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "another array item is invalid",
"data": [
2
],
"schema_id": "const_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "array with additional items is invalid",
"data": [
1,
2,
3
],
"schema_id": "const_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with null",
"database": {
"schemas": [
{
"const": null,
"$id": "const_3_0"
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"schema_id": "const_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "not null is invalid",
"data": 0,
"schema_id": "const_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with false does not match 0",
"database": {
"schemas": [
{
"const": false,
"$id": "const_4_0"
}
]
},
"tests": [
{
"description": "false is valid",
"data": false,
"schema_id": "const_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer zero is invalid",
"data": 0,
"schema_id": "const_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float zero is invalid",
"data": 0,
"schema_id": "const_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with true does not match 1",
"database": {
"schemas": [
{
"const": true,
"$id": "const_5_0"
}
]
},
"tests": [
{
"description": "true is valid",
"data": true,
"schema_id": "const_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer one is invalid",
"data": 1,
"schema_id": "const_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float one is invalid",
"data": 1,
"schema_id": "const_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with [false] does not match [0]",
"database": {
"schemas": [
{
"const": [
false
],
"$id": "const_6_0"
}
]
},
"tests": [
{
"description": "[false] is valid",
"data": [
false
],
"schema_id": "const_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[0] is invalid",
"data": [
0
],
"schema_id": "const_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[0.0] is invalid",
"data": [
0
],
"schema_id": "const_6_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with [true] does not match [1]",
"database": {
"schemas": [
{
"const": [
true
],
"$id": "const_7_0"
}
]
},
"tests": [
{
"description": "[true] is valid",
"data": [
true
],
"schema_id": "const_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[1] is invalid",
"data": [
1
],
"schema_id": "const_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[1.0] is invalid",
"data": [
1
],
"schema_id": "const_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with {\"a\": false} does not match {\"a\": 0}",
"database": {
"schemas": [
{
"const": {
"a": false
},
"$id": "const_8_0"
}
]
},
"tests": [
{
"description": "{\"a\": false} is valid",
"data": {
"a": false
},
"schema_id": "const_8_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "{\"a\": 0} is invalid",
"data": {
"a": 0
},
"schema_id": "const_8_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "{\"a\": 0.0} is invalid",
"data": {
"a": 0
},
"schema_id": "const_8_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with {\"a\": true} does not match {\"a\": 1}",
"database": {
"schemas": [
{
"const": {
"a": true
},
"$id": "const_9_0"
}
]
},
"tests": [
{
"description": "{\"a\": true} is valid",
"data": {
"a": true
},
"schema_id": "const_9_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "{\"a\": 1} is invalid",
"data": {
"a": 1
},
"schema_id": "const_9_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "{\"a\": 1.0} is invalid",
"data": {
"a": 1
},
"schema_id": "const_9_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with 0 does not match other zero-like types",
"database": {
"schemas": [
{
"const": 0,
"$id": "const_10_0"
}
]
},
"tests": [
{
"description": "false is invalid",
"data": false,
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "integer zero is valid",
"data": 0,
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float zero is valid",
"data": 0,
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty object is invalid",
"data": {},
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty string is invalid",
"data": "",
"schema_id": "const_10_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "const with 1 does not match true",
"database": {
"schemas": [
{
"const": 1,
"$id": "const_11_0"
}
]
},
"tests": [
{
"description": "true is invalid",
"data": true,
"schema_id": "const_11_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "integer one is valid",
"data": 1,
"schema_id": "const_11_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float one is valid",
"data": 1,
"schema_id": "const_11_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "const with -2.0 matches integer and float types",
"database": {
"schemas": [
{
"const": -2,
"$id": "const_12_0"
}
]
},
"tests": [
{
"description": "integer -2 is valid",
"data": -2,
"schema_id": "const_12_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer 2 is invalid",
"data": 2,
"schema_id": "const_12_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float -2.0 is valid",
"data": -2,
"schema_id": "const_12_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float 2.0 is invalid",
"data": 2,
"schema_id": "const_12_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float -2.00001 is invalid",
"data": -2.00001,
"schema_id": "const_12_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "float and integers are equal up to 64-bit representation limits",
"database": {
"schemas": [
{
"const": 9007199254740992,
"$id": "const_13_0"
}
]
},
"tests": [
{
"description": "integer is valid",
"data": 9007199254740992,
"schema_id": "const_13_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer minus one is invalid",
"data": 9007199254740991,
"schema_id": "const_13_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float is valid",
"data": 9007199254740992,
"schema_id": "const_13_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float minus one is invalid",
"data": 9007199254740991,
"schema_id": "const_13_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "nul characters in strings",
"database": {
"schemas": [
{
"const": "hello\u0000there",
"$id": "const_14_0"
}
]
},
"tests": [
{
"description": "match string with nul",
"data": "hello\u0000there",
"schema_id": "const_14_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "do not match string lacking nul",
"data": "hellothere",
"schema_id": "const_14_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "characters with the same visual representation but different codepoint",
"database": {
"schemas": [
{
"const": "μ",
"$comment": "U+03BC",
"$id": "const_15_0"
}
]
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "μ",
"comment": "U+03BC",
"schema_id": "const_15_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "character looks the same but uses a different codepoint",
"data": "µ",
"comment": "U+00B5",
"schema_id": "const_15_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "characters with the same visual representation, but different number of codepoints",
"database": {
"schemas": [
{
"const": "ä",
"$comment": "U+00E4",
"$id": "const_16_0"
}
]
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "ä",
"comment": "U+00E4",
"schema_id": "const_16_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "character looks the same but uses combining marks",
"data": "ä",
"comment": "a, U+0308",
"schema_id": "const_16_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties in const object match",
"database": {
"schemas": [
{
"const": {
"a": 1
},
"extensible": true,
"$id": "const_17_0"
}
]
},
"tests": [
{
"description": "extra property ignored during strict check, but const check still applies (mismatch)",
"data": {
"a": 1,
"b": 2
},
"schema_id": "const_17_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "extra property match in const (this is effectively impossible if data has extra props not in const, it implicitly fails const check unless we assume const check ignored extra props? No, const check is strict. So this test is just to show strictness passes.)",
"data": {
"a": 1
},
"schema_id": "const_17_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

418
fixtures/contains.json Normal file
View File

@ -0,0 +1,418 @@
[
{
"description": "contains keyword validation",
"database": {
"schemas": [
{
"contains": {
"minimum": 5
},
"items": true,
"$id": "contains_0_0"
}
]
},
"tests": [
{
"description": "array with item matching schema (5) is valid (items: true)",
"data": [
3,
4,
5
],
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with item matching schema (6) is valid (items: true)",
"data": [
3,
4,
6
],
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with two items matching schema (5, 6) is valid (items: true)",
"data": [
3,
4,
5,
6
],
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array without items matching schema is invalid",
"data": [
2,
3,
4
],
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "not array is valid",
"data": {},
"schema_id": "contains_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "contains keyword with const keyword",
"database": {
"schemas": [
{
"contains": {
"const": 5
},
"items": true,
"$id": "contains_1_0"
}
]
},
"tests": [
{
"description": "array with item 5 is valid (items: true)",
"data": [
3,
4,
5
],
"schema_id": "contains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with two items 5 is valid (items: true)",
"data": [
3,
4,
5,
5
],
"schema_id": "contains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array without item 5 is invalid",
"data": [
1,
2,
3,
4
],
"schema_id": "contains_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "contains keyword with boolean schema true",
"database": {
"schemas": [
{
"contains": true,
"$id": "contains_2_0"
}
]
},
"tests": [
{
"description": "any non-empty array is valid",
"data": [
"foo"
],
"schema_id": "contains_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "contains_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "contains keyword with boolean schema false",
"database": {
"schemas": [
{
"contains": false,
"$id": "contains_3_0"
}
]
},
"tests": [
{
"description": "any non-empty array is invalid",
"data": [
"foo"
],
"schema_id": "contains_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "contains_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "non-arrays are valid",
"data": "contains does not apply to strings",
"schema_id": "contains_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items + contains",
"database": {
"schemas": [
{
"items": {
"multipleOf": 2
},
"contains": {
"multipleOf": 3
},
"$id": "contains_4_0"
}
]
},
"tests": [
{
"description": "matches items, does not match contains",
"data": [
2,
4,
8
],
"schema_id": "contains_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "does not match items, matches contains",
"data": [
3,
6,
9
],
"schema_id": "contains_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "matches both items and contains",
"data": [
6,
12
],
"schema_id": "contains_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "matches neither items nor contains",
"data": [
1,
5
],
"schema_id": "contains_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "contains with false if subschema",
"database": {
"schemas": [
{
"contains": {
"if": false,
"else": true
},
"$id": "contains_5_0"
}
]
},
"tests": [
{
"description": "any non-empty array is valid",
"data": [
"foo"
],
"schema_id": "contains_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "contains_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "contains with null instance elements",
"database": {
"schemas": [
{
"contains": {
"type": "null"
},
"$id": "contains_6_0"
}
]
},
"tests": [
{
"description": "allows null items",
"data": [
null
],
"schema_id": "contains_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows non-matching items in contains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"extensible": true,
"$id": "contains_7_0"
}
]
},
"tests": [
{
"description": "extra items acceptable",
"data": [
1,
2
],
"schema_id": "contains_7_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "strict by default: non-matching items in contains are invalid",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"$id": "contains_8_0"
}
]
},
"tests": [
{
"description": "extra items cause failure",
"data": [
1,
2
],
"schema_id": "contains_8_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "only matching items is valid",
"data": [
1,
1
],
"schema_id": "contains_8_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

232
fixtures/content.json Normal file
View File

@ -0,0 +1,232 @@
[
{
"description": "validation of string-encoded content based on media type",
"database": {
"schemas": [
{
"contentMediaType": "application/json",
"$id": "content_0_0"
}
]
},
"tests": [
{
"description": "a valid JSON document",
"data": "{\"foo\": \"bar\"}",
"schema_id": "content_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid JSON document; validates true",
"data": "{:}",
"schema_id": "content_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores non-strings",
"data": 100,
"schema_id": "content_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "validation of binary string-encoding",
"database": {
"schemas": [
{
"contentEncoding": "base64",
"$id": "content_1_0"
}
]
},
"tests": [
{
"description": "a valid base64 string",
"data": "eyJmb28iOiAiYmFyIn0K",
"schema_id": "content_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid base64 string (% is not a valid character); validates true",
"data": "eyJmb28iOi%iYmFyIn0K",
"schema_id": "content_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores non-strings",
"data": 100,
"schema_id": "content_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "validation of binary-encoded media type documents",
"database": {
"schemas": [
{
"contentMediaType": "application/json",
"contentEncoding": "base64",
"$id": "content_2_0"
}
]
},
"tests": [
{
"description": "a valid base64-encoded JSON document",
"data": "eyJmb28iOiAiYmFyIn0K",
"schema_id": "content_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a validly-encoded invalid JSON document; validates true",
"data": "ezp9Cg==",
"schema_id": "content_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid base64 string that is valid JSON; validates true",
"data": "{}",
"schema_id": "content_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores non-strings",
"data": 100,
"schema_id": "content_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "validation of binary-encoded media type documents with schema",
"database": {
"schemas": [
{
"contentMediaType": "application/json",
"contentEncoding": "base64",
"contentSchema": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string"
},
"boo": {
"type": "integer"
}
}
},
"$id": "content_3_0"
}
]
},
"tests": [
{
"description": "a valid base64-encoded JSON document",
"data": "eyJmb28iOiAiYmFyIn0K",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "another valid base64-encoded JSON document",
"data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid base64-encoded JSON document; validates true",
"data": "eyJib28iOiAyMH0=",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an empty object as a base64-encoded JSON document; validates true",
"data": "e30=",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an empty array as a base64-encoded JSON document",
"data": "W10=",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a validly-encoded invalid JSON document; validates true",
"data": "ezp9Cg==",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid base64 string that is valid JSON; validates true",
"data": "{}",
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores non-strings",
"data": 100,
"schema_id": "content_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

748
fixtures/dependencies.json Normal file
View File

@ -0,0 +1,748 @@
[
{
"description": "single dependency (required)",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema1",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
}
]
},
"tests": [
{
"description": "neither",
"data": {},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nondependant",
"data": {
"foo": 1
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "with dependency",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "missing dependency",
"data": {
"bar": 2
},
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays",
"data": [
"bar"
],
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "foobar",
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "schema1",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "empty dependents",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema2",
"dependencies": {
"bar": []
},
"extensible": true
}
]
},
"tests": [
{
"description": "empty object",
"data": {},
"schema_id": "schema2",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with one property",
"data": {
"bar": 2
},
"schema_id": "schema2",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-object is valid",
"data": 1,
"schema_id": "schema2",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "multiple dependents required",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema3",
"dependencies": {
"quux": [
"foo",
"bar"
]
},
"extensible": true
}
]
},
"tests": [
{
"description": "neither",
"data": {},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nondependants",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "with dependencies",
"data": {
"foo": 1,
"bar": 2,
"quux": 3
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "missing dependency",
"data": {
"foo": 1,
"quux": 2
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "missing other dependency",
"data": {
"bar": 1,
"quux": 2
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "missing both dependencies",
"data": {
"quux": 1
},
"schema_id": "schema3",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "dependencies with escaped characters",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema4",
"dependencies": {
"foo\nbar": [
"foo\rbar"
],
"foo\"bar": [
"foo'bar"
]
},
"extensible": true
}
]
},
"tests": [
{
"description": "CRLF",
"data": {
"foo\nbar": 1,
"foo\rbar": 2
},
"schema_id": "schema4",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "quoted quotes",
"data": {
"foo'bar": 1,
"foo\"bar": 2
},
"schema_id": "schema4",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "CRLF missing dependent",
"data": {
"foo\nbar": 1,
"foo": 2
},
"schema_id": "schema4",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "quoted quotes missing dependent",
"data": {
"foo\"bar": 2
},
"schema_id": "schema4",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties in dependentRequired",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema5",
"dependencies": {
"bar": [
"foo"
]
},
"extensible": true
}
]
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"schema_id": "schema5",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "single dependency (schemas, STRICT)",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema1",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
}
}
]
},
"tests": [
{
"description": "valid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "no dependency",
"data": {
"foo": "quux"
},
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong type",
"data": {
"foo": "quux",
"bar": 2
},
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "wrong type other",
"data": {
"foo": 2,
"bar": "quux"
},
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "wrong type both",
"data": {
"foo": "quux",
"bar": "quux"
},
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays (invalid in strict mode)",
"data": [
"bar"
],
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_ITEM_VIOLATION"
}
]
}
},
{
"description": "ignores strings",
"data": "foobar",
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "single dependency (schemas, EXTENSIBLE)",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema2",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"bar": {
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
},
"extensible": true
}
]
},
"tests": [
{
"description": "ignores arrays (valid in extensible mode)",
"data": [
"bar"
],
"schema_id": "schema_schema2",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "boolean subschemas",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema3",
"properties": {
"foo": true,
"bar": true
},
"dependencies": {
"foo": true,
"bar": false
}
}
]
},
"tests": [
{
"description": "object with property having schema true is valid",
"data": {
"foo": 1
},
"schema_id": "schema_schema3",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with property having schema false is invalid",
"data": {
"bar": 2
},
"schema_id": "schema_schema3",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object with both properties is invalid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "schema_schema3",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "schema_schema3",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "dependencies with escaped characters",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema4",
"properties": {
"foo\tbar": true,
"foo'bar": true,
"a": true,
"b": true,
"c": true
},
"dependencies": {
"foo\tbar": {
"minProperties": 4,
"extensible": true
},
"foo'bar": {
"required": [
"foo\"bar"
]
}
}
}
]
},
"tests": [
{
"description": "quoted tab",
"data": {
"foo\tbar": 1,
"a": 2,
"b": 3,
"c": 4
},
"schema_id": "schema_schema4",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "quoted quote",
"data": {
"foo'bar": {
"foo\"bar": 1
}
},
"schema_id": "schema_schema4",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "quoted tab invalid under dependent schema",
"data": {
"foo\tbar": 1,
"a": 2
},
"schema_id": "schema_schema4",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "quoted quote invalid under dependent schema",
"data": {
"foo'bar": 1
},
"schema_id": "schema_schema4",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "dependent subschema incompatible with root (STRICT)",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema5",
"properties": {
"foo": {},
"baz": true
},
"dependencies": {
"foo": {
"properties": {
"bar": {}
}
}
}
}
]
},
"tests": [
{
"description": "matches root",
"data": {
"foo": 1
},
"schema_id": "schema_schema5",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "matches dependency (invalid in strict mode - bar not allowed if foo missing)",
"data": {
"bar": 1
},
"schema_id": "schema_schema5",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_PROPERTY_VIOLATION"
}
]
}
},
{
"description": "matches both",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "schema_schema5",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "no dependency",
"data": {
"baz": 1
},
"schema_id": "schema_schema5",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "dependent subschema incompatible with root (EXTENSIBLE)",
"database": {
"schemas": [
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "schema_schema6",
"properties": {
"foo": {},
"baz": true
},
"dependencies": {
"foo": {
"properties": {
"bar": {}
},
"additionalProperties": false
}
},
"extensible": true
}
]
},
"tests": [
{
"description": "matches dependency (valid in extensible mode)",
"data": {
"bar": 1
},
"schema_id": "schema_schema6",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

163
fixtures/emptyString.json Normal file
View File

@ -0,0 +1,163 @@
[
{
"description": "empty string is valid for all types (except const)",
"database": {
"schemas": [
{
"properties": {
"obj": {
"type": "object"
},
"arr": {
"type": "array"
},
"str": {
"type": "string"
},
"int": {
"type": "integer"
},
"num": {
"type": "number"
},
"bool": {
"type": "boolean"
},
"nul": {
"type": "null"
},
"fmt": {
"type": "string",
"format": "uuid"
},
"con": {
"const": "value"
},
"con_empty": {
"const": ""
}
},
"$id": "emptyString_0_0"
}
]
},
"tests": [
{
"description": "empty string valid for object",
"data": {
"obj": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for array",
"data": {
"arr": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for string",
"data": {
"str": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for integer",
"data": {
"int": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for number",
"data": {
"num": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for boolean",
"data": {
"bool": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for null",
"data": {
"nul": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string valid for format",
"data": {
"fmt": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty string INVALID for const (unless const is empty string)",
"data": {
"con": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "CONST_VIOLATED",
"path": "/con"
}
]
}
},
{
"description": "empty string VALID for const if const IS empty string",
"data": {
"con_empty": ""
},
"schema_id": "emptyString_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

736
fixtures/enum.json Normal file
View File

@ -0,0 +1,736 @@
[
{
"description": "simple enum validation",
"database": {
"schemas": [
{
"enum": [
1,
2,
3
],
"$id": "enum_0_0"
}
]
},
"tests": [
{
"description": "one of the enum is valid",
"data": 1,
"schema_id": "enum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "something else is invalid",
"data": 4,
"schema_id": "enum_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "heterogeneous enum validation",
"database": {
"schemas": [
{
"enum": [
6,
"foo",
[],
true,
{
"foo": 12
}
],
"properties": {
"foo": {}
},
"$id": "enum_1_0"
}
]
},
"tests": [
{
"description": "one of the enum is valid",
"data": [],
"schema_id": "enum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "something else is invalid",
"data": null,
"schema_id": "enum_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "objects are deep compared",
"data": {
"foo": false
},
"schema_id": "enum_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "valid object matches",
"data": {
"foo": 12
},
"schema_id": "enum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "extra properties in object is invalid",
"data": {
"foo": 12,
"boo": 42
},
"schema_id": "enum_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "heterogeneous enum-with-null validation",
"database": {
"schemas": [
{
"enum": [
6,
null
],
"$id": "enum_2_0"
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"schema_id": "enum_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "number is valid",
"data": 6,
"schema_id": "enum_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "something else is invalid",
"data": "test",
"schema_id": "enum_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enums in properties",
"database": {
"schemas": [
{
"type": "object",
"properties": {
"foo": {
"enum": [
"foo"
]
},
"bar": {
"enum": [
"bar"
]
}
},
"required": [
"bar"
],
"$id": "enum_3_0"
}
]
},
"tests": [
{
"description": "both properties are valid",
"data": {
"foo": "foo",
"bar": "bar"
},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong foo value",
"data": {
"foo": "foot",
"bar": "bar"
},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "wrong bar value",
"data": {
"foo": "foo",
"bar": "bart"
},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "missing optional property is valid",
"data": {
"bar": "bar"
},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "missing required property is invalid",
"data": {
"foo": "foo"
},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "missing all properties is invalid",
"data": {},
"schema_id": "enum_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with escaped characters",
"database": {
"schemas": [
{
"enum": [
"foo\nbar",
"foo\rbar"
],
"$id": "enum_4_0"
}
]
},
"tests": [
{
"description": "member 1 is valid",
"data": "foo\nbar",
"schema_id": "enum_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "member 2 is valid",
"data": "foo\rbar",
"schema_id": "enum_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "another string is invalid",
"data": "abc",
"schema_id": "enum_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with false does not match 0",
"database": {
"schemas": [
{
"enum": [
false
],
"$id": "enum_5_0"
}
]
},
"tests": [
{
"description": "false is valid",
"data": false,
"schema_id": "enum_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer zero is invalid",
"data": 0,
"schema_id": "enum_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float zero is invalid",
"data": 0,
"schema_id": "enum_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with [false] does not match [0]",
"database": {
"schemas": [
{
"enum": [
[
false
]
],
"$id": "enum_6_0"
}
]
},
"tests": [
{
"description": "[false] is valid",
"data": [
false
],
"schema_id": "enum_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[0] is invalid",
"data": [
0
],
"schema_id": "enum_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[0.0] is invalid",
"data": [
0
],
"schema_id": "enum_6_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with true does not match 1",
"database": {
"schemas": [
{
"enum": [
true
],
"$id": "enum_7_0"
}
]
},
"tests": [
{
"description": "true is valid",
"data": true,
"schema_id": "enum_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "integer one is invalid",
"data": 1,
"schema_id": "enum_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "float one is invalid",
"data": 1,
"schema_id": "enum_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with [true] does not match [1]",
"database": {
"schemas": [
{
"enum": [
[
true
]
],
"$id": "enum_8_0"
}
]
},
"tests": [
{
"description": "[true] is valid",
"data": [
true
],
"schema_id": "enum_8_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[1] is invalid",
"data": [
1
],
"schema_id": "enum_8_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[1.0] is invalid",
"data": [
1
],
"schema_id": "enum_8_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "enum with 0 does not match false",
"database": {
"schemas": [
{
"enum": [
0
],
"$id": "enum_9_0"
}
]
},
"tests": [
{
"description": "false is invalid",
"data": false,
"schema_id": "enum_9_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "integer zero is valid",
"data": 0,
"schema_id": "enum_9_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float zero is valid",
"data": 0,
"schema_id": "enum_9_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "enum with [0] does not match [false]",
"database": {
"schemas": [
{
"enum": [
[
0
]
],
"$id": "enum_10_0"
}
]
},
"tests": [
{
"description": "[false] is invalid",
"data": [
false
],
"schema_id": "enum_10_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[0] is valid",
"data": [
0
],
"schema_id": "enum_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[0.0] is valid",
"data": [
0
],
"schema_id": "enum_10_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "enum with 1 does not match true",
"database": {
"schemas": [
{
"enum": [
1
],
"$id": "enum_11_0"
}
]
},
"tests": [
{
"description": "true is invalid",
"data": true,
"schema_id": "enum_11_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "integer one is valid",
"data": 1,
"schema_id": "enum_11_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float one is valid",
"data": 1,
"schema_id": "enum_11_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "enum with [1] does not match [true]",
"database": {
"schemas": [
{
"enum": [
[
1
]
],
"$id": "enum_12_0"
}
]
},
"tests": [
{
"description": "[true] is invalid",
"data": [
true
],
"schema_id": "enum_12_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "[1] is valid",
"data": [
1
],
"schema_id": "enum_12_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[1.0] is valid",
"data": [
1
],
"schema_id": "enum_12_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "nul characters in strings",
"database": {
"schemas": [
{
"enum": [
"hello\u0000there"
],
"$id": "enum_13_0"
}
]
},
"tests": [
{
"description": "match string with nul",
"data": "hello\u0000there",
"schema_id": "enum_13_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "do not match string lacking nul",
"data": "hellothere",
"schema_id": "enum_13_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties in enum object match",
"database": {
"schemas": [
{
"enum": [
{
"foo": 1
}
],
"extensible": true,
"$id": "enum_14_0"
}
]
},
"tests": [
{
"description": "extra property ignored during strict check, but enum check still applies (mismatch here)",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "enum_14_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "extra property ignored during strict check, enum match succeeds",
"data": {
"foo": 1
},
"schema_id": "enum_14_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

View File

@ -0,0 +1,51 @@
[
{
"description": "exclusiveMaximum validation",
"database": {
"schemas": [
{
"exclusiveMaximum": 3,
"$id": "exclusiveMaximum_0_0"
}
]
},
"tests": [
{
"description": "below the exclusiveMaximum is valid",
"data": 2.2,
"schema_id": "exclusiveMaximum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point is invalid",
"data": 3,
"schema_id": "exclusiveMaximum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "above the exclusiveMaximum is invalid",
"data": 3.5,
"schema_id": "exclusiveMaximum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "x",
"schema_id": "exclusiveMaximum_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

View File

@ -0,0 +1,51 @@
[
{
"description": "exclusiveMinimum validation",
"database": {
"schemas": [
{
"exclusiveMinimum": 1.1,
"$id": "exclusiveMinimum_0_0"
}
]
},
"tests": [
{
"description": "above the exclusiveMinimum is valid",
"data": 1.2,
"schema_id": "exclusiveMinimum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point is invalid",
"data": 1.1,
"schema_id": "exclusiveMinimum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "below the exclusiveMinimum is invalid",
"data": 0.6,
"schema_id": "exclusiveMinimum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "x",
"schema_id": "exclusiveMinimum_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

217
fixtures/families.json Normal file
View File

@ -0,0 +1,217 @@
[
{
"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
}
}
]
}
]

5456
fixtures/format.json Normal file

File diff suppressed because it is too large Load Diff

594
fixtures/if-then-else.json Normal file
View File

@ -0,0 +1,594 @@
[
{
"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
}
}
]
}
]

966
fixtures/items.json Normal file
View File

@ -0,0 +1,966 @@
[
{
"description": "a schema given for items",
"database": {
"schemas": [
{
"items": {
"type": "integer"
},
"$id": "items_0_0"
}
]
},
"tests": [
{
"description": "valid items",
"data": [
1,
2,
3
],
"schema_id": "items_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong type of items",
"data": [
1,
"x"
],
"schema_id": "items_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "non-arrays are invalid",
"data": {
"foo": "bar"
},
"schema_id": "items_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "JavaScript pseudo-arrays are invalid",
"data": {
"0": "invalid",
"length": 1
},
"schema_id": "items_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "items with boolean schema (true)",
"database": {
"schemas": [
{
"items": true,
"$id": "items_1_0"
}
]
},
"tests": [
{
"description": "any array is valid",
"data": [
1,
"foo",
true
],
"schema_id": "items_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty array is valid",
"data": [],
"schema_id": "items_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items with boolean schema (false)",
"database": {
"schemas": [
{
"items": false,
"$id": "items_2_0"
}
]
},
"tests": [
{
"description": "any non-empty array is invalid",
"data": [
1,
"foo",
true
],
"schema_id": "items_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is valid",
"data": [],
"schema_id": "items_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items and subitems",
"database": {
"schemas": [
{
"type": "array",
"items": false,
"prefixItems": [
{
"$ref": "item"
},
{
"$ref": "item"
},
{
"$ref": "item"
}
],
"$id": "items_3_0"
},
{
"$id": "item",
"type": "array",
"items": false,
"prefixItems": [
{
"$ref": "sub-item"
},
{
"$ref": "sub-item"
}
]
},
{
"$id": "sub-item",
"type": "object",
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "valid items",
"data": [
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "too many items",
"data": [
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "too many sub-items",
"data": [
[
{
"foo": null
},
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "wrong item",
"data": [
{
"foo": null
},
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "wrong sub-item",
"data": [
[
{},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "fewer items is invalid",
"data": [
[
{
"foo": null
}
],
[
{
"foo": null
}
]
],
"schema_id": "items_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "nested items",
"database": {
"schemas": [
{
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"$id": "items_4_0"
}
]
},
"tests": [
{
"description": "valid nested array",
"data": [
[
[
[
1
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"schema_id": "items_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nested array with invalid type",
"data": [
[
[
[
"1"
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"schema_id": "items_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "not deep enough",
"data": [
[
[
1
],
[
2
],
[
3
]
],
[
[
4
],
[
5
],
[
6
]
]
],
"schema_id": "items_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "prefixItems with no additional items allowed",
"database": {
"schemas": [
{
"prefixItems": [
{},
{},
{}
],
"items": false,
"$id": "items_5_0"
}
]
},
"tests": [
{
"description": "empty array",
"data": [],
"schema_id": "items_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "fewer number of items present (1)",
"data": [
1
],
"schema_id": "items_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "fewer number of items present (2)",
"data": [
1,
2
],
"schema_id": "items_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "equal number of items present",
"data": [
1,
2,
3
],
"schema_id": "items_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "additional items are not permitted",
"data": [
1,
2,
3,
4
],
"schema_id": "items_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "items does not look in applicators, valid case",
"database": {
"schemas": [
{
"allOf": [
{
"prefixItems": [
{
"minimum": 3
}
]
}
],
"items": {
"minimum": 5
},
"$id": "items_6_0"
}
]
},
"tests": [
{
"description": "prefixItems in allOf does not constrain items, invalid case",
"data": [
3,
5
],
"schema_id": "items_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "prefixItems in allOf does not constrain items, valid case",
"data": [
5,
5
],
"schema_id": "items_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "prefixItems validation adjusts the starting index for items",
"database": {
"schemas": [
{
"prefixItems": [
{
"type": "string"
}
],
"items": {
"type": "integer"
},
"$id": "items_7_0"
}
]
},
"tests": [
{
"description": "valid items",
"data": [
"x",
2,
3
],
"schema_id": "items_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong type of second item",
"data": [
"x",
"y"
],
"schema_id": "items_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "items with heterogeneous array",
"database": {
"schemas": [
{
"prefixItems": [
{}
],
"items": false,
"$id": "items_8_0"
}
]
},
"tests": [
{
"description": "heterogeneous invalid instance",
"data": [
"foo",
"bar",
37
],
"schema_id": "items_8_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "valid instance",
"data": [
null
],
"schema_id": "items_8_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items with null instance elements",
"database": {
"schemas": [
{
"items": {
"type": "null"
},
"$id": "items_9_0"
}
]
},
"tests": [
{
"description": "allows null elements",
"data": [
null
],
"schema_id": "items_9_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra items (when items is false)",
"database": {
"schemas": [
{
"items": false,
"extensible": true,
"$id": "items_10_0"
}
]
},
"tests": [
{
"description": "extra item is valid",
"data": [
1
],
"schema_id": "items_10_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties for items",
"database": {
"schemas": [
{
"items": {
"minimum": 5
},
"extensible": true,
"$id": "items_11_0"
}
]
},
"tests": [
{
"description": "valid item is valid",
"data": [
5,
6
],
"schema_id": "items_11_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid item (less than min) is invalid even with extensible: true",
"data": [
4
],
"schema_id": "items_11_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "array: simple extensible array",
"database": {
"schemas": [
{
"type": "array",
"extensible": true,
"$id": "items_12_0"
}
]
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"schema_id": "items_12_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with items is valid (extensible)",
"data": [
1,
"foo"
],
"schema_id": "items_12_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "array: strict array",
"database": {
"schemas": [
{
"type": "array",
"extensible": false,
"$id": "items_13_0"
}
]
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"schema_id": "items_13_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with items is invalid (strict)",
"data": [
1
],
"schema_id": "items_13_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "array: items extensible",
"database": {
"schemas": [
{
"type": "array",
"items": {
"extensible": true
},
"$id": "items_14_0"
}
]
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"schema_id": "items_14_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with items is valid (items explicitly allowed to be anything extensible)",
"data": [
1,
"foo",
{}
],
"schema_id": "items_14_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "array: items strict",
"database": {
"schemas": [
{
"type": "array",
"items": {
"type": "object",
"extensible": false
},
"$id": "items_15_0"
}
]
},
"tests": [
{
"description": "empty array is valid (empty objects)",
"data": [
{}
],
"schema_id": "items_15_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with strict object items is valid",
"data": [
{}
],
"schema_id": "items_15_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with invalid strict object items (extra property)",
"data": [
{
"extra": 1
}
],
"schema_id": "items_15_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

235
fixtures/maxContains.json Normal file
View File

@ -0,0 +1,235 @@
[
{
"description": "maxContains without contains is ignored",
"database": {
"schemas": [
{
"maxContains": 1,
"extensible": true,
"$id": "maxContains_0_0"
}
]
},
"tests": [
{
"description": "one item valid against lone maxContains",
"data": [
1
],
"schema_id": "maxContains_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "two items still valid against lone maxContains",
"data": [
1,
2
],
"schema_id": "maxContains_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxContains with contains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_1_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "maxContains_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, valid maxContains",
"data": [
1
],
"schema_id": "maxContains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "all elements match, invalid maxContains",
"data": [
1,
1
],
"schema_id": "maxContains_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "some elements match, valid maxContains",
"data": [
1,
2
],
"schema_id": "maxContains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "some elements match, invalid maxContains",
"data": [
1,
2,
1
],
"schema_id": "maxContains_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "maxContains with contains, value with a decimal",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_2_0"
}
]
},
"tests": [
{
"description": "one element matches, valid maxContains",
"data": [
1
],
"schema_id": "maxContains_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too many elements match, invalid maxContains",
"data": [
1,
1
],
"schema_id": "maxContains_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "minContains < maxContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 1,
"maxContains": 3,
"extensible": true,
"$id": "maxContains_3_0"
}
]
},
"tests": [
{
"description": "actual < minContains < maxContains",
"data": [],
"schema_id": "maxContains_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "minContains < actual < maxContains",
"data": [
1,
1
],
"schema_id": "maxContains_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "minContains < maxContains < actual",
"data": [
1,
1,
1,
1
],
"schema_id": "maxContains_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows non-matching items in maxContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true,
"$id": "maxContains_4_0"
}
]
},
"tests": [
{
"description": "extra items disregarded for maxContains",
"data": [
1,
2
],
"schema_id": "maxContains_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

126
fixtures/maxItems.json Normal file
View File

@ -0,0 +1,126 @@
[
{
"description": "maxItems validation",
"database": {
"schemas": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_0_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": [
1
],
"schema_id": "maxItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": [
1,
2
],
"schema_id": "maxItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": [
1,
2,
3
],
"schema_id": "maxItems_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-arrays",
"data": "foobar",
"schema_id": "maxItems_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxItems validation with a decimal",
"database": {
"schemas": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_1_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": [
1
],
"schema_id": "maxItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": [
1,
2,
3
],
"schema_id": "maxItems_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra items in maxItems (but counted)",
"database": {
"schemas": [
{
"maxItems": 2,
"extensible": true,
"$id": "maxItems_2_0"
}
]
},
"tests": [
{
"description": "extra item counted towards maxItems",
"data": [
1,
2,
3
],
"schema_id": "maxItems_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

91
fixtures/maxLength.json Normal file
View File

@ -0,0 +1,91 @@
[
{
"description": "maxLength validation",
"database": {
"schemas": [
{
"maxLength": 2,
"$id": "maxLength_0_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": "f",
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": "fo",
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": "foo",
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-strings",
"data": 100,
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "two graphemes is long enough",
"data": "💩💩",
"schema_id": "maxLength_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxLength validation with a decimal",
"database": {
"schemas": [
{
"maxLength": 2,
"$id": "maxLength_1_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": "f",
"schema_id": "maxLength_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": "foo",
"schema_id": "maxLength_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

193
fixtures/maxProperties.json Normal file
View File

@ -0,0 +1,193 @@
[
{
"description": "maxProperties validation",
"database": {
"schemas": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_0_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": {
"foo": 1
},
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays",
"data": [
1,
2,
3
],
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "foobar",
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "maxProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxProperties validation with a decimal",
"database": {
"schemas": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_1_0"
}
]
},
"tests": [
{
"description": "shorter is valid",
"data": {
"foo": 1
},
"schema_id": "maxProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too long is invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"schema_id": "maxProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "maxProperties = 0 means the object is empty",
"database": {
"schemas": [
{
"maxProperties": 0,
"extensible": true,
"$id": "maxProperties_2_0"
}
]
},
"tests": [
{
"description": "no properties is valid",
"data": {},
"schema_id": "maxProperties_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "one property is invalid",
"data": {
"foo": 1
},
"schema_id": "maxProperties_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties in maxProperties (though maxProperties still counts them!)",
"database": {
"schemas": [
{
"maxProperties": 2,
"extensible": true,
"$id": "maxProperties_3_0"
}
]
},
"tests": [
{
"description": "extra property is counted towards maxProperties",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"schema_id": "maxProperties_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "extra property is valid if below maxProperties",
"data": {
"foo": 1
},
"schema_id": "maxProperties_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

100
fixtures/maximum.json Normal file
View File

@ -0,0 +1,100 @@
[
{
"description": "maximum validation",
"database": {
"schemas": [
{
"maximum": 3,
"$id": "maximum_0_0"
}
]
},
"tests": [
{
"description": "below the maximum is valid",
"data": 2.6,
"schema_id": "maximum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point is valid",
"data": 3,
"schema_id": "maximum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "above the maximum is invalid",
"data": 3.5,
"schema_id": "maximum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "x",
"schema_id": "maximum_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maximum validation with unsigned integer",
"database": {
"schemas": [
{
"maximum": 300,
"$id": "maximum_1_0"
}
]
},
"tests": [
{
"description": "below the maximum is invalid",
"data": 299.97,
"schema_id": "maximum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point integer is valid",
"data": 300,
"schema_id": "maximum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point float is valid",
"data": 300,
"schema_id": "maximum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "above the maximum is invalid",
"data": 300.5,
"schema_id": "maximum_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

274
fixtures/merge.json Normal file
View File

@ -0,0 +1,274 @@
[
{
"description": "merging: properties accumulate",
"database": {
"schemas": [
{
"$id": "base_0",
"properties": {
"base_prop": {
"type": "string"
}
}
},
{
"$ref": "base_0",
"properties": {
"child_prop": {
"type": "string"
}
},
"$id": "merge_0_0"
}
]
},
"tests": [
{
"description": "valid with both properties",
"data": {
"base_prop": "a",
"child_prop": "b"
},
"schema_id": "merge_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid when base property has wrong type",
"data": {
"base_prop": 1,
"child_prop": "b"
},
"schema_id": "merge_0_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "TYPE_MISMATCH",
"path": "/base_prop"
}
]
}
}
]
},
{
"description": "merging: required fields accumulate",
"database": {
"schemas": [
{
"$id": "base_1",
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
},
{
"$ref": "base_1",
"properties": {
"b": {
"type": "string"
}
},
"required": [
"b"
],
"$id": "merge_1_0"
}
]
},
"tests": [
{
"description": "valid when both present",
"data": {
"a": "ok",
"b": "ok"
},
"schema_id": "merge_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid when base required missing",
"data": {
"b": "ok"
},
"schema_id": "merge_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"path": "/a"
}
]
}
},
{
"description": "invalid when child required missing",
"data": {
"a": "ok"
},
"schema_id": "merge_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"path": "/b"
}
]
}
}
]
},
{
"description": "merging: dependencies accumulate",
"database": {
"schemas": [
{
"$id": "base_2",
"properties": {
"trigger": {
"type": "string"
},
"base_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"base_dep"
]
}
},
{
"$ref": "base_2",
"properties": {
"child_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"child_dep"
]
},
"$id": "merge_2_0"
}
]
},
"tests": [
{
"description": "valid with all deps",
"data": {
"trigger": "go",
"base_dep": "ok",
"child_dep": "ok"
},
"schema_id": "merge_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid missing base dep",
"data": {
"trigger": "go",
"child_dep": "ok"
},
"schema_id": "merge_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "DEPENDENCY_FAILED",
"path": "/base_dep"
}
]
}
},
{
"description": "invalid missing child dep",
"data": {
"trigger": "go",
"base_dep": "ok"
},
"schema_id": "merge_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "DEPENDENCY_FAILED",
"path": "/child_dep"
}
]
}
}
]
},
{
"description": "merging: form and display do NOT merge",
"database": {
"schemas": [
{
"$id": "base_3",
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
}
},
"form": [
"a",
"b"
]
},
{
"$ref": "base_3",
"properties": {
"c": {
"type": "string"
}
},
"form": [
"c"
],
"$id": "merge_3_0"
}
]
},
"tests": [
{
"description": "child schema validation",
"data": {
"a": "ok",
"b": "ok",
"c": "ok"
},
"comment": "Verifies validator handles the unmerged metadata correctly (ignores it or handles replacement)",
"schema_id": "merge_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

2061
fixtures/merger.json Normal file

File diff suppressed because it is too large Load Diff

477
fixtures/minContains.json Normal file
View File

@ -0,0 +1,477 @@
[
{
"description": "minContains without contains is ignored",
"database": {
"schemas": [
{
"minContains": 1,
"extensible": true,
"$id": "minContains_0_0"
}
]
},
"tests": [
{
"description": "one item valid against lone minContains",
"data": [
1
],
"schema_id": "minContains_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "zero items still valid against lone minContains",
"data": [],
"schema_id": "minContains_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minContains=1 with contains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true,
"$id": "minContains_1_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "no elements match",
"data": [
2
],
"schema_id": "minContains_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "single element matches, valid minContains",
"data": [
1
],
"schema_id": "minContains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "some elements match, valid minContains",
"data": [
1,
2
],
"schema_id": "minContains_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "all elements match, valid minContains",
"data": [
1,
1
],
"schema_id": "minContains_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minContains=2 with contains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true,
"$id": "minContains_2_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, invalid minContains",
"data": [
1
],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "some elements match, invalid minContains",
"data": [
1,
2
],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, valid minContains (exactly as needed)",
"data": [
1,
1
],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "all elements match, valid minContains (more than needed)",
"data": [
1,
1,
1
],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "some elements match, valid minContains",
"data": [
1,
2,
1
],
"schema_id": "minContains_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minContains=2 with contains with a decimal value",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true,
"$id": "minContains_3_0"
}
]
},
"tests": [
{
"description": "one element matches, invalid minContains",
"data": [
1
],
"schema_id": "minContains_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "both elements match, valid minContains",
"data": [
1,
1
],
"schema_id": "minContains_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxContains = minContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"maxContains": 2,
"minContains": 2,
"extensible": true,
"$id": "minContains_4_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, invalid minContains",
"data": [
1
],
"schema_id": "minContains_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, invalid maxContains",
"data": [
1,
1,
1
],
"schema_id": "minContains_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all elements match, valid maxContains and minContains",
"data": [
1,
1
],
"schema_id": "minContains_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "maxContains < minContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"maxContains": 1,
"minContains": 3,
"extensible": true,
"$id": "minContains_5_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "invalid minContains",
"data": [
1
],
"schema_id": "minContains_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "invalid maxContains",
"data": [
1,
1,
1
],
"schema_id": "minContains_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "invalid maxContains and minContains",
"data": [
1,
1
],
"schema_id": "minContains_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "minContains = 0",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 0,
"extensible": true,
"$id": "minContains_6_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "minContains = 0 makes contains always pass",
"data": [
2
],
"schema_id": "minContains_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minContains = 0 with maxContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 0,
"maxContains": 1,
"extensible": true,
"$id": "minContains_7_0"
}
]
},
"tests": [
{
"description": "empty data",
"data": [],
"schema_id": "minContains_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "not more than maxContains",
"data": [
1
],
"schema_id": "minContains_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too many",
"data": [
1,
1
],
"schema_id": "minContains_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows non-matching items in minContains",
"database": {
"schemas": [
{
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true,
"$id": "minContains_8_0"
}
]
},
"tests": [
{
"description": "extra items disregarded for minContains",
"data": [
1,
2
],
"schema_id": "minContains_8_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

117
fixtures/minItems.json Normal file
View File

@ -0,0 +1,117 @@
[
{
"description": "minItems validation",
"database": {
"schemas": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_0_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": [
1,
2
],
"schema_id": "minItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": [
1
],
"schema_id": "minItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": [],
"schema_id": "minItems_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-arrays",
"data": "",
"schema_id": "minItems_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minItems validation with a decimal",
"database": {
"schemas": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_1_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": [
1,
2
],
"schema_id": "minItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": [],
"schema_id": "minItems_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra items in minItems",
"database": {
"schemas": [
{
"minItems": 1,
"extensible": true,
"$id": "minItems_2_0"
}
]
},
"tests": [
{
"description": "extra item counted towards minItems",
"data": [
1
],
"schema_id": "minItems_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

91
fixtures/minLength.json Normal file
View File

@ -0,0 +1,91 @@
[
{
"description": "minLength validation",
"database": {
"schemas": [
{
"minLength": 2,
"$id": "minLength_0_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": "foo",
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": "fo",
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": "f",
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-strings",
"data": 1,
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "one grapheme is not long enough",
"data": "💩",
"schema_id": "minLength_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "minLength validation with a decimal",
"database": {
"schemas": [
{
"minLength": 2,
"$id": "minLength_1_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": "foo",
"schema_id": "minLength_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": "f",
"schema_id": "minLength_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

135
fixtures/minProperties.json Normal file
View File

@ -0,0 +1,135 @@
[
{
"description": "minProperties validation",
"database": {
"schemas": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_0_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "exact length is valid",
"data": {
"foo": 1
},
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": {},
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays",
"data": [],
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "",
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "minProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minProperties validation with a decimal",
"database": {
"schemas": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_1_0"
}
]
},
"tests": [
{
"description": "longer is valid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "minProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "too short is invalid",
"data": {},
"schema_id": "minProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties in minProperties",
"database": {
"schemas": [
{
"minProperties": 1,
"extensible": true,
"$id": "minProperties_2_0"
}
]
},
"tests": [
{
"description": "extra property counts towards minProperties",
"data": {
"foo": 1
},
"schema_id": "minProperties_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

127
fixtures/minimum.json Normal file
View File

@ -0,0 +1,127 @@
[
{
"description": "minimum validation",
"database": {
"schemas": [
{
"minimum": 1.1,
"$id": "minimum_0_0"
}
]
},
"tests": [
{
"description": "above the minimum is valid",
"data": 2.6,
"schema_id": "minimum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point is valid",
"data": 1.1,
"schema_id": "minimum_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "below the minimum is invalid",
"data": 0.6,
"schema_id": "minimum_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "x",
"schema_id": "minimum_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "minimum validation with signed integer",
"database": {
"schemas": [
{
"minimum": -2,
"$id": "minimum_1_0"
}
]
},
"tests": [
{
"description": "negative above the minimum is valid",
"data": -1,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "positive above the minimum is valid",
"data": 0,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point is valid",
"data": -2,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boundary point with float is valid",
"data": -2,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "float below the minimum is invalid",
"data": -2.0001,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "int below the minimum is invalid",
"data": -3,
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "x",
"schema_id": "minimum_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

136
fixtures/multipleOf.json Normal file
View File

@ -0,0 +1,136 @@
[
{
"description": "by int",
"database": {
"schemas": [
{
"multipleOf": 2,
"$id": "multipleOf_0_0"
}
]
},
"tests": [
{
"description": "int by int",
"data": 10,
"schema_id": "multipleOf_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "int by int fail",
"data": 7,
"schema_id": "multipleOf_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores non-numbers",
"data": "foo",
"schema_id": "multipleOf_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "by number",
"database": {
"schemas": [
{
"multipleOf": 1.5,
"$id": "multipleOf_1_0"
}
]
},
"tests": [
{
"description": "zero is multiple of anything",
"data": 0,
"schema_id": "multipleOf_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "4.5 is multiple of 1.5",
"data": 4.5,
"schema_id": "multipleOf_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "35 is not multiple of 1.5",
"data": 35,
"schema_id": "multipleOf_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "by small number",
"database": {
"schemas": [
{
"multipleOf": 0.0001,
"$id": "multipleOf_2_0"
}
]
},
"tests": [
{
"description": "0.0075 is multiple of 0.0001",
"data": 0.0075,
"schema_id": "multipleOf_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "0.00751 is not multiple of 0.0001",
"data": 0.00751,
"schema_id": "multipleOf_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "small multiple of large integer",
"database": {
"schemas": [
{
"type": "integer",
"multipleOf": 1e-8,
"$id": "multipleOf_3_0"
}
]
},
"tests": [
{
"description": "any integer is a multiple of 1e-8",
"data": 12391239123,
"schema_id": "multipleOf_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

618
fixtures/not.json Normal file
View File

@ -0,0 +1,618 @@
[
{
"description": "not",
"database": {
"schemas": [
{
"not": {
"type": "integer"
},
"$id": "not_0_0"
}
]
},
"tests": [
{
"description": "allowed",
"data": "foo",
"schema_id": "not_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "disallowed",
"data": 1,
"schema_id": "not_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "not multiple types",
"database": {
"schemas": [
{
"not": {
"type": [
"integer",
"boolean"
]
},
"$id": "not_1_0"
}
]
},
"tests": [
{
"description": "valid",
"data": "foo",
"schema_id": "not_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "mismatch",
"data": 1,
"schema_id": "not_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "other mismatch",
"data": true,
"schema_id": "not_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "not more complex schema",
"database": {
"schemas": [
{
"not": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
},
"extensible": true,
"$id": "not_2_0"
}
]
},
"tests": [
{
"description": "match",
"data": 1,
"schema_id": "not_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "other match",
"data": {
"foo": 1
},
"schema_id": "not_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "mismatch",
"data": {
"foo": "bar"
},
"schema_id": "not_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "forbidden property",
"database": {
"schemas": [
{
"properties": {
"foo": {
"not": {}
}
},
"$id": "not_3_0"
}
]
},
"tests": [
{
"description": "property present",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "not_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "not_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "forbid everything with empty schema",
"database": {
"schemas": [
{
"not": {},
"$id": "not_4_0"
}
]
},
"tests": [
{
"description": "number is invalid",
"data": 1,
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "string is invalid",
"data": "foo",
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean true is invalid",
"data": true,
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean false is invalid",
"data": false,
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is invalid",
"data": null,
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is invalid",
"data": {},
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "array is invalid",
"data": [
"foo"
],
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "not_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "forbid everything with boolean schema true",
"database": {
"schemas": [
{
"not": true,
"$id": "not_5_0"
}
]
},
"tests": [
{
"description": "number is invalid",
"data": 1,
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "string is invalid",
"data": "foo",
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean true is invalid",
"data": true,
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "boolean false is invalid",
"data": false,
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is invalid",
"data": null,
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is invalid",
"data": {},
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "array is invalid",
"data": [
"foo"
],
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is invalid",
"data": [],
"schema_id": "not_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "allow everything with boolean schema false",
"database": {
"schemas": [
{
"not": false,
"extensible": true,
"$id": "not_6_0"
}
]
},
"tests": [
{
"description": "number is valid",
"data": 1,
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "string is valid",
"data": "foo",
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boolean true is valid",
"data": true,
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "boolean false is valid",
"data": false,
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "null is valid",
"data": null,
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object is valid",
"data": {
"foo": "bar"
},
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array is valid",
"data": [
"foo"
],
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty array is valid",
"data": [],
"schema_id": "not_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "double negation",
"database": {
"schemas": [
{
"not": {
"not": {}
},
"$id": "not_7_0"
}
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"schema_id": "not_7_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra properties in not",
"database": {
"schemas": [
{
"not": {
"type": "integer"
},
"extensible": true,
"$id": "not_8_0"
}
]
},
"tests": [
{
"description": "extra property is valid (not integer matches)",
"data": {
"foo": 1
},
"schema_id": "not_8_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: false (default) forbids extra properties in not",
"database": {
"schemas": [
{
"not": {
"type": "integer"
},
"$id": "not_9_0"
}
]
},
"tests": [
{
"description": "extra property is invalid due to strictness",
"data": {
"foo": 1
},
"schema_id": "not_9_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "property next to not (extensible: true)",
"database": {
"schemas": [
{
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
},
"extensible": true,
"$id": "not_10_0"
}
]
},
"tests": [
{
"description": "extra property allowed",
"data": {
"bar": "baz",
"foo": 1
},
"schema_id": "not_10_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "property next to not (extensible: false)",
"database": {
"schemas": [
{
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
},
"$id": "not_11_0"
}
]
},
"tests": [
{
"description": "extra property forbidden",
"data": {
"bar": "baz",
"foo": 1
},
"schema_id": "not_11_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "defined property allowed",
"data": {
"bar": "baz"
},
"schema_id": "not_11_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

670
fixtures/oneOf.json Normal file
View File

@ -0,0 +1,670 @@
[
{
"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
}
}
]
}
]

109
fixtures/pattern.json Normal file
View File

@ -0,0 +1,109 @@
[
{
"description": "pattern validation",
"database": {
"schemas": [
{
"pattern": "^a*$",
"$id": "pattern_0_0"
}
]
},
"tests": [
{
"description": "a matching pattern is valid",
"data": "aaa",
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a non-matching pattern is invalid",
"data": "abc",
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores booleans",
"data": true,
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores integers",
"data": 123,
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores floats",
"data": 1,
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores objects",
"data": {},
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores arrays",
"data": [],
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores null",
"data": null,
"schema_id": "pattern_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "pattern is not anchored",
"database": {
"schemas": [
{
"pattern": "a+",
"$id": "pattern_1_0"
}
]
},
"tests": [
{
"description": "matches a substring",
"data": "xxaayy",
"schema_id": "pattern_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

View File

@ -0,0 +1,399 @@
[
{
"description": "patternProperties validates properties matching a regex",
"database": {
"schemas": [
{
"patternProperties": {
"f.*o": {
"type": "integer"
}
},
"items": {},
"$id": "patternProperties_0_0"
}
]
},
"tests": [
{
"description": "a single valid match is valid",
"data": {
"foo": 1
},
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "multiple valid matches is valid",
"data": {
"foo": 1,
"foooooo": 2
},
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a single invalid match is invalid",
"data": {
"foo": "bar",
"fooooo": 2
},
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "multiple invalid matches is invalid",
"data": {
"foo": "bar",
"foooooo": "baz"
},
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays",
"data": [
"foo"
],
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "foo",
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "extra property not matching pattern is INVALID (strict by default)",
"data": {
"foo": 1,
"extra": 2
},
"schema_id": "patternProperties_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "multiple simultaneous patternProperties are validated",
"database": {
"schemas": [
{
"patternProperties": {
"a*": {
"type": "integer"
},
"aaa*": {
"maximum": 20
}
},
"$id": "patternProperties_1_0"
}
]
},
"tests": [
{
"description": "a single valid match is valid",
"data": {
"a": 21
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a simultaneous match is valid",
"data": {
"aaaa": 18
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "multiple matches is valid",
"data": {
"a": 21,
"aaaa": 18
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an invalid due to one is invalid",
"data": {
"a": "bar"
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an invalid due to the other is invalid",
"data": {
"aaaa": 31
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an invalid due to both is invalid",
"data": {
"aaa": "foo",
"aaaa": 31
},
"schema_id": "patternProperties_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "regexes are not anchored by default and are case sensitive",
"database": {
"schemas": [
{
"patternProperties": {
"[0-9]{2,}": {
"type": "boolean"
},
"X_": {
"type": "string"
}
},
"extensible": true,
"$id": "patternProperties_2_0"
}
]
},
"tests": [
{
"description": "non recognized members are ignored",
"data": {
"answer 1": "42"
},
"schema_id": "patternProperties_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "recognized members are accounted for",
"data": {
"a31b": null
},
"schema_id": "patternProperties_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "regexes are case sensitive",
"data": {
"a_x_3": 3
},
"schema_id": "patternProperties_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "regexes are case sensitive, 2",
"data": {
"a_X_3": 3
},
"schema_id": "patternProperties_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "patternProperties with boolean schemas",
"database": {
"schemas": [
{
"patternProperties": {
"f.*": true,
"b.*": false
},
"$id": "patternProperties_3_0"
}
]
},
"tests": [
{
"description": "object with property matching schema true is valid",
"data": {
"foo": 1
},
"schema_id": "patternProperties_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with property matching schema false is invalid",
"data": {
"bar": 2
},
"schema_id": "patternProperties_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object with both properties is invalid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "patternProperties_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object with a property matching both true and false is invalid",
"data": {
"foobar": 1
},
"schema_id": "patternProperties_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "patternProperties_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "patternProperties with null valued instance properties",
"database": {
"schemas": [
{
"patternProperties": {
"^.*bar$": {
"type": "null"
}
},
"$id": "patternProperties_4_0"
}
]
},
"tests": [
{
"description": "allows null values",
"data": {
"foobar": null
},
"schema_id": "patternProperties_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra properties NOT matching pattern",
"database": {
"schemas": [
{
"patternProperties": {
"f.*o": {
"type": "integer"
}
},
"extensible": true,
"$id": "patternProperties_5_0"
}
]
},
"tests": [
{
"description": "extra property not matching pattern is valid",
"data": {
"bar": 1
},
"schema_id": "patternProperties_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "property matching pattern MUST still be valid",
"data": {
"foo": "invalid string"
},
"schema_id": "patternProperties_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

229
fixtures/prefixItems.json Normal file
View File

@ -0,0 +1,229 @@
[
{
"description": "a schema given for prefixItems",
"database": {
"schemas": [
{
"prefixItems": [
{
"type": "integer"
},
{
"type": "string"
}
],
"$id": "prefixItems_0_0"
}
]
},
"tests": [
{
"description": "correct types",
"data": [
1,
"foo"
],
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong types",
"data": [
"foo",
1
],
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "incomplete array of items",
"data": [
1
],
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with additional items (invalid due to strictness)",
"data": [
1,
"foo",
true
],
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array",
"data": [],
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "JavaScript pseudo-array is valid (invalid due to strict object validation)",
"data": {
"0": "invalid",
"1": "valid",
"length": 2
},
"schema_id": "prefixItems_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "prefixItems with boolean schemas",
"database": {
"schemas": [
{
"prefixItems": [
true,
false
],
"$id": "prefixItems_1_0"
}
]
},
"tests": [
{
"description": "array with one item is valid",
"data": [
1
],
"schema_id": "prefixItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with two items is invalid",
"data": [
1,
"foo"
],
"schema_id": "prefixItems_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty array is valid",
"data": [],
"schema_id": "prefixItems_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "additional items are allowed by default",
"database": {
"schemas": [
{
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true,
"$id": "prefixItems_2_0"
}
]
},
"tests": [
{
"description": "only the first item is validated",
"data": [
1,
"foo",
false
],
"schema_id": "prefixItems_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "prefixItems with null instance elements",
"database": {
"schemas": [
{
"prefixItems": [
{
"type": "null"
}
],
"$id": "prefixItems_3_0"
}
]
},
"tests": [
{
"description": "allows null elements",
"data": [
null
],
"schema_id": "prefixItems_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra items with prefixItems",
"database": {
"schemas": [
{
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true,
"$id": "prefixItems_4_0"
}
]
},
"tests": [
{
"description": "extra item is valid",
"data": [
1,
"foo"
],
"schema_id": "prefixItems_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

627
fixtures/properties.json Normal file
View File

@ -0,0 +1,627 @@
[
{
"description": "object properties validation",
"database": {
"schemas": [
{
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "string"
}
},
"$id": "properties_0_0"
}
]
},
"tests": [
{
"description": "both properties present and valid is valid",
"data": {
"foo": 1,
"bar": "baz"
},
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "one property invalid is invalid",
"data": {
"foo": 1,
"bar": {}
},
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "both properties invalid is invalid",
"data": {
"foo": [],
"bar": {}
},
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "doesn't invalidate other properties",
"data": {},
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores arrays",
"data": [],
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "properties_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "properties with boolean schema",
"database": {
"schemas": [
{
"properties": {
"foo": true,
"bar": false
},
"$id": "properties_1_0"
}
]
},
"tests": [
{
"description": "no property present is valid",
"data": {},
"schema_id": "properties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "only 'true' property present is valid",
"data": {
"foo": 1
},
"schema_id": "properties_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "only 'false' property present is invalid",
"data": {
"bar": 2
},
"schema_id": "properties_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "both properties present is invalid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "properties_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "properties with escaped characters",
"database": {
"schemas": [
{
"properties": {
"foo\nbar": {
"type": "number"
},
"foo\"bar": {
"type": "number"
},
"foo\\bar": {
"type": "number"
},
"foo\rbar": {
"type": "number"
},
"foo\tbar": {
"type": "number"
},
"foo\fbar": {
"type": "number"
}
},
"$id": "properties_2_0"
}
]
},
"tests": [
{
"description": "object with all numbers is valid",
"data": {
"foo\nbar": 1,
"foo\"bar": 1,
"foo\\bar": 1,
"foo\rbar": 1,
"foo\tbar": 1,
"foo\fbar": 1
},
"schema_id": "properties_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with strings is invalid",
"data": {
"foo\nbar": "1",
"foo\"bar": "1",
"foo\\bar": "1",
"foo\rbar": "1",
"foo\tbar": "1",
"foo\fbar": "1"
},
"schema_id": "properties_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "properties with null valued instance properties",
"database": {
"schemas": [
{
"properties": {
"foo": {
"type": "null"
}
},
"$id": "properties_3_0"
}
]
},
"tests": [
{
"description": "allows null values",
"data": {
"foo": null
},
"schema_id": "properties_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"database": {
"schemas": [
{
"properties": {
"__proto__": {
"type": "number"
},
"toString": {
"properties": {
"length": {
"type": "string"
}
}
},
"constructor": {
"type": "number"
}
},
"$id": "properties_4_0"
}
]
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "none of the properties mentioned",
"data": {},
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "__proto__ not valid",
"data": {
"__proto__": "foo"
},
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "toString not valid",
"data": {
"toString": {
"length": 37
}
},
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "constructor not valid",
"data": {
"constructor": {
"length": 37
}
},
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all present and valid",
"data": {
"__proto__": 12,
"toString": {
"length": "foo"
},
"constructor": 37
},
"schema_id": "properties_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra properties",
"database": {
"schemas": [
{
"properties": {
"foo": {
"type": "integer"
}
},
"extensible": true,
"$id": "properties_5_0"
}
]
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": "baz"
},
"schema_id": "properties_5_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "strict by default: extra properties invalid",
"database": {
"schemas": [
{
"properties": {
"foo": {
"type": "string"
}
},
"$id": "properties_6_0"
}
]
},
"tests": [
{
"description": "extra property is invalid",
"data": {
"foo": "bar",
"extra": 1
},
"schema_id": "properties_6_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "inheritance: nested object inherits strictness from strict parent",
"database": {
"schemas": [
{
"properties": {
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
},
"$id": "properties_7_0"
}
]
},
"tests": [
{
"description": "nested extra property is invalid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"schema_id": "properties_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "override: nested object allows extra properties if extensible: true",
"database": {
"schemas": [
{
"properties": {
"nested": {
"extensible": true,
"properties": {
"foo": {
"type": "string"
}
}
}
},
"$id": "properties_8_0"
}
]
},
"tests": [
{
"description": "nested extra property is valid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"schema_id": "properties_8_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "inheritance: nested object inherits looseness from loose parent",
"database": {
"schemas": [
{
"extensible": true,
"properties": {
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
},
"$id": "properties_9_0"
}
]
},
"tests": [
{
"description": "nested extra property is valid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"schema_id": "properties_9_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "override: nested object enforces strictness if extensible: false",
"database": {
"schemas": [
{
"extensible": true,
"properties": {
"nested": {
"extensible": false,
"properties": {
"foo": {
"type": "string"
}
}
}
},
"$id": "properties_10_0"
}
]
},
"tests": [
{
"description": "nested extra property is invalid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"schema_id": "properties_10_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "arrays: inline items inherit strictness from strict parent",
"database": {
"schemas": [
{
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"$id": "properties_11_0"
}
]
},
"tests": [
{
"description": "array item with extra property is invalid (strict parent)",
"data": {
"list": [
{
"foo": "bar",
"extra": 1
}
]
},
"schema_id": "properties_11_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "arrays: inline items inherit looseness from loose parent",
"database": {
"schemas": [
{
"extensible": true,
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"$id": "properties_12_0"
}
]
},
"tests": [
{
"description": "array item with extra property is valid (loose parent)",
"data": {
"list": [
{
"foo": "bar",
"extra": 1
}
]
},
"schema_id": "properties_12_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

347
fixtures/propertyNames.json Normal file
View File

@ -0,0 +1,347 @@
[
{
"description": "propertyNames validation",
"database": {
"schemas": [
{
"propertyNames": {
"maxLength": 3
},
"extensible": true,
"$id": "propertyNames_0_0"
}
]
},
"tests": [
{
"description": "all property names valid",
"data": {
"f": {},
"foo": {}
},
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "some property names invalid",
"data": {
"foo": {},
"foobar": {}
},
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object without properties is valid",
"data": {},
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores arrays",
"data": [
1,
2,
3,
4
],
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "foobar",
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "propertyNames_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "propertyNames validation with pattern",
"database": {
"schemas": [
{
"propertyNames": {
"pattern": "^a+$"
},
"extensible": true,
"$id": "propertyNames_1_0"
}
]
},
"tests": [
{
"description": "matching property names valid",
"data": {
"a": {},
"aa": {},
"aaa": {}
},
"schema_id": "propertyNames_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-matching property name is invalid",
"data": {
"aaA": {}
},
"schema_id": "propertyNames_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "object without properties is valid",
"data": {},
"schema_id": "propertyNames_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "propertyNames with boolean schema true",
"database": {
"schemas": [
{
"propertyNames": true,
"extensible": true,
"$id": "propertyNames_2_0"
}
]
},
"tests": [
{
"description": "object with any properties is valid",
"data": {
"foo": 1
},
"schema_id": "propertyNames_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "propertyNames_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "propertyNames with boolean schema false",
"database": {
"schemas": [
{
"propertyNames": false,
"extensible": true,
"$id": "propertyNames_3_0"
}
]
},
"tests": [
{
"description": "object with any properties is invalid",
"data": {
"foo": 1
},
"schema_id": "propertyNames_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "propertyNames_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "propertyNames with const",
"database": {
"schemas": [
{
"propertyNames": {
"const": "foo"
},
"extensible": true,
"$id": "propertyNames_4_0"
}
]
},
"tests": [
{
"description": "object with property foo is valid",
"data": {
"foo": 1
},
"schema_id": "propertyNames_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with any other property is invalid",
"data": {
"bar": 1
},
"schema_id": "propertyNames_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "propertyNames_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "propertyNames with enum",
"database": {
"schemas": [
{
"propertyNames": {
"enum": [
"foo",
"bar"
]
},
"extensible": true,
"$id": "propertyNames_5_0"
}
]
},
"tests": [
{
"description": "object with property foo is valid",
"data": {
"foo": 1
},
"schema_id": "propertyNames_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with property foo and bar is valid",
"data": {
"foo": 1,
"bar": 1
},
"schema_id": "propertyNames_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with any other property is invalid",
"data": {
"baz": 1
},
"schema_id": "propertyNames_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "empty object is valid",
"data": {},
"schema_id": "propertyNames_5_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra properties (checked by propertyNames)",
"database": {
"schemas": [
{
"propertyNames": {
"maxLength": 3
},
"extensible": true,
"$id": "propertyNames_6_0"
}
]
},
"tests": [
{
"description": "extra property with valid name is valid",
"data": {
"foo": 1
},
"schema_id": "propertyNames_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "extra property with invalid name is invalid",
"data": {
"foobar": 1
},
"schema_id": "propertyNames_6_0",
"action": "validate",
"expect": {
"success": false
}
}
]
}
]

1275
fixtures/queryer.json Normal file

File diff suppressed because it is too large Load Diff

929
fixtures/ref.json Normal file
View File

@ -0,0 +1,929 @@
[
{
"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
}
}
]
}
]

312
fixtures/required.json Normal file
View File

@ -0,0 +1,312 @@
[
{
"description": "required validation",
"database": {
"schemas": [
{
"properties": {
"foo": {},
"bar": {}
},
"required": [
"foo"
],
"$id": "required_0_0"
}
]
},
"tests": [
{
"description": "present required property is valid",
"data": {
"foo": 1
},
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-present required property is invalid",
"data": {
"bar": 1
},
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "ignores arrays",
"data": [],
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores strings",
"data": "",
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores null",
"data": null,
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores boolean",
"data": true,
"schema_id": "required_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "required default validation",
"database": {
"schemas": [
{
"properties": {
"foo": {}
},
"$id": "required_1_0"
}
]
},
"tests": [
{
"description": "not required by default",
"data": {},
"schema_id": "required_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "required with empty array",
"database": {
"schemas": [
{
"properties": {
"foo": {}
},
"required": [],
"$id": "required_2_0"
}
]
},
"tests": [
{
"description": "property not required",
"data": {},
"schema_id": "required_2_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "required with escaped characters",
"database": {
"schemas": [
{
"required": [
"foo\nbar",
"foo\"bar",
"foo\\bar",
"foo\rbar",
"foo\tbar",
"foo\fbar"
],
"extensible": true,
"$id": "required_3_0"
}
]
},
"tests": [
{
"description": "object with all properties present is valid",
"data": {
"foo\nbar": 1,
"foo\"bar": 1,
"foo\\bar": 1,
"foo\rbar": 1,
"foo\tbar": 1,
"foo\fbar": 1
},
"schema_id": "required_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object with some properties missing is invalid",
"data": {
"foo\nbar": "1",
"foo\"bar": "1"
},
"schema_id": "required_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "required properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"database": {
"schemas": [
{
"required": [
"__proto__",
"toString",
"constructor"
],
"extensible": true,
"$id": "required_4_0"
}
]
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "ignores other non-objects",
"data": 12,
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "none of the properties mentioned",
"data": {},
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "__proto__ present",
"data": {
"__proto__": "foo"
},
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "toString present",
"data": {
"toString": {
"length": 37
}
},
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "constructor present",
"data": {
"constructor": {
"length": 37
}
},
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "all present",
"data": {
"__proto__": 12,
"toString": {
"length": "foo"
},
"constructor": 37
},
"schema_id": "required_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "extensible: true allows extra properties in required",
"database": {
"schemas": [
{
"required": [
"foo"
],
"extensible": true,
"$id": "required_5_0"
}
]
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": 2
},
"schema_id": "required_5_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

175
fixtures/stems.json Normal file
View File

@ -0,0 +1,175 @@
[
{
"description": "Stem Engine Unit Tests",
"database": {
"puncs": [],
"enums": [],
"relations": [
{
"id": "rel1",
"type": "relation",
"constraint": "fk_contact_entity",
"source_type": "contact",
"source_columns": ["entity_id"],
"destination_type": "person",
"destination_columns": ["id"],
"prefix": null
},
{
"id": "rel2",
"type": "relation",
"constraint": "fk_relationship_target",
"source_type": "relationship",
"source_columns": ["target_id", "target_type"],
"destination_type": "entity",
"destination_columns": ["id", "type"],
"prefix": "target"
}
],
"types": [
{
"name": "entity",
"hierarchy": ["entity"],
"schemas": [{
"$id": "entity",
"type": "object",
"properties": {}
}]
},
{
"name": "person",
"hierarchy": ["person", "entity"],
"schemas": [{
"$id": "person",
"$ref": "entity",
"properties": {}
}]
},
{
"name": "email_address",
"hierarchy": ["email_address", "entity"],
"schemas": [{
"$id": "email_address",
"$ref": "entity",
"properties": {}
}]
},
{
"name": "phone_number",
"hierarchy": ["phone_number", "entity"],
"schemas": [{
"$id": "phone_number",
"$ref": "entity",
"properties": {}
}]
},
{
"name": "relationship",
"relationship": true,
"hierarchy": ["relationship", "entity"],
"schemas": [{
"$id": "relationship",
"$ref": "entity",
"properties": {}
}]
},
{
"name": "contact",
"relationship": true,
"hierarchy": ["contact", "relationship", "entity"],
"schemas": [{
"$id": "contact",
"$ref": "relationship",
"properties": {
"target": {
"oneOf": [
{ "$ref": "phone_number" },
{ "$ref": "email_address" }
]
}
}
}]
},
{
"name": "save_person",
"schemas": [{
"$id": "save_person.response",
"$ref": "person",
"properties": {
"contacts": {
"type": "array",
"items": { "$ref": "contact" }
}
}
}]
}
]
},
"tests": [
{
"description": "correctly squashes deep oneOf refs through array paths",
"action": "compile",
"expect": {
"success": true,
"stems": {
"save_person.response": {
"": {
"type": "person"
},
"contacts/contact": {
"type": "contact",
"relation": "contacts_id"
},
"contacts/contact/email_address": {
"type": "email_address",
"relation": "target_id"
},
"contacts/contact/phone_number": {
"type": "phone_number",
"relation": "target_id"
}
},
"contact": {
"": {
"type": "contact"
},
"email_address": {
"type": "email_address",
"relation": "target_id"
},
"phone_number": {
"type": "phone_number",
"relation": "target_id"
}
},
"person": {
"": {
"type": "person"
}
},
"email_address": {
"": {
"type": "email_address"
}
},
"phone_number": {
"": {
"type": "phone_number"
}
},
"relationship": {
"": {
"type": "relationship"
}
},
"entity": {
"": {
"type": "entity"
}
}
}
}
}
]
}
]

912
fixtures/type.json Normal file
View File

@ -0,0 +1,912 @@
[
{
"description": "integer type matches integers",
"database": {
"schemas": [
{
"type": "integer",
"$id": "type_0_0"
}
]
},
"tests": [
{
"description": "an integer is an integer",
"data": 1,
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a float with zero fractional part is an integer",
"data": 1,
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a float is not an integer",
"data": 1.1,
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is not an integer",
"data": "foo",
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is still not an integer, even if it looks like one",
"data": "1",
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an object is not an integer",
"data": {},
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is not an integer",
"data": [],
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a boolean is not an integer",
"data": true,
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is not an integer",
"data": null,
"schema_id": "type_0_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "number type matches numbers",
"database": {
"schemas": [
{
"type": "number",
"$id": "type_1_0"
}
]
},
"tests": [
{
"description": "an integer is a number",
"data": 1,
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a float with zero fractional part is a number (and an integer)",
"data": 1,
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a float is a number",
"data": 1.1,
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a string is not a number",
"data": "foo",
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is still not a number, even if it looks like one",
"data": "1",
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an object is not a number",
"data": {},
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is not a number",
"data": [],
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a boolean is not a number",
"data": true,
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is not a number",
"data": null,
"schema_id": "type_1_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "string type matches strings",
"database": {
"schemas": [
{
"type": "string",
"$id": "type_2_0"
}
]
},
"tests": [
{
"description": "1 is not a string",
"data": 1,
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a float is not a string",
"data": 1.1,
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is a string",
"data": "foo",
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a string is still a string, even if it looks like a number",
"data": "1",
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an empty string is still a string",
"data": "",
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an object is not a string",
"data": {},
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is not a string",
"data": [],
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a boolean is not a string",
"data": true,
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is not a string",
"data": null,
"schema_id": "type_2_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "object type matches objects",
"database": {
"schemas": [
{
"type": "object",
"$id": "type_3_0"
}
]
},
"tests": [
{
"description": "an integer is not an object",
"data": 1,
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a float is not an object",
"data": 1.1,
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is not an object",
"data": "foo",
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an object is an object",
"data": {},
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an array is not an object",
"data": [],
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a boolean is not an object",
"data": true,
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is not an object",
"data": null,
"schema_id": "type_3_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "array type matches arrays",
"database": {
"schemas": [
{
"type": "array",
"$id": "type_4_0"
}
]
},
"tests": [
{
"description": "an integer is not an array",
"data": 1,
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a float is not an array",
"data": 1.1,
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is not an array",
"data": "foo",
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an object is not an array",
"data": {},
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is an array",
"data": [],
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a boolean is not an array",
"data": true,
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is not an array",
"data": null,
"schema_id": "type_4_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "boolean type matches booleans",
"database": {
"schemas": [
{
"type": "boolean",
"$id": "type_5_0"
}
]
},
"tests": [
{
"description": "an integer is not a boolean",
"data": 1,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "zero is not a boolean",
"data": 0,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a float is not a boolean",
"data": 1.1,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is not a boolean",
"data": "foo",
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an empty string is a null",
"data": "",
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an object is not a boolean",
"data": {},
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is not a boolean",
"data": [],
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "true is a boolean",
"data": true,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "false is a boolean",
"data": false,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "null is not a boolean",
"data": null,
"schema_id": "type_5_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "null type matches only the null object",
"database": {
"schemas": [
{
"type": "null",
"$id": "type_6_0"
}
]
},
"tests": [
{
"description": "an integer is not null",
"data": 1,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a float is not null",
"data": 1.1,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "zero is not null",
"data": 0,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a string is not null",
"data": "foo",
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an empty string is null",
"data": "",
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "an object is not null",
"data": {},
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is not null",
"data": [],
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "true is not null",
"data": true,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "false is not null",
"data": false,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is null",
"data": null,
"schema_id": "type_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "multiple types can be specified in an array",
"database": {
"schemas": [
{
"type": [
"integer",
"string"
],
"$id": "type_7_0"
}
]
},
"tests": [
{
"description": "an integer is valid",
"data": 1,
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a string is valid",
"data": "foo",
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "a float is invalid",
"data": 1.1,
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an object is invalid",
"data": {},
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "an array is invalid",
"data": [],
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "a boolean is invalid",
"data": true,
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is invalid",
"data": null,
"schema_id": "type_7_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "type as array with one item",
"database": {
"schemas": [
{
"type": [
"string"
],
"$id": "type_8_0"
}
]
},
"tests": [
{
"description": "string is valid",
"data": "foo",
"schema_id": "type_8_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "number is invalid",
"data": 123,
"schema_id": "type_8_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "type: array or object",
"database": {
"schemas": [
{
"type": [
"array",
"object"
],
"items": {},
"$id": "type_9_0"
}
]
},
"tests": [
{
"description": "array is valid",
"data": [
1,
2,
3
],
"schema_id": "type_9_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object is valid",
"data": {},
"schema_id": "type_9_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "number is invalid",
"data": 123,
"schema_id": "type_9_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "string is invalid",
"data": "foo",
"schema_id": "type_9_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "null is invalid",
"data": null,
"schema_id": "type_9_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "type: array, object or null",
"database": {
"schemas": [
{
"type": [
"array",
"object",
"null"
],
"items": {},
"$id": "type_10_0"
}
]
},
"tests": [
{
"description": "array is valid",
"data": [
1,
2,
3
],
"schema_id": "type_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "object is valid",
"data": {},
"schema_id": "type_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "null is valid",
"data": null,
"schema_id": "type_10_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "number is invalid",
"data": 123,
"schema_id": "type_10_0",
"action": "validate",
"expect": {
"success": false
}
},
{
"description": "string is invalid",
"data": "foo",
"schema_id": "type_10_0",
"action": "validate",
"expect": {
"success": false
}
}
]
},
{
"description": "extensible: true allows extra properties",
"database": {
"schemas": [
{
"type": "object",
"extensible": true,
"$id": "type_11_0"
}
]
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1
},
"schema_id": "type_11_0",
"action": "validate",
"expect": {
"success": true
}
}
]
}
]

1171
fixtures/uniqueItems.json Normal file

File diff suppressed because it is too large Load Diff

71
flow
View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Flows
source ./flows/base
@ -8,32 +8,35 @@ source ./flows/packaging
source ./flows/rust
# Vars
POSTGRES_VERSION="17"
POSTGRES_VERSION="18"
POSTGRES_CONFIG_PATH="/opt/homebrew/opt/postgresql@${POSTGRES_VERSION}/bin/pg_config"
DEPENDENCIES+=(icu4c pkg-config "postgresql@${POSTGRES_VERSION}")
CARGO_DEPENDENCIES=(cargo-pgrx==0.14.0)
CARGO_DEPENDENCIES=(cargo-pgrx==0.16.1)
GITEA_ORGANIZATION="cellular"
GITEA_REPOSITORY="jspg"
pgrx-prepare() {
pgrx-up() {
info "Initializing pgrx..."
# Explicitly point to the postgresql@${POSTGRES_VERSION} pg_config, don't rely on 'which'
local POSTGRES_CONFIG_PATH="/opt/homebrew/opt/postgresql@${POSTGRES_VERSION}/bin/pg_config"
if [ ! -x "$POSTGRES_CONFIG_PATH" ]; then
error "pg_config not found or not executable at $POSTGRES_CONFIG_PATH."
warning "Ensure postgresql@${POSTGRES_VERSION} is installed correctly via Homebrew."
return 2
abort "pg_config not found or not executable at $POSTGRES_CONFIG_PATH." 2
fi
if cargo pgrx init --pg"$POSTGRES_VERSION"="$POSTGRES_CONFIG_PATH"; then
success "pgrx initialized successfully."
else
error "Failed to initialize pgrx. Check PostgreSQL development packages are installed and $POSTGRES_CONFIG_PATH is valid."
return 2
success "pgrx initialized successfully." && return 0
fi
abort "Failed to initialize pgrx. Check PostgreSQL development packages are installed and $POSTGRES_CONFIG_PATH is valid." 2
}
pgrx-down() {
info "Taking pgrx down..."
}
build() {
local version
version=$(get-version) || return $?
@ -49,12 +52,12 @@ build() {
# Create the source tarball excluding specified patterns
info "Creating tarball: ${tarball_path}"
if tar --exclude='.git*' --exclude='./target' --exclude='./package' --exclude='./flows' --exclude='./flow' -czf "${tarball_path}" .; then
success "Successfully created source tarball: ${tarball_path}"
else
error "Failed to create source tarball."
return 2
# Set COPYFILE_DISABLE=1 to prevent macOS tar from including ._ metadata files
if COPYFILE_DISABLE=1 tar --exclude='.git*' --exclude='./target' --exclude='./package' --exclude='./flows' --exclude='./flow' -czf "${tarball_path}" .; then
success "Successfully created source tarball: ${tarball_path}" && return 0
fi
abort "Failed to create source tarball." 2
}
install() {
@ -65,8 +68,7 @@ install() {
# Run the pgrx install command
if ! cargo pgrx install; then
error "cargo pgrx install command failed."
return 2
abort "cargo pgrx install command failed." 2
fi
success "PGRX extension v$version successfully built and installed."
@ -75,31 +77,28 @@ install() {
pg_sharedir=$("$POSTGRES_CONFIG_PATH" --sharedir)
local pg_config_status=$?
if [ $pg_config_status -ne 0 ] || [ -z "$pg_sharedir" ]; then
error "Failed to determine PostgreSQL shared directory using pg_config."
return 2
abort "Failed to determine PostgreSQL shared directory using pg_config." 2
fi
local installed_control_path="${pg_sharedir}/extension/jspg.control"
# Modify the control file
if [ ! -f "$installed_control_path" ]; then
error "Installed control file not found: '$installed_control_path'"
return 2
abort "Installed control file not found: '$installed_control_path'" 2
fi
info "Modifying control file for non-superuser access: ${installed_control_path}"
# Use sed -i '' for macOS compatibility
if sed -i '' '/^superuser = false/d' "$installed_control_path" && \
echo 'trusted = true' >> "$installed_control_path"; then
success "Control file modified successfully."
else
error "Failed to modify control file: ${installed_control_path}"
return 2
success "Control file modified successfully." && return 0
fi
abort "Failed to modify control file: ${installed_control_path}" 2
}
test() {
info "Running jspg tests..."
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
cargo test --tests "$@" || return $?
}
clean() {
@ -108,25 +107,27 @@ clean() {
}
jspg-usage() {
printf "prepare\tCheck OS, Cargo, and PGRX dependencies.\n"
printf "install\tBuild and install the extension locally (after prepare).\n"
printf "reinstall\tClean, build, and install the extension locally (after prepare).\n"
printf "test\t\tRun pgrx integration tests.\n"
printf "clean\t\tRemove pgrx build artifacts.\n"
echo "up|Check OS, Cargo, and PGRX dependencies."
echo "install|Build and install the extension locally (after up)."
echo "reinstall|Clean, build, and install the extension locally (after up)."
echo "test-jspg|Run pgrx integration tests."
echo "test-validator|Run validator integration tests."
echo "clean|Remove pgrx build artifacts."
}
jspg-flow() {
case "$1" in
prepare) prepare && cargo-prepare && pgrx-prepare; return $?;;
up) up && rust-up && pgrx-up; return $?;;
down) pgrx-down && rust-down && down; return $?;;
build) build; return $?;;
install) install; return $?;;
reinstall) clean && install; return $?;;
test) test "${@:2}"; return $?;;
clean) clean; return $?;;
*) return 1 ;;
*) return 127 ;;
esac
}
register-flow "jspg-usage" "jspg-flow"
register-flow "jspg"
dispatch "$@"
dispatch "$@"

2
flows

Submodule flows updated: e154758056...a7b0f5dc4d

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
tab_spaces = 2

View File

@ -1 +1 @@
::pgrx::pgrx_embed!();
::pgrx::pgrx_embed!();

12
src/database/enum.rs Normal file
View File

@ -0,0 +1,12 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Enum {
pub name: String,
pub module: String,
pub source: String,
pub values: Vec<String>,
pub schemas: Vec<Schema>,
}

View File

@ -0,0 +1,196 @@
#[cfg(test)]
use crate::database::executors::DatabaseExecutor;
#[cfg(test)]
use regex::Regex;
#[cfg(test)]
use serde_json::Value;
#[cfg(test)]
use std::cell::RefCell;
#[cfg(test)]
pub struct MockState {
pub captured_queries: Vec<String>,
pub query_responses: Vec<Result<Value, String>>,
pub execute_responses: Vec<Result<(), String>>,
pub mocks: Vec<Value>,
}
#[cfg(test)]
impl MockState {
pub fn new() -> Self {
Self {
captured_queries: Default::default(),
query_responses: Default::default(),
execute_responses: Default::default(),
mocks: Default::default(),
}
}
}
#[cfg(test)]
thread_local! {
pub static MOCK_STATE: RefCell<MockState> = RefCell::new(MockState::new());
}
#[cfg(test)]
pub struct MockExecutor {}
#[cfg(test)]
impl MockExecutor {
pub fn new() -> Self {
Self {}
}
}
#[cfg(test)]
impl DatabaseExecutor for MockExecutor {
fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> {
println!("DEBUG SQL QUERY: {}", sql);
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string());
if !s.mocks.is_empty() {
if let Some(matches) = parse_and_match_mocks(sql, &s.mocks) {
if !matches.is_empty() {
return Ok(Value::Array(matches));
}
}
}
if s.query_responses.is_empty() {
return Ok(Value::Array(vec![]));
}
s.query_responses.remove(0)
})
}
fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> {
println!("DEBUG SQL EXECUTE: {}", sql);
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string());
if s.execute_responses.is_empty() {
return Ok(());
}
s.execute_responses.remove(0)
})
}
fn auth_user_id(&self) -> Result<String, String> {
Ok("00000000-0000-0000-0000-000000000000".to_string())
}
fn timestamp(&self) -> Result<String, String> {
Ok("2026-03-10T00:00:00Z".to_string())
}
#[cfg(test)]
fn get_queries(&self) -> Vec<String> {
MOCK_STATE.with(|state| state.borrow().captured_queries.clone())
}
#[cfg(test)]
fn set_mocks(&self, mocks: Vec<Value>) {
MOCK_STATE.with(|state| {
state.borrow_mut().mocks = mocks;
});
}
#[cfg(test)]
fn reset_mocks(&self) {
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.clear();
s.query_responses.clear();
s.execute_responses.clear();
s.mocks.clear();
});
}
}
#[cfg(test)]
fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
let sql_upper = sql.to_uppercase();
if !sql_upper.starts_with("SELECT") {
return None;
}
// 1. Extract table name
let table_regex = Regex::new(r#"(?i)\s+FROM\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#).ok()?;
let table = if let Some(caps) = table_regex.captures(sql) {
caps.get(1)?.as_str()
} else {
return None;
};
// 2. Extract WHERE conditions
let mut conditions = Vec::new();
if let Some(where_idx) = sql_upper.find(" WHERE ") {
let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql.len());
if let Some(limit_idx) = sql_upper.find(" LIMIT ") {
if limit_idx < where_end {
where_end = limit_idx;
}
}
let where_clause = &sql[where_idx + 7..where_end];
let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?;
let parts = and_regex.split(where_clause);
for part in parts {
if let Some(eq_idx) = part.find('=') {
let left = part[..eq_idx]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let right = part[eq_idx + 1..].trim().trim_matches('\'');
conditions.push((left.to_string(), right.to_string()));
} else if part.to_uppercase().contains(" IS NULL") {
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
.trim()
.split('.')
.last()
.unwrap_or("")
.replace('"', ""); // Remove quotes explicitly
conditions.push((left, "null".to_string()));
}
}
}
// 3. Find matching mocks
let mut matches = Vec::new();
for mock in mocks {
if let Some(mock_obj) = mock.as_object() {
if let Some(t) = mock_obj.get("type") {
if t.as_str() != Some(table) {
continue;
}
}
let mut matches_all = true;
for (k, v) in &conditions {
let mock_val_str = match mock_obj.get(k) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(),
Some(Value::Bool(b)) => b.to_string(),
Some(Value::Null) => "null".to_string(),
_ => {
matches_all = false;
break;
}
};
if mock_val_str != *v {
matches_all = false;
break;
}
}
if matches_all {
matches.push(mock.clone());
}
}
}
Some(matches)
}

View File

@ -0,0 +1,31 @@
pub mod mock;
#[cfg(not(test))]
pub mod pgrx;
use serde_json::Value;
/// An abstraction over database execution to allow for isolated unit testing
/// 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>;
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String>;
/// Returns the current authenticated user's ID
fn auth_user_id(&self) -> Result<String, String>;
/// Returns the current transaction timestamp
fn timestamp(&self) -> Result<String, String>;
#[cfg(test)]
fn get_queries(&self) -> Vec<String>;
#[cfg(test)]
fn reset_mocks(&self);
#[cfg(test)]
fn set_mocks(&self, mocks: Vec<Value>);
}

View File

@ -0,0 +1,96 @@
use crate::database::executors::DatabaseExecutor;
use pgrx::prelude::*;
use serde_json::Value;
/// The production executor that wraps `pgrx::spi::Spi`.
pub struct SpiExecutor;
impl SpiExecutor {
pub fn new() -> Self {
Self {}
}
}
impl DatabaseExecutor for SpiExecutor {
fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String> {
let mut json_args = Vec::new();
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));
}
}
Spi::connect(|client| {
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
Ok(tup_table) => {
let mut results = Vec::new();
for row in tup_table {
if let Ok(Some(jsonb)) = row.get::<pgrx::JsonB>(1) {
results.push(jsonb.0);
}
}
Ok(Value::Array(results))
}
Err(e) => Err(format!("SPI Query Fetch Failure: {}", e)),
}
})
}
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String> {
let mut json_args = Vec::new();
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));
}
}
Spi::connect_mut(|client| {
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
Ok(_) => Ok(()),
Err(e) => Err(format!("SPI Execution Failure: {}", e)),
}
})
}
fn auth_user_id(&self) -> Result<String, String> {
Spi::connect(|client| {
let mut tup_table = client
.select(
"SELECT COALESCE(current_setting('auth.user_id', true), 'ffffffff-ffff-ffff-ffff-ffffffffffff')",
None,
&[],
)
.map_err(|e| format!("SPI Select Error: {}", e))?;
let row = tup_table
.next()
.ok_or("No user id setting returned from context".to_string())?;
let user_id: Option<String> = row.get(1).map_err(|e| e.to_string())?;
user_id.ok_or("Missing user_id".to_string())
})
}
fn timestamp(&self) -> Result<String, String> {
Spi::connect(|client| {
let mut tup_table = client
.select("SELECT clock_timestamp()::text", None, &[])
.map_err(|e| format!("SPI Select Error: {}", e))?;
let row = tup_table
.next()
.ok_or("No clock timestamp returned".to_string())?;
let timestamp: Option<String> = row.get(1).map_err(|e| e.to_string())?;
timestamp.ok_or("Missing timestamp".to_string())
})
}
}

880
src/database/formats.rs Normal file
View File

@ -0,0 +1,880 @@
use std::{
collections::HashMap,
error::Error,
net::{Ipv4Addr, Ipv6Addr},
};
use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use serde_json::Value;
use url::Url;
// use crate::ecma; // Assuming ecma is not yet available, stubbing regex for now
/// Defines format for `format` keyword.
#[derive(Clone, Copy)]
pub struct Format {
/// Name of the format
pub name: &'static str,
/// validates given value.
pub func: fn(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>>, // Ensure thread safety if needed
}
lazy_static! {
pub(crate) static ref FORMATS: HashMap<&'static str, Format> = {
let mut m = HashMap::<&'static str, Format>::new();
// Helper to register formats
let mut register = |name, func| m.insert(name, Format { name, func });
// register("regex", validate_regex); // Stubbed
register("ipv4", validate_ipv4);
register("ipv6", validate_ipv6);
register("hostname", validate_hostname);
register("idn-hostname", validate_idn_hostname);
register("email", validate_email);
register("idn-email", validate_idn_email);
register("date", validate_date);
register("time", validate_time);
register("date-time", validate_date_time);
register("duration", validate_duration);
register("period", validate_period);
register("json-pointer", validate_json_pointer);
register("relative-json-pointer", validate_relative_json_pointer);
register("uuid", validate_uuid);
register("uri", validate_uri);
register("iri", validate_iri);
register("uri-reference", validate_uri_reference);
register("iri-reference", validate_iri_reference);
register("uri-template", validate_uri_template);
m
};
}
/*
fn validate_regex(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
// ecma::convert(s).map(|_| ())
Ok(())
}
*/
fn validate_ipv4(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv4Addr>()?;
Ok(())
}
fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv6Addr>()?;
Ok(())
}
fn validate_date(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_date(s)?;
Ok(())
}
fn matches_char(s: &str, index: usize, ch: char) -> bool {
s.is_char_boundary(index) && s[index..].starts_with(ch)
}
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
fn check_date(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// yyyy-mm-dd
if s.len() != 10 {
Err("must be 10 characters long")?;
}
if !matches_char(s, 4, '-') || !matches_char(s, 7, '-') {
Err("missing hyphen in correct place")?;
}
let mut ymd = s.splitn(3, '-').filter_map(|t| t.parse::<usize>().ok());
let (Some(y), Some(m), Some(d)) = (ymd.next(), ymd.next(), ymd.next()) else {
Err("non-positive year/month/day")?
};
if !matches!(m, 1..=12) {
Err(format!("{m} months in year"))?;
}
if !matches!(d, 1..=31) {
Err(format!("{d} days in month"))?;
}
match m {
2 => {
let mut feb_days = 28;
if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
feb_days += 1; // leap year
};
if d > feb_days {
Err(format!("february has {feb_days} days only"))?;
}
}
4 | 6 | 9 | 11 => {
if d > 30 {
Err("month has 30 days only")?;
}
}
_ => {}
}
Ok(())
}
fn validate_time(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_time(s)
}
fn check_time(mut str: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// min: hh:mm:ssZ
if str.len() < 9 {
Err("less than 9 characters long")?
}
if !matches_char(str, 2, ':') || !matches_char(str, 5, ':') {
Err("missing colon in correct place")?
}
// parse hh:mm:ss
if !str.is_char_boundary(8) {
Err("contains non-ascii char")?
}
let mut hms = (str[..8])
.splitn(3, ':')
.filter_map(|t| t.parse::<usize>().ok());
let (Some(mut h), Some(mut m), Some(s)) = (hms.next(), hms.next(), hms.next()) else {
Err("non-positive hour/min/sec")?
};
if h > 23 || m > 59 || s > 60 {
Err("hour/min/sec out of range")?
}
str = &str[8..];
// parse sec-frac if present
if let Some(rem) = str.strip_prefix('.') {
let n_digits = rem.chars().take_while(char::is_ascii_digit).count();
if n_digits == 0 {
Err("no digits in second fraction")?;
}
str = &rem[n_digits..];
}
if str != "z" && str != "Z" {
// parse time-numoffset
if str.len() != 6 {
Err("offset must be 6 characters long")?;
}
let sign: isize = match str.chars().next() {
Some('+') => -1,
Some('-') => 1,
_ => return Err("offset must begin with plus/minus")?,
};
str = &str[1..];
if !matches_char(str, 2, ':') {
Err("missing colon in offset at correct place")?
}
let mut zhm = str.splitn(2, ':').filter_map(|t| t.parse::<usize>().ok());
let (Some(zh), Some(zm)) = (zhm.next(), zhm.next()) else {
Err("non-positive hour/min in offset")?
};
if zh > 23 || zm > 59 {
Err("hour/min in offset out of range")?
}
// apply timezone
let mut hm = (h * 60 + m) as isize + sign * (zh * 60 + zm) as isize;
if hm < 0 {
hm += 24 * 60;
debug_assert!(hm >= 0);
}
let hm = hm as usize;
(h, m) = (hm / 60, hm % 60);
}
// check leap second
if !(s < 60 || (h == 23 && m == 59)) {
Err("invalid leap second")?
}
Ok(())
}
fn validate_date_time(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_date_time(s)
}
fn check_date_time(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// min: yyyy-mm-ddThh:mm:ssZ
if s.len() < 20 {
Err("less than 20 characters long")?;
}
if !s.is_char_boundary(10) || !s[10..].starts_with(['t', 'T']) {
Err("11th character must be t or T")?;
}
if let Err(e) = check_date(&s[..10]) {
Err(format!("invalid date element: {e}"))?;
}
if let Err(e) = check_time(&s[11..]) {
Err(format!("invalid time element: {e}"))?;
}
Ok(())
}
fn validate_duration(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_duration(s)?;
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
fn check_duration(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// must start with 'P'
let Some(s) = s.strip_prefix('P') else {
Err("must start with P")?
};
if s.is_empty() {
Err("nothing after P")?
}
// dur-week
if let Some(s) = s.strip_suffix('W') {
if s.is_empty() {
Err("no number in week")?
}
if !s.chars().all(|c| c.is_ascii_digit()) {
Err("invalid week")?
}
return Ok(());
}
static UNITS: [&str; 2] = ["YMD", "HMS"];
for (i, s) in s.split('T').enumerate() {
let mut s = s;
if i != 0 && s.is_empty() {
Err("no time elements")?
}
let Some(mut units) = UNITS.get(i).cloned() else {
Err("more than one T")?
};
while !s.is_empty() {
let digit_count = s.chars().take_while(char::is_ascii_digit).count();
if digit_count == 0 {
Err("missing number")?
}
s = &s[digit_count..];
let Some(unit) = s.chars().next() else {
Err("missing unit")?
};
let Some(j) = units.find(unit) else {
if UNITS[i].contains(unit) {
Err(format!("unit {unit} out of order"))?
}
Err(format!("invalid unit {unit}"))?
};
units = &units[j + 1..];
s = &s[1..];
}
}
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
fn validate_period(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let Some(slash) = s.find('/') else {
Err("missing slash")?
};
let (start, end) = (&s[..slash], &s[slash + 1..]);
if start.starts_with('P') {
if let Err(e) = check_duration(start) {
Err(format!("invalid start duration: {e}"))?
}
if let Err(e) = check_date_time(end) {
Err(format!("invalid end date-time: {e}"))?
}
} else {
if let Err(e) = check_date_time(start) {
Err(format!("invalid start date-time: {e}"))?
}
if end.starts_with('P') {
if let Err(e) = check_duration(end) {
Err(format!("invalid end duration: {e}"))?;
}
} else if let Err(e) = check_date_time(end) {
Err(format!("invalid end date-time: {e}"))?;
}
}
Ok(())
}
fn validate_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_hostname(s)?;
Ok(())
}
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
fn check_hostname(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
if s.len() > 253 {
Err("more than 253 characters long")?
}
// Hostnames are composed of series of labels concatenated with dots, as are all domain names
for label in s.split('.') {
// Each label must be from 1 to 63 characters long
if !matches!(label.len(), 1..=63) {
Err("label must be 1 to 63 characters long")?;
}
// labels must not start or end with a hyphen
if label.starts_with('-') {
Err("label starts with hyphen")?;
}
if label.ends_with('-') {
Err("label ends with hyphen")?;
}
// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner),
// the digits '0' through '9', and the hyphen ('-')
if let Some(ch) = label
.chars()
.find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-'))
{
Err(format!("invalid character {ch:?}"))?;
}
// labels must not contain "--" in 3rd and 4th position unless they start with "xn--"
if label.len() >= 4 && &label[2..4] == "--" {
if !label.starts_with("xn--") {
Err("label has -- in 3rd/4th position but does not start with xn--")?;
} else {
let (unicode, errors) = idna::domain_to_unicode(label);
if errors.is_err() {
Err("invalid punycode")?;
}
check_unicode_idn_constraints(&unicode)
.map_err(|e| format!("invalid punycode/IDN: {e}"))?;
}
}
}
Ok(())
}
fn validate_idn_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_idn_hostname(s)?;
Ok(())
}
static DISALLOWED: [char; 10] = [
'\u{0640}', // ARABIC TATWEEL
'\u{07FA}', // NKO LAJANYALAN
'\u{302E}', // HANGUL SINGLE DOT TONE MARK
'\u{302F}', // HANGUL DOUBLE DOT TONE MARK
'\u{3031}', // VERTICAL KANA REPEAT MARK
'\u{3032}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK
'\u{3033}', // VERTICAL KANA REPEAT MARK UPPER HALF
'\u{3034}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK UPPER HA
'\u{3035}', // VERTICAL KANA REPEAT MARK LOWER HALF
'\u{303B}', // VERTICAL IDEOGRAPHIC ITERATION MARK
];
fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
let s = idna::domain_to_ascii_strict(s).map_err(|e| format!("idna error: {:?}", e))?;
let (unicode, errors) = idna::domain_to_unicode(&s);
if let Err(e) = errors {
Err(format!("idna decoding error: {:?}", e))?;
}
check_unicode_idn_constraints(&unicode)?;
check_hostname(&s)?;
Ok(())
}
fn check_unicode_idn_constraints(unicode: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// see https://www.rfc-editor.org/rfc/rfc5892#section-2.6
{
if unicode.contains(DISALLOWED) {
Err("contains disallowed character")?;
}
}
// unicode string must not contain "--" in 3rd and 4th position
// and must not start and end with a '-'
// see https://www.rfc-editor.org/rfc/rfc5891#section-4.2.3.1
{
let count: usize = unicode
.chars()
.skip(2)
.take(2)
.map(|c| if c == '-' { 1 } else { 0 })
.sum();
if count == 2 {
Err("unicode string must not contain '--' in 3rd and 4th position")?;
}
}
// MIDDLE DOT is allowed between 'l' characters only
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.3
{
let middle_dot = '\u{00b7}';
let mut s = unicode;
while let Some(i) = s.find(middle_dot) {
let prefix = &s[..i];
let suffix = &s[i + middle_dot.len_utf8()..];
if !prefix.ends_with('l') || !suffix.ends_with('l') {
Err("MIDDLE DOT is allowed between 'l' characters only")?;
}
s = suffix;
}
}
// Greek KERAIA must be followed by Greek character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.4
{
let keralia = '\u{0375}';
let greek = '\u{0370}'..='\u{03FF}';
let mut s = unicode;
while let Some(i) = s.find(keralia) {
let suffix = &s[i + keralia.len_utf8()..];
if !suffix.starts_with(|c| greek.contains(&c)) {
Err("Greek KERAIA must be followed by Greek character")?;
}
s = suffix;
}
}
// Hebrew GERESH must be preceded by Hebrew character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.5
//
// Hebrew GERSHAYIM must be preceded by Hebrew character
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.6
{
let geresh = '\u{05F3}';
let gereshayim = '\u{05F4}';
let hebrew = '\u{0590}'..='\u{05FF}';
for ch in [geresh, gereshayim] {
let mut s = unicode;
while let Some(i) = s.find(ch) {
let prefix = &s[..i];
if !prefix.ends_with(|c| hebrew.contains(&c)) {
if i == 0 {
Err("Hebrew GERESH must be preceded by Hebrew character")?;
} else {
Err("Hebrew GERESHYIM must be preceded by Hebrew character")?;
}
}
let suffix = &s[i + ch.len_utf8()..];
s = suffix;
}
}
}
// KATAKANA MIDDLE DOT must be with Hiragana, Katakana, or Han
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.7
{
let katakana_middle_dot = '\u{30FB}';
if unicode.contains(katakana_middle_dot) {
let hiragana = '\u{3040}'..='\u{309F}';
let katakana = '\u{30A0}'..='\u{30FF}';
let han = '\u{4E00}'..='\u{9FFF}'; // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block): is this range correct??
if unicode.contains(|c| hiragana.contains(&c))
|| unicode.contains(|c| c != katakana_middle_dot && katakana.contains(&c))
|| unicode.contains(|c| han.contains(&c))
{
// ok
} else {
Err("KATAKANA MIDDLE DOT must be with Hiragana, Katakana, or Han")?;
}
}
}
// ARABIC-INDIC DIGITS and Extended Arabic-Indic Digits cannot be mixed
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.9
{
let arabic_indic_digits = '\u{0660}'..='\u{0669}';
let extended_arabic_indic_digits = '\u{06F0}'..='\u{06F9}';
if unicode.contains(|c| arabic_indic_digits.contains(&c))
&& unicode.contains(|c| extended_arabic_indic_digits.contains(&c))
{
Err("ARABIC-INDIC DIGITS and Extended Arabic-Indic Digits cannot be mixed")?;
}
}
// ZERO WIDTH JOINER must be preceded by Virama
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.2
{
let zero_width_jointer = '\u{200D}';
static VIRAMA: [char; 61] = [
'\u{094D}',
'\u{09CD}',
'\u{0A4D}',
'\u{0ACD}',
'\u{0B4D}',
'\u{0BCD}',
'\u{0C4D}',
'\u{0CCD}',
'\u{0D3B}',
'\u{0D3C}',
'\u{0D4D}',
'\u{0DCA}',
'\u{0E3A}',
'\u{0EBA}',
'\u{0F84}',
'\u{1039}',
'\u{103A}',
'\u{1714}',
'\u{1734}',
'\u{17D2}',
'\u{1A60}',
'\u{1B44}',
'\u{1BAA}',
'\u{1BAB}',
'\u{1BF2}',
'\u{1BF3}',
'\u{2D7F}',
'\u{A806}',
'\u{A82C}',
'\u{A8C4}',
'\u{A953}',
'\u{A9C0}',
'\u{AAF6}',
'\u{ABED}',
'\u{10A3F}',
'\u{11046}',
'\u{1107F}',
'\u{110B9}',
'\u{11133}',
'\u{11134}',
'\u{111C0}',
'\u{11235}',
'\u{112EA}',
'\u{1134D}',
'\u{11442}',
'\u{114C2}',
'\u{115BF}',
'\u{1163F}',
'\u{116B6}',
'\u{1172B}',
'\u{11839}',
'\u{1193D}',
'\u{1193E}',
'\u{119E0}',
'\u{11A34}',
'\u{11A47}',
'\u{11A99}',
'\u{11C3F}',
'\u{11D44}',
'\u{11D45}',
'\u{11D97}',
]; // https://www.compart.com/en/unicode/combining/9
let mut s = unicode;
while let Some(i) = s.find(zero_width_jointer) {
let prefix = &s[..i];
if !prefix.ends_with(VIRAMA) {
Err("ZERO WIDTH JOINER must be preceded by Virama")?;
}
let suffix = &s[i + zero_width_jointer.len_utf8()..];
s = suffix;
}
}
Ok(())
}
fn validate_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_email(s)?;
Ok(())
}
// see https://en.wikipedia.org/wiki/Email_address
fn check_email(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
// entire email address to be no more than 254 characters long
if s.len() > 254 {
Err("more than 254 characters long")?
}
// email address is generally recognized as having two parts joined with an at-sign
let Some(at) = s.rfind('@') else {
Err("missing @")?
};
let (local, domain) = (&s[..at], &s[at + 1..]);
// local part may be up to 64 characters long
if local.len() > 64 {
Err("local part more than 64 characters long")?
}
if local.len() > 1 && local.starts_with('"') && local.ends_with('"') {
// quoted
let local = &local[1..local.len() - 1];
if local.contains(['\\', '"']) {
Err("backslash and quote not allowed within quoted local part")?
}
} else {
// unquoted
if local.starts_with('.') {
Err("starts with dot")?
}
if local.ends_with('.') {
Err("ends with dot")?
}
// consecutive dots not allowed
if local.contains("..") {
Err("consecutive dots")?
}
// check allowd chars
if let Some(ch) = local
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || ".!#$%&'*+-/=?^_`{|}~".contains(*c)))
{
Err(format!("invalid character {ch:?}"))?
}
}
// domain if enclosed in brackets, must match an IP address
if domain.starts_with('[') && domain.ends_with(']') {
let s = &domain[1..domain.len() - 1];
if let Some(s) = s.strip_prefix("IPv6:") {
if let Err(e) = s.parse::<Ipv6Addr>() {
Err(format!("invalid ipv6 address: {e}"))?
}
return Ok(());
}
if let Err(e) = s.parse::<Ipv4Addr>() {
Err(format!("invalid ipv4 address: {e}"))?
}
return Ok(());
}
// domain must match the requirements for a hostname
if let Err(e) = check_hostname(domain) {
Err(format!("invalid domain: {e}"))?
}
Ok(())
}
fn validate_idn_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let Some(at) = s.rfind('@') else {
Err("missing @")?
};
let (local, domain) = (&s[..at], &s[at + 1..]);
let local = idna::domain_to_ascii_strict(local).map_err(|e| format!("idna error: {:?}", e))?;
let domain = idna::domain_to_ascii_strict(domain).map_err(|e| format!("idna error: {:?}", e))?;
if let Err(e) = check_idn_hostname(&domain) {
Err(format!("invalid domain: {e}"))?
}
check_email(&format!("{local}@{domain}"))
}
fn validate_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
check_json_pointer(s)?;
Ok(())
}
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
fn check_json_pointer(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
if s.is_empty() {
return Ok(());
}
if !s.starts_with('/') {
Err("not starting with slash")?;
}
for token in s.split('/').skip(1) {
let mut chars = token.chars();
while let Some(ch) = chars.next() {
if ch == '~' {
if !matches!(chars.next(), Some('0' | '1')) {
Err("~ must be followed by 0 or 1")?;
}
} else if !matches!(ch, '\x00'..='\x2E' | '\x30'..='\x7D' | '\x7F'..='\u{10FFFF}') {
Err("contains disallowed character")?;
}
}
}
Ok(())
}
// see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3
fn validate_relative_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
// start with non-negative-integer
let num_digits = s.chars().take_while(char::is_ascii_digit).count();
if num_digits == 0 {
Err("must start with non-negative integer")?;
}
if num_digits > 1 && s.starts_with('0') {
Err("starts with zero")?;
}
let s = &s[num_digits..];
// followed by either json-pointer or '#'
if s == "#" {
return Ok(());
}
if let Err(e) = check_json_pointer(s) {
Err(format!("invalid json-pointer element: {e}"))?;
}
Ok(())
}
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
fn validate_uuid(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
static HEX_GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
let mut i = 0;
for group in s.split('-') {
if i >= HEX_GROUPS.len() {
Err("more than 5 elements")?;
}
if group.len() != HEX_GROUPS[i] {
Err(format!(
"element {} must be {} characters long",
i + 1,
HEX_GROUPS[i]
))?;
}
if let Some(ch) = group.chars().find(|c| !c.is_ascii_hexdigit()) {
Err(format!("non-hex character {ch:?}"))?;
}
i += 1;
}
if i != HEX_GROUPS.len() {
Err("must have 5 elements")?;
}
Ok(())
}
fn validate_uri(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
if fluent_uri::UriRef::parse(s.as_str())
.map_err(|e| e.to_string())?
.scheme()
.is_none()
{
Err("relative url")?;
};
Ok(())
}
fn validate_iri(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
match Url::parse(s) {
Ok(_) => Ok(()),
Err(url::ParseError::RelativeUrlWithoutBase) => Err("relative url")?,
Err(e) => Err(e)?,
}
}
lazy_static! {
static ref TEMP_URL: Url = Url::parse("http://temp.com").unwrap();
}
fn parse_uri_reference(s: &str) -> Result<Url, Box<dyn Error + Send + Sync>> {
if s.contains('\\') {
Err("contains \\\\")?;
}
Ok(TEMP_URL.join(s)?)
}
fn validate_uri_reference(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
fluent_uri::UriRef::parse(s.as_str()).map_err(|e| e.to_string())?;
Ok(())
}
fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
parse_uri_reference(s)?;
Ok(())
}
fn validate_uri_template(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
let Value::String(s) = v else {
return Ok(());
};
let url = parse_uri_reference(s)?;
let path = url.path();
// path we got has curly bases percent encoded
let path = percent_decode_str(path).decode_utf8()?;
// ensure curly brackets are not nested and balanced
for part in path.as_ref().split('/') {
let mut want = true;
for got in part
.chars()
.filter(|c| matches!(c, '{' | '}'))
.map(|c| c == '{')
{
if got != want {
Err("nested curly braces")?;
}
want = !want;
}
if !want {
Err("no matching closing brace")?
}
}
Ok(())
}

506
src/database/mod.rs Normal file
View File

@ -0,0 +1,506 @@
pub mod r#enum;
pub mod executors;
pub mod formats;
pub mod page;
pub mod punc;
pub mod relation;
pub mod schema;
pub mod r#type;
// External mock exports inside the executor sub-folder
use r#enum::Enum;
use executors::DatabaseExecutor;
#[cfg(not(test))]
use executors::pgrx::SpiExecutor;
#[cfg(test)]
use executors::mock::MockExecutor;
pub mod stem;
use punc::Punc;
use relation::Relation;
use schema::Schema;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use stem::Stem;
use r#type::Type;
pub struct Database {
pub enums: HashMap<String, Enum>,
pub types: HashMap<String, Type>,
pub puncs: HashMap<String, Punc>,
pub relations: HashMap<String, Relation>,
pub schemas: HashMap<String, Schema>,
// Map of Schema ID -> { Entity Type -> Target Subschema Arc }
pub stems: HashMap<String, HashMap<String, Arc<Stem>>>,
pub descendants: HashMap<String, Vec<String>>,
pub depths: HashMap<String, usize>,
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
}
impl Database {
pub fn new(val: &serde_json::Value) -> Result<Self, crate::drop::Drop> {
let mut db = Self {
enums: HashMap::new(),
types: HashMap::new(),
relations: HashMap::new(),
puncs: HashMap::new(),
schemas: HashMap::new(),
stems: HashMap::new(),
descendants: HashMap::new(),
depths: HashMap::new(),
#[cfg(not(test))]
executor: Box::new(SpiExecutor::new()),
#[cfg(test)]
executor: Box::new(MockExecutor::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);
}
}
}
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);
}
}
}
if let Some(arr) = val.get("relations").and_then(|v| v.as_array()) {
for item in arr {
match serde_json::from_value::<Relation>(item.clone()) {
Ok(def) => {
db.relations.insert(def.constraint.clone(), def);
}
Err(e) => println!("DATABASE RELATION PARSE FAILED: {:?}", e),
}
}
}
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);
}
}
}
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);
}
}
}
db.compile()?;
Ok(db)
}
/// Override the default executor for unit testing
pub fn with_executor(mut self, executor: Box<dyn DatabaseExecutor + Send + Sync>) -> Self {
self.executor = executor;
self
}
/// Executes a query expecting a single JSONB array return, representing rows.
pub fn query(&self, sql: &str, args: Option<&[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> {
self.executor.execute(sql, args)
}
/// Returns the current authenticated user's ID
pub fn auth_user_id(&self) -> Result<String, String> {
self.executor.auth_user_id()
}
/// Returns the current transaction timestamp
pub fn timestamp(&self) -> Result<String, String> {
self.executor.timestamp()
}
/// Organizes the graph of the database, compiling regex, format functions, and caching relationships.
pub fn compile(&mut self) -> Result<(), crate::drop::Drop> {
self.collect_schemas();
self.collect_depths();
self.collect_descendants();
self.compile_schemas();
self.collect_stems()?;
Ok(())
}
fn collect_schemas(&mut self) {
let mut to_insert = Vec::new();
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
for type_def in self.types.values() {
for mut schema in type_def.schemas.clone() {
schema.harvest(&mut to_insert);
}
}
for punc_def in self.puncs.values() {
for mut schema in punc_def.schemas.clone() {
schema.harvest(&mut to_insert);
}
}
for enum_def in self.enums.values() {
for mut schema in enum_def.schemas.clone() {
schema.harvest(&mut to_insert);
}
}
for (id, schema) in to_insert {
self.schemas.insert(id, schema);
}
}
fn collect_depths(&mut self) {
let mut depths: HashMap<String, usize> = HashMap::new();
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
let mut current_id = id.clone();
let mut depth = 0;
let mut visited = HashSet::new();
while let Some(schema) = self.schemas.get(&current_id) {
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;
}
}
depths.insert(id, depth);
}
self.depths = depths;
}
fn collect_descendants(&mut self) {
let mut direct_refs: HashMap<String, Vec<String>> = HashMap::new();
for (id, schema) in &self.schemas {
if let Some(ref_str) = &schema.obj.r#ref {
direct_refs
.entry(ref_str.clone())
.or_default()
.push(id.clone());
}
}
// Cache generic descendants for $family runtime lookups
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();
// 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);
}
}
self.descendants = descendants;
}
fn collect_descendants_recursively(
target: &str,
direct_refs: &HashMap<String, Vec<String>>,
descendants: &mut HashSet<String>,
) {
if let Some(children) = direct_refs.get(target) {
for child in children {
if descendants.insert(child.clone()) {
Self::collect_descendants_recursively(child, direct_refs, descendants);
}
}
}
}
fn compile_schemas(&mut self) {
// Pass 3: compile_internals across pure structure
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for id in schema_ids {
if let Some(schema) = self.schemas.get_mut(&id) {
schema.compile_internals();
}
}
}
fn collect_stems(&mut self) -> Result<(), crate::drop::Drop> {
let mut db_stems: HashMap<String, HashMap<String, Arc<Stem>>> = HashMap::new();
let mut errors: Vec<crate::drop::Error> = Vec::new();
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
for schema_id in schema_ids {
if let Some(schema) = self.schemas.get(&schema_id) {
let mut inner_map = HashMap::new();
Self::discover_stems(
self,
&schema_id,
schema,
String::from(""),
None,
None,
true,
&mut inner_map,
&mut errors,
);
if !inner_map.is_empty() {
println!("SCHEMA: {} STEMS: {:?}", schema_id, inner_map.keys());
db_stems.insert(schema_id, inner_map);
}
}
}
self.stems = db_stems;
if !errors.is_empty() {
return Err(crate::drop::Drop::with_errors(errors));
}
Ok(())
}
fn discover_stems(
db: &Database,
root_schema_id: &str,
schema: &Schema,
mut current_path: String,
parent_type: Option<String>,
property_name: Option<String>,
is_root: bool,
inner_map: &mut HashMap<String, Arc<Stem>>,
errors: &mut Vec<crate::drop::Error>,
) {
let mut is_entity = false;
let mut entity_type = String::new();
// First check if the Schema's $id is a native Database Type
if let Some(ref id) = schema.obj.id {
let parts: Vec<&str> = id.split('.').collect();
if let Some(last_seg) = parts.last() {
if db.types.contains_key(*last_seg) {
is_entity = true;
entity_type = last_seg.to_string();
}
}
}
// If not found via $id, check the $ref pointer
// This allows ad-hoc schemas (like `save_person.response`) to successfully adopt the Type of what they $ref
if !is_entity {
if let Some(ref r) = schema.obj.r#ref {
let parts: Vec<&str> = r.split('.').collect();
if let Some(last_seg) = parts.last() {
if db.types.contains_key(*last_seg) {
is_entity = true;
entity_type = last_seg.to_string();
}
}
}
}
let mut relation_col = None;
if is_entity {
if let (Some(pt), Some(prop)) = (&parent_type, &property_name) {
let expected_col = format!("{}_id", prop);
let mut found = false;
for rel in db.relations.values() {
if (rel.source_type == *pt && rel.destination_type == entity_type)
|| (rel.source_type == entity_type && rel.destination_type == *pt)
{
if rel.source_columns.contains(&expected_col) {
relation_col = Some(expected_col.clone());
found = true;
break;
}
}
}
if !found {
relation_col = Some(expected_col);
}
}
let stem = Stem {
r#type: entity_type.clone(),
relation: relation_col,
schema: Arc::new(schema.clone()),
};
let mut branch_path = if is_root {
String::new()
} else if current_path.is_empty() {
entity_type.clone()
} else {
format!("{}/{}", current_path, entity_type)
};
// DEDUPLICATION: If we just recursed into the EXACT same entity type definition,
// do not append again and do not re-register the stem.
let already_registered =
if current_path == entity_type || current_path.ends_with(&format!("/{}", entity_type)) {
branch_path = current_path.clone();
true
} else {
false
};
if !already_registered {
if inner_map.contains_key(&branch_path) {
errors.push(crate::drop::Error {
code: "STEM_COLLISION".to_string(),
message: format!("The stem path `{}` resolves to multiple Entity boundaries. This usually occurs during un-wrapped $family or oneOf polymorphic schemas where multiple Entities are directly assigned to the same property. To fix this, encapsulate the polymorphic branch.", branch_path),
details: crate::drop::ErrorDetails {
path: root_schema_id.to_string(),
},
});
}
inner_map.insert(branch_path.clone(), Arc::new(stem));
}
// Update current_path for structural children
current_path = branch_path;
}
let next_parent = if is_entity {
Some(entity_type.clone())
} else {
parent_type.clone()
};
// Properties branch
if let Some(props) = &schema.obj.properties {
for (k, v) in props {
// Bypass target and source properties if we are in a relationship
if let Some(parent_str) = &next_parent {
if let Some(pt) = db.types.get(parent_str) {
if pt.relationship && (k == "target" || k == "source") {
Self::discover_stems(
db,
root_schema_id,
v,
current_path.clone(),
next_parent.clone(),
Some(k.clone()),
false,
inner_map,
errors,
);
continue;
}
}
}
// Standard Property Pathing
let next_path = if current_path.is_empty() {
k.clone()
} else {
format!("{}/{}", current_path, k)
};
Self::discover_stems(
db,
root_schema_id,
v,
next_path,
next_parent.clone(),
Some(k.clone()),
false,
inner_map,
errors,
);
}
}
// Array Item branch
if let Some(items) = &schema.obj.items {
Self::discover_stems(
db,
root_schema_id,
items,
current_path.clone(),
next_parent.clone(),
property_name.clone(),
false, // Arrays themselves aren't polymorphic branches, their items might be
inner_map,
errors,
);
}
// Follow external reference if we didn't just crawl local properties
if schema.obj.properties.is_none() && schema.obj.items.is_none() && schema.obj.one_of.is_none()
{
if let Some(ref r) = schema.obj.r#ref {
if let Some(target_schema) = db.schemas.get(r) {
Self::discover_stems(
db,
root_schema_id,
target_schema,
current_path.clone(),
next_parent.clone(),
property_name.clone(),
false,
inner_map,
errors,
);
}
}
}
// Polymorphism branch
if let Some(arr) = &schema.obj.one_of {
for v in arr {
Self::discover_stems(
db,
root_schema_id,
v.as_ref(),
current_path.clone(),
next_parent.clone(),
property_name.clone(),
false,
inner_map,
errors,
);
}
}
if let Some(arr) = &schema.obj.all_of {
for v in arr {
Self::discover_stems(
db,
root_schema_id,
v.as_ref(),
current_path.clone(),
next_parent.clone(),
property_name.clone(),
false,
inner_map,
errors,
);
}
}
}
}

35
src/database/page.rs Normal file
View File

@ -0,0 +1,35 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Page {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Sidebar {
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Action {
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub navigate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub present: Option<String>,
}

20
src/database/punc.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::database::page::Page;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Punc {
pub id: String,
pub r#type: String,
pub name: String,
pub module: String,
pub source: String,
pub description: Option<String>,
pub public: bool,
pub form: bool,
pub get: Option<String>,
pub page: Option<Page>,
#[serde(default)]
pub schemas: Vec<Schema>,
}

12
src/database/relation.rs Normal file
View File

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Relation {
pub constraint: String,
pub source_type: String,
pub source_columns: Vec<String>,
pub destination_type: String,
pub destination_columns: Vec<String>,
pub prefix: Option<String>,
}

342
src/database/schema.rs Normal file
View File

@ -0,0 +1,342 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
// Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema.
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
Ok(Some(v))
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
pub id: Option<String>,
#[serde(rename = "$ref")]
pub r#ref: Option<String>,
/*
Note: The `Ref` field in the Go struct is a pointer populated by the linker.
In Rust, we might handle this differently (e.g., separate lookup or Rc/Arc),
so we omit the direct recursive `Ref` field for now and rely on `ref_string`.
*/
pub description: Option<String>,
pub title: Option<String>,
#[serde(default)] // Allow missing type
#[serde(rename = "type")]
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
// Object Keywords
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")]
pub family: Option<String>,
pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies
pub dependencies: Option<BTreeMap<String, Dependency>>,
// Array Keywords
#[serde(rename = "items")]
pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")]
pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation
#[serde(rename = "minLength")]
pub min_length: Option<f64>,
#[serde(rename = "maxLength")]
pub max_length: Option<f64>,
pub pattern: Option<String>,
// Array Validation
#[serde(rename = "minItems")]
pub min_items: Option<f64>,
#[serde(rename = "maxItems")]
pub max_items: Option<f64>,
#[serde(rename = "uniqueItems")]
pub unique_items: Option<bool>,
#[serde(rename = "contains")]
pub contains: Option<Arc<Schema>>,
#[serde(rename = "minContains")]
pub min_contains: Option<f64>,
#[serde(rename = "maxContains")]
pub max_contains: Option<f64>,
// Object Validation
#[serde(rename = "minProperties")]
pub min_properties: Option<f64>,
#[serde(rename = "maxProperties")]
pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")]
pub property_names: Option<Arc<Schema>>,
// Numeric Validation
pub format: Option<String>,
#[serde(rename = "enum")]
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
#[serde(
default,
rename = "const",
deserialize_with = "crate::database::schema::deserialize_some"
)]
pub const_: Option<Value>,
// Numeric Validation
#[serde(rename = "multipleOf")]
pub multiple_of: Option<f64>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
pub exclusive_maximum: Option<f64>,
// Combining Keywords
#[serde(rename = "allOf")]
pub all_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "oneOf")]
pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")]
pub not: Option<Arc<Schema>>,
#[serde(rename = "if")]
pub if_: Option<Arc<Schema>>,
#[serde(rename = "then")]
pub then_: Option<Arc<Schema>>,
#[serde(rename = "else")]
pub else_: Option<Arc<Schema>>,
// Custom Vocabularies
pub form: Option<Vec<String>>,
pub display: Option<Vec<String>>,
#[serde(rename = "enumNames")]
pub enum_names: Option<Vec<String>>,
pub control: Option<String>,
pub actions: Option<BTreeMap<String, Action>>,
pub computer: Option<String>,
#[serde(default)]
pub extensible: Option<bool>,
#[serde(skip)]
pub compiled_format: Option<CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: Option<CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(CompiledRegex, Arc<Schema>)>>,
}
/// Represents a compiled format validator
#[derive(Clone)]
pub enum CompiledFormat {
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
Regex(regex::Regex),
}
impl std::fmt::Debug for CompiledFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"),
CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r),
}
}
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub regex::Regex);
#[derive(Debug, Clone, Serialize, Default)]
pub struct Schema {
#[serde(flatten)]
pub obj: SchemaObject,
#[serde(skip)]
pub always_fail: bool,
}
impl std::ops::Deref for Schema {
type Target = SchemaObject;
fn deref(&self) -> &Self::Target {
&self.obj
}
}
impl std::ops::DerefMut for Schema {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.obj
}
}
impl Schema {
pub fn compile_internals(&mut self) {
self.map_children(|child| child.compile_internals());
if let Some(format_str) = &self.obj.format
&& let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str())
{
self.obj.compiled_format = Some(crate::database::schema::CompiledFormat::Func(fmt.func));
}
if let Some(pattern_str) = &self.obj.pattern
&& let Ok(re) = regex::Regex::new(pattern_str)
{
self.obj.compiled_pattern = Some(crate::database::schema::CompiledRegex(re));
}
if let Some(pattern_props) = &self.obj.pattern_properties {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::schema::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
self.obj.compiled_pattern_properties = Some(compiled);
}
}
}
pub fn harvest(&mut self, to_insert: &mut Vec<(String, Schema)>) {
if let Some(id) = &self.obj.id {
to_insert.push((id.clone(), self.clone()));
}
self.map_children(|child| child.harvest(to_insert));
}
pub fn map_children<F>(&mut self, mut f: F)
where
F: FnMut(&mut Schema),
{
if let Some(props) = &mut self.obj.properties {
for v in props.values_mut() {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);
}
}
if let Some(pattern_props) = &mut self.obj.pattern_properties {
for v in pattern_props.values_mut() {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);
}
}
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);
}
};
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);
}
let mut map_opt = |opt: &mut Option<Arc<Schema>>| {
if let Some(v) = opt {
let mut inner = (**v).clone();
f(&mut inner);
*v = Arc::new(inner);
}
};
map_opt(&mut self.obj.additional_properties);
map_opt(&mut self.obj.items);
map_opt(&mut self.obj.contains);
map_opt(&mut self.obj.property_names);
map_opt(&mut self.obj.not);
map_opt(&mut self.obj.if_);
map_opt(&mut self.obj.then_);
map_opt(&mut self.obj.else_);
}
}
impl<'de> Deserialize<'de> for Schema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v: Value = Deserialize::deserialize(deserializer)?;
if let Some(b) = v.as_bool() {
let mut obj = SchemaObject::default();
if b {
obj.extensible = Some(true);
}
return Ok(Schema {
obj,
always_fail: !b,
});
}
let mut obj: SchemaObject =
serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
// If a schema is effectively empty (except for potentially carrying an ID),
// it functions as a boolean `true` schema in Draft2020 which means it should not
// restrict additional properties natively
let is_empty = obj.type_.is_none()
&& obj.properties.is_none()
&& obj.pattern_properties.is_none()
&& obj.additional_properties.is_none()
&& obj.required.is_none()
&& obj.dependencies.is_none()
&& obj.items.is_none()
&& obj.prefix_items.is_none()
&& obj.contains.is_none()
&& obj.format.is_none()
&& obj.enum_.is_none()
&& obj.const_.is_none()
&& obj.all_of.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() {
obj.extensible = Some(true);
}
Ok(Schema {
obj,
always_fail: false,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaTypeOrArray {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub navigate: Option<String>,
pub punc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Props(Vec<String>),
Schema(Arc<Schema>),
}

17
src/database/stem.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stem {
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub relation: Option<String>,
// The actual database schema node mapping for
// O(1) jump table execution for queryer.
//
// Automatically skipped from `jspg_stems()` JSON payload output.
#[serde(skip)]
pub schema: Arc<Schema>,
}

40
src/database/type.rs Normal file
View File

@ -0,0 +1,40 @@
use std::collections::HashSet;
use crate::database::schema::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Type {
pub id: String,
pub r#type: String,
pub name: String,
pub module: String,
pub source: String,
#[serde(default)]
pub historical: bool,
#[serde(default)]
pub sensitive: bool,
#[serde(default)]
pub ownable: bool,
pub longevity: Option<i32>,
#[serde(default)]
pub hierarchy: Vec<String>,
#[serde(default)]
pub variations: HashSet<String>,
#[serde(default)]
pub relationship: bool,
#[serde(default)]
pub fields: Vec<String>,
pub grouped_fields: Option<Value>,
#[serde(default)]
pub lookup_fields: Vec<String>,
#[serde(default)]
pub null_fields: Vec<String>,
#[serde(default)]
pub default_fields: Vec<String>,
pub field_types: Option<Value>,
#[serde(default)]
pub schemas: Vec<Schema>,
}

72
src/drop.rs Normal file
View File

@ -0,0 +1,72 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Drop {
// We don't need id, frequency, etc. for the validation result specifically,
// as they are added by the SQL wrapper. We just need to conform to the structure.
// The user said "Validator::validate always needs to return this drop type".
// So we should match it as closely as possible.
#[serde(rename = "type")]
pub type_: String, // "drop"
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Error>,
}
impl Default for Drop {
fn default() -> Self {
Self::new()
}
}
impl Drop {
pub fn new() -> Self {
Self {
type_: "drop".to_string(),
response: None,
errors: vec![],
}
}
pub fn success() -> Self {
Self {
type_: "drop".to_string(),
response: Some(serde_json::json!("success")),
errors: vec![],
}
}
pub fn success_with_val(val: Value) -> Self {
Self {
type_: "drop".to_string(),
response: Some(val),
errors: vec![],
}
}
pub fn with_errors(errors: Vec<Error>) -> Self {
Self {
type_: "drop".to_string(),
response: None,
errors,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Error {
pub code: String,
pub message: String,
pub details: ErrorDetails,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ErrorDetails {
pub path: String,
// Extensions can be added here (package, cause, etc)
// For now, validator only provides path
}

29
src/jspg.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::database::Database;
use crate::merger::Merger;
use crate::queryer::Queryer;
use crate::validator::Validator;
use std::sync::Arc;
pub struct Jspg {
pub database: Arc<Database>,
pub validator: Validator,
pub queryer: Queryer,
pub merger: Merger,
}
impl Jspg {
pub fn new(database_val: &serde_json::Value) -> Result<Self, crate::drop::Drop> {
let database_instance = 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,
})
}
}

View File

@ -1,352 +1,139 @@
#[cfg(not(test))]
use pgrx::*;
#[cfg(not(test))]
pg_module_magic!();
use serde_json::{json, Value};
use std::{collections::HashMap, sync::RwLock};
use boon::{Compiler, Schemas, ValidationError, SchemaIndex, CompileError};
use lazy_static::lazy_static;
struct BoonCache {
schemas: Schemas,
id_to_index: HashMap<String, SchemaIndex>,
}
lazy_static! {
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
schemas: Schemas::new(),
id_to_index: HashMap::new(),
});
}
#[pg_extern(strict)]
fn cache_json_schema(schema_id: &str, schema: JsonB) -> JsonB {
let mut cache = SCHEMA_CACHE.write().unwrap();
let schema_value: Value = schema.0;
let schema_path = format!("urn:{}", schema_id);
let mut compiler = Compiler::new();
compiler.enable_format_assertions();
// Use schema_path when adding the resource
if let Err(e) = compiler.add_resource(&schema_path, schema_value.clone()) {
return JsonB(json!({
"errors": [{
"code": "SCHEMA_RESOURCE_ADD_FAILED",
"message": format!("Failed to add schema resource '{}'", schema_id),
"details": {
"path": schema_path,
"cause": format!("{}", e)
}
}]
}));
}
// Use schema_path when compiling
match compiler.compile(&schema_path, &mut cache.schemas) {
Ok(sch_index) => {
// Store the index using the original schema_id as the key
cache.id_to_index.insert(schema_id.to_string(), sch_index);
JsonB(json!({ "response": "success" }))
}
Err(e) => {
let errors = match &e {
CompileError::ValidationError { url: _url, src } => {
// Collect leaf errors from the meta-schema validation failure
let mut error_list = Vec::new();
collect_validation_errors(src, &mut error_list);
// Filter and format errors properly - no instance for schema compilation
format_drop_errors(error_list, &schema_value)
}
_ => {
// Other compilation errors
vec![json!({
"code": "SCHEMA_COMPILATION_FAILED",
"message": format!("Schema '{}' compilation failed", schema_id),
"details": {
"path": schema_path,
"cause": format!("{:?}", e)
}
})]
}
};
JsonB(json!({ "errors": errors }))
}
}
}
#[pg_extern(strict, parallel_safe)]
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
let cache = SCHEMA_CACHE.read().unwrap();
// Lookup uses the original schema_id
match cache.id_to_index.get(schema_id) {
None => JsonB(json!({
"errors": [{
"code": "SCHEMA_NOT_FOUND",
"message": format!("Schema '{}' not found in cache", schema_id),
"details": {
"cause": "Schema must be cached before validation"
}
}]
})),
Some(sch_index) => {
let instance_value: Value = instance.0;
match cache.schemas.validate(&instance_value, *sch_index) {
Ok(_) => JsonB(json!({ "response": "success" })),
Err(validation_error) => {
let mut error_list = Vec::new();
collect_validation_errors(&validation_error, &mut error_list);
let errors = format_drop_errors(error_list, &instance_value);
JsonB(json!({ "errors": errors }))
}
}
}
}
}
// Recursively collects validation errors
fn collect_validation_errors(error: &ValidationError, errors_list: &mut Vec<(String, String, String)>) {
// Check if this is a structural error that we should skip
let error_message = format!("{}", error.kind);
let is_structural = error_message == "validation failed" ||
error_message == "allOf failed" ||
error_message == "anyOf failed" ||
error_message == "not failed" ||
error_message.starts_with("oneOf failed");
if error.causes.is_empty() && !is_structural {
// This is a leaf error that's not structural
// Format just the error kind, not the whole validation error
let message = format!("{}", error.kind);
errors_list.push((
error.instance_location.to_string(),
error.schema_url.to_string(),
message
));
} else {
// Recurse into causes
for cause in &error.causes {
collect_validation_errors(cause, errors_list);
}
}
}
// Formats errors according to DropError structure
fn format_drop_errors(raw_errors: Vec<(String, String, String)>, instance: &Value) -> Vec<Value> {
use std::collections::HashMap;
use std::collections::hash_map::Entry;
// We don't filter structural paths from instance paths anymore
// because instance paths shouldn't contain these segments anyway
// The issue was likely with schema paths, not instance paths
let plausible_errors = raw_errors;
// 2. Deduplicate by instance_path and format as DropError
let mut unique_errors: HashMap<String, Value> = HashMap::new();
for (instance_path, _schema_path, message) in plausible_errors {
if let Entry::Vacant(entry) = unique_errors.entry(instance_path.clone()) {
// Convert message to error code and make it human readable
let (code, human_message) = enhance_error_message(&message);
// Extract the failing value from the instance
let failing_value = extract_value_at_path(instance, &instance_path);
entry.insert(json!({
"code": code,
"message": human_message,
"details": {
"path": instance_path,
"context": failing_value,
"cause": message // Original error message
}
}));
}
}
unique_errors.into_values().collect()
}
// Helper function to extract value at a JSON pointer path
fn extract_value_at_path(instance: &Value, path: &str) -> Value {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current = instance;
for part in parts {
match current {
Value::Object(map) => {
if let Some(value) = map.get(part) {
current = value;
} else {
return Value::Null;
}
}
Value::Array(arr) => {
if let Ok(index) = part.parse::<usize>() {
if let Some(value) = arr.get(index) {
current = value;
} else {
return Value::Null;
}
} else {
return Value::Null;
}
}
_ => return Value::Null,
}
}
current.clone()
}
// Helper to convert validation messages to error codes and human-readable messages
fn enhance_error_message(message: &str) -> (String, String) {
// Match exact boon error message patterns
let trimmed = message.trim();
if trimmed.contains("value must be one of") {
("ENUM_VIOLATED".to_string(),
"Value is not one of the allowed options".to_string())
} else if trimmed.contains("length must be >=") && trimmed.contains("but got") {
("MIN_LENGTH_VIOLATED".to_string(),
"Field length is below the minimum required".to_string())
} else if trimmed.contains("length must be <=") && trimmed.contains("but got") {
("MAX_LENGTH_VIOLATED".to_string(),
"Field length exceeds the maximum allowed".to_string())
} else if trimmed.contains("must be >=") && trimmed.contains("but got") {
("MINIMUM_VIOLATED".to_string(),
"Value is below the minimum allowed".to_string())
} else if trimmed.contains("must be <=") && trimmed.contains("but got") {
("MAXIMUM_VIOLATED".to_string(),
"Value exceeds the maximum allowed".to_string())
} else if trimmed.contains("must be >") && trimmed.contains("but got") {
("EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
"Value must be greater than the minimum".to_string())
} else if trimmed.contains("must be <") && trimmed.contains("but got") {
("EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
"Value must be less than the maximum".to_string())
} else if trimmed.contains("does not match pattern") {
("PATTERN_VIOLATED".to_string(),
"Value does not match the required pattern".to_string())
} else if trimmed.contains("missing properties") {
("REQUIRED_FIELD_MISSING".to_string(),
"Required field is missing".to_string())
} else if trimmed.contains("want") && trimmed.contains("but got") {
("TYPE_MISMATCH".to_string(),
"Field type does not match the expected type".to_string())
} else if trimmed.starts_with("value must be") && !trimmed.contains("one of") {
("CONST_VIOLATED".to_string(),
"Value does not match the required constant".to_string())
} else if trimmed.contains("is not valid") && trimmed.contains(":") {
("FORMAT_INVALID".to_string(),
extract_format_message(trimmed))
} else if trimmed.contains("items at") && trimmed.contains("are equal") {
("UNIQUE_ITEMS_VIOLATED".to_string(),
"Array contains duplicate items".to_string())
} else if trimmed.contains("additionalProperties") && trimmed.contains("not allowed") {
("ADDITIONAL_PROPERTIES_NOT_ALLOWED".to_string(),
"Object contains properties that are not allowed".to_string())
} else if trimmed.contains("is not multipleOf") {
("MULTIPLE_OF_VIOLATED".to_string(),
"Value is not a multiple of the required factor".to_string())
} else if trimmed.contains("minimum") && trimmed.contains("properties required") {
("MIN_PROPERTIES_VIOLATED".to_string(),
"Object has fewer properties than required".to_string())
} else if trimmed.contains("maximum") && trimmed.contains("properties required") {
("MAX_PROPERTIES_VIOLATED".to_string(),
"Object has more properties than allowed".to_string())
} else if trimmed.contains("minimum") && trimmed.contains("items required") {
("MIN_ITEMS_VIOLATED".to_string(),
"Array has fewer items than required".to_string())
} else if trimmed.contains("maximum") && trimmed.contains("items required") {
("MAX_ITEMS_VIOLATED".to_string(),
"Array has more items than allowed".to_string())
} else if trimmed == "false schema" {
("FALSE_SCHEMA".to_string(),
"Schema validation always fails".to_string())
} else if trimmed == "not failed" {
("NOT_VIOLATED".to_string(),
"Value matched a schema it should not match".to_string())
} else if trimmed == "allOf failed" {
("ALL_OF_VIOLATED".to_string(),
"Value does not match all required schemas".to_string())
} else if trimmed == "anyOf failed" {
("ANY_OF_VIOLATED".to_string(),
"Value does not match any of the allowed schemas".to_string())
} else if trimmed.contains("oneOf failed") {
("ONE_OF_VIOLATED".to_string(),
"Value must match exactly one schema".to_string())
} else if trimmed == "validation failed" {
("VALIDATION_FAILED".to_string(),
"Validation failed".to_string())
} else {
// For any unmatched patterns, try to provide a generic human-readable message
// while preserving the original error in details.cause
("VALIDATION_FAILED".to_string(),
"Validation failed".to_string())
}
}
// Extract a better format message
fn extract_format_message(message: &str) -> String {
if message.contains("date-time") {
"Invalid date-time format".to_string()
} else if message.contains("email") {
"Invalid email format".to_string()
} else if message.contains("uri") {
"Invalid URI format".to_string()
} else if message.contains("uuid") {
"Invalid UUID format".to_string()
} else {
"Invalid format".to_string()
}
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
let cache = SCHEMA_CACHE.read().unwrap();
cache.id_to_index.contains_key(schema_id)
}
#[pg_extern(strict)]
fn clear_json_schemas() -> JsonB {
let mut cache = SCHEMA_CACHE.write().unwrap();
*cache = BoonCache {
schemas: Schemas::new(),
id_to_index: HashMap::new(),
};
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> JsonB {
let cache = SCHEMA_CACHE.read().unwrap();
let ids: Vec<String> = cache.id_to_index.keys().cloned().collect();
JsonB(json!({ "response": ids }))
}
/// This module is required by `cargo pgrx test` invocations.
/// It must be visible at the root of your extension crate.
#[cfg(test)]
pub mod pg_test {
pub fn setup(_options: Vec<&str>) {
// perform one-off initialization when the pg_test framework starts
}
pub struct JsonB(pub serde_json::Value);
#[must_use]
pub fn postgresql_conf_options() -> Vec<&'static str> {
// return any postgresql.conf settings that are required for your tests
vec![]
pub mod database;
pub mod drop;
pub mod jspg;
pub mod merger;
pub mod queryer;
pub mod validator;
use std::sync::{Arc, RwLock};
lazy_static::lazy_static! {
// Global Atomic Swap Container:
// - RwLock: To protect the SWAP of the Option.
// - Option: Because it starts empty.
// - Arc: Because multiple running threads might hold the OLD engine while we swap.
// - Jspg: The root semantic engine encapsulating the database metadata, validator, queryer, and merger.
static ref GLOBAL_JSPG: RwLock<Option<Arc<jspg::Jspg>>> = RwLock::new(None);
}
fn jspg_failure() -> JsonB {
let error = crate::drop::Error {
code: "ENGINE_NOT_INITIALIZED".to_string(),
message: "JSPG extension has not been initialized via jspg_setup".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
#[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);
// 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()),
}
}
#[cfg_attr(not(test), pg_extern)]
pub fn jspg_merge(data: JsonB) -> JsonB {
// Try to acquire a read lock to get a clone of the Engine Arc
let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
};
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
include!("tests.rs");
match engine_opt {
Some(engine) => {
let drop = engine.merger.merge(data.0);
JsonB(serde_json::to_value(drop).unwrap())
}
None => jspg_failure(),
}
}
#[cfg_attr(not(test), pg_extern)]
pub fn jspg_query(schema_id: &str, stem: Option<&str>, filters: Option<JsonB>) -> JsonB {
let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
};
match engine_opt {
Some(engine) => {
let drop = engine
.queryer
.query(schema_id, stem, filters.as_ref().map(|f| &f.0));
JsonB(serde_json::to_value(drop).unwrap())
}
None => jspg_failure(),
}
}
// `mask_json_schema` has been removed as the mask architecture is fully replaced by Spi string queries during DB interactions.
#[cfg_attr(not(test), pg_extern(strict, parallel_safe))]
pub fn jspg_validate(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let jspg_arc = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(engine) = jspg_arc {
let drop = engine.validator.validate(schema_id, &instance.0);
JsonB(serde_json::to_value(drop).unwrap())
} else {
jspg_failure()
}
}
#[cfg_attr(not(test), pg_extern)]
pub fn jspg_stems() -> JsonB {
use serde_json::{Map, Value};
let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap();
lock.clone()
};
match engine_opt {
Some(engine) => {
JsonB(serde_json::to_value(&engine.database.stems).unwrap_or(Value::Object(Map::new())))
}
None => JsonB(Value::Object(Map::new())),
}
}
#[cfg_attr(not(test), pg_extern(strict))]
pub fn jspg_teardown() -> JsonB {
let mut lock = GLOBAL_JSPG.write().unwrap();
*lock = None;
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
}
#[cfg(test)]
pub mod tests;

24
src/merger/cache.rs Normal file
View File

@ -0,0 +1,24 @@
use dashmap::DashMap;
pub struct StatementCache {
/// Maps a Cache Key (String) -> SQL String (String)
statements: DashMap<String, String>,
}
impl StatementCache {
pub fn new(_max_capacity: u64) -> Self {
Self {
statements: DashMap::new(),
}
}
/// Retrieve an existing statement name by key, or None if it missed
pub fn get(&self, key: &str) -> Option<String> {
self.statements.get(key).map(|v| v.clone())
}
/// Insert a completely verified/compiled statement string into the cache
pub fn insert(&self, key: String, sql: String) {
self.statements.insert(key, sql);
}
}

882
src/merger/mod.rs Normal file
View File

@ -0,0 +1,882 @@
//! The `merger` module handles executing Postgres SPI directives dynamically based on JSON payloads
//! using the structurally isolated schema rules provided by the `Database` registry.
pub mod cache;
use crate::database::Database;
use serde_json::Value;
use std::sync::Arc;
pub struct Merger {
pub db: Arc<Database>,
pub cache: cache::StatementCache,
}
impl Merger {
pub fn new(db: Arc<Database>) -> Self {
Self {
db,
cache: cache::StatementCache::new(10_000),
}
}
pub fn merge(&self, data: Value) -> crate::drop::Drop {
match self.merge_internal(data) {
Ok(val) => {
let stripped_val = match val {
Value::Object(mut map) => {
let mut out = serde_json::Map::new();
if let Some(id) = map.remove("id") {
out.insert("id".to_string(), id);
}
Value::Object(out)
}
Value::Array(arr) => {
let mut out_arr = Vec::new();
for item in arr {
if let Value::Object(mut map) = item {
let mut out = serde_json::Map::new();
if let Some(id) = map.remove("id") {
out.insert("id".to_string(), id);
}
out_arr.push(Value::Object(out));
} else {
out_arr.push(Value::Null);
}
}
Value::Array(out_arr)
}
other => other,
};
crate::drop::Drop::success_with_val(stripped_val)
}
Err(msg) => crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "MERGE_FAILED".to_string(),
message: msg,
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
}]),
}
}
pub(crate) fn merge_internal(&self, data: Value) -> Result<Value, String> {
match data {
Value::Array(items) => self.merge_array(items),
Value::Object(map) => self.merge_object(map),
_ => Err("Invalid merge payload: root must be an Object or Array".to_string()),
}
}
fn merge_array(&self, items: Vec<Value>) -> Result<Value, String> {
let mut resolved_items = Vec::new();
for item in items {
let resolved = self.merge_internal(item)?;
resolved_items.push(resolved);
}
Ok(Value::Array(resolved_items))
}
fn merge_object(&self, obj: serde_json::Map<String, Value>) -> Result<Value, String> {
let type_name = match obj.get("type").and_then(|v| v.as_str()) {
Some(t) => t.to_string(),
None => return Err("Missing required 'type' field on object".to_string()),
};
let type_def = match self.db.types.get(&type_name) {
Some(t) => t,
None => return Err(format!("Unknown entity type: {}", type_name)),
};
// 1. Segment the entity: fields in type_def.fields are database fields, others are relationships
let mut entity_fields = serde_json::Map::new();
let mut entity_objects = serde_json::Map::new();
let mut entity_arrays = serde_json::Map::new();
for (k, v) in obj {
let is_field = type_def.fields.contains(&k) || k == "created";
let typeof_v = match &v {
Value::Object(_) => "object",
Value::Array(_) => "array",
_ => "other",
};
if is_field {
entity_fields.insert(k, v);
} else if typeof_v == "object" {
entity_objects.insert(k, v);
} else if typeof_v == "array" {
entity_arrays.insert(k, v);
}
}
let user_id = self.db.auth_user_id()?;
let timestamp = self.db.timestamp()?;
let mut entity_change_kind = None;
let mut entity_fetched = None;
// 2. Pre-stage the entity (for non-relationships)
if !type_def.relationship {
let (fields, kind, fetched) =
self.stage_entity(entity_fields.clone(), type_def, &user_id, &timestamp)?;
entity_fields = fields;
entity_change_kind = kind;
entity_fetched = fetched;
}
let mut entity_response = serde_json::Map::new();
// 3. Handle related objects
for (relation_name, relative_val) in entity_objects {
let mut relative = match relative_val {
Value::Object(m) => m,
_ => continue,
};
let relative_relation = self.get_entity_relation(type_def, &relative, &relation_name)?;
if let Some(relation) = relative_relation {
let parent_is_source = type_def.hierarchy.contains(&relation.source_type);
if parent_is_source {
// Parent holds FK to Child. Child MUST be generated FIRST.
if !relative.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative.insert("organization_id".to_string(), org_id.clone());
}
}
let merged_relative = match self.merge_internal(Value::Object(relative))? {
Value::Object(m) => m,
_ => continue,
};
Self::apply_entity_relation(
&mut entity_fields,
&relation.source_columns,
&relation.destination_columns,
&merged_relative,
);
entity_response.insert(relation_name, Value::Object(merged_relative));
} else {
// Child holds FK back to Parent.
if !relative.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative.insert("organization_id".to_string(), org_id.clone());
}
}
Self::apply_entity_relation(
&mut relative,
&relation.source_columns,
&relation.destination_columns,
&entity_fields,
);
let merged_relative = match self.merge_internal(Value::Object(relative))? {
Value::Object(m) => m,
_ => continue,
};
entity_response.insert(relation_name, Value::Object(merged_relative));
}
}
}
// 4. Post-stage the entity (for relationships)
if type_def.relationship {
let (fields, kind, fetched) =
self.stage_entity(entity_fields.clone(), type_def, &user_id, &timestamp)?;
entity_fields = fields;
entity_change_kind = kind;
entity_fetched = fetched;
}
// 5. Process the main entity fields
self.merge_entity_fields(
entity_change_kind.as_deref().unwrap_or(""),
&type_name,
type_def,
&entity_fields,
entity_fetched.as_ref(),
)?;
// Add main entity fields to response
for (k, v) in &entity_fields {
entity_response.insert(k.clone(), v.clone());
}
// 6. Handle related arrays
for (relation_name, relative_val) in entity_arrays {
let relative_arr = match relative_val {
Value::Array(a) => a,
_ => continue,
};
if relative_arr.is_empty() {
continue;
}
let first_relative = match &relative_arr[0] {
Value::Object(m) => m,
_ => continue,
};
let relative_relation = self.get_entity_relation(type_def, first_relative, &relation_name)?;
if let Some(relation) = relative_relation {
let mut relative_responses = Vec::new();
for relative_item_val in relative_arr {
if let Value::Object(mut relative_item) = relative_item_val {
if !relative_item.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative_item.insert("organization_id".to_string(), org_id.clone());
}
}
Self::apply_entity_relation(
&mut relative_item,
&relation.source_columns,
&relation.destination_columns,
&entity_fields,
);
let merged_relative = match self.merge_internal(Value::Object(relative_item))? {
Value::Object(m) => m,
_ => continue,
};
relative_responses.push(Value::Object(merged_relative));
}
}
entity_response.insert(relation_name, Value::Array(relative_responses));
}
}
// 7. Perform change tracking
self.merge_entity_change(
&entity_fields,
entity_fetched.as_ref(),
entity_change_kind.as_deref(),
&user_id,
&timestamp,
)?;
// Produce the full tree response
let mut final_response = serde_json::Map::new();
if let Some(fetched) = entity_fetched {
for (k, v) in fetched {
final_response.insert(k, v);
}
}
for (k, v) in entity_response {
final_response.insert(k, v);
}
Ok(Value::Object(final_response))
}
fn stage_entity(
&self,
mut entity_fields: serde_json::Map<String, Value>,
type_def: &crate::database::r#type::Type,
user_id: &str,
timestamp: &str,
) -> Result<
(
serde_json::Map<String, Value>,
Option<String>,
Option<serde_json::Map<String, Value>>,
),
String,
> {
let type_name = type_def.name.as_str();
let entity_fetched = self.fetch_entity(&entity_fields, type_def)?;
let system_keys = vec![
"id".to_string(),
"type".to_string(),
"created_by".to_string(),
"modified_by".to_string(),
"created_at".to_string(),
"modified_at".to_string(),
];
let changes = self.compare_entities(
entity_fetched.as_ref(),
&entity_fields,
&type_def.fields,
&system_keys,
);
let mut entity_change_kind = None;
if entity_fetched.is_none() {
let entity_id = entity_fields
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("");
let id_val = if entity_id.is_empty() {
Value::String(uuid::Uuid::new_v4().to_string())
} else {
Value::String(entity_id.to_string())
};
entity_change_kind = Some("create".to_string());
let mut new_fields = changes.clone();
new_fields.insert("id".to_string(), id_val);
new_fields.insert("type".to_string(), Value::String(type_name.to_string()));
new_fields.insert("created_by".to_string(), Value::String(user_id.to_string()));
new_fields.insert(
"created_at".to_string(),
Value::String(timestamp.to_string()),
);
new_fields.insert(
"modified_by".to_string(),
Value::String(user_id.to_string()),
);
new_fields.insert(
"modified_at".to_string(),
Value::String(timestamp.to_string()),
);
entity_fields = new_fields;
} else if changes.is_empty() {
let mut new_fields = serde_json::Map::new();
new_fields.insert(
"id".to_string(),
entity_fetched.as_ref().unwrap().get("id").unwrap().clone(),
);
new_fields.insert("type".to_string(), Value::String(type_name.to_string()));
entity_fields = new_fields;
} else {
let is_archived = changes
.get("archived")
.and_then(|v| v.as_bool())
.unwrap_or(false);
entity_change_kind = if is_archived {
Some("delete".to_string())
} else {
Some("update".to_string())
};
let mut new_fields = changes.clone();
new_fields.insert(
"id".to_string(),
entity_fetched.as_ref().unwrap().get("id").unwrap().clone(),
);
new_fields.insert("type".to_string(), Value::String(type_name.to_string()));
new_fields.insert(
"modified_by".to_string(),
Value::String(user_id.to_string()),
);
new_fields.insert(
"modified_at".to_string(),
Value::String(timestamp.to_string()),
);
entity_fields = new_fields;
}
Ok((entity_fields, entity_change_kind, entity_fetched))
}
fn fetch_entity(
&self,
entity_fields: &serde_json::Map<String, Value>,
entity_type: &crate::database::r#type::Type,
) -> Result<Option<serde_json::Map<String, Value>>, String> {
let id_val = entity_fields.get("id");
let entity_type_name = entity_type.name.as_str();
let mut lookup_complete = false;
if !entity_type.lookup_fields.is_empty() {
lookup_complete = true;
for column in &entity_type.lookup_fields {
match entity_fields.get(column) {
Some(Value::Null) | None => {
lookup_complete = false;
break;
}
Some(Value::String(s)) if s.is_empty() => {
lookup_complete = false;
break;
}
_ => {}
}
}
}
if id_val.is_none() && !lookup_complete {
return Ok(None);
}
let fetch_sql_template = if let Some(cached) = self.cache.get(entity_type_name) {
cached
} else {
let mut select_list = String::from("to_jsonb(t1.*)");
let mut join_clauses = format!("FROM agreego.\"{}\" t1", entity_type.hierarchy[0]);
for (i, table_name) in entity_type.hierarchy.iter().enumerate().skip(1) {
let t_alias = format!("t{}", i + 1);
join_clauses.push_str(&format!(
" LEFT JOIN agreego.\"{}\" {} ON {}.id = t1.id",
table_name, t_alias, t_alias
));
select_list.push_str(&format!(" || to_jsonb({}.*)", t_alias));
}
let template = format!("SELECT {} {}", select_list, join_clauses);
self
.cache
.insert(entity_type_name.to_string(), template.clone());
template
};
let where_clause = if let Some(id) = id_val {
format!("WHERE t1.id = {}", Self::quote_literal(id))
} else if lookup_complete {
let mut lookup_predicates = Vec::new();
for column in &entity_type.lookup_fields {
let val = entity_fields.get(column).unwrap_or(&Value::Null);
if column == "type" {
lookup_predicates.push(format!("t1.\"{}\" = {}", column, Self::quote_literal(val)));
} else {
lookup_predicates.push(format!("\"{}\" = {}", column, Self::quote_literal(val)));
}
}
format!("WHERE {}", lookup_predicates.join(" AND "))
} else {
return Ok(None);
};
let final_sql = format!("{} {}", fetch_sql_template, where_clause);
let fetched = match self.db.query(&final_sql, None) {
Ok(Value::Array(table)) => {
if table.len() > 1 {
Err(format!(
"TOO_MANY_LOOKUP_ROWS: Lookup for {} found too many existing rows",
entity_type_name
))
} else if table.is_empty() {
Ok(None)
} else {
let row = table.first().unwrap();
match row {
Value::Object(map) => Ok(Some(map.clone())),
other => Err(format!("Expected JSON object, got: {:?}", other)),
}
}
}
Ok(_) => Err("Expected array from query in fetch_entity".to_string()),
Err(e) => Err(format!("SPI error in fetch_entity: {:?}", e)),
}?;
Ok(fetched)
}
fn merge_entity_fields(
&self,
change_kind: &str,
entity_type_name: &str,
entity_type: &crate::database::r#type::Type,
entity_fields: &serde_json::Map<String, Value>,
_entity_fetched: Option<&serde_json::Map<String, Value>>,
) -> Result<(), String> {
if change_kind.is_empty() {
return Ok(());
}
let id_str = match entity_fields.get("id").and_then(|v| v.as_str()) {
Some(id) => id,
None => return Err("Missing 'id' for merge execution".to_string()),
};
let grouped_fields = match &entity_type.grouped_fields {
Some(Value::Object(map)) => map,
_ => {
return Err(format!(
"Grouped fields missing for type {}",
entity_type_name
));
}
};
let mut execute_order: Vec<String> = entity_type.hierarchy.clone();
if change_kind == "create" {
execute_order.reverse();
}
for table_name in execute_order {
let table_fields = match grouped_fields.get(&table_name).and_then(|v| v.as_array()) {
Some(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>(),
None => continue,
};
let mut entity_pairs = serde_json::Map::new();
for (k, v) in entity_fields {
if table_fields.contains(k) {
entity_pairs.insert(k.clone(), v.clone());
}
}
if change_kind == "create" {
if !entity_pairs.contains_key("id") && table_fields.contains(&"id".to_string()) {
entity_pairs.insert("id".to_string(), Value::String(id_str.to_string()));
}
if !entity_pairs.contains_key("type") && table_fields.contains(&"type".to_string()) {
entity_pairs.insert(
"type".to_string(),
Value::String(entity_type_name.to_string()),
);
}
let mut columns = Vec::new();
let mut values = Vec::new();
let mut sorted_keys: Vec<_> = entity_pairs.keys().cloned().collect();
sorted_keys.sort();
for key in &sorted_keys {
columns.push(format!("\"{}\"", key));
let val = entity_pairs.get(key).unwrap();
if val.as_str() == Some("") {
values.push("NULL".to_string());
} else {
values.push(Self::quote_literal(val));
}
}
if columns.is_empty() {
continue;
}
let sql = format!(
"INSERT INTO agreego.\"{}\" ({}) VALUES ({})",
table_name,
columns.join(", "),
values.join(", ")
);
self
.db
.execute(&sql, None)
.map_err(|e| format!("SPI Error in INSERT: {:?}", e))?;
} else if change_kind == "update" || change_kind == "delete" {
entity_pairs.remove("id");
entity_pairs.remove("type");
if entity_pairs.is_empty() {
continue;
}
let mut set_clauses = Vec::new();
let mut sorted_keys: Vec<_> = entity_pairs.keys().cloned().collect();
sorted_keys.sort();
for key in &sorted_keys {
let val = entity_pairs.get(key).unwrap();
if val.as_str() == Some("") {
set_clauses.push(format!("\"{}\" = NULL", key));
} else {
set_clauses.push(format!("\"{}\" = {}", key, Self::quote_literal(val)));
}
}
let sql = format!(
"UPDATE agreego.\"{}\" SET {} WHERE id = {}",
table_name,
set_clauses.join(", "),
Self::quote_literal(&Value::String(id_str.to_string()))
);
self
.db
.execute(&sql, None)
.map_err(|e| format!("SPI Error in UPDATE: {:?}", e))?;
}
}
Ok(())
}
fn merge_entity_change(
&self,
entity_fields: &serde_json::Map<String, Value>,
entity_fetched: Option<&serde_json::Map<String, Value>>,
entity_change_kind: Option<&str>,
user_id: &str,
timestamp: &str,
) -> Result<(), String> {
let change_kind = match entity_change_kind {
Some(k) => k,
None => return Ok(()),
};
let id_str = entity_fields.get("id").unwrap();
let type_name = entity_fields.get("type").unwrap();
let mut changes = serde_json::Map::new();
let is_update = change_kind == "update" || change_kind == "delete";
if !is_update {
let system_keys = vec![
"id".to_string(),
"created_by".to_string(),
"modified_by".to_string(),
"created_at".to_string(),
"modified_at".to_string(),
];
for (k, v) in entity_fields {
if !system_keys.contains(k) {
changes.insert(k.clone(), v.clone());
}
}
} else {
let system_keys = vec![
"id".to_string(),
"type".to_string(),
"created_by".to_string(),
"modified_by".to_string(),
"created_at".to_string(),
"modified_at".to_string(),
];
for (k, v) in entity_fields {
if !system_keys.contains(k) {
if let Some(fetched) = entity_fetched {
let old_val = fetched.get(k).unwrap_or(&Value::Null);
if v != old_val {
changes.insert(k.clone(), v.clone());
}
}
}
}
changes.insert("type".to_string(), type_name.clone());
}
let mut complete = entity_fields.clone();
if is_update {
if let Some(fetched) = entity_fetched {
let mut temp = fetched.clone();
for (k, v) in entity_fields {
temp.insert(k.clone(), v.clone());
}
complete = temp;
}
}
let mut notification = serde_json::Map::new();
notification.insert("complete".to_string(), Value::Object(complete));
if is_update {
notification.insert("changes".to_string(), Value::Object(changes.clone()));
}
let change_sql = format!(
"INSERT INTO agreego.change (changes, entity_id, id, kind, modified_at, modified_by) VALUES ({}, {}, {}, {}, {}, {})",
Self::quote_literal(&Value::Object(changes)),
Self::quote_literal(id_str),
Self::quote_literal(&Value::String(uuid::Uuid::new_v4().to_string())),
Self::quote_literal(&Value::String(change_kind.to_string())),
Self::quote_literal(&Value::String(timestamp.to_string())),
Self::quote_literal(&Value::String(user_id.to_string()))
);
let notify_sql = format!(
"SELECT pg_notify('entity', {})",
Self::quote_literal(&Value::String(Value::Object(notification).to_string()))
);
self
.db
.execute(&change_sql, None)
.map_err(|e| format!("Executor Error in change: {:?}", e))?;
self
.db
.execute(&notify_sql, None)
.map_err(|e| format!("Executor Error in notify: {:?}", e))?;
Ok(())
}
fn compare_entities(
&self,
fetched_entity: Option<&serde_json::Map<String, Value>>,
new_fields: &serde_json::Map<String, Value>,
type_fields: &[String],
system_keys: &[String],
) -> serde_json::Map<String, Value> {
let mut changes = serde_json::Map::new();
if fetched_entity.is_none() {
for (k, v) in new_fields {
if type_fields.contains(k) && !system_keys.contains(k) {
changes.insert(k.clone(), v.clone());
}
}
return changes;
}
let old_map = fetched_entity.unwrap();
for (k, v) in new_fields {
if type_fields.contains(k) && !system_keys.contains(k) {
let old_val = old_map.get(k).unwrap_or(&Value::Null);
if v != old_val {
changes.insert(k.clone(), v.clone());
}
}
}
changes
}
fn reduce_entity_relations(
&self,
mut matching_relations: Vec<crate::database::relation::Relation>,
relative: &serde_json::Map<String, Value>,
relation_name: &str,
) -> Result<Option<crate::database::relation::Relation>, String> {
if matching_relations.is_empty() {
return Ok(None);
}
if matching_relations.len() == 1 {
return Ok(Some(matching_relations.pop().unwrap()));
}
let exact_match: Vec<_> = matching_relations
.iter()
.filter(|r| r.prefix.as_deref() == Some(relation_name))
.cloned()
.collect();
if exact_match.len() == 1 {
return Ok(Some(exact_match.into_iter().next().unwrap()));
}
matching_relations.retain(|r| {
if let Some(prefix) = &r.prefix {
!relative.contains_key(prefix)
} else {
true
}
});
if matching_relations.len() == 1 {
Ok(Some(matching_relations.pop().unwrap()))
} else {
let constraints: Vec<_> = matching_relations
.iter()
.map(|r| r.constraint.clone())
.collect();
Err(format!(
"AMBIGUOUS_TYPE_RELATIONS: Could not reduce ambiguous type relations: {}",
constraints.join(", ")
))
}
}
fn get_entity_relation(
&self,
entity_type: &crate::database::r#type::Type,
relative: &serde_json::Map<String, Value>,
relation_name: &str,
) -> Result<Option<crate::database::relation::Relation>, String> {
let relative_type_name = match relative.get("type").and_then(|v| v.as_str()) {
Some(t) => t,
None => return Ok(None),
};
let relative_type = match self.db.types.get(relative_type_name) {
Some(t) => t,
None => return Ok(None),
};
let mut relative_relations: Vec<crate::database::relation::Relation> = Vec::new();
for r in self.db.relations.values() {
if r.source_type != "entity" && r.destination_type != "entity" {
let condition1 = relative_type.hierarchy.contains(&r.source_type)
&& entity_type.hierarchy.contains(&r.destination_type);
let condition2 = entity_type.hierarchy.contains(&r.source_type)
&& relative_type.hierarchy.contains(&r.destination_type);
if condition1 || condition2 {
relative_relations.push(r.clone());
}
}
}
let mut relative_relation =
self.reduce_entity_relations(relative_relations, relative, relation_name)?;
if relative_relation.is_none() {
let mut poly_relations: Vec<crate::database::relation::Relation> = Vec::new();
for r in self.db.relations.values() {
if r.destination_type == "entity" {
let condition1 = relative_type.hierarchy.contains(&r.source_type);
let condition2 = entity_type.hierarchy.contains(&r.source_type);
if condition1 || condition2 {
poly_relations.push(r.clone());
}
}
}
relative_relation = self.reduce_entity_relations(poly_relations, relative, relation_name)?;
}
Ok(relative_relation)
}
fn apply_entity_relation(
source_entity: &mut serde_json::Map<String, Value>,
source_columns: &[String],
destination_columns: &[String],
destination_entity: &serde_json::Map<String, Value>,
) {
if source_columns.len() != destination_columns.len() {
return;
}
for i in 0..source_columns.len() {
if let Some(dest_val) = destination_entity.get(&destination_columns[i]) {
source_entity.insert(source_columns[i].clone(), dest_val.clone());
}
}
}
fn quote_literal(val: &Value) -> String {
match val {
Value::Null => "NULL".to_string(),
Value::Bool(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
Value::Number(n) => {
if let Some(f) = n.as_f64() {
if f.fract() == 0.0 {
return f.trunc().to_string();
}
}
n.to_string()
}
Value::String(s) => {
if s.is_empty() {
"NULL".to_string()
} else {
format!("'{}'", s.replace('\'', "''"))
}
}
_ => format!(
"'{}'",
serde_json::to_string(val).unwrap().replace('\'', "''")
),
}
}
}

499
src/queryer/compiler.rs Normal file
View File

@ -0,0 +1,499 @@
use crate::database::Database;
use std::sync::Arc;
pub struct SqlCompiler {
pub db: Arc<Database>,
}
impl SqlCompiler {
pub fn new(db: Arc<Database>) -> Self {
Self { db }
}
/// Compiles a JSON schema into a nested PostgreSQL query returning JSONB
pub fn compile(
&self,
schema_id: &str,
stem_path: Option<&str>,
filter_keys: &[String],
) -> Result<String, String> {
let schema = self
.db
.schemas
.get(schema_id)
.ok_or_else(|| format!("Schema not found: {}", schema_id))?;
let resolved_arc;
let target_schema = if let Some(path) = stem_path.filter(|p| !p.is_empty() && *p != "/") {
if let Some(stems_map) = self.db.stems.get(schema_id) {
if let Some(stem) = stems_map.get(path) {
resolved_arc = stem.schema.clone();
} else {
return Err(format!(
"Stem entity type '{}' not found in schema '{}'",
path, schema_id
));
}
} else {
return Err(format!(
"Stem entity type '{}' not found in schema '{}'",
path, schema_id
));
}
resolved_arc.as_ref()
} else {
schema
};
// We expect the top level to typically be an Object or Array
let is_stem_query = stem_path.is_some();
let (sql, _) = self.walk_schema(target_schema, "t1", None, filter_keys, is_stem_query, 0)?;
Ok(sql)
}
/// Recursively walks the schema AST emitting native PostgreSQL jsonb mapping
/// Returns a tuple of (SQL_String, Field_Type)
fn walk_schema(
&self,
schema: &crate::database::schema::Schema,
parent_alias: &str,
prop_name_context: Option<&str>,
filter_keys: &[String],
is_stem_query: bool,
depth: usize,
) -> Result<(String, String), String> {
// Determine the base schema type (could be an array, object, or literal)
match &schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) if t == "array" => {
// Handle Arrays:
if let Some(items) = &schema.obj.items {
if let Some(ref_id) = &items.obj.r#ref {
if let Some(type_def) = self.db.types.get(ref_id) {
if is_stem_query && depth > 0 {
return Ok(("".to_string(), "abort".to_string()));
}
return self.compile_entity_node(
items,
type_def,
parent_alias,
prop_name_context,
true,
filter_keys,
is_stem_query,
depth,
);
}
}
let (item_sql, _) = self.walk_schema(
items,
parent_alias,
prop_name_context,
filter_keys,
is_stem_query,
depth + 1,
)?;
return Ok((
format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql),
"array".to_string(),
));
}
Ok((
"SELECT jsonb_agg(TODO) FROM TODO".to_string(),
"array".to_string(),
))
}
_ => {
// Determine if this schema represents a Database Entity
let mut resolved_type = None;
// Target is generally a specific schema (e.g. 'base.person'), but it tells us what physical
// database table hierarchy it maps to via the `schema.id` prefix/suffix convention.
if let Some(lookup_key) = schema.obj.id.as_ref().or(schema.obj.r#ref.as_ref()) {
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
resolved_type = self.db.types.get(&base_type_name);
}
if let Some(type_def) = resolved_type {
if is_stem_query && depth > 0 {
return Ok(("".to_string(), "abort".to_string()));
}
return self.compile_entity_node(
schema,
type_def,
parent_alias,
prop_name_context,
false,
filter_keys,
is_stem_query,
depth,
);
}
// Handle Direct Refs
if let Some(ref_id) = &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) {
return self.walk_schema(
target_schema,
parent_alias,
prop_name_context,
filter_keys,
is_stem_query,
depth,
);
}
return Err(format!("Unresolved $ref: {}", ref_id));
}
// Just an inline object definition?
if let Some(props) = &schema.obj.properties {
return self.compile_inline_object(
props,
parent_alias,
filter_keys,
is_stem_query,
depth,
);
}
// Literal fallback
Ok((
format!(
"{}.{}",
parent_alias,
prop_name_context.unwrap_or("unknown_prop")
),
"string".to_string(),
))
}
}
}
fn get_merged_properties(
&self,
schema: &crate::database::schema::Schema,
) -> std::collections::BTreeMap<String, Arc<crate::database::schema::Schema>> {
let mut props = std::collections::BTreeMap::new();
if let Some(ref_id) = &schema.obj.r#ref {
if let Some(parent_schema) = self.db.schemas.get(ref_id) {
props.extend(self.get_merged_properties(parent_schema));
}
}
if let Some(local_props) = &schema.obj.properties {
for (k, v) in local_props {
props.insert(k.clone(), v.clone());
}
}
props
}
fn compile_entity_node(
&self,
schema: &crate::database::schema::Schema,
type_def: &crate::database::r#type::Type,
parent_alias: &str,
prop_name: Option<&str>,
is_array: bool,
filter_keys: &[String],
is_stem_query: bool,
depth: usize,
) -> Result<(String, String), String> {
let local_ctx = format!("{}_{}", parent_alias, prop_name.unwrap_or("obj"));
// 1. Build FROM clauses and table aliases
let (table_aliases, from_clauses) = self.build_hierarchy_from_clauses(type_def, &local_ctx);
// 2. Map properties and build jsonb_build_object args
let select_args = self.map_properties_to_aliases(
schema,
type_def,
&table_aliases,
parent_alias,
filter_keys,
is_stem_query,
depth,
)?;
let jsonb_obj_sql = if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
};
// 3. Build WHERE clauses
let where_clauses = self.build_filter_where_clauses(
schema,
type_def,
&table_aliases,
parent_alias,
prop_name,
filter_keys,
)?;
let selection = if is_array {
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
} else {
jsonb_obj_sql
};
let full_sql = format!(
"(SELECT {} FROM {} WHERE {})",
selection,
from_clauses.join(" "),
where_clauses.join(" AND ")
);
Ok((
full_sql,
if is_array {
"array".to_string()
} else {
"object".to_string()
},
))
}
fn build_hierarchy_from_clauses(
&self,
type_def: &crate::database::r#type::Type,
local_ctx: &str,
) -> (std::collections::HashMap<String, String>, Vec<String>) {
let mut table_aliases = std::collections::HashMap::new();
let mut from_clauses = Vec::new();
for (i, table_name) in type_def.hierarchy.iter().enumerate() {
let alias = format!("{}_t{}", local_ctx, i + 1);
table_aliases.insert(table_name.clone(), alias.clone());
if i == 0 {
from_clauses.push(format!("agreego.{} {}", table_name, alias));
} else {
let prev_alias = format!("{}_t{}", local_ctx, i);
from_clauses.push(format!(
"JOIN agreego.{} {} ON {}.id = {}.id",
table_name, alias, alias, prev_alias
));
}
}
(table_aliases, from_clauses)
}
fn map_properties_to_aliases(
&self,
schema: &crate::database::schema::Schema,
type_def: &crate::database::r#type::Type,
table_aliases: &std::collections::HashMap<String, String>,
parent_alias: &str,
filter_keys: &[String],
is_stem_query: bool,
depth: usize,
) -> Result<Vec<String>, String> {
let mut select_args = Vec::new();
let grouped_fields = type_def.grouped_fields.as_ref().and_then(|v| v.as_object());
let merged_props = self.get_merged_properties(schema);
for (prop_key, prop_schema) in &merged_props {
let mut owner_alias = table_aliases
.get("entity")
.cloned()
.unwrap_or_else(|| format!("{}_t_err", parent_alias));
if let Some(gf) = grouped_fields {
for (t_name, fields_val) in gf {
if let Some(fields_arr) = fields_val.as_array() {
if fields_arr.iter().any(|v| v.as_str() == Some(prop_key)) {
owner_alias = table_aliases
.get(t_name)
.cloned()
.unwrap_or_else(|| parent_alias.to_string());
break;
}
}
}
}
let (val_sql, val_type) = self.walk_schema(
prop_schema,
&owner_alias,
Some(prop_key),
filter_keys,
is_stem_query,
depth + 1,
)?;
if val_type != "abort" {
select_args.push(format!("'{}', {}", prop_key, val_sql));
}
}
Ok(select_args)
}
fn build_filter_where_clauses(
&self,
schema: &crate::database::schema::Schema,
type_def: &crate::database::r#type::Type,
table_aliases: &std::collections::HashMap<String, String>,
parent_alias: &str,
prop_name: Option<&str>,
filter_keys: &[String],
) -> Result<Vec<String>, String> {
let base_alias = table_aliases
.get(&type_def.name)
.cloned()
.unwrap_or_else(|| "err".to_string());
let mut where_clauses = Vec::new();
where_clauses.push(format!("NOT {}.archived", base_alias));
if parent_alias == "t1" {
for (i, filter_key) in filter_keys.iter().enumerate() {
let mut parts = filter_key.split(':');
let field_name = parts.next().unwrap_or(filter_key);
let op = parts.next().unwrap_or("$eq");
let mut filter_alias = base_alias.clone();
if let Some(gf) = type_def.grouped_fields.as_ref().and_then(|v| v.as_object()) {
for (t_name, fields_val) in gf {
if let Some(fields_arr) = fields_val.as_array() {
if fields_arr.iter().any(|v| v.as_str() == Some(field_name)) {
filter_alias = table_aliases
.get(t_name)
.cloned()
.unwrap_or_else(|| base_alias.clone());
break;
}
}
}
}
let mut is_ilike = false;
let mut cast = "";
if let Some(field_types) = type_def.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type_val) = field_types.get(field_name) {
if let Some(pg_type) = pg_type_val.as_str() {
if pg_type == "uuid" {
cast = "::uuid";
} else if pg_type == "boolean" || pg_type == "bool" {
cast = "::boolean";
} else if pg_type.contains("timestamp")
|| pg_type == "timestamptz"
|| pg_type == "date"
{
cast = "::timestamptz";
} else if pg_type == "numeric"
|| pg_type.contains("int")
|| pg_type == "real"
|| pg_type == "double precision"
{
cast = "::numeric";
} else if pg_type == "text" || pg_type.contains("char") {
let mut is_enum = false;
if let Some(props) = &schema.obj.properties {
if let Some(ps) = props.get(field_name) {
is_enum = ps.obj.enum_.is_some();
}
}
if !is_enum {
is_ilike = true;
}
}
}
}
}
let param_index = i + 1;
let p_val = format!("${}#>>'{{}}'", param_index);
if op == "$in" || op == "$nin" {
let sql_op = if op == "$in" { "IN" } else { "NOT IN" };
let subquery = format!(
"(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))",
cast, p_val
);
where_clauses.push(format!(
"{}.{} {} {}",
filter_alias, field_name, sql_op, subquery
));
} else {
let sql_op = match op {
"$eq" => {
if is_ilike {
"ILIKE"
} else {
"="
}
}
"$ne" => {
if is_ilike {
"NOT ILIKE"
} else {
"!="
}
}
"$gt" => ">",
"$gte" => ">=",
"$lt" => "<",
"$lte" => "<=",
_ => {
if is_ilike {
"ILIKE"
} else {
"="
}
}
};
let param_sql = if is_ilike && (op == "$eq" || op == "$ne") {
p_val
} else {
format!("({}){}", p_val, cast)
};
where_clauses.push(format!(
"{}.{} {} {}",
filter_alias, field_name, sql_op, param_sql
));
}
}
}
if let Some(_prop) = prop_name {
where_clauses.push(format!("{}.parent_id = {}.id", base_alias, parent_alias));
}
Ok(where_clauses)
}
fn compile_inline_object(
&self,
props: &std::collections::BTreeMap<String, std::sync::Arc<crate::database::schema::Schema>>,
parent_alias: &str,
filter_keys: &[String],
is_stem_query: bool,
depth: usize,
) -> Result<(String, String), String> {
let mut build_args = Vec::new();
for (k, v) in props {
let (child_sql, val_type) = self.walk_schema(
v,
parent_alias,
Some(k),
filter_keys,
is_stem_query,
depth + 1,
)?;
if val_type == "abort" {
continue;
}
build_args.push(format!("'{}', {}", k, child_sql));
}
let combined = format!("jsonb_build_object({})", build_args.join(", "));
Ok((combined, "object".to_string()))
}
}

145
src/queryer/mod.rs Normal file
View File

@ -0,0 +1,145 @@
use crate::database::Database;
use std::sync::Arc;
pub mod compiler;
use dashmap::DashMap;
pub struct Queryer {
pub db: Arc<Database>,
cache: DashMap<String, String>,
}
impl Queryer {
pub fn new(db: Arc<Database>) -> Self {
Self {
db,
cache: DashMap::new(),
}
}
pub fn query(
&self,
schema_id: &str,
stem_opt: Option<&str>,
filters: Option<&serde_json::Value>,
) -> crate::drop::Drop {
let filters_map = filters.and_then(|f| f.as_object());
// 1. Process filters into structured $op keys and linear values
let (filter_keys, args) = match self.parse_filter_entries(filters_map) {
Ok(res) => res,
Err(msg) => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "FILTER_PARSE_FAILED".to_string(),
message: msg,
details: crate::drop::ErrorDetails {
path: schema_id.to_string(),
},
}]);
}
};
let stem_key = stem_opt.unwrap_or("/");
let cache_key = format!("{}(Stem:{}):{}", schema_id, stem_key, filter_keys.join(","));
// 2. Fetch from cache or compile
let sql = match self.get_or_compile_sql(&cache_key, schema_id, stem_opt, &filter_keys) {
Ok(sql) => sql,
Err(drop) => return drop,
};
// 3. Execute via Database Executor
self.execute_sql(schema_id, &sql, &args)
}
fn parse_filter_entries(
&self,
filters_map: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Result<(Vec<String>, Vec<serde_json::Value>), String> {
let mut filter_entries: Vec<(String, serde_json::Value)> = Vec::new();
if let Some(fm) = filters_map {
for (key, val) in fm {
if let Some(obj) = val.as_object() {
for (op, op_val) in obj {
if !op.starts_with('$') {
return Err(format!("Filter operator must start with '$', got: {}", op));
}
filter_entries.push((format!("{}:{}", key, op), op_val.clone()));
}
} else {
return Err(format!(
"Filter for field '{}' must be an object with operators like $eq, $in, etc.",
key
));
}
}
}
filter_entries.sort_by(|a, b| a.0.cmp(&b.0));
let filter_keys: Vec<String> = filter_entries.iter().map(|(k, _)| k.clone()).collect();
let args: Vec<serde_json::Value> = filter_entries.into_iter().map(|(_, v)| v).collect();
Ok((filter_keys, args))
}
fn get_or_compile_sql(
&self,
cache_key: &str,
schema_id: &str,
stem_opt: Option<&str>,
filter_keys: &[String],
) -> Result<String, crate::drop::Drop> {
if let Some(cached_sql) = self.cache.get(cache_key) {
return Ok(cached_sql.value().clone());
}
let compiler = compiler::SqlCompiler::new(self.db.clone());
match compiler.compile(schema_id, stem_opt, filter_keys) {
Ok(compiled_sql) => {
self
.cache
.insert(cache_key.to_string(), compiled_sql.clone());
Ok(compiled_sql)
}
Err(e) => Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "QUERY_COMPILATION_FAILED".to_string(),
message: e,
details: crate::drop::ErrorDetails {
path: schema_id.to_string(),
},
}])),
}
}
fn execute_sql(
&self,
schema_id: &str,
sql: &str,
args: &[serde_json::Value],
) -> crate::drop::Drop {
match self.db.query(sql, Some(args)) {
Ok(serde_json::Value::Array(table)) => {
if table.is_empty() {
crate::drop::Drop::success_with_val(serde_json::Value::Null)
} else {
crate::drop::Drop::success_with_val(table.first().unwrap().clone())
}
}
Ok(other) => crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "QUERY_FAILED".to_string(),
message: format!("Expected array from generic query, got: {:?}", other),
details: crate::drop::ErrorDetails {
path: schema_id.to_string(),
},
}]),
Err(e) => crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "QUERY_FAILED".to_string(),
message: format!("SPI error in queryer: {}", e),
details: crate::drop::ErrorDetails {
path: schema_id.to_string(),
},
}]),
}
}
}

View File

@ -1,377 +0,0 @@
use crate::*;
use serde_json::{json, Value};
use pgrx::{JsonB, pg_test};
// Helper macro for asserting success with Drop-style response
macro_rules! assert_success_with_json {
($result_jsonb:expr, $fmt:literal $(, $($args:tt)*)?) => {
let has_response = $result_jsonb.0.get("response").is_some();
let has_errors = $result_jsonb.0.get("errors").is_some();
if !has_response || has_errors {
let base_msg = format!($fmt $(, $($args)*)?);
let pretty_json = serde_json::to_string_pretty(&$result_jsonb.0)
.unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", $result_jsonb.0));
let panic_msg = format!("Assertion Failed (expected success with 'response' field): {}\nResult JSON:\n{}", base_msg, pretty_json);
panic!("{}", panic_msg);
}
};
// Simpler version without message
($result_jsonb:expr) => {
let has_response = $result_jsonb.0.get("response").is_some();
let has_errors = $result_jsonb.0.get("errors").is_some();
if !has_response || has_errors {
let pretty_json = serde_json::to_string_pretty(&$result_jsonb.0)
.unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", $result_jsonb.0));
let panic_msg = format!("Assertion Failed (expected success with 'response' field)\nResult JSON:\n{}", pretty_json);
panic!("{}", panic_msg);
}
};
}
// Helper macro for asserting failed JSON results with Drop-style errors
macro_rules! assert_failure_with_json {
// --- Arms with error count and message substring check ---
// With custom message:
($result:expr, $expected_error_count:expr, $expected_first_message_contains:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let has_response = json_result.get("response").is_some();
let errors_opt = json_result.get("errors");
let base_msg = format!($fmt $(, $($args)*)?);
if has_response || errors_opt.is_none() {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure with 'errors' field): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array");
if errors_array.len() != $expected_error_count {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json);
}
if $expected_error_count > 0 {
let first_error_message = errors_array[0].get("message").and_then(Value::as_str);
match first_error_message {
Some(msg) => {
if !msg.contains($expected_first_message_contains) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (first error message mismatch): Expected contains '{}', got: '{}'. {}\nResult JSON:\n{}", $expected_first_message_contains, msg, base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (first error in array has no 'message' string): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
}
};
// Without custom message (calls the one above with ""):
($result:expr, $expected_error_count:expr, $expected_first_message_contains:expr) => {
assert_failure_with_json!($result, $expected_error_count, $expected_first_message_contains, "");
};
// --- Arms with error count check only ---
// With custom message:
($result:expr, $expected_error_count:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let has_response = json_result.get("response").is_some();
let errors_opt = json_result.get("errors");
let base_msg = format!($fmt $(, $($args)*)?);
if has_response || errors_opt.is_none() {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure with 'errors' field): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array");
if errors_array.len() != $expected_error_count {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json);
}
};
// Without custom message (calls the one above with ""):
($result:expr, $expected_error_count:expr) => {
assert_failure_with_json!($result, $expected_error_count, "");
};
// --- Arms checking failure only (expects at least one error) ---
// With custom message:
($result:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let has_response = json_result.get("response").is_some();
let errors_opt = json_result.get("errors");
let base_msg = format!($fmt $(, $($args)*)?);
if has_response || errors_opt.is_none() {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure with 'errors' field): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
let errors_array = errors_opt.unwrap().as_array().expect("'errors' should be an array");
if errors_array.is_empty() {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected errors, but 'errors' array is empty): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
};
// Without custom message (calls the one above with ""):
($result:expr) => {
assert_failure_with_json!($result, "");
};
}
fn jsonb(val: Value) -> JsonB {
JsonB(val)
}
#[pg_test]
fn test_cache_and_validate_json_schema() {
clear_json_schemas(); // Call clear directly
let schema_id = "my_schema";
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name", "age"]
});
let valid_instance = json!({ "name": "Alice", "age": 30 });
let invalid_instance_type = json!({ "name": "Bob", "age": -5 });
let invalid_instance_missing = json!({ "name": "Charlie" });
let cache_result = cache_json_schema(schema_id, jsonb(schema.clone()));
assert_success_with_json!(cache_result, "Cache operation should succeed.");
let valid_result = validate_json_schema(schema_id, jsonb(valid_instance));
assert_success_with_json!(valid_result, "Validation of valid instance should succeed.");
// Invalid type - age is negative
let invalid_result_type = validate_json_schema(schema_id, jsonb(invalid_instance_type));
assert_failure_with_json!(invalid_result_type, 1, "Value is below the minimum allowed", "Validation with invalid type should fail.");
let errors_type = invalid_result_type.0["errors"].as_array().unwrap();
assert_eq!(errors_type[0]["details"]["path"], "/age");
assert_eq!(errors_type[0]["details"]["context"], -5);
assert_eq!(errors_type[0]["code"], "MINIMUM_VIOLATED");
// Missing field
let invalid_result_missing = validate_json_schema(schema_id, jsonb(invalid_instance_missing));
assert_failure_with_json!(invalid_result_missing, 1, "Required field is missing", "Validation with missing field should fail.");
let errors_missing = invalid_result_missing.0["errors"].as_array().unwrap();
assert_eq!(errors_missing[0]["details"]["path"], "");
assert_eq!(errors_missing[0]["code"], "REQUIRED_FIELD_MISSING");
// Schema not found
let non_existent_id = "non_existent_schema";
let invalid_schema_result = validate_json_schema(non_existent_id, jsonb(json!({})));
assert_failure_with_json!(invalid_schema_result, 1, "Schema 'non_existent_schema' not found", "Validation with non-existent schema should fail.");
let errors_notfound = invalid_schema_result.0["errors"].as_array().unwrap();
assert_eq!(errors_notfound[0]["code"], "SCHEMA_NOT_FOUND");
}
#[pg_test]
fn test_validate_json_schema_not_cached() {
clear_json_schemas();
let instance = json!({ "foo": "bar" });
let result = validate_json_schema("non_existent_schema", jsonb(instance));
assert_failure_with_json!(result, 1, "Schema 'non_existent_schema' not found", "Validation with non-existent schema should fail.");
}
#[pg_test]
fn test_cache_invalid_json_schema() {
clear_json_schemas();
let schema_id = "invalid_schema";
// Schema with an invalid type *value*
let invalid_schema = json!({
"$id": "urn:invalid_schema",
"type": ["invalid_type_value"]
});
let cache_result = cache_json_schema(schema_id, jsonb(invalid_schema));
// Expect 2 leaf errors because the meta-schema validation fails at the type value
// and within the type array itself.
assert_failure_with_json!(
cache_result,
2, // Expect exactly two leaf errors
"Value is not one of the allowed options", // Updated to human-readable message
"Caching invalid schema should fail with specific meta-schema validation errors."
);
// Ensure the errors array exists and check specifics
let errors_array = cache_result.0["errors"].as_array().expect("Errors field should be an array");
assert_eq!(errors_array.len(), 2);
// Both errors should have ENUM_VIOLATED code
assert_eq!(errors_array[0]["code"], "ENUM_VIOLATED");
assert_eq!(errors_array[1]["code"], "ENUM_VIOLATED");
// Check instance paths are preserved in path field
let paths: Vec<&str> = errors_array.iter()
.map(|e| e["details"]["path"].as_str().unwrap())
.collect();
assert!(paths.contains(&"/type"));
assert!(paths.contains(&"/type/0"));
}
#[pg_test]
fn test_validate_json_schema_detailed_validation_errors() {
clear_json_schemas(); // Call clear directly
let schema_id = "detailed_errors";
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string", "maxLength": 10 }
},
"required": ["street", "city"]
}
},
"required": ["address"]
});
let _ = cache_json_schema(schema_id, jsonb(schema));
let invalid_instance = json!({
"address": {
"street": 123, // Wrong type
"city": "Supercalifragilisticexpialidocious" // Too long
}
});
let result = validate_json_schema(schema_id, jsonb(invalid_instance));
// Update: Expect 2 errors again, as boon reports both nested errors.
assert_failure_with_json!(result, 2);
}
#[pg_test]
fn test_validate_json_schema_oneof_validation_errors() {
clear_json_schemas(); // Call clear directly
let schema_id = "oneof_schema";
let schema = json!({
"oneOf": [
{ // Option 1: Object with string prop
"type": "object",
"properties": {
"string_prop": { "type": "string", "maxLength": 5 }
},
"required": ["string_prop"]
},
{ // Option 2: Object with number prop
"type": "object",
"properties": {
"number_prop": { "type": "number", "minimum": 10 }
},
"required": ["number_prop"]
}
]
});
let _ = cache_json_schema(schema_id, jsonb(schema));
// --- Test case 1: Fails string maxLength (in branch 0) AND missing number_prop (in branch 1) ---
let invalid_string_instance = json!({ "string_prop": "toolongstring" });
let result_invalid_string = validate_json_schema(schema_id, jsonb(invalid_string_instance));
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_string, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_string = result_invalid_string.0["errors"].as_array().expect("Expected error array for invalid string");
assert!(errors_string.iter().any(|e|
e["details"]["path"] == "/string_prop" &&
e["code"] == "MAX_LENGTH_VIOLATED"
), "Missing maxLength error");
assert!(errors_string.iter().any(|e|
e["details"]["path"] == "" &&
e["code"] == "REQUIRED_FIELD_MISSING"
), "Missing number_prop required error");
// --- Test case 2: Fails number minimum (in branch 1) AND missing string_prop (in branch 0) ---
let invalid_number_instance = json!({ "number_prop": 5 });
let result_invalid_number = validate_json_schema(schema_id, jsonb(invalid_number_instance));
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_number, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_number = result_invalid_number.0["errors"].as_array().expect("Expected error array for invalid number");
assert!(errors_number.iter().any(|e|
e["details"]["path"] == "/number_prop" &&
e["code"] == "MINIMUM_VIOLATED"
), "Missing minimum error");
assert!(errors_number.iter().any(|e|
e["details"]["path"] == "" &&
e["code"] == "REQUIRED_FIELD_MISSING"
), "Missing string_prop required error");
// --- Test case 3: Fails type check (not object) for both branches ---
// Input: boolean, expected object for both branches
let invalid_bool_instance = json!(true); // Not an object
let result_invalid_bool = validate_json_schema(schema_id, jsonb(invalid_bool_instance));
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_invalid_bool, 1);
// Explicitly check that the single remaining error is the type error for the root instance path
let errors_bool = result_invalid_bool.0["errors"].as_array().expect("Expected error array for invalid bool");
assert_eq!(errors_bool.len(), 1, "Expected exactly one error after deduplication");
assert_eq!(errors_bool[0]["code"], "TYPE_MISMATCH");
assert_eq!(errors_bool[0]["details"]["path"], "");
// --- Test case 4: Fails missing required for both branches ---
// Input: empty object, expected string_prop (branch 0) OR number_prop (branch 1)
let invalid_empty_obj = json!({});
let result_empty_obj = validate_json_schema(schema_id, jsonb(invalid_empty_obj));
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_empty_obj, 1);
// Explicitly check that the single remaining error is one of the expected missing properties errors
let errors_empty = result_empty_obj.0["errors"].as_array().expect("Expected error array for empty object");
assert_eq!(errors_empty.len(), 1, "Expected exactly one error after filtering empty object");
assert_eq!(errors_empty[0]["code"], "REQUIRED_FIELD_MISSING");
assert_eq!(errors_empty[0]["details"]["path"], "");
// The human message should be generic
assert_eq!(errors_empty[0]["message"], "Required field is missing");
}
#[pg_test]
fn test_clear_json_schemas() {
let clear_result = clear_json_schemas();
assert_success_with_json!(clear_result);
let schema_id = "schema_to_clear";
let schema = json!({ "type": "string" });
let cache_result = cache_json_schema(schema_id, jsonb(schema.clone()));
assert_success_with_json!(cache_result);
let show_result1 = show_json_schemas();
let schemas1 = show_result1.0["response"].as_array().unwrap();
assert!(schemas1.contains(&json!(schema_id)));
let clear_result2 = clear_json_schemas();
assert_success_with_json!(clear_result2);
let show_result2 = show_json_schemas();
let schemas2 = show_result2.0["response"].as_array().unwrap();
assert!(schemas2.is_empty());
let instance = json!("test");
let validate_result = validate_json_schema(schema_id, jsonb(instance));
assert_failure_with_json!(validate_result, 1, "Schema 'schema_to_clear' not found", "Validation should fail after clearing schemas.");
}
#[pg_test]
fn test_show_json_schemas() {
let _ = clear_json_schemas();
let schema_id1 = "schema1";
let schema_id2 = "schema2";
let schema = json!({ "type": "boolean" });
let _ = cache_json_schema(schema_id1, jsonb(schema.clone()));
let _ = cache_json_schema(schema_id2, jsonb(schema.clone()));
let result = show_json_schemas();
let schemas = result.0["response"].as_array().unwrap();
assert_eq!(schemas.len(), 2);
assert!(schemas.contains(&json!(schema_id1)));
assert!(schemas.contains(&json!(schema_id2)));
}

8562
src/tests/fixtures.rs Normal file

File diff suppressed because it is too large Load Diff

94
src/tests/mod.rs Normal file
View File

@ -0,0 +1,94 @@
use crate::*;
pub mod runner;
pub mod types;
use serde_json::json;
// Database module tests moved to src/database/executors/mock.rs
#[test]
fn test_library_api() {
// 1. Initially, schemas are not cached.
// Expected uninitialized drop format: errors + null response
let uninitialized_drop = jspg_validate("test_schema", JsonB(json!({})));
assert_eq!(
uninitialized_drop.0,
json!({
"type": "drop",
"errors": [{
"code": "ENGINE_NOT_INITIALIZED",
"message": "JSPG extension has not been initialized via jspg_setup",
"details": { "path": "" }
}]
})
);
// 2. Cache schemas
let db_json = json!({
"puncs": [],
"enums": [],
"relations": [],
"types": [{
"schemas": [{
"$id": "test_schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}]
}]
});
let cache_drop = jspg_setup(JsonB(db_json));
assert_eq!(
cache_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 4. Validate Happy Path
let happy_drop = jspg_validate("test_schema", JsonB(json!({"name": "Neo"})));
assert_eq!(
happy_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 5. Validate Unhappy Path
let unhappy_drop = jspg_validate("test_schema", JsonB(json!({"wrong": "data"})));
assert_eq!(
unhappy_drop.0,
json!({
"type": "drop",
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"message": "Missing name",
"details": { "path": "/name" }
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'",
"details": { "path": "/wrong" }
}
]
})
);
// 6. Clear Schemas
let clear_drop = jspg_teardown();
assert_eq!(
clear_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
}
include!("fixtures.rs");

153
src/tests/runner.rs Normal file
View File

@ -0,0 +1,153 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Debug, Deserialize)]
pub struct TestSuite {
#[allow(dead_code)]
pub description: String,
pub database: serde_json::Value,
pub tests: Vec<TestCase>,
}
use crate::tests::types::TestCase;
use serde_json::Value;
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
Ok(Some(v))
}
// Type alias for easier reading
type CompiledSuite = Arc<Vec<(TestSuite, Arc<crate::database::Database>)>>;
// Global cache mapping filename -> Vector of (Parsed JSON suite, Compiled Database)
static CACHE: OnceLock<RwLock<HashMap<String, CompiledSuite>>> = OnceLock::new();
fn get_cached_file(path: &str) -> CompiledSuite {
let cache_lock = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
let file_data = {
let read_guard = cache_lock.read().unwrap();
read_guard.get(path).cloned()
};
match file_data {
Some(data) => data,
None => {
let mut write_guard = cache_lock.write().unwrap();
// double check in case another thread compiled while we waited for lock
if let Some(data) = write_guard.get(path) {
data.clone()
} else {
let content =
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
let suites: Vec<TestSuite> = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
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 new_data = Arc::new(compiled_suites);
write_guard.insert(path.to_string(), new_data.clone());
new_data
}
}
}
}
pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<(), String> {
let file_data = get_cached_file(path);
if suite_idx >= file_data.len() {
panic!("Suite Index {} out of bounds for file {}", suite_idx, path);
}
let (group, db) = &file_data[suite_idx];
if case_idx >= group.tests.len() {
panic!(
"Case Index {} out of bounds for suite {} in file {}",
case_idx, suite_idx, path
);
}
let test = &group.tests[case_idx];
let mut failures = Vec::<String>::new();
// 4. Run Tests
match test.action.as_str() {
"compile" => {
let result = test.run_compile(db.clone());
if let Err(e) = result {
println!("TEST COMPILE ERROR FOR '{}': {}", test.description, e);
failures.push(format!(
"[{}] Compile Test '{}' failed. Error: {}",
group.description, test.description, e
));
}
}
"validate" => {
let result = test.run_validate(db.clone());
if let Err(e) = result {
println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e);
failures.push(format!(
"[{}] Validate Test '{}' failed. Error: {}",
group.description, test.description, e
));
}
}
"merge" => {
let result = test.run_merge(db.clone());
if let Err(e) = result {
println!("TEST MERGE ERROR FOR '{}': {}", test.description, e);
failures.push(format!(
"[{}] Merge Test '{}' failed. Error: {}",
group.description, test.description, e
));
}
}
"query" => {
let result = test.run_query(db.clone());
if let Err(e) = result {
println!("TEST QUERY ERROR FOR '{}': {}", test.description, e);
failures.push(format!(
"[{}] Query Test '{}' failed. Error: {}",
group.description, test.description, e
));
}
}
_ => {
failures.push(format!(
"[{}] Unknown action '{}' for test '{}'",
group.description, test.action, test.description
));
}
}
if !failures.is_empty() {
return Err(failures.join("\n"));
}
Ok(())
}

187
src/tests/types/case.rs Normal file
View File

@ -0,0 +1,187 @@
use super::expect::ExpectBlock;
use crate::database::Database;
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Deserialize)]
pub struct TestCase {
pub description: String,
#[serde(default = "default_action")]
pub action: String, // "validate", "merge", or "query"
// For Validate & Query
#[serde(default)]
pub schema_id: String,
// For Query
#[serde(default)]
pub stem: Option<String>,
#[serde(default)]
pub filters: Option<serde_json::Value>,
// For Merge & Validate
#[serde(default)]
pub data: Option<serde_json::Value>,
// For Merge & Query mocks
#[serde(default)]
pub mocks: Option<serde_json::Value>,
pub expect: Option<ExpectBlock>,
}
fn default_action() -> String {
"validate".to_string()
}
impl TestCase {
pub fn run_compile(&self, db: Arc<Database>) -> Result<(), String> {
let expected_success = self.expect.as_ref().map(|e| e.success).unwrap_or(false);
// 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
if expected_success != got_success {
return Err(format!(
"Expected success: {}, Got: {}",
expected_success, got_success
));
}
// Assert stems
if let Some(expect) = &self.expect {
if let Some(expected_stems) = &expect.stems {
// Convert the Db stems (HashMap<String, HashMap<String, Arc<Stem>>>) to matching JSON shape
let db_stems_json = serde_json::to_value(&db.stems).unwrap();
let expect_stems_json = serde_json::to_value(expected_stems).unwrap();
if db_stems_json != expect_stems_json {
let expected_pretty = serde_json::to_string_pretty(&expect_stems_json).unwrap();
let got_pretty = serde_json::to_string_pretty(&db_stems_json).unwrap();
return Err(format!(
"Stem validation failed.\nExpected:\n{}\n\nGot:\n{}",
expected_pretty, got_pretty
));
}
}
}
Ok(())
}
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> {
use crate::validator::Validator;
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!(
"Missing Schema: Cannot find schema ID '{}'",
schema_id
));
}
let test_data = self.data.clone().unwrap_or(Value::Null);
let result = validator.validate(schema_id, &test_data);
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
));
}
Ok(())
}
pub fn run_merge(&self, db: Arc<Database>) -> Result<(), String> {
if let Some(mocks) = &self.mocks {
if let Some(arr) = mocks.as_array() {
db.executor.set_mocks(arr.clone());
}
}
use crate::merger::Merger;
let merger = Merger::new(db.clone());
let test_data = self.data.clone().unwrap_or(Value::Null);
let result = merger.merge(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_sql(&queries)
} else {
Ok(())
};
db.executor.reset_mocks();
return_val
}
pub fn run_query(&self, db: Arc<Database>) -> Result<(), String> {
if let Some(mocks) = &self.mocks {
if let Some(arr) = mocks.as_array() {
db.executor.set_mocks(arr.clone());
}
}
use crate::queryer::Queryer;
let queryer = Queryer::new(db.clone());
let stem_opt = self.stem.as_deref();
let result = queryer.query(&self.schema_id, stem_opt, 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_sql(&queries)
} else {
Ok(())
};
db.executor.reset_mocks();
return_val
}
}

149
src/tests/types/expect.rs Normal file
View File

@ -0,0 +1,149 @@
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum SqlExpectation {
Single(String),
Multi(Vec<String>),
}
#[derive(Debug, Deserialize)]
pub struct ExpectBlock {
pub success: bool,
pub result: Option<serde_json::Value>,
pub errors: Option<Vec<serde_json::Value>>,
pub stems: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
#[serde(default)]
pub sql: Option<Vec<SqlExpectation>>,
}
impl ExpectBlock {
/// Advanced SQL execution assertion algorithm ported from `assert.go`.
/// This compares two arrays of strings, one containing {{uuid:name}} or {{timestamp}} placeholders,
/// and the other containing actual executed database queries. It ensures that placeholder UUIDs
/// are consistently mapped to the same actual UUIDs across all lines, and strictly validates line-by-line sequences.
pub fn assert_sql(&self, actual: &[String]) -> Result<(), String> {
let patterns = match &self.sql {
Some(s) => s,
None => return Ok(()),
};
if patterns.len() != actual.len() {
return Err(format!(
"Length mismatch: expected {} SQL executions, got {}.\nActual Execution Log:\n{}",
patterns.len(),
actual.len(),
actual.join("\n")
));
}
let ws_re = Regex::new(r"\s+").unwrap();
let types = HashMap::from([
(
"uuid",
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
),
(
"timestamp",
r"\d{4}-\d{2}-\d{2}(?:[ T])\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+\d{2}(?::\d{2})?)?",
),
("integer", r"-?\d+"),
("float", r"-?\d+\.\d+"),
("text", r"(?:''|[^'])*"),
("json", r"(?:''|[^'])*"),
]);
let mut seen: HashMap<String, String> = HashMap::new();
let system_uuid = "00000000-0000-0000-0000-000000000000";
// Placeholder regex: {{type:name}} or {{type}}
let ph_rx = Regex::new(r"\{\{([a-z]+)(?:[:]([^}]+))?\}\}").unwrap();
let clean_str = |s: &str| -> String {
let mut s = ws_re.replace_all(s, " ").into_owned();
for token in ["(", ")", ",", "{", "}", "\"", "=", "'"] {
s = s.replace(&format!(" {}", token), token);
s = s.replace(&format!("{} ", token), token);
}
s.trim().to_string()
};
for (i, pattern_expect) in patterns.iter().enumerate() {
let aline_raw = &actual[i];
let aline = clean_str(aline_raw);
let pattern_str_raw = match pattern_expect {
SqlExpectation::Single(s) => s.clone(),
SqlExpectation::Multi(m) => m.join(" "),
};
let pattern_str = clean_str(&pattern_str_raw);
let mut pp = regex::escape(&pattern_str);
pp = pp.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
let mut cap_names = HashMap::new(); // cg_X -> var_name
let mut group_idx = 0;
let mut final_rx_str = String::new();
let mut last_match = 0;
let pp_clone = pp.clone();
for caps in ph_rx.captures_iter(&pp_clone) {
let full_match = caps.get(0).unwrap();
final_rx_str.push_str(&pp[last_match..full_match.start()]);
let type_name = caps.get(1).unwrap().as_str();
let var_name = caps.get(2).map(|m| m.as_str());
if let Some(name) = var_name {
if let Some(val) = seen.get(name) {
final_rx_str.push_str(&regex::escape(val));
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
let cg_name = format!("cg_{}", group_idx);
final_rx_str.push_str(&format!("(?P<{}>{})", cg_name, type_pattern));
cap_names.insert(cg_name, name.to_string());
group_idx += 1;
}
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
final_rx_str.push_str(&format!("(?:{})", type_pattern));
}
last_match = full_match.end();
}
final_rx_str.push_str(&pp[last_match..]);
let final_rx = match Regex::new(&format!("^{}$", final_rx_str)) {
Ok(r) => r,
Err(e) => return Err(format!("Bad constructed regex: {} -> {}", final_rx_str, e)),
};
if let Some(captures) = final_rx.captures(&aline) {
for (cg_name, var_name) in cap_names {
if let Some(m) = captures.name(&cg_name) {
let matched_str = m.as_str();
if matched_str != system_uuid {
seen.insert(var_name, matched_str.to_string());
}
}
}
} else {
return Err(format!(
"Line mismatched at execution sequence {}.\nExpected Pattern: {}\nActual SQL: {}\nRegex used: {}\nVariables Mapped: {:?}",
i + 1,
pattern_str,
aline,
final_rx_str,
seen
));
}
}
Ok(())
}
}

7
src/tests/types/mod.rs Normal file
View File

@ -0,0 +1,7 @@
pub mod case;
pub mod expect;
pub mod suite;
pub use case::TestCase;
pub use expect::ExpectBlock;
pub use suite::TestSuite;

10
src/tests/types/suite.rs Normal file
View File

@ -0,0 +1,10 @@
use super::case::TestCase;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct TestSuite {
#[allow(dead_code)]
pub description: String,
pub database: serde_json::Value,
pub tests: Vec<TestCase>,
}

82
src/validator/context.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::database::Database;
use crate::database::schema::Schema;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use std::collections::HashSet;
use std::sync::Arc;
pub struct ValidationContext<'a> {
pub db: &'a Arc<Database>,
pub root: &'a Schema,
pub schema: &'a Schema,
pub instance: &'a serde_json::Value,
pub path: String,
pub depth: usize,
pub extensible: bool,
pub reporter: bool,
pub overrides: HashSet<String>,
}
impl<'a> ValidationContext<'a> {
pub fn new(
db: &'a Arc<Database>,
root: &'a Schema,
schema: &'a Schema,
instance: &'a serde_json::Value,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
db,
root,
schema,
instance,
path: String::new(),
depth: 0,
extensible: effective_extensible,
reporter,
overrides,
}
}
pub fn derive(
&self,
schema: &'a Schema,
instance: &'a serde_json::Value,
path: &str,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
db: self.db,
root: self.root,
schema,
instance,
path: path.to_string(),
depth: self.depth + 1,
extensible: effective_extensible,
reporter,
overrides,
}
}
pub fn derive_for_schema(&self, schema: &'a Schema, reporter: bool) -> Self {
self.derive(
schema,
self.instance,
&self.path,
HashSet::new(),
self.extensible,
reporter,
)
}
pub fn validate(&self) -> Result<ValidationResult, ValidationError> {
self.validate_scoped()
}
}

6
src/validator/error.rs Normal file
View File

@ -0,0 +1,6 @@
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub path: String,
}

98
src/validator/instance.rs Normal file
View File

@ -0,0 +1,98 @@
use serde_json::Value;
use HashSet;
use std::ptr::NonNull;
pub trait ValidationInstance<'a>: Copy + Clone {
fn as_value(&self) -> &'a Value;
fn child_at_key(&self, key: &str) -> Option<Self>;
fn child_at_index(&self, idx: usize) -> Option<Self>;
fn prune_object(&self, _keys: &HashSet<String>) {}
fn prune_array(&self, _indices: &HashSet<usize>) {}
}
#[derive(Clone, Copy)]
pub struct ReadOnlyInstance<'a>(pub &'a Value);
impl<'a> ValidationInstance<'a> for ReadOnlyInstance<'a> {
fn as_value(&self) -> &'a Value {
self.0
}
fn child_at_key(&self, key: &str) -> Option<Self> {
self.0.get(key).map(ReadOnlyInstance)
}
fn child_at_index(&self, idx: usize) -> Option<Self> {
self.0.get(idx).map(ReadOnlyInstance)
}
}
#[derive(Clone, Copy)]
pub struct MutableInstance {
ptr: NonNull<Value>,
}
impl MutableInstance {
pub fn new(val: &mut Value) -> Self {
Self {
ptr: NonNull::from(val),
}
}
}
impl<'a> ValidationInstance<'a> for MutableInstance {
fn as_value(&self) -> &'a Value {
unsafe { self.ptr.as_ref() }
}
fn child_at_key(&self, key: &str) -> Option<Self> {
unsafe {
if let Some(obj) = self.ptr.as_ref().as_object() {
if obj.contains_key(key) {
let parent_mut = &mut *self.ptr.as_ptr();
if let Some(child_val) = parent_mut.get_mut(key) {
return Some(MutableInstance::new(child_val));
}
}
}
None
}
}
fn child_at_index(&self, idx: usize) -> Option<Self> {
unsafe {
if let Some(arr) = self.ptr.as_ref().as_array() {
if idx < arr.len() {
let parent_mut = &mut *self.ptr.as_ptr();
if let Some(child_val) = parent_mut.get_mut(idx) {
return Some(MutableInstance::new(child_val));
}
}
}
None
}
}
fn prune_object(&self, keys: &HashSet<String>) {
unsafe {
let val_mut = &mut *self.ptr.as_ptr();
if let Some(obj) = val_mut.as_object_mut() {
obj.retain(|k, _| keys.contains(k));
}
}
}
fn prune_array(&self, indices: &HashSet<usize>) {
unsafe {
let val_mut = &mut *self.ptr.as_ptr();
if let Some(arr) = val_mut.as_array_mut() {
let mut i = 0;
arr.retain(|_| {
let keep = indices.contains(&i);
i += 1;
keep
});
}
}
}
}

92
src/validator/mod.rs Normal file
View File

@ -0,0 +1,92 @@
use std::collections::HashSet;
pub mod context;
pub mod error;
pub mod result;
pub mod rules;
pub use context::ValidationContext;
pub use error::ValidationError;
pub use result::ValidationResult;
use crate::database::Database;
use crate::validator::rules::util::is_integer;
use serde_json::Value;
use std::sync::Arc;
pub struct Validator {
pub db: Arc<Database>,
}
impl Validator {
pub fn new(db: Arc<Database>) -> Self {
Self { db }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.db.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val
&& s.is_empty()
{
return true;
}
match t {
"null" => val.is_null(),
"boolean" => val.is_boolean(),
"string" => val.is_string(),
"number" => val.is_number(),
"integer" => is_integer(val),
"object" => val.is_object(),
"array" => val.is_array(),
_ => true,
}
}
pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop {
if let Some(schema) = self.db.schemas.get(schema_id) {
let ctx = ValidationContext::new(
&self.db,
schema,
schema,
instance,
HashSet::new(),
false,
false,
);
match ctx.validate_scoped() {
Ok(result) => {
if result.is_valid() {
crate::drop::Drop::success()
} else {
let errors: Vec<crate::drop::Error> = result
.errors
.into_iter()
.map(|e| crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
})
.collect();
crate::drop::Drop::with_errors(errors)
}
}
Err(e) => crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
}]),
}
} else {
crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
}])
}
}
}

28
src/validator/result.rs Normal file
View File

@ -0,0 +1,28 @@
use std::collections::HashSet;
use crate::validator::error::ValidationError;
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ValidationResult {
pub errors: Vec<ValidationError>,
#[serde(skip)]
pub evaluated_keys: HashSet<String>,
#[serde(skip)]
pub evaluated_indices: HashSet<usize>,
}
impl ValidationResult {
pub fn new() -> Self {
Self::default()
}
pub fn merge(&mut self, other: ValidationResult) {
self.errors.extend(other.errors);
self.evaluated_keys.extend(other.evaluated_keys);
self.evaluated_indices.extend(other.evaluated_indices);
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
}

View File

@ -0,0 +1,135 @@
use std::collections::HashSet;
use serde_json::Value;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_array(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(arr) = current.as_array() {
if let Some(min) = self.schema.min_items
&& (arr.len() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_ITEMS".to_string(),
message: "Too few items".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_items
&& (arr.len() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_ITEMS".to_string(),
message: "Too many items".to_string(),
path: self.path.to_string(),
});
}
if self.schema.unique_items.unwrap_or(false) {
let mut seen: Vec<&Value> = Vec::new();
for item in arr {
if seen.contains(&item) {
result.errors.push(ValidationError {
code: "UNIQUE_ITEMS_VIOLATED".to_string(),
message: "Array has duplicate items".to_string(),
path: self.path.to_string(),
});
break;
}
seen.push(item);
}
}
if let Some(ref contains_schema) = self.schema.contains {
let mut _match_count = 0;
for (i, child_instance) in arr.iter().enumerate() {
let derived = self.derive(
contains_schema,
child_instance,
&self.path,
HashSet::new(),
self.extensible,
false,
);
let check = derived.validate()?;
if check.is_valid() {
_match_count += 1;
result.evaluated_indices.insert(i);
}
}
let min = self.schema.min_contains.unwrap_or(1.0) as usize;
if _match_count < min {
result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(),
message: format!("Contains matches {} < min {}", _match_count, min),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_contains
&& _match_count > max as usize
{
result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(),
message: format!("Contains matches {} > max {}", _match_count, max),
path: self.path.to_string(),
});
}
}
let len = arr.len();
let mut validation_index = 0;
if let Some(ref prefix) = self.schema.prefix_items {
for (i, sub_schema) in prefix.iter().enumerate() {
if i < len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(
sub_schema,
child_instance,
&path,
HashSet::new(),
self.extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
validation_index += 1;
}
}
}
}
if let Some(ref items_schema) = self.schema.items {
for i in validation_index..len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) {
let derived = self.derive(
items_schema,
child_instance,
&path,
HashSet::new(),
self.extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
}
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,92 @@
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)
}
}

View File

@ -0,0 +1,67 @@
use crate::validator::context::ValidationContext;
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()?);
}
}
Ok(true)
}
pub(crate) fn validate_strictness(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if self.extensible || self.reporter {
return Ok(true);
}
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),
message: format!("Unexpected property '{}'", key),
path: format!("{}/{}", self.path, key),
});
}
}
}
if let Some(arr) = self.instance.as_array() {
for i in 0..arr.len() {
if !result.evaluated_indices.contains(&i) {
result.errors.push(ValidationError {
code: "STRICT_ITEM_VIOLATION".to_string(),
message: format!("Unexpected item at index {}", i),
path: format!("{}/{}", self.path, i),
});
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,81 @@
use crate::validator::Validator;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use crate::validator::rules::util::equals;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_core(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(ref type_) = self.schema.type_ {
match type_ {
crate::database::schema::SchemaTypeOrArray::Single(t) => {
if !Validator::check_type(t, current) {
result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(),
message: format!("Expected type '{}'", t),
path: self.path.to_string(),
});
}
}
crate::database::schema::SchemaTypeOrArray::Multiple(types) => {
let mut valid = false;
for t in types {
if Validator::check_type(t, current) {
valid = true;
break;
}
}
if !valid {
result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(),
message: format!("Expected one of types {:?}", types),
path: self.path.to_string(),
});
}
}
}
}
if let Some(ref const_val) = self.schema.const_ {
if !equals(current, const_val) {
result.errors.push(ValidationError {
code: "CONST_VIOLATED".to_string(),
message: "Value does not match const".to_string(),
path: self.path.to_string(),
});
} else if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
if let Some(ref enum_vals) = self.schema.enum_ {
let mut found = false;
for val in enum_vals {
if equals(current, val) {
found = true;
break;
}
}
if !found {
result.errors.push(ValidationError {
code: "ENUM_MISMATCH".to_string(),
message: "Value is not in enum".to_string(),
path: self.path.to_string(),
});
} else if let Some(obj) = current.as_object() {
result.evaluated_keys.extend(obj.keys().cloned());
} else if let Some(arr) = current.as_array() {
result.evaluated_indices.extend(0..arr.len());
}
}
Ok(true)
}
}

View File

@ -0,0 +1,42 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_format(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(ref compiled_fmt) = self.schema.compiled_format {
match compiled_fmt {
crate::database::schema::CompiledFormat::Func(f) => {
let should = if let Some(s) = current.as_str() {
!s.is_empty()
} else {
true
};
if should && let Err(e) = f(current) {
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: format!("Format error: {}", e),
path: self.path.to_string(),
});
}
}
crate::database::schema::CompiledFormat::Regex(re) => {
if let Some(s) = current.as_str()
&& !re.is_match(s)
{
result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(),
message: "Format regex mismatch".to_string(),
path: self.path.to_string(),
});
}
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,91 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
pub mod array;
pub mod combinators;
pub mod conditionals;
pub mod core;
pub mod format;
pub mod numeric;
pub mod object;
pub mod polymorphism;
pub mod string;
pub mod util;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_scoped(&self) -> Result<ValidationResult, ValidationError> {
let mut result = ValidationResult::new();
// Structural Limits
if !self.validate_depth(&mut result)? {
return Ok(result);
}
if !self.validate_always_fail(&mut result)? {
return Ok(result);
}
if !self.validate_family(&mut result)? {
return Ok(result);
}
if !self.validate_refs(&mut result)? {
return Ok(result);
}
// Core Type Constraints
self.validate_core(&mut result)?;
self.validate_numeric(&mut result)?;
self.validate_string(&mut result)?;
self.validate_format(&mut result)?;
// Complex Structures
self.validate_object(&mut result)?;
self.validate_array(&mut result)?;
// Multipliers & Conditionals
self.validate_combinators(&mut result)?;
self.validate_conditionals(&mut result)?;
// State Tracking
self.validate_extensible(&mut result)?;
self.validate_strictness(&mut result)?;
Ok(result)
}
fn validate_depth(&self, _result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.depth > 100 {
Err(ValidationError {
code: "RECURSION_LIMIT_EXCEEDED".to_string(),
message: "Recursion limit exceeded".to_string(),
path: self.path.to_string(),
})
} else {
Ok(true)
}
}
fn validate_always_fail(&self, result: &mut ValidationResult) -> Result<bool, ValidationError> {
if self.schema.always_fail {
result.errors.push(ValidationError {
code: "FALSE_SCHEMA".to_string(),
message: "Schema is false".to_string(),
path: self.path.to_string(),
});
// Short-circuit
Ok(false)
} else {
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)
}
}

View File

@ -0,0 +1,61 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_numeric(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(num) = current.as_f64() {
if let Some(min) = self.schema.minimum
&& num < min
{
result.errors.push(ValidationError {
code: "MINIMUM_VIOLATED".to_string(),
message: format!("Value {} < min {}", num, min),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.maximum
&& num > max
{
result.errors.push(ValidationError {
code: "MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} > max {}", num, max),
path: self.path.to_string(),
});
}
if let Some(ex_min) = self.schema.exclusive_minimum
&& num <= ex_min
{
result.errors.push(ValidationError {
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
message: format!("Value {} <= ex_min {}", num, ex_min),
path: self.path.to_string(),
});
}
if let Some(ex_max) = self.schema.exclusive_maximum
&& num >= ex_max
{
result.errors.push(ValidationError {
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
message: format!("Value {} >= ex_max {}", num, ex_max),
path: self.path.to_string(),
});
}
if let Some(multiple_of) = self.schema.multiple_of {
let val: f64 = num / multiple_of;
if (val - val.round()).abs() > f64::EPSILON {
result.errors.push(ValidationError {
code: "MULTIPLE_OF_VIOLATED".to_string(),
message: format!("Value {} not multiple of {}", num, multiple_of),
path: self.path.to_string(),
});
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,220 @@
use std::collections::HashSet;
use serde_json::Value;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_object(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(obj) = current.as_object() {
// Entity Bound Implicit Type Validation
if let Some(lookup_key) = self.schema.id.as_ref().or(self.schema.r#ref.as_ref()) {
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
if let Some(type_def) = self.db.types.get(&base_type_name)
&& let Some(type_val) = obj.get("type")
&& let Some(type_str) = type_val.as_str()
{
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: format!("{}/type", self.path),
});
}
}
}
if let Some(min) = self.schema.min_properties
&& (obj.len() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_PROPERTIES".to_string(),
message: "Too few properties".to_string(),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_properties
&& (obj.len() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_PROPERTIES".to_string(),
message: "Too many properties".to_string(),
path: self.path.to_string(),
});
}
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: format!("{}/{}", self.path, field),
});
}
}
}
if let Some(ref deps) = self.schema.dependencies {
for (prop, dep) in deps {
if obj.contains_key(prop) {
match dep {
crate::database::schema::Dependency::Props(required_props) => {
for req_prop in required_props {
if !obj.contains_key(req_prop) {
result.errors.push(ValidationError {
code: "DEPENDENCY_MISSING".to_string(),
message: format!("Property '{}' requires property '{}'", prop, req_prop),
path: self.path.to_string(),
});
}
}
}
crate::database::schema::Dependency::Schema(dep_schema) => {
let derived = self.derive_for_schema(dep_schema, false);
let dep_res = derived.validate()?;
result.evaluated_keys.extend(dep_res.evaluated_keys.clone());
result.merge(dep_res);
}
}
}
}
}
if let Some(props) = &self.schema.properties {
for (key, sub_schema) in props {
if self.overrides.contains(key) {
continue; // Skip validation if exactly this property was overridden by a child
}
if let Some(child_instance) = obj.get(key) {
let new_path = format!("{}/{}", self.path, key);
let is_ref = sub_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
);
let mut item_res = derived.validate()?;
// Entity Bound Implicit Type Interception
if key == "type"
&& let Some(lookup_key) = sub_schema.id.as_ref().or(sub_schema.r#ref.as_ref())
{
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
if let Some(type_def) = self.db.types.get(&base_type_name)
&& 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());
}
}
}
if let Some(ref compiled_pp) = self.schema.compiled_pattern_properties {
for (compiled_re, sub_schema) in compiled_pp {
for (key, child_instance) in obj {
if compiled_re.0.is_match(key) {
let new_path = format!("{}/{}", self.path, key);
let is_ref = sub_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
}
if let Some(ref additional_schema) = self.schema.additional_properties {
for (key, child_instance) in obj {
let mut locally_matched = false;
if let Some(props) = &self.schema.properties
&& props.contains_key(&key.to_string())
{
locally_matched = true;
}
if !locally_matched && let Some(ref compiled_pp) = self.schema.compiled_pattern_properties
{
for (compiled_re, _) in compiled_pp {
if compiled_re.0.is_match(key) {
locally_matched = true;
break;
}
}
}
if !locally_matched {
let new_path = format!("{}/{}", self.path, key);
let is_ref = additional_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
additional_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
if let Some(ref property_names) = self.schema.property_names {
for key in obj.keys() {
let _new_path = format!("{}/propertyNames/{}", self.path, key);
let val_str = Value::String(key.to_string());
let ctx = ValidationContext::new(
self.db,
self.root,
property_names,
&val_str,
HashSet::new(),
self.extensible,
self.reporter,
);
result.merge(ctx.validate()?);
}
}
}
Ok(true)
}
}

View File

@ -0,0 +1,155 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_family(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
if self.schema.family.is_some() {
let conflicts = self.schema.type_.is_some()
|| self.schema.properties.is_some()
|| self.schema.required.is_some()
|| self.schema.additional_properties.is_some()
|| self.schema.items.is_some()
|| self.schema.r#ref.is_some()
|| self.schema.one_of.is_some()
|| self.schema.all_of.is_some()
|| self.schema.enum_.is_some()
|| self.schema.const_.is_some();
if conflicts {
result.errors.push(ValidationError {
code: "INVALID_SCHEMA".to_string(),
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 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);
}
}
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(),
});
}
}
}
Ok(true)
}
pub(crate) fn validate_refs(
&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) {
let mut new_overrides = self.overrides.clone();
if let Some(props) = &self.schema.properties {
new_overrides.extend(props.keys().map(|k| k.to_string()));
}
let mut shadow = self.derive(
global_schema,
self.instance,
&self.path,
new_overrides,
self.extensible,
true,
);
shadow.root = global_schema;
result.merge(shadow.validate()?);
} else {
result.errors.push(ValidationError {
code: "REF_RESOLUTION_FAILED".to_string(),
message: format!(
"Reference pointer to '{}' was not found in schema registry",
ref_str
),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -0,0 +1,52 @@
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
use regex::Regex;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_string(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(s) = current.as_str() {
if let Some(min) = self.schema.min_length
&& (s.chars().count() as f64) < min
{
result.errors.push(ValidationError {
code: "MIN_LENGTH_VIOLATED".to_string(),
message: format!("Length < min {}", min),
path: self.path.to_string(),
});
}
if let Some(max) = self.schema.max_length
&& (s.chars().count() as f64) > max
{
result.errors.push(ValidationError {
code: "MAX_LENGTH_VIOLATED".to_string(),
message: format!("Length > max {}", max),
path: self.path.to_string(),
});
}
if let Some(ref compiled_re) = self.schema.compiled_pattern {
if !compiled_re.0.is_match(s) {
result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(),
message: format!("Pattern mismatch {:?}", self.schema.pattern),
path: self.path.to_string(),
});
}
} else if let Some(ref pattern) = self.schema.pattern
&& let Ok(re) = Regex::new(pattern)
&& !re.is_match(s)
{
result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(),
message: format!("Pattern mismatch {}", pattern),
path: self.path.to_string(),
});
}
}
Ok(true)
}
}

View File

@ -0,0 +1,53 @@
use serde_json::Value;
pub fn is_integer(v: &Value) -> bool {
match v {
Value::Number(n) => {
n.is_i64() || n.is_u64() || n.as_f64().filter(|n| n.fract() == 0.0).is_some()
}
_ => false,
}
}
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub fn equals(v1: &Value, v2: &Value) -> bool {
match (v1, v2) {
(Value::Null, Value::Null) => true,
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Number(n1), Value::Number(n2)) => {
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
return (n1 - n2).abs() < f64::EPSILON;
}
false
}
(Value::String(s1), Value::String(s2)) => s1 == s2,
(Value::Array(arr1), Value::Array(arr2)) => {
if arr1.len() != arr2.len() {
return false;
}
arr1.iter().zip(arr2).all(|(e1, e2)| equals(e1, e2))
}
(Value::Object(obj1), Value::Object(obj2)) => {
if obj1.len() != obj2.len() {
return false;
}
for (k1, v1) in obj1 {
if let Some(v2) = obj2.get(k1) {
if !equals(v1, v2) {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
}
}

View File

@ -1 +1 @@
1.0.22
1.0.59