Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 737c0b04ac | |||
| 2d1fca599b | |||
| 8d0369c672 | |||
| 566b599512 | |||
| e7f20e2cb6 | |||
| 3898c43742 | |||
| e14f53e7d9 | |||
| 960a99034a | |||
| 81388149e8 | |||
| b8b3f7a501 | |||
| bc5489b1ea | |||
| 7b55277116 | |||
| ed636b05a4 | |||
| 2aec2da2fd | |||
| ad78896f72 | |||
| 55b93d9957 | |||
| 7ec6e09ae0 | |||
| 9d9c6d2c06 | |||
| 12e952fa94 | |||
| 776a912098 | |||
| 612188a54b | |||
| 29c5160b49 | |||
| 944675d669 | |||
| 53a40d1099 | |||
| e55977c11b | |||
| 8e50d4852d | |||
| 623c34c0bc | |||
| 32ed463df8 | |||
| 6e06b6fdc2 | |||
| 61735646ca | |||
| 54c34b2848 | |||
| 0f912c12b2 | |||
| b225afdd1b |
49
.agent/workflows/jspg.md
Normal file
49
.agent/workflows/jspg.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
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: Documentation
|
||||
|
||||
- GEMINI.md at the root
|
||||
|
||||
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 tests/fixtures/*
|
||||
|
||||
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:
|
||||
|
||||
- punc/sql/tables.sql
|
||||
- punc/sql/domains.sql
|
||||
- punc/sql/indexes.sql
|
||||
- punc/sql/functions/entity.sql
|
||||
- punc/sql/functions/puncs.sql
|
||||
- punc/sql/puncs/entity.sql
|
||||
- punc/sql/puncs/persons.sql
|
||||
- punc/sql/puncs/puncs.sql
|
||||
- punc/sql/puncs/job.sql
|
||||
|
||||
Now you are ready to help me work on this extension.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/target
|
||||
/package
|
||||
.env
|
||||
.env
|
||||
/src/tests.rs
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
||||
|
||||
1513
Cargo.lock
generated
1513
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@ -1,9 +1,3 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "jspg"
|
||||
version = "0.1.0"
|
||||
@ -11,14 +5,29 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
pgrx = "0.16.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.228", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.149"
|
||||
lazy_static = "1.5.0"
|
||||
boon = { path = "validator" }
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pgrx-tests = "0.16.1"
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
@ -27,7 +36,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 = []
|
||||
|
||||
@ -39,4 +49,7 @@ lto = "thin"
|
||||
panic = "unwind"
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
codegen-units = 1
|
||||
|
||||
[package.metadata.jspg]
|
||||
target_draft = "draft2020-12"
|
||||
|
||||
208
GEMINI.md
208
GEMINI.md
@ -1,135 +1,159 @@
|
||||
# Gemini Project Overview: `jspg`
|
||||
# JSPG: JSON Schema Postgres
|
||||
|
||||
This document outlines the purpose of the `jspg` project, its architecture, and the specific modifications made to the vendored `boon` JSON schema validator crate.
|
||||
**JSPG** is a high-performance PostgreSQL extension for in-memory JSON Schema validation, specifically targeting **Draft 2020-12**.
|
||||
|
||||
## What is `jspg`?
|
||||
It is designed to serve as the validation engine for the "Punc" architecture, where the database is the single source of truth for all data models and API contracts.
|
||||
|
||||
`jspg` is a PostgreSQL extension written in Rust using the `pgrx` framework. Its primary function is to provide fast, in-database JSON schema validation against the 2020-12 draft of the JSON Schema specification.
|
||||
## 🎯 Goals
|
||||
|
||||
### How It Works
|
||||
1. **Draft 2020-12 Compliance**: Attempt to adhere to the official JSON Schema Draft 2020-12 specification.
|
||||
2. **Ultra-Fast Validation**: Compile schemas into an optimized in-memory representation for near-instant validation during high-throughput workloads.
|
||||
3. **Connection-Bound Caching**: Leverage the PostgreSQL session lifecycle to maintain a per-connection schema cache, eliminating the need for repetitive parsing.
|
||||
4. **Structural Inheritance**: Support object-oriented schema design via Implicit Keyword Shadowing and virtual `$family` references.
|
||||
5. **Punc Integration**: validation is aware of the "Punc" context (request/response) and can validate `cue` objects efficiently.
|
||||
|
||||
The extension is designed for high-performance scenarios where schemas are defined once and used many times for validation. It achieves this through an in-memory cache.
|
||||
## 🔌 API Reference
|
||||
|
||||
1. **Caching and Pre-processing:** A user first calls the `cache_json_schemas(enums, types, puncs)` SQL function. This function takes arrays of JSON objects representing different kinds of schemas:
|
||||
- `enums`: Standalone enum schemas (e.g., for a `task_priority` list).
|
||||
- `types`: Schemas for core application data models (e.g., `person`, `organization`). These may contain a `hierarchy` array for inheritance information.
|
||||
- `puncs`: Schemas for API/function-specific requests and responses.
|
||||
The extension exposes the following functions to PostgreSQL:
|
||||
|
||||
Before compiling, `jspg` performs a crucial **pre-processing step** for type hierarchies. It inspects each definition in the `types` array. If a type includes a `hierarchy` array (e.g., a `person` type with `["entity", "organization", "user", "person"]`), `jspg` uses this to build a map of "type families."
|
||||
### `cache_json_schemas(enums jsonb, types jsonb, puncs jsonb) -> jsonb`
|
||||
|
||||
From this map, it generates new, virtual schemas on the fly. For example, for the `organization` type, it will generate a schema with `$id: "organization.family"` that contains an `enum` of all its descendant types, such as `["organization", "user", 'person"]`.
|
||||
Loads and compiles the entire schema registry into the session's memory, atomically replacing the previous validator.
|
||||
|
||||
This allows developers to write more flexible schemas. Instead of strictly requiring a `const` type, you can validate against an entire inheritance chain:
|
||||
* **Inputs**:
|
||||
* `enums`: Array of enum definitions.
|
||||
* `types`: Array of type definitions (core entities).
|
||||
* `puncs`: Array of punc (function) definitions with request/response schemas.
|
||||
* **Behavior**:
|
||||
* Parses all inputs into an internal schema graph.
|
||||
* Resolves all internal references (`$ref`).
|
||||
* Generates virtual union schemas for type hierarchies referenced via `$family`.
|
||||
* Compiles schemas into validators.
|
||||
* **Returns**: `{"response": "success"}` or an error object.
|
||||
|
||||
```json
|
||||
// In an "organization" schema definition
|
||||
"properties": {
|
||||
"type": {
|
||||
// Allows the 'type' field to be "organization", "user", or "person"
|
||||
"$ref": "organization.family",
|
||||
"override": true
|
||||
}
|
||||
}
|
||||
```
|
||||
### `mask_json_schema(schema_id text, instance jsonb) -> jsonb`
|
||||
|
||||
Finally, all user-defined schemas and the newly generated `.family` schemas are passed to the vendored `boon` crate, compiled into an efficient internal format, and stored in a static, in-memory `SCHEMA_CACHE`. This cache is managed by a `RwLock` to allow for high-performance, concurrent reads during validation.
|
||||
Validates a JSON instance and returns a new JSON object with unknown properties removed (pruned) based on the schema.
|
||||
|
||||
2. **Validation:** The `validate_json_schema(schema_id, instance)` SQL function is then used to validate a JSONB `instance` against a specific, pre-cached schema identified by its `$id`. This function looks up the compiled schema in the cache and runs the validation, returning a success response or a detailed error report.
|
||||
* **Inputs**:
|
||||
* `schema_id`: The `$id` of the schema to mask against.
|
||||
* `instance`: The JSON data to mask.
|
||||
* **Returns**:
|
||||
* On success: A `Drop` containing the **masked data**.
|
||||
* On failure: A `Drop` containing validation errors.
|
||||
|
||||
3. **Custom Logic:** `jspg` uses a locally modified (vendored) version of the `boon` crate. This allows for powerful, application-specific validation logic that goes beyond the standard JSON Schema specification, such as runtime-based strictness.
|
||||
### `validate_json_schema(schema_id text, instance jsonb) -> jsonb`
|
||||
|
||||
### Error Handling
|
||||
Validates a JSON instance against a pre-compiled schema.
|
||||
|
||||
When validation fails, `jspg` provides a detailed error report in a consistent JSON format, which we refer to as a "DropError". This process involves two main helper functions in `src/lib.rs`:
|
||||
* **Inputs**:
|
||||
* `schema_id`: The `$id` of the schema to validate against (e.g., `person`, `save_person.request`).
|
||||
* `instance`: The JSON data to validate.
|
||||
* **Returns**:
|
||||
* On success: `{"response": "success"}`
|
||||
* On failure: A JSON object containing structured errors (e.g., `{"errors": [...]}`).
|
||||
|
||||
1. **`collect_errors`**: `boon` returns a nested tree of `ValidationError` objects. This function recursively traverses that tree to find the most specific, underlying causes of the failure. It filters out structural errors (like `allOf` or `anyOf`) to create a flat list of concrete validation failures.
|
||||
### `json_schema_cached(schema_id text) -> bool`
|
||||
|
||||
2. **`format_errors`**: This function takes the flat list of errors and transforms each one into the final DropError JSON format. It also de-duplicates errors that occur at the same JSON Pointer path, ensuring a cleaner output if a single value violates multiple constraints.
|
||||
Checks if a specific schema ID is currently present in the cache.
|
||||
|
||||
#### DropError Format
|
||||
### `clear_json_schemas() -> jsonb`
|
||||
|
||||
A DropError object provides a clear, structured explanation of a validation failure:
|
||||
Clears the current session's schema cache, freeing memory.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "ADDITIONAL_PROPERTIES_NOT_ALLOWED",
|
||||
"message": "Property 'extra' is not allowed",
|
||||
"details": {
|
||||
"path": "/extra",
|
||||
"context": "not allowed",
|
||||
"cause": {
|
||||
"got": [
|
||||
"extra"
|
||||
]
|
||||
},
|
||||
"schema": "basic_strict_test.request"
|
||||
}
|
||||
}
|
||||
```
|
||||
### `show_json_schemas() -> jsonb`
|
||||
|
||||
- `code` (string): A machine-readable error code (e.g., `ADDITIONAL_PROPERTIES_NOT_ALLOWED`, `MIN_LENGTH_VIOLATED`).
|
||||
- `message` (string): A human-readable summary of the error.
|
||||
- `details` (object):
|
||||
- `path` (string): The JSON Pointer path to the invalid data within the instance.
|
||||
- `context` (any): The actual value that failed validation.
|
||||
- `cause` (any): The low-level reason from the validator, often including the expected value (`want`) and the actual value (`got`).
|
||||
- `schema` (string): The `$id` of the schema that was being validated.
|
||||
Returns a debug dump of the currently cached schemas (for development/debugging).
|
||||
|
||||
---
|
||||
## ✨ Custom Features & Deviations
|
||||
|
||||
## `boon` Crate Modifications
|
||||
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.
|
||||
|
||||
The version of `boon` located in the `validator/` directory has been significantly modified to support application-specific validation logic that goes beyond the standard JSON Schema specification.
|
||||
### 1. Polymorphism & Referencing (`$ref`, `$family`, and Native Types)
|
||||
|
||||
### 1. Property-Level Overrides for Inheritance
|
||||
JSPG replaces the complex, dynamic reference resolution logic of standard JSON Schema (e.g., `$defs`, relative URIs, `$dynamicRef`, `$dynamicAnchor`, `anyOf`) with a strict, explicitly structured global `$id` system. This powers predictable code generation and blazing-fast runtime validation.
|
||||
|
||||
- **Problem:** A primary use case for this project is validating data models that use `$ref` to create inheritance chains (e.g., a `person` schema `$ref`s a `user` schema, which `$ref`s an `entity` schema). A common pattern is to use a `const` keyword on a `type` property to identify the specific model (e.g., `"type": {"const": "person"}`). However, standard JSON Schema composition with `allOf` (which is implicitly used by `$ref`) treats these as a logical AND. This creates an impossible condition where an instance's `type` property would need to be "person" AND "user" AND "entity" simultaneously.
|
||||
#### A. Global `$id` Conventions & Schema Buckets
|
||||
Every schema is part of a flat, globally addressable namespace. However, where a schema is defined in the database determines its physical boundaries:
|
||||
* **Types (Entities)**: Schemas defined within a Postgres `type` represent entities. The `$id` must be exactly the type name (`person`) or suffixed (`full.person`). All schemas in this bucket receive strict Native Type Discrimination based on the physical table hierarchy.
|
||||
* **Puncs (APIs)**: Schemas defined within a `punc` are ad-hoc containers. The `$id` must be exactly `[punc_name].request` or `[punc_name].response`. They are never entities themselves.
|
||||
* **Enums (Domains)**: Schemas defined within an `enum` represent enum definitions. The `$id` must be exactly the enum name (`job_status`) or suffixed (`short.job_status`).
|
||||
|
||||
- **Solution:** We've implemented a custom, explicit override mechanism. A new keyword, `"override": true`, can be added to any property definition within a schema.
|
||||
#### B. Native Type Discrimination (The `variations` Property)
|
||||
Because `jspg` knows which schemas are Entities based on their origin bucket (Types), it securely and implicitly manages the `"type"` property by attaching `compiled_variations`.
|
||||
If a schema originates in the `user` bucket, the validator does *not* rigidly require `{"type": "user"}`. Instead, it queries the physical Postgres type inheritance graph (e.g. `[entity, organization, user]`) and allows the JSON to be `{"type": "person"}` or `{"type": "bot"}` automatically, enabling seamless API polymorphism.
|
||||
|
||||
```json
|
||||
// person.json
|
||||
{
|
||||
"$id": "person",
|
||||
"$ref": "user",
|
||||
"properties": {
|
||||
"type": { "const": "person", "override": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
#### C. Structural Inheritance & Viral Infection (`$ref`)
|
||||
`$ref` is used exclusively for structural inheritance.
|
||||
* **Viral Infection**: If an anonymous schema or an ad-hoc schema (like a Punc Request) `$ref`s a strict Entity schema (like `person.light`), it *virally inherits* the `compiled_variations` of that target. This means a Punc request instantly gains the exact polymorphic security boundaries of the Entity it points to.
|
||||
* **`$ref` never creates a Union.** When you use `$ref`, you are asking for a single, concrete struct/shape.
|
||||
|
||||
This signals to the validator that this definition of the `type` property should be the *only* one applied, and any definitions for `type` found in base schemas (like `user` or `entity`) should be ignored for the duration of this validation.
|
||||
#### D. Shape Polymorphism & Virtual Unions (`$family`)
|
||||
To support polymorphic API contracts (e.g., heterogeneous arrays of generic widgets) without manually writing massive `oneOf` blocks, JSPG provides the `$family` macro.
|
||||
While `$ref` defines rigid structure, `$family` relies on an abstract **Descendants Graph**.
|
||||
|
||||
#### Key Changes
|
||||
During compilation, `jspg` temporarily tracks every `$ref` pointer globally to build a reverse-lookup graph of "Descendants". It also calculates the **Inheritance Depth** of every schema (how far removed it is from the root entity).
|
||||
When `{"$family": "widget"}` is encountered, JSPG:
|
||||
1. Locates the `widget` schema in the Descendants graph.
|
||||
2. Expands the macro by finding *every* schema in the entire database that structurally `$ref`s `widget`, directly or indirectly (e.g., `stock.widget`, an anonymous object, etc.).
|
||||
3. Evaluates the incoming JSON against **every** descendant schema in that family *strictly*.
|
||||
|
||||
This was achieved by making the validator stateful, using a pattern already present in `boon` for handling `unevaluatedProperties`.
|
||||
If you request `{"$family": "light.widget"}`, it simply evaluates all schemas that `$ref` the generic abstract `light.widget` interface.
|
||||
|
||||
1. **Meta-Schema Update**: The meta-schema for Draft 2020-12 was modified to recognize `"override": true` as a valid keyword within a schema object, preventing the compiler from rejecting our custom schemas.
|
||||
#### E. Strict Matches & The Depth Heuristic
|
||||
JSPG strictly enforces that polymorphic structures (`oneOf`, or a `$family` expansion) match **exactly one** valid schema permutation. It does not support fuzzy matching (`anyOf`).
|
||||
If a JSON payload matches more than one schema in a union (which happens frequently due to implicit inheritance where an object might technically satisfy the requirements of both `entity` and `user`), JSPG automatically applies the **Depth Heuristic Tie-Breaker**:
|
||||
* It looks up the pre-calculated Inheritance Depth for all valid passing candidates.
|
||||
* It selects the candidate that is **deepest** in the inheritance tree (the most explicitly defined descendant).
|
||||
* If multiple passing candidates tie at the exact same depth level, an `AMBIGUOUS` error is thrown, forcing the developer to supply a more precise type discriminator or payload.
|
||||
|
||||
2. **Compiler Modification**: The schema compiler in `validator/src/compiler.rs` was updated. It now inspects sub-schemas within a `properties` keyword and, if it finds `"override": true`, it records the name of that property in a new `override_properties` `HashSet` on the compiled `Schema` struct.
|
||||
This cleanly separates **Database Physics** (derived from the Postgres `Types` bucket and viral `$ref` inheritance) from **Structural Polymorphism** (derived purely from the abstract `$ref` tree).
|
||||
|
||||
3. **Stateful Validator with `Override` Context**: The core `Validator` in `validator/src/validator.rs` was modified to carry an `Override` context (a `HashSet` of property names) throughout the validation process.
|
||||
- **Initialization**: When validation begins, the `Override` context is created and populated with the names of any properties that the top-level schema has marked with `override`.
|
||||
- **Propagation**: As the validator descends through a `$ref` or `allOf`, this `Override` context is cloned and passed down. The child schema adds its own override properties to the set, ensuring that higher-level overrides are always maintained.
|
||||
- **Enforcement**: In `obj_validate`, before a property is validated, the validator first checks if the property's name exists in the `Override` context it has received. If it does, it means a parent schema has already claimed responsibility for validating this property, so the child validator **skips** it entirely. This effectively achieves the "top-level wins" inheritance model.
|
||||
### 2. Strict by Default & Extensibility
|
||||
JSPG enforces a "Secure by Default" philosophy. All schemas are treated as if `unevaluatedProperties: false` (and `unevaluatedItems: false`) is set, unless explicitly overridden.
|
||||
|
||||
This approach cleanly integrates our desired inheritance behavior directly into the validator with minimal and explicit deviation from the standard, avoiding the need for a complex, post-processing validation function like the old `walk_and_validate_refs`.
|
||||
* **Strictness**: By default, any property or array item in the instance data that is not explicitly defined in the schema causes a validation error. This prevents clients from sending undeclared fields or extra array elements.
|
||||
* **Extensibility (`extensible: true`)**: To allow a free-for-all of additional, undefined properties or extra array items, you must add `"extensible": true` to the schema. This globally disables the strictness check for that object or array, useful for types designed to be completely open.
|
||||
* **Structured Additional Properties (`additionalProperties: {...}`)**: Instead of a boolean free-for-all, you can define `additionalProperties` as a schema object (e.g., `{"type": "string"}`). This maintains strictness (no arbitrary keys) but allows any extra keys as long as their values match the defined structure.
|
||||
* **Ref Boundaries**: Strictness is reset when crossing `$ref` boundaries. The referenced schema's strictness is determined by its own definition (strict by default unless `extensible: true`), ignoring the caller's state.
|
||||
* **Inheritance**: Strictness is inherited. A schema extending a strict parent will also be strict unless it declares itself `extensible: true`. Conversely, a schema extending a loose parent will also be loose unless it declares itself `extensible: false`.
|
||||
|
||||
### 2. Recursive Runtime Strictness Control
|
||||
### 3. Implicit Keyword Shadowing
|
||||
Standard JSON Schema composition (`allOf`) is additive (Intersection), meaning constraints can only be tightened, not replaced. However, JSPG treats `$ref` differently when it appears alongside other properties to support object-oriented inheritance.
|
||||
|
||||
- **Problem:** The `jspg` project requires that certain schemas (specifically those for public `puncs` and global `type`s) enforce a strict "no extra properties" policy. This strictness needs to be decided at runtime and must cascade through the entire validation hierarchy, including all nested objects and `$ref` chains. A compile-time flag was unsuitable because it would incorrectly apply strictness to shared, reusable schemas.
|
||||
* **Inheritance (`$ref` + properties)**: When a schema uses `$ref` and defines its own properties, JSPG implements Smart Merge (or Shadowing). If a property is defined in the current schema, its constraints take precedence over the inherited constraints for that specific keyword.
|
||||
* **Example**: If Entity defines `type: { const: "entity" }` and Person (which refs Entity) defines `type: { const: "person" }`, validation passes for "person". The local const shadows the inherited const.
|
||||
* **Granularity**: Shadowing is per-keyword. If Entity defined `type: { const: "entity", minLength: 5 }`, Person would shadow `const` but still inherit `minLength: 5`.
|
||||
* **Composition (`allOf`)**: When using `allOf`, standard intersection rules apply. No shadowing occurs; all constraints from all branches must pass. This is used for mixins or interfaces.
|
||||
|
||||
- **Solution:** A runtime validation option was implemented to enforce strictness recursively. This required several coordinated changes to the `boon` validator.
|
||||
### 4. Format Leniency for Empty Strings
|
||||
To simplify frontend form logic, the format validators for `uuid`, `date-time`, and `email` explicitly allow empty strings (`""`). This treats an empty string as "present but unset" rather than "invalid format".
|
||||
|
||||
#### Key Changes
|
||||
## 🏗️ Architecture
|
||||
|
||||
1. **`ValidationOptions` Struct**: A new `ValidationOptions { be_strict: bool }` struct was added to `validator/src/lib.rs`. The `jspg` code in `src/lib.rs` determines if a validation run should be strict and passes this struct to the validator.
|
||||
The extension is written in Rust using `pgrx` and structures its schema parser to mirror the Punc Generator's design:
|
||||
|
||||
2. **Strictness Check in `uneval_validate`**: The original `boon` only checked for unevaluated properties if the `unevaluatedProperties` keyword was present in the schema. We added an `else if be_strict` block to `uneval_validate` in `validator/src/validator.rs`. This block triggers a check for any leftover unevaluated properties at the end of a validation pass and reports them as errors, effectively enforcing our runtime strictness rule.
|
||||
* **Single `Schema` Struct**: A unified struct representing the exact layout of a JSON Schema object, including standard keywords and custom vocabularies (`form`, `display`, etc.).
|
||||
* **Compiler Phase**: schema JSONs are parsed into this struct, linked (references resolved), and then compiled into an efficient validation tree.
|
||||
* **Validation Phase**: The compiled validators traverse the JSON instance using `serde_json::Value`.
|
||||
|
||||
3. **Correct Context Propagation**: The most complex part of the fix was ensuring the set of unevaluated properties was correctly maintained across different validation contexts (especially `$ref` and nested property validations). Three critical changes were made:
|
||||
- **Inheriting Context in `_validate_self`**: When validating keywords that apply to the same instance (like `$ref` or `allOf`), the sub-validator must know what properties the parent has already evaluated. We changed the creation of the `Validator` inside `_validate_self` to pass a clone of the parent's `uneval` state (`uneval: self.uneval.clone()`) instead of creating a new one from scratch. This allows the context to flow downwards.
|
||||
- **Isolating Context in `validate_val`**: Conversely, when validating a property's value, that value is a *different* part of the JSON instance. The sub-validation should not affect the parent's list of unevaluated properties. We fixed this by commenting out the `self.uneval.merge(...)` call in the `validate_val` function.
|
||||
- **Simplifying `Uneval::merge`**: The original logic for merging `uneval` state was different for `$ref` keywords. This was incorrect. We simplified the `merge` function to *always* perform an intersection (`retain`), which correctly combines the knowledge of evaluated properties from different schema parts that apply to the same instance.
|
||||
### Concurrency & Threading ("Immutable Graphs")
|
||||
|
||||
4. **Removing Incompatible Assertions**: The changes to context propagation broke several `debug_assert!` macros in the `arr_validate` function, which were part of `boon`'s original design. Since our new validation flow is different but correct, these assertions were removed.
|
||||
To support high-throughput validation while allowing for runtime schema updates (e.g., during development or hot-reloading), JSPG uses an **Atomic Swap** pattern based on 100% immutable schemas.
|
||||
|
||||
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**: A reverse `$ref` lookup graph for instant `$family` resolution.
|
||||
* **Depths Map**: The `$ref` lineage distance of every schema for heuristic tie-breaking.
|
||||
* **Variations Map**: The Native Type inheritance hierarchy.
|
||||
3. **Immutable Validator**: The `Validator` struct immutably owns the `Database` registry and all its global maps. Once created, a validator instance (and its registry) never changes. Schemas themselves are completely frozen; `$ref` strings are resolved dynamically at runtime using the pre-computed O(1) maps, eliminating the need to physically mutate or link pointers across structures.
|
||||
4. **Global Pointer**: A global `RwLock<Option<Arc<Validator>>>` holds the current active validator.
|
||||
5. **Lock-Free Reads**: Validation requests acquire a read lock just long enough to clone the `Arc` (incrementing a reference count), then release the lock immediately. Validation proceeds on the snapshot, ensuring no blocking during schema updates.
|
||||
6. **Atomic Updates**: When schemas are reloaded (`cache_json_schemas`), a new `Registry` and `Validator` are built entirely on the stack. The global pointer is then atomically swapped to the new instance under a write lock.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Testing is driven by standard Rust unit tests that load JSON fixtures.
|
||||
|
||||
* **Isolation**: Each test file runs with its own isolated `Registry` and `Validator` instance, created on the stack. This eliminates global state interference and allows tests to run in parallel.
|
||||
* **Fixtures**: The tests are located in `tests/fixtures/*.json` and are executed via `cargo test`.
|
||||
0
agreego.sql
Normal file
0
agreego.sql
Normal file
124
build.rs
Normal file
124
build.rs
Normal file
@ -0,0 +1,124 @@
|
||||
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=tests/fixtures");
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
|
||||
// File 1: src/tests/fixtures.rs for #[pg_test]
|
||||
let pg_dest_path = Path::new("src/tests/fixtures.rs");
|
||||
let mut pg_file = File::create(pg_dest_path).unwrap();
|
||||
|
||||
// File 2: tests/fixtures.rs for standard #[test] integration
|
||||
let std_dest_path = Path::new("tests/fixtures.rs");
|
||||
let mut std_file = File::create(std_dest_path).unwrap();
|
||||
|
||||
// Write headers
|
||||
writeln!(std_file, "use jspg::validator::util;").unwrap();
|
||||
|
||||
// Walk tests/fixtures directly
|
||||
let fixtures_path = "tests/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");
|
||||
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")
|
||||
|| !t_obj.contains_key("data")
|
||||
|| !t_obj.contains_key("valid")
|
||||
|| !t_obj.contains_key("schema_id")
|
||||
{
|
||||
panic!(
|
||||
"File {} suite {} test {} is missing required case fields (description, data, valid, schema_id)",
|
||||
file_name, i, t_idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use deterministic names: test_{filename}_{index}
|
||||
let safe_filename = to_safe_identifier(file_name);
|
||||
let fn_name = format!("test_{}_{}", safe_filename, i);
|
||||
|
||||
// Write to src/tests.rs (PG Test)
|
||||
// CARGO_MANIFEST_DIR is used to find the absolute path to fixtures at runtime
|
||||
write!(
|
||||
pg_file,
|
||||
r#"
|
||||
#[pg_test]
|
||||
fn {}() {{
|
||||
let path = format!("{{}}/tests/fixtures/{}.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::validator::util::run_test_file_at_index(&path, {}).unwrap();
|
||||
}}
|
||||
"#,
|
||||
fn_name, file_name, i
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write to tests/tests.rs (Std Test)
|
||||
write!(
|
||||
std_file,
|
||||
r#"
|
||||
#[test]
|
||||
fn {}() {{
|
||||
let path = format!("{{}}/tests/fixtures/{}.json", env!("CARGO_MANIFEST_DIR"));
|
||||
util::run_test_file_at_index(&path, {}).unwrap();
|
||||
}}
|
||||
"#,
|
||||
fn_name, file_name, i
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
flow
76
flow
@ -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.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,36 +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-jspg() {
|
||||
test() {
|
||||
info "Running jspg tests..."
|
||||
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
|
||||
}
|
||||
|
||||
test-validator() {
|
||||
info "Running validator tests..."
|
||||
cargo test -p boon --features "pgrx/pg${POSTGRES_VERSION}" "$@" || return $?
|
||||
cargo test --tests "$@" || return $?
|
||||
}
|
||||
|
||||
clean() {
|
||||
@ -113,27 +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-jspg\t\tRun pgrx integration tests.\n"
|
||||
printf "test-validator\t\tRun validator 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-jspg) test-jspg "${@:2}"; return $?;;
|
||||
test-validator) test-validator "${@:2}"; return $?;;
|
||||
test) test "${@:2}"; return $?;;
|
||||
clean) clean; return $?;;
|
||||
*) return 1 ;;
|
||||
*) return 127 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
register-flow "jspg-usage" "jspg-flow"
|
||||
register-flow "jspg"
|
||||
|
||||
dispatch "$@"
|
||||
2
flows
2
flows
Submodule flows updated: e154758056...a7b0f5dc4d
@ -1 +1 @@
|
||||
::pgrx::pgrx_embed!();
|
||||
::pgrx::pgrx_embed!();
|
||||
|
||||
12
src/database/enum.rs
Normal file
12
src/database/enum.rs
Normal 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>,
|
||||
}
|
||||
@ -4,12 +4,12 @@ use std::{
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use lazy_static::lazy_static;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use serde_json::Value;
|
||||
use url::Url;
|
||||
|
||||
use crate::ecma;
|
||||
// use crate::ecma; // Assuming ecma is not yet available, stubbing regex for now
|
||||
|
||||
/// Defines format for `format` keyword.
|
||||
#[derive(Clone, Copy)]
|
||||
@ -18,43 +18,50 @@ pub struct Format {
|
||||
pub name: &'static str,
|
||||
|
||||
/// validates given value.
|
||||
pub func: fn(v: &Value) -> Result<(), Box<dyn Error>>,
|
||||
pub func: fn(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>>, // Ensure thread safety if needed
|
||||
}
|
||||
|
||||
pub(crate) static FORMATS: Lazy<HashMap<&'static str, Format>> = Lazy::new(|| {
|
||||
let mut m = HashMap::<&'static str, Format>::new();
|
||||
let mut register = |name, func| m.insert(name, Format { name, func });
|
||||
register("regex", validate_regex);
|
||||
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
|
||||
});
|
||||
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 });
|
||||
|
||||
fn validate_regex(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
ecma::convert(s).map(|_| ())
|
||||
// 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_ipv4(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
/*
|
||||
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(());
|
||||
};
|
||||
@ -62,7 +69,7 @@ fn validate_ipv4(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -70,11 +77,12 @@ fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_date(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_date(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_date(s)
|
||||
check_date(s)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn matches_char(s: &str, index: usize, ch: char) -> bool {
|
||||
@ -82,7 +90,7 @@ fn matches_char(s: &str, index: usize, ch: char) -> bool {
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||
fn check_date(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
fn check_date(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
// yyyy-mm-dd
|
||||
if s.len() != 10 {
|
||||
Err("must be 10 characters long")?;
|
||||
@ -123,14 +131,14 @@ fn check_date(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_time(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
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>> {
|
||||
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")?
|
||||
@ -203,14 +211,14 @@ fn check_time(mut str: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_date_time(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
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>> {
|
||||
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")?;
|
||||
@ -227,15 +235,16 @@ fn check_date_time(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_duration(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_duration(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_duration(s)
|
||||
check_duration(s)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
|
||||
fn check_duration(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
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")?
|
||||
@ -288,7 +297,7 @@ fn check_duration(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
|
||||
fn validate_period(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_period(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -320,17 +329,18 @@ fn validate_period(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_hostname(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_hostname(s)
|
||||
check_hostname(s)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
fn check_hostname(mut s: &str) -> Result<(), Box<dyn Error>> {
|
||||
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
|
||||
s = s.strip_suffix('.').unwrap_or(s);
|
||||
|
||||
if s.len() > 253 {
|
||||
Err("more than 253 characters long")?
|
||||
}
|
||||
@ -359,36 +369,60 @@ fn check_hostname(mut s: &str) -> Result<(), Box<dyn Error>> {
|
||||
{
|
||||
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>> {
|
||||
fn validate_idn_hostname(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_idn_hostname(s)
|
||||
check_idn_hostname(s)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
let s = idna::domain_to_ascii_strict(s)?;
|
||||
let unicode = idna::domain_to_unicode(&s).0;
|
||||
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
|
||||
{
|
||||
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
|
||||
];
|
||||
if unicode.contains(DISALLOWED) {
|
||||
Err("contains disallowed character")?;
|
||||
}
|
||||
@ -413,7 +447,7 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.3
|
||||
{
|
||||
let middle_dot = '\u{00b7}';
|
||||
let mut s = unicode.as_str();
|
||||
let mut s = unicode;
|
||||
while let Some(i) = s.find(middle_dot) {
|
||||
let prefix = &s[..i];
|
||||
let suffix = &s[i + middle_dot.len_utf8()..];
|
||||
@ -429,7 +463,7 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
{
|
||||
let keralia = '\u{0375}';
|
||||
let greek = '\u{0370}'..='\u{03FF}';
|
||||
let mut s = unicode.as_str();
|
||||
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)) {
|
||||
@ -449,10 +483,9 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
let gereshayim = '\u{05F4}';
|
||||
let hebrew = '\u{0590}'..='\u{05FF}';
|
||||
for ch in [geresh, gereshayim] {
|
||||
let mut s = unicode.as_str();
|
||||
let mut s = unicode;
|
||||
while let Some(i) = s.find(ch) {
|
||||
let prefix = &s[..i];
|
||||
let suffix = &s[i + ch.len_utf8()..];
|
||||
if !prefix.ends_with(|c| hebrew.contains(&c)) {
|
||||
if i == 0 {
|
||||
Err("Hebrew GERESH must be preceded by Hebrew character")?;
|
||||
@ -460,6 +493,7 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
Err("Hebrew GERESHYIM must be preceded by Hebrew character")?;
|
||||
}
|
||||
}
|
||||
let suffix = &s[i + ch.len_utf8()..];
|
||||
s = suffix;
|
||||
}
|
||||
}
|
||||
@ -469,10 +503,10 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
// see https://www.rfc-editor.org/rfc/rfc5892#appendix-A.7
|
||||
{
|
||||
let katakana_middle_dot = '\u{30FB}';
|
||||
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(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))
|
||||
@ -564,29 +598,30 @@ fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
'\u{11D45}',
|
||||
'\u{11D97}',
|
||||
]; // https://www.compart.com/en/unicode/combining/9
|
||||
let mut s = unicode.as_str();
|
||||
let mut s = unicode;
|
||||
while let Some(i) = s.find(zero_width_jointer) {
|
||||
let prefix = &s[..i];
|
||||
let suffix = &s[i + zero_width_jointer.len_utf8()..];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
check_hostname(&s)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_email(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_email(s)
|
||||
check_email(s)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// see https://en.wikipedia.org/wiki/Email_address
|
||||
fn check_email(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
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")?
|
||||
@ -656,7 +691,7 @@ fn check_email(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_idn_email(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_idn_email(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -666,23 +701,24 @@ fn validate_idn_email(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
};
|
||||
let (local, domain) = (&s[..at], &s[at + 1..]);
|
||||
|
||||
let local = idna::domain_to_ascii_strict(local)?;
|
||||
let domain = idna::domain_to_ascii_strict(domain)?;
|
||||
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>> {
|
||||
fn validate_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
check_json_pointer(s)
|
||||
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>> {
|
||||
fn check_json_pointer(s: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
if s.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@ -705,7 +741,7 @@ fn check_json_pointer(s: &str) -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// 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>> {
|
||||
fn validate_relative_json_pointer(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -731,7 +767,7 @@ fn validate_relative_json_pointer(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
// see https://datatracker.ietf.org/doc/html/rfc4122#page-4
|
||||
fn validate_uuid(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_uuid(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -760,17 +796,21 @@ fn validate_uuid(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_uri(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
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())?.scheme().is_none() {
|
||||
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>> {
|
||||
fn validate_iri(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -781,24 +821,26 @@ fn validate_iri(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
static TEMP_URL: Lazy<Url> = Lazy::new(|| Url::parse("http://temp.com").unwrap());
|
||||
lazy_static! {
|
||||
static ref TEMP_URL: Url = Url::parse("http://temp.com").unwrap();
|
||||
}
|
||||
|
||||
fn parse_uri_reference(s: &str) -> Result<Url, Box<dyn Error>> {
|
||||
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>> {
|
||||
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())?;
|
||||
fluent_uri::UriRef::parse(s.as_str()).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -806,7 +848,7 @@ fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_uri_template(v: &Value) -> Result<(), Box<dyn Error>> {
|
||||
fn validate_uri_template(v: &Value) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let Value::String(s) = v else {
|
||||
return Ok(());
|
||||
};
|
||||
187
src/database/mod.rs
Normal file
187
src/database/mod.rs
Normal file
@ -0,0 +1,187 @@
|
||||
pub mod r#enum;
|
||||
pub mod formats;
|
||||
pub mod page;
|
||||
pub mod punc;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
|
||||
use crate::database::r#enum::Enum;
|
||||
use crate::database::punc::Punc;
|
||||
use crate::database::schema::Schema;
|
||||
use crate::database::r#type::Type;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub struct Database {
|
||||
pub enums: HashMap<String, Enum>,
|
||||
pub types: HashMap<String, Type>,
|
||||
pub puncs: HashMap<String, Punc>,
|
||||
pub schemas: HashMap<String, Schema>,
|
||||
pub descendants: HashMap<String, Vec<String>>,
|
||||
pub depths: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(val: &serde_json::Value) -> Self {
|
||||
let mut db = Self {
|
||||
enums: HashMap::new(),
|
||||
types: HashMap::new(),
|
||||
puncs: HashMap::new(),
|
||||
schemas: HashMap::new(),
|
||||
descendants: HashMap::new(),
|
||||
depths: HashMap::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("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = db.compile();
|
||||
db
|
||||
}
|
||||
|
||||
/// Organizes the graph of the database, compiling regex, format functions, and caching relationships.
|
||||
fn compile(&mut self) -> Result<(), String> {
|
||||
self.collect_schemas();
|
||||
self.collect_depths();
|
||||
self.collect_descendants();
|
||||
self.compile_schemas();
|
||||
|
||||
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(¤t_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/database/page.rs
Normal file
35
src/database/page.rs
Normal 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
20
src/database/punc.rs
Normal 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>,
|
||||
}
|
||||
340
src/database/schema.rs
Normal file
340
src/database/schema.rs
Normal file
@ -0,0 +1,340 @@
|
||||
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.
|
||||
#[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::validator::util::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 = "anyOf")]
|
||||
pub any_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.any_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.any_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>),
|
||||
}
|
||||
39
src/database/type.rs
Normal file
39
src/database/type.rs
Normal file
@ -0,0 +1,39 @@
|
||||
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>,
|
||||
pub relationship: Option<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
72
src/drop.rs
Normal 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
|
||||
}
|
||||
79
src/entity/GEMINI.md
Normal file
79
src/entity/GEMINI.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Entity Engine (jspg)
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the architecture for moving the complex, CPU-bound row merging (`merge_entity`) and dynamic querying (`query_entity`) functionality out of PL/pgSQL and directly into the Rust-based `jspg` extension.
|
||||
|
||||
By treating the `jspg` schema registry as the absolute Single Source of Truth, we can leverage Rust and the Postgres query planner (via SPI) to achieve near O(1) execution planning for deeply nested reads, complex relational writes, and partial hydration beats.
|
||||
|
||||
## The Problem
|
||||
|
||||
Historically, `agreego.merge_entity` (PL/pgSQL) handled nested writes by segmenting JSON, resolving types, searching hierarchies, and dynamically concatenating `INSERT`/`UPDATE` statements. `agreego.query_entity` was conceived to do the same for reads (handling base security, inheritance JOINs, and filtering automatically).
|
||||
|
||||
However, this design hits three major limitations:
|
||||
1. **CPU Bound Operations**: PL/pgSQL is comparatively slow at complex string concatenation and massive JSON graph traversals.
|
||||
2. **Query Planning Cache Busting**: Generating massive, dynamic SQL strings prevents Postgres from caching query plans. `EXECUTE dynamic_sql` forces the planner to re-evaluate statistics and execution paths on every function call, leading to extreme latency spikes at scale.
|
||||
3. **The Hydration Beat Problem**: The Punc framework requires fetching specific UI "fragments" (e.g. just the `target` of a specific `contact` array element) to feed WebSockets. Hand-rolling CTEs for every possible sub-tree permutation to serve beats will quickly become unmaintainable.
|
||||
|
||||
## The Solution: Semantic Engine Database
|
||||
|
||||
By migrating `merge_entity` and `query_entity` to `jspg`, we turn the database into a pre-compiled Semantic Engine.
|
||||
|
||||
1. **Schema-to-SQL Compilation**: During the connection lifecycle (`cache_json_schemas()`), `jspg` statically analyzes the JSON Schema AST. It acts as a compiler, translating the schema layout into perfectly optimized, multi-JOIN SQL query strings for *every* node/fragment in the schema.
|
||||
2. **Prepared Statements (SPI)**: `jspg` feeds these computed SQL strings into the Postgres SPI (Server Programming Interface) using `Spi::prepare()`. Postgres calculates the query execution plan *once* and caches it in memory.
|
||||
3. **Instant Execution**: When a Punc needs data, `jspg` retrieves the cached PreparedStatement, securely binds binary parameters, and executes the pre-planned query instantly.
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. The `cache_json_schemas()` Expansion
|
||||
The initialization function must now ingest `types` and `agreego.relation` data so the internal `Registry` holds the full Relational Graph.
|
||||
|
||||
During schema compilation, if a schema is associated with a database Type, it triggers the **SQL Compiler Phase**:
|
||||
- It builds a table-resolution AST mapping to `JOIN` clauses based on foreign keys.
|
||||
- It translates JSON schema properties to `SELECT jsonb_build_object(...)`.
|
||||
- It generates static SQL for `INSERT`, `UPDATE`, and `SELECT` (including path-based fragment SELECTs).
|
||||
- It calls `Spi::prepare()` to cache these plans inside the Session Context.
|
||||
|
||||
### 2. `agreego.query_entity` (Reads)
|
||||
* **API**: `agreego.query_entity(schema_id TEXT, fragment_path TEXT, cue JSONB)`
|
||||
* **Execution**:
|
||||
* Rust locates the target Schema in memory.
|
||||
* It uses the `fragment_path` (e.g., `/` for a full read, or `/contacts/0/target` for a hydration beat) to fetch the exact PreparedStatement.
|
||||
* It binds variables (Row Level Security IDs, filtering, pagination limit/offset) parsed from the `cue`.
|
||||
* SPI returns the heavily nested, pre-aggregated `JSONB` instantly.
|
||||
|
||||
### 3. Unified Aggregations & Computeds (Schema `query` objects)
|
||||
We replace the concept of a complex string parser (PEL) with native structured JSON JSON objects using the `query` keyword.
|
||||
|
||||
A structured `query` block in the schema:
|
||||
```json
|
||||
"total": {
|
||||
"type": "number",
|
||||
"readOnly": true,
|
||||
"query": {
|
||||
"aggregate": "sum",
|
||||
"source": "lines",
|
||||
"field": "amount"
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Frontend (Dart)**: The Go generator parses the JSON object directly and emits the native UI aggregation code (e.g. `lines.fold(...)`) for instant UI updates before the server responds.
|
||||
* **Backend (jspg)**: The Rust SQL compiler natively deserializes the `query` object into an internal struct. It recognizes the `aggregate` instruction and outputs a Postgres native aggregation: `(SELECT SUM(amount) FROM agreego.invoice_line WHERE invoice_id = t1.id)` as a column in the prepared `SELECT` statement.
|
||||
* **Unification**: The database-calculated value acts as the authoritative truth, synchronizing and correcting the client automatically on the resulting `beat`.
|
||||
|
||||
### 4. `agreego.merge_entity` (Writes)
|
||||
* **API**: `agreego.merge_entity(cue JSONB)`
|
||||
* **Execution**:
|
||||
* Parses the incoming `cue` JSON via `serde_json` at C-like speeds.
|
||||
* Recursively validates and *constructively masks* the tree against the strict schema.
|
||||
* Traverses the relational graph (which is fully loaded in the `jspg` registry).
|
||||
* Binds the new values directly into the cached `INSERT` or `UPDATE` SPI prepared statements for each table in the hierarchy.
|
||||
* Evaluates field differences and natively uses `pg_notify` to fire atomic row-level changes for the Go Beat framework.
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. **Relational Ingestion**: Update `cache_json_schemas` to pass relational metadata (`agreego.relation` rows) into the `jspg` registry cache.
|
||||
2. **The SQL Compiler**: Build the AST-to-String compiler in Rust that reads properties, `$ref`s, and `$family` trees to piece together generic SQL.
|
||||
3. **SPI Caching**: Integrate `Spi::prepare` into the `Validator` creation phase.
|
||||
4. **Rust `merge_entity`**: Port the constructive structural extraction loop from PL/pgSQL to Rust.
|
||||
5. **Rust `query_entity`**: Abstract the query runtime, mapping Punc JSON `filters` arrays to SPI-bound parameters safely.
|
||||
@ -1,88 +0,0 @@
|
||||
use serde_json::Value;
|
||||
use pgrx::JsonB;
|
||||
|
||||
// Simple test helpers for cleaner test code
|
||||
pub fn assert_success(result: &JsonB) {
|
||||
let json = &result.0;
|
||||
if !json.get("response").is_some() || json.get("errors").is_some() {
|
||||
let pretty = serde_json::to_string_pretty(json).unwrap_or_else(|_| format!("{:?}", json));
|
||||
panic!("Expected success but got:\n{}", pretty);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_failure(result: &JsonB) {
|
||||
let json = &result.0;
|
||||
if json.get("response").is_some() || !json.get("errors").is_some() {
|
||||
let pretty = serde_json::to_string_pretty(json).unwrap_or_else(|_| format!("{:?}", json));
|
||||
panic!("Expected failure but got:\n{}", pretty);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_error_count(result: &JsonB, expected_count: usize) {
|
||||
assert_failure(result);
|
||||
let errors = get_errors(result);
|
||||
if errors.len() != expected_count {
|
||||
let pretty = serde_json::to_string_pretty(&result.0).unwrap_or_else(|_| format!("{:?}", result.0));
|
||||
panic!("Expected {} errors, got {}:\n{}", expected_count, errors.len(), pretty);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_errors(result: &JsonB) -> &Vec<Value> {
|
||||
result.0["errors"].as_array().expect("errors should be an array")
|
||||
}
|
||||
|
||||
pub fn has_error_with_code(result: &JsonB, code: &str) -> bool {
|
||||
get_errors(result).iter().any(|e| e["code"] == code)
|
||||
}
|
||||
|
||||
|
||||
pub fn has_error_with_code_and_path(result: &JsonB, code: &str, path: &str) -> bool {
|
||||
get_errors(result).iter().any(|e| e["code"] == code && e["details"]["path"] == path)
|
||||
}
|
||||
|
||||
pub fn assert_has_error(result: &JsonB, code: &str, path: &str) {
|
||||
if !has_error_with_code_and_path(result, code, path) {
|
||||
let pretty = serde_json::to_string_pretty(&result.0).unwrap_or_else(|_| format!("{:?}", result.0));
|
||||
panic!("Expected error with code='{}' and path='{}' but not found:\n{}", code, path, pretty);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_error_with_code<'a>(result: &'a JsonB, code: &str) -> &'a Value {
|
||||
get_errors(result).iter().find(|e| e["code"] == code)
|
||||
.unwrap_or_else(|| panic!("No error found with code '{}'", code))
|
||||
}
|
||||
|
||||
|
||||
pub fn find_error_with_code_and_path<'a>(result: &'a JsonB, code: &str, path: &str) -> &'a Value {
|
||||
get_errors(result).iter().find(|e| e["code"] == code && e["details"]["path"] == path)
|
||||
.unwrap_or_else(|| panic!("No error found with code '{}' and path '{}'", code, path))
|
||||
}
|
||||
|
||||
pub fn assert_error_detail(error: &Value, detail_key: &str, expected_value: &str) {
|
||||
let actual = error["details"][detail_key].as_str()
|
||||
.unwrap_or_else(|| panic!("Error detail '{}' is not a string", detail_key));
|
||||
assert_eq!(actual, expected_value, "Error detail '{}' mismatch", detail_key);
|
||||
}
|
||||
|
||||
|
||||
// Additional convenience helpers for common patterns
|
||||
|
||||
pub fn assert_error_message_contains(error: &Value, substring: &str) {
|
||||
let message = error["message"].as_str().expect("error should have message");
|
||||
assert!(message.contains(substring), "Expected message to contain '{}', got '{}'", substring, message);
|
||||
}
|
||||
|
||||
pub fn assert_error_cause_json(error: &Value, expected_cause: &Value) {
|
||||
let cause = &error["details"]["cause"];
|
||||
assert!(cause.is_object(), "cause should be JSON object");
|
||||
assert_eq!(cause, expected_cause, "cause mismatch");
|
||||
}
|
||||
|
||||
pub fn assert_error_context(error: &Value, expected_context: &Value) {
|
||||
assert_eq!(&error["details"]["context"], expected_context, "context mismatch");
|
||||
}
|
||||
|
||||
|
||||
pub fn jsonb(val: Value) -> JsonB {
|
||||
JsonB(val)
|
||||
}
|
||||
29
src/jspg.rs
Normal file
29
src/jspg.rs
Normal 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) -> Self {
|
||||
let database_instance = Database::new(database_val);
|
||||
let database = Arc::new(database_instance);
|
||||
let validator = Validator::new(database.clone());
|
||||
let queryer = Queryer::new();
|
||||
let merger = Merger::new();
|
||||
|
||||
Self {
|
||||
database,
|
||||
validator,
|
||||
queryer,
|
||||
merger,
|
||||
}
|
||||
}
|
||||
}
|
||||
937
src/lib.rs
937
src/lib.rs
@ -2,859 +2,144 @@ use pgrx::*;
|
||||
|
||||
pg_module_magic!();
|
||||
|
||||
use boon::{CompileError, Compiler, ErrorKind, SchemaIndex, Schemas, ValidationError, Type, Types, ValidationOptions};
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::{json, Value, Number};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::{collections::{HashMap, HashSet}, sync::RwLock};
|
||||
pub mod database;
|
||||
pub mod drop;
|
||||
pub mod jspg;
|
||||
pub mod merger;
|
||||
pub mod queryer;
|
||||
pub mod validator;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum SchemaType {
|
||||
Enum,
|
||||
Type,
|
||||
Family, // Added for generated hierarchy schemas
|
||||
PublicPunc,
|
||||
PrivatePunc,
|
||||
}
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
struct Schema {
|
||||
index: SchemaIndex,
|
||||
t: SchemaType,
|
||||
}
|
||||
|
||||
struct Cache {
|
||||
schemas: Schemas,
|
||||
map: HashMap<String, Schema>,
|
||||
}
|
||||
|
||||
// Structure to hold error information without lifetimes
|
||||
#[derive(Debug)]
|
||||
struct Error {
|
||||
path: String,
|
||||
code: String,
|
||||
message: String,
|
||||
cause: Value, // Changed from String to Value to store JSON
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SCHEMA_CACHE: RwLock<Cache> = RwLock::new(Cache {
|
||||
schemas: Schemas::new(),
|
||||
map: HashMap::new(),
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
#[pg_extern(strict)]
|
||||
fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
|
||||
let mut cache = SCHEMA_CACHE.write().unwrap();
|
||||
let enums_value: Value = enums.0;
|
||||
let types_value: Value = types.0;
|
||||
let puncs_value: Value = puncs.0;
|
||||
pub fn jspg_cache_database(database: JsonB) -> JsonB {
|
||||
let new_jspg = crate::jspg::Jspg::new(&database.0);
|
||||
let new_arc = Arc::new(new_jspg);
|
||||
|
||||
*cache = Cache {
|
||||
schemas: Schemas::new(),
|
||||
map: HashMap::new(),
|
||||
// 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())
|
||||
}
|
||||
// `mask_json_schema` has been removed as the mask architecture is fully replaced by Spi string queries during DB interactions.
|
||||
|
||||
#[pg_extern(strict, parallel_safe)]
|
||||
pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
||||
// 1. Acquire Snapshot
|
||||
let jspg_arc = {
|
||||
let lock = GLOBAL_JSPG.read().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
let mut compiler = Compiler::new();
|
||||
compiler.enable_format_assertions();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut schemas_to_compile = Vec::new();
|
||||
|
||||
// Phase 1: Enums
|
||||
if let Some(enums_array) = enums_value.as_array() {
|
||||
for enum_row in enums_array {
|
||||
if let Some(schemas_raw) = enum_row.get("schemas") {
|
||||
if let Some(schemas_array) = schemas_raw.as_array() {
|
||||
for schema_def in schemas_array {
|
||||
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
|
||||
schemas_to_compile.push((schema_id.to_string(), schema_def.clone(), SchemaType::Enum));
|
||||
}
|
||||
}
|
||||
// 2. Validate (Lock-Free)
|
||||
if let Some(engine) = jspg_arc {
|
||||
match engine.validator.validate(schema_id, &instance.0) {
|
||||
Ok(result) => {
|
||||
if result.is_valid() {
|
||||
let drop = crate::drop::Drop::success();
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
} 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();
|
||||
let drop = crate::drop::Drop::with_errors(errors);
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Types & Hierarchy Pre-processing
|
||||
let mut hierarchy_map: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
if let Some(types_array) = types_value.as_array() {
|
||||
for type_row in types_array {
|
||||
// Process main schemas for the type
|
||||
if let Some(schemas_raw) = type_row.get("schemas") {
|
||||
if let Some(schemas_array) = schemas_raw.as_array() {
|
||||
for schema_def in schemas_array {
|
||||
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
|
||||
schemas_to_compile.push((schema_id.to_string(), schema_def.clone(), SchemaType::Type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process hierarchy to build .family enums
|
||||
if let Some(type_name) = type_row.get("name").and_then(|v| v.as_str()) {
|
||||
if let Some(hierarchy_raw) = type_row.get("hierarchy") {
|
||||
if let Some(hierarchy_array) = hierarchy_raw.as_array() {
|
||||
for ancestor_val in hierarchy_array {
|
||||
if let Some(ancestor_name) = ancestor_val.as_str() {
|
||||
hierarchy_map
|
||||
.entry(ancestor_name.to_string())
|
||||
.or_default()
|
||||
.insert(type_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and add the .family schemas
|
||||
for (base_type, descendant_types) in hierarchy_map {
|
||||
let family_schema_id = format!("{}.family", base_type);
|
||||
let enum_values: Vec<String> = descendant_types.into_iter().collect();
|
||||
let family_schema = json!({
|
||||
"$id": family_schema_id,
|
||||
"type": "string",
|
||||
"enum": enum_values
|
||||
});
|
||||
schemas_to_compile.push((family_schema_id, family_schema, SchemaType::Family));
|
||||
}
|
||||
|
||||
// Phase 3: Puncs
|
||||
if let Some(puncs_array) = puncs_value.as_array() {
|
||||
for punc_row in puncs_array {
|
||||
if let Some(punc_obj) = punc_row.as_object() {
|
||||
if let Some(punc_name) = punc_obj.get("name").and_then(|v| v.as_str()) {
|
||||
let is_public = punc_obj.get("public").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let punc_schema_type = if is_public { SchemaType::PublicPunc } else { SchemaType::PrivatePunc };
|
||||
if let Some(schemas_raw) = punc_obj.get("schemas") {
|
||||
if let Some(schemas_array) = schemas_raw.as_array() {
|
||||
for schema_def in schemas_array {
|
||||
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
|
||||
let request_schema_id = format!("{}.request", punc_name);
|
||||
let response_schema_id = format!("{}.response", punc_name);
|
||||
let schema_type_for_def = if schema_id == request_schema_id || schema_id == response_schema_id {
|
||||
punc_schema_type
|
||||
} else {
|
||||
SchemaType::Type
|
||||
};
|
||||
schemas_to_compile.push((schema_id.to_string(), schema_def.clone(), schema_type_for_def));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all resources to compiler first
|
||||
for (id, value, schema_type) in &schemas_to_compile {
|
||||
add_schema_resource(&mut compiler, id, value.clone(), *schema_type, &mut errors);
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return JsonB(json!({ "errors": errors }));
|
||||
}
|
||||
|
||||
// Compile all schemas
|
||||
compile_all_schemas(&mut compiler, &mut cache, &schemas_to_compile, &mut errors);
|
||||
|
||||
if errors.is_empty() {
|
||||
JsonB(json!({ "response": "success" }))
|
||||
} else {
|
||||
JsonB(json!({ "errors": errors }))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add a schema resource (without compiling)
|
||||
fn add_schema_resource(
|
||||
compiler: &mut Compiler,
|
||||
schema_id: &str,
|
||||
schema_value: Value,
|
||||
_schema_type: SchemaType,
|
||||
errors: &mut Vec<Value>
|
||||
) {
|
||||
if let Err(e) = compiler.add_resource(schema_id, schema_value) {
|
||||
errors.push(json!({
|
||||
"code": "SCHEMA_RESOURCE_FAILED",
|
||||
"message": format!("Failed to add schema resource '{}'", schema_id),
|
||||
"details": { "schema": schema_id, "cause": format!("{}", e) }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compile all added resources
|
||||
fn compile_all_schemas(
|
||||
compiler: &mut Compiler,
|
||||
cache: &mut Cache,
|
||||
schemas_to_compile: &[(String, Value, SchemaType)],
|
||||
errors: &mut Vec<Value>,
|
||||
) {
|
||||
for (id, value, schema_type) in schemas_to_compile {
|
||||
match compiler.compile(id, &mut cache.schemas) {
|
||||
Ok(index) => {
|
||||
cache.map.insert(id.clone(), Schema { index, t: *schema_type });
|
||||
}
|
||||
Err(e) => {
|
||||
match &e {
|
||||
CompileError::ValidationError { src, .. } => {
|
||||
let mut error_list = Vec::new();
|
||||
collect_errors(src, &mut error_list);
|
||||
let formatted_errors = format_errors(error_list, value, id);
|
||||
errors.extend(formatted_errors);
|
||||
}
|
||||
_ => {
|
||||
errors.push(json!({
|
||||
"code": "SCHEMA_COMPILATION_FAILED",
|
||||
"message": format!("Schema '{}' compilation failed", id),
|
||||
"details": { "schema": id, "cause": format!("{:?}", e) }
|
||||
}));
|
||||
}
|
||||
let error = crate::drop::Error {
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
details: crate::drop::ErrorDetails { path: e.path },
|
||||
};
|
||||
let drop = crate::drop::Drop::with_errors(vec![error]);
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pg_extern(strict, parallel_safe)]
|
||||
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
|
||||
let cache = SCHEMA_CACHE.read().unwrap();
|
||||
match cache.map.get(schema_id) {
|
||||
None => JsonB(json!({
|
||||
"errors": [{
|
||||
"code": "SCHEMA_NOT_FOUND",
|
||||
"message": format!("Schema '{}' not found in cache", schema_id),
|
||||
"details": {
|
||||
"schema": schema_id,
|
||||
"cause": "Schema was not found in bulk cache - ensure cache_json_schemas was called"
|
||||
}
|
||||
}]
|
||||
})),
|
||||
Some(schema) => {
|
||||
let instance_value: Value = instance.0;
|
||||
let options = match schema.t {
|
||||
SchemaType::PublicPunc => Some(ValidationOptions { be_strict: true }),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match cache.schemas.validate(&instance_value, schema.index, options) {
|
||||
Ok(_) => {
|
||||
JsonB(json!({ "response": "success" }))
|
||||
}
|
||||
Err(validation_error) => {
|
||||
let mut error_list = Vec::new();
|
||||
collect_errors(&validation_error, &mut error_list);
|
||||
let errors = format_errors(error_list, &instance_value, schema_id);
|
||||
if errors.is_empty() {
|
||||
JsonB(json!({ "response": "success" }))
|
||||
} else {
|
||||
JsonB(json!({ "errors": errors }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively collects validation errors
|
||||
fn collect_errors(error: &ValidationError, errors_list: &mut Vec<Error>) {
|
||||
// Check if this is a structural error that we should skip
|
||||
let is_structural = matches!(
|
||||
&error.kind,
|
||||
ErrorKind::Group | ErrorKind::AllOf | ErrorKind::AnyOf | ErrorKind::Not | ErrorKind::OneOf(_)
|
||||
);
|
||||
|
||||
if !error.causes.is_empty() || is_structural {
|
||||
for cause in &error.causes {
|
||||
collect_errors(cause, errors_list);
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let base_path = error.instance_location.to_string();
|
||||
let errors_to_add = match &error.kind {
|
||||
ErrorKind::Type { got, want } => handle_type_error(&base_path, got, want),
|
||||
ErrorKind::Required { want } => handle_required_error(&base_path, want),
|
||||
ErrorKind::Dependency { prop, missing } => handle_dependency_error(&base_path, prop, missing, false),
|
||||
ErrorKind::DependentRequired { prop, missing } => handle_dependency_error(&base_path, prop, missing, true),
|
||||
ErrorKind::AdditionalProperties { got } => handle_additional_properties_error(&base_path, got),
|
||||
ErrorKind::Enum { want } => handle_enum_error(&base_path, want),
|
||||
ErrorKind::Const { want } => handle_const_error(&base_path, want),
|
||||
ErrorKind::MinLength { got, want } => handle_min_length_error(&base_path, *got, *want),
|
||||
ErrorKind::MaxLength { got, want } => handle_max_length_error(&base_path, *got, *want),
|
||||
ErrorKind::Pattern { got, want } => handle_pattern_error(&base_path, got, want),
|
||||
ErrorKind::Minimum { got, want } => handle_minimum_error(&base_path, got, want),
|
||||
ErrorKind::Maximum { got, want } => handle_maximum_error(&base_path, got, want),
|
||||
ErrorKind::ExclusiveMinimum { got, want } => handle_exclusive_minimum_error(&base_path, got, want),
|
||||
ErrorKind::ExclusiveMaximum { got, want } => handle_exclusive_maximum_error(&base_path, got, want),
|
||||
ErrorKind::MultipleOf { got, want } => handle_multiple_of_error(&base_path, got, want),
|
||||
ErrorKind::MinItems { got, want } => handle_min_items_error(&base_path, *got, *want),
|
||||
ErrorKind::MaxItems { got, want } => handle_max_items_error(&base_path, *got, *want),
|
||||
ErrorKind::UniqueItems { got } => handle_unique_items_error(&base_path, got),
|
||||
ErrorKind::MinProperties { got, want } => handle_min_properties_error(&base_path, *got, *want),
|
||||
ErrorKind::MaxProperties { got, want } => handle_max_properties_error(&base_path, *got, *want),
|
||||
ErrorKind::AdditionalItems { got } => handle_additional_items_error(&base_path, *got),
|
||||
ErrorKind::Format { want, got, err } => handle_format_error(&base_path, want, got, err),
|
||||
ErrorKind::PropertyName { prop } => handle_property_name_error(&base_path, prop),
|
||||
ErrorKind::Contains => handle_contains_error(&base_path),
|
||||
ErrorKind::MinContains { got, want } => handle_min_contains_error(&base_path, got, *want),
|
||||
ErrorKind::MaxContains { got, want } => handle_max_contains_error(&base_path, got, *want),
|
||||
ErrorKind::ContentEncoding { want, err } => handle_content_encoding_error(&base_path, want, err),
|
||||
ErrorKind::ContentMediaType { want, err, .. } => handle_content_media_type_error(&base_path, want, err),
|
||||
ErrorKind::FalseSchema => handle_false_schema_error(&base_path),
|
||||
ErrorKind::Not => handle_not_error(&base_path),
|
||||
ErrorKind::RefCycle { url, kw_loc1, kw_loc2 } => handle_ref_cycle_error(&base_path, url, kw_loc1, kw_loc2),
|
||||
ErrorKind::Reference { kw, url } => handle_reference_error(&base_path, kw, url),
|
||||
ErrorKind::Schema { url } => handle_schema_error(&base_path, url),
|
||||
ErrorKind::ContentSchema => handle_content_schema_error(&base_path),
|
||||
ErrorKind::Group => handle_group_error(&base_path),
|
||||
ErrorKind::AllOf => handle_all_of_error(&base_path),
|
||||
ErrorKind::AnyOf => handle_any_of_error(&base_path),
|
||||
ErrorKind::OneOf(matched) => handle_one_of_error(&base_path, matched),
|
||||
} else {
|
||||
let error = crate::drop::Error {
|
||||
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
|
||||
message: "The JSPG database has not been cached yet. Run jspg_cache_database()".to_string(),
|
||||
details: crate::drop::ErrorDetails {
|
||||
path: "".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
errors_list.extend(errors_to_add);
|
||||
}
|
||||
|
||||
// Handler functions for each error kind
|
||||
fn handle_type_error(base_path: &str, got: &Type, want: &Types) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "TYPE_MISMATCH".to_string(),
|
||||
message: format!("Expected {} but got {}",
|
||||
want.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" or "),
|
||||
got
|
||||
),
|
||||
cause: json!({
|
||||
"got": got.to_string(),
|
||||
"want": want.iter().map(|t| t.to_string()).collect::<Vec<_>>()
|
||||
}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_required_error(base_path: &str, want: &[&str]) -> Vec<Error> {
|
||||
// Create a separate error for each missing required field
|
||||
want.iter().map(|missing_field| {
|
||||
let field_path = if base_path.is_empty() {
|
||||
format!("/{}", missing_field)
|
||||
} else {
|
||||
format!("{}/{}", base_path, missing_field)
|
||||
};
|
||||
|
||||
Error {
|
||||
path: field_path,
|
||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||
message: format!("Required field '{}' is missing", missing_field),
|
||||
cause: json!({ "want": [missing_field] }),
|
||||
let drop = crate::drop::Drop::with_errors(vec![error]);
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn handle_dependency_error(base_path: &str, prop: &str, missing: &[&str], is_dependent_required: bool) -> Vec<Error> {
|
||||
// Create a separate error for each missing field
|
||||
missing.iter().map(|missing_field| {
|
||||
let field_path = if base_path.is_empty() {
|
||||
format!("/{}", missing_field)
|
||||
} else {
|
||||
format!("{}/{}", base_path, missing_field)
|
||||
};
|
||||
|
||||
let (code, message) = if is_dependent_required {
|
||||
(
|
||||
"DEPENDENT_REQUIRED_MISSING".to_string(),
|
||||
format!("Field '{}' is required when '{}' is present", missing_field, prop),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"DEPENDENCY_FAILED".to_string(),
|
||||
format!("Field '{}' is required when '{}' is present", missing_field, prop),
|
||||
)
|
||||
};
|
||||
|
||||
Error {
|
||||
path: field_path,
|
||||
code,
|
||||
message,
|
||||
cause: json!({ "prop": prop, "missing": [missing_field] }),
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn handle_additional_properties_error(base_path: &str, got: &[Cow<str>]) -> Vec<Error> {
|
||||
let mut errors = Vec::new();
|
||||
for extra_prop in got {
|
||||
let field_path = if base_path.is_empty() {
|
||||
format!("/{}", extra_prop)
|
||||
} else {
|
||||
format!("{}/{}", base_path, extra_prop)
|
||||
};
|
||||
errors.push(Error {
|
||||
path: field_path,
|
||||
code: "ADDITIONAL_PROPERTIES_NOT_ALLOWED".to_string(),
|
||||
message: format!("Property '{}' is not allowed", extra_prop),
|
||||
cause: json!({ "got": [extra_prop.to_string()] }),
|
||||
});
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
fn handle_enum_error(base_path: &str, want: &[Value]) -> Vec<Error> {
|
||||
let message = if want.len() == 1 {
|
||||
format!("Value must be {}", serde_json::to_string(&want[0]).unwrap_or_else(|_| "unknown".to_string()))
|
||||
} else {
|
||||
format!("Value must be one of: {}",
|
||||
want.iter()
|
||||
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "unknown".to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "ENUM_VIOLATED".to_string(),
|
||||
message,
|
||||
cause: json!({ "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_const_error(base_path: &str, want: &Value) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "CONST_VIOLATED".to_string(),
|
||||
message: format!("Value must be exactly {}", serde_json::to_string(want).unwrap_or_else(|_| "unknown".to_string())),
|
||||
cause: json!({ "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_min_length_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MIN_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("String length must be at least {} characters, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_max_length_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MAX_LENGTH_VIOLATED".to_string(),
|
||||
message: format!("String length must be at most {} characters, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_pattern_error(base_path: &str, got: &Cow<str>, want: &str) -> Vec<Error> {
|
||||
let display_value = if got.len() > 50 {
|
||||
format!("{}...", &got[..50])
|
||||
} else {
|
||||
got.to_string()
|
||||
};
|
||||
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "PATTERN_VIOLATED".to_string(),
|
||||
message: format!("Value '{}' does not match pattern '{}'", display_value, want),
|
||||
cause: json!({ "got": got.to_string(), "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_minimum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value must be at least {}, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_maximum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value must be at most {}, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_exclusive_minimum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value must be greater than {}, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_exclusive_maximum_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
|
||||
message: format!("Value must be less than {}, but got {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_multiple_of_error(base_path: &str, got: &Cow<Number>, want: &Number) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MULTIPLE_OF_VIOLATED".to_string(),
|
||||
message: format!("{} is not a multiple of {}", got, want),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_min_items_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MIN_ITEMS_VIOLATED".to_string(),
|
||||
message: format!("Array must have at least {} items, but has {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_max_items_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MAX_ITEMS_VIOLATED".to_string(),
|
||||
message: format!("Array must have at most {} items, but has {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_unique_items_error(base_path: &str, got: &[usize; 2]) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "UNIQUE_ITEMS_VIOLATED".to_string(),
|
||||
message: format!("Array items at positions {} and {} are duplicates", got[0], got[1]),
|
||||
cause: json!({ "got": got }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_min_properties_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MIN_PROPERTIES_VIOLATED".to_string(),
|
||||
message: format!("Object must have at least {} properties, but has {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_max_properties_error(base_path: &str, got: usize, want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MAX_PROPERTIES_VIOLATED".to_string(),
|
||||
message: format!("Object must have at most {} properties, but has {}", want, got),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_additional_items_error(base_path: &str, got: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "ADDITIONAL_ITEMS_NOT_ALLOWED".to_string(),
|
||||
message: format!("Last {} array items are not allowed", got),
|
||||
cause: json!({ "got": got }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_format_error(base_path: &str, want: &str, got: &Cow<Value>, err: &Box<dyn std::error::Error>) -> Vec<Error> {
|
||||
// If the value is an empty string, skip format validation.
|
||||
if let Value::String(s) = got.as_ref() {
|
||||
if s.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "FORMAT_INVALID".to_string(),
|
||||
message: format!("Value {} is not a valid {} format",
|
||||
serde_json::to_string(got.as_ref()).unwrap_or_else(|_| "unknown".to_string()),
|
||||
want
|
||||
),
|
||||
cause: json!({ "got": got, "want": want, "err": err.to_string() }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_property_name_error(base_path: &str, prop: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "INVALID_PROPERTY_NAME".to_string(),
|
||||
message: format!("Property name '{}' is invalid", prop),
|
||||
cause: json!({ "prop": prop }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_contains_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "CONTAINS_FAILED".to_string(),
|
||||
message: "No array items match the required schema".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_min_contains_error(base_path: &str, got: &[usize], want: usize) -> Vec<Error> {
|
||||
let message = if got.is_empty() {
|
||||
format!("At least {} array items must match the schema, but none do", want)
|
||||
} else {
|
||||
format!("At least {} array items must match the schema, but only {} do (at positions {})",
|
||||
want,
|
||||
got.len(),
|
||||
got.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
|
||||
)
|
||||
};
|
||||
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MIN_CONTAINS_VIOLATED".to_string(),
|
||||
message,
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_max_contains_error(base_path: &str, got: &[usize], want: usize) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "MAX_CONTAINS_VIOLATED".to_string(),
|
||||
message: format!("At most {} array items can match the schema, but {} do (at positions {})",
|
||||
want,
|
||||
got.len(),
|
||||
got.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
|
||||
),
|
||||
cause: json!({ "got": got, "want": want }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_content_encoding_error(base_path: &str, want: &str, err: &Box<dyn std::error::Error>) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "CONTENT_ENCODING_INVALID".to_string(),
|
||||
message: format!("Content is not valid {} encoding: {}", want, err),
|
||||
cause: json!({ "want": want, "err": err.to_string() }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_content_media_type_error(base_path: &str, want: &str, err: &Box<dyn std::error::Error>) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "CONTENT_MEDIA_TYPE_INVALID".to_string(),
|
||||
message: format!("Content is not valid {} media type: {}", want, err),
|
||||
cause: json!({ "want": want, "err": err.to_string() }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_false_schema_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "FALSE_SCHEMA".to_string(),
|
||||
message: "This schema always fails validation".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_not_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "NOT_VIOLATED".to_string(),
|
||||
message: "Value matches a schema that it should not match".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_ref_cycle_error(base_path: &str, url: &str, kw_loc1: &str, kw_loc2: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "REFERENCE_CYCLE".to_string(),
|
||||
message: format!("Reference cycle detected: both '{}' and '{}' resolve to '{}'", kw_loc1, kw_loc2, url),
|
||||
cause: json!({ "url": url, "kw_loc1": kw_loc1, "kw_loc2": kw_loc2 }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_reference_error(base_path: &str, kw: &str, url: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "REFERENCE_FAILED".to_string(),
|
||||
message: format!("{} reference to '{}' failed validation", kw, url),
|
||||
cause: json!({ "kw": kw, "url": url }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_schema_error(base_path: &str, url: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "SCHEMA_FAILED".to_string(),
|
||||
message: format!("Schema '{}' validation failed", url),
|
||||
cause: json!({ "url": url }),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_content_schema_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "CONTENT_SCHEMA_FAILED".to_string(),
|
||||
message: "Content schema validation failed".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_group_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "VALIDATION_FAILED".to_string(),
|
||||
message: "Validation failed".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_all_of_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "ALL_OF_VIOLATED".to_string(),
|
||||
message: "Value does not match all required schemas".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_any_of_error(base_path: &str) -> Vec<Error> {
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "ANY_OF_VIOLATED".to_string(),
|
||||
message: "Value does not match any of the allowed schemas".to_string(),
|
||||
cause: json!({}),
|
||||
}]
|
||||
}
|
||||
|
||||
fn handle_one_of_error(base_path: &str, matched: &Option<(usize, usize)>) -> Vec<Error> {
|
||||
let (message, cause) = match matched {
|
||||
None => (
|
||||
"Value must match exactly one schema, but matches none".to_string(),
|
||||
json!({ "matched_indices": null })
|
||||
),
|
||||
Some((i, j)) => (
|
||||
format!("Value must match exactly one schema, but matches schemas at positions {} and {}", i, j),
|
||||
json!({ "matched_indices": [i, j] })
|
||||
),
|
||||
};
|
||||
|
||||
vec![Error {
|
||||
path: base_path.to_string(),
|
||||
code: "ONE_OF_VIOLATED".to_string(),
|
||||
message,
|
||||
cause,
|
||||
}]
|
||||
}
|
||||
|
||||
// Formats errors according to DropError structure
|
||||
fn format_errors(errors: Vec<Error>, instance: &Value, schema_id: &str) -> Vec<Value> {
|
||||
let mut unique_errors: HashMap<String, Value> = HashMap::new();
|
||||
for error in errors {
|
||||
let error_path = error.path.clone();
|
||||
if let Entry::Vacant(entry) = unique_errors.entry(error_path.clone()) {
|
||||
let failing_value = extract_value_at_path(instance, &error.path);
|
||||
entry.insert(json!({
|
||||
"code": error.code,
|
||||
"message": error.message,
|
||||
"details": {
|
||||
"path": error.path,
|
||||
"context": failing_value,
|
||||
"cause": error.cause,
|
||||
"schema": schema_id
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
unique_errors.into_values().collect::<Vec<Value>>()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
#[pg_extern(strict, parallel_safe)]
|
||||
fn json_schema_cached(schema_id: &str) -> bool {
|
||||
let cache = SCHEMA_CACHE.read().unwrap();
|
||||
cache.map.contains_key(schema_id)
|
||||
pub fn json_schema_cached(schema_id: &str) -> bool {
|
||||
if let Some(engine) = GLOBAL_JSPG.read().unwrap().as_ref() {
|
||||
match engine
|
||||
.validator
|
||||
.validate(schema_id, &serde_json::Value::Null)
|
||||
{
|
||||
Err(e) if e.code == "SCHEMA_NOT_FOUND" => false,
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[pg_extern(strict)]
|
||||
fn clear_json_schemas() -> JsonB {
|
||||
let mut cache = SCHEMA_CACHE.write().unwrap();
|
||||
*cache = Cache {
|
||||
schemas: Schemas::new(),
|
||||
map: HashMap::new(),
|
||||
};
|
||||
JsonB(json!({ "response": "success" }))
|
||||
pub fn clear_json_schemas() -> JsonB {
|
||||
let mut lock = GLOBAL_JSPG.write().unwrap();
|
||||
*lock = None;
|
||||
let drop = crate::drop::Drop::success();
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
|
||||
#[pg_extern(strict, parallel_safe)]
|
||||
fn show_json_schemas() -> JsonB {
|
||||
let cache = SCHEMA_CACHE.read().unwrap();
|
||||
let ids: Vec<String> = cache.map.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
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn postgresql_conf_options() -> Vec<&'static str> {
|
||||
// return any postgresql.conf settings that are required for your tests
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "pg_test"))]
|
||||
mod helpers {
|
||||
include!("helpers.rs");
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "pg_test"))]
|
||||
mod schemas {
|
||||
include!("schemas.rs");
|
||||
pub fn show_json_schemas() -> JsonB {
|
||||
if let Some(engine) = GLOBAL_JSPG.read().unwrap().as_ref() {
|
||||
let mut keys = engine.validator.get_schema_ids();
|
||||
keys.sort();
|
||||
let drop = crate::drop::Drop::success_with_val(json!(keys));
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
} else {
|
||||
let drop = crate::drop::Drop::success_with_val(json!([]));
|
||||
JsonB(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "pg_test"))]
|
||||
#[pg_schema]
|
||||
mod tests {
|
||||
include!("tests.rs");
|
||||
}
|
||||
use pgrx::prelude::*;
|
||||
include!("tests/fixtures.rs");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod pg_test {
|
||||
pub fn setup(_options: Vec<&str>) {
|
||||
// perform any initialization common to all tests
|
||||
}
|
||||
|
||||
pub fn postgresql_conf_options() -> Vec<&'static str> {
|
||||
// return any postgresql.conf settings that are required for your tests
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
15
src/merger/mod.rs
Normal file
15
src/merger/mod.rs
Normal file
@ -0,0 +1,15 @@
|
||||
pub struct Merger {
|
||||
// To be implemented
|
||||
}
|
||||
|
||||
impl Default for Merger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Merger {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
15
src/queryer/mod.rs
Normal file
15
src/queryer/mod.rs
Normal file
@ -0,0 +1,15 @@
|
||||
pub struct Queryer {
|
||||
// To be implemented
|
||||
}
|
||||
|
||||
impl Default for Queryer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Queryer {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
1128
src/schemas.rs
1128
src/schemas.rs
File diff suppressed because it is too large
Load Diff
1089
src/tests.rs
1089
src/tests.rs
File diff suppressed because it is too large
Load Diff
1668
src/tests/fixtures.rs
Normal file
1668
src/tests/fixtures.rs
Normal file
File diff suppressed because it is too large
Load Diff
82
src/validator/context.rs
Normal file
82
src/validator/context.rs
Normal 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
6
src/validator/error.rs
Normal 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
98
src/validator/instance.rs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/validator/mod.rs
Normal file
73
src/validator/mod.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
pub mod result;
|
||||
pub mod rules;
|
||||
pub mod util;
|
||||
|
||||
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,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
if let Some(schema) = self.db.schemas.get(schema_id) {
|
||||
let ctx = ValidationContext::new(
|
||||
&self.db,
|
||||
schema,
|
||||
schema,
|
||||
instance,
|
||||
HashSet::new(),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
ctx.validate_scoped()
|
||||
} else {
|
||||
Err(ValidationError {
|
||||
code: "SCHEMA_NOT_FOUND".to_string(),
|
||||
message: format!("Schema {} not found", schema_id),
|
||||
path: "".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/validator/result.rs
Normal file
28
src/validator/result.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
135
src/validator/rules/array.rs
Normal file
135
src/validator/rules/array.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
92
src/validator/rules/combinators.rs
Normal file
92
src/validator/rules/combinators.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
67
src/validator/rules/conditionals.rs
Normal file
67
src/validator/rules/conditionals.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
81
src/validator/rules/core.rs
Normal file
81
src/validator/rules/core.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
42
src/validator/rules/format.rs
Normal file
42
src/validator/rules/format.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
91
src/validator/rules/mod.rs
Normal file
91
src/validator/rules/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
61
src/validator/rules/numeric.rs
Normal file
61
src/validator/rules/numeric.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
220
src/validator/rules/object.rs
Normal file
220
src/validator/rules/object.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
155
src/validator/rules/polymorphism.rs
Normal file
155
src/validator/rules/polymorphism.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
52
src/validator/rules/string.rs
Normal file
52
src/validator/rules/string.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
53
src/validator/rules/util.rs
Normal file
53
src/validator/rules/util.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
91
src/validator/util.rs
Normal file
91
src/validator/util.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestSuite {
|
||||
#[allow(dead_code)]
|
||||
description: String,
|
||||
database: serde_json::Value,
|
||||
tests: Vec<TestCase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TestCase {
|
||||
description: String,
|
||||
data: serde_json::Value,
|
||||
valid: bool,
|
||||
// Support explicit schema ID target for test case
|
||||
schema_id: String,
|
||||
}
|
||||
|
||||
// use crate::validator::registry::REGISTRY; // No longer used directly for tests!
|
||||
use crate::validator::Validator;
|
||||
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))
|
||||
}
|
||||
|
||||
pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
|
||||
let content =
|
||||
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read file: {}", path));
|
||||
let suite: Vec<TestSuite> = serde_json::from_str(&content)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse JSON in {}: {}", path, e));
|
||||
|
||||
if index >= suite.len() {
|
||||
panic!("Index {} out of bounds for file {}", index, path);
|
||||
}
|
||||
|
||||
let group = &suite[index];
|
||||
let mut failures = Vec::<String>::new();
|
||||
|
||||
let db_json = group.database.clone();
|
||||
let db = crate::database::Database::new(&db_json);
|
||||
let validator = Validator::new(std::sync::Arc::new(db));
|
||||
|
||||
// 4. Run Tests
|
||||
for test in group.tests.iter() {
|
||||
let schema_id = &test.schema_id;
|
||||
|
||||
if !validator.db.schemas.contains_key(schema_id) {
|
||||
failures.push(format!(
|
||||
"[{}] Missing Schema: Cannot find schema ID '{}'",
|
||||
group.description, schema_id
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = validator.validate(schema_id, &test.data);
|
||||
|
||||
let (got_valid, _errors) = match &result {
|
||||
Ok(res) => (res.is_valid(), &res.errors),
|
||||
Err(_e) => {
|
||||
// If we encounter an execution error (e.g. Schema Not Found),
|
||||
// we treat it as a test failure.
|
||||
(false, &vec![])
|
||||
}
|
||||
};
|
||||
|
||||
if got_valid != test.valid {
|
||||
let error_msg = match &result {
|
||||
Ok(res) => format!("{:?}", res.errors),
|
||||
Err(e) => format!("Execution Error: {:?}", e),
|
||||
};
|
||||
|
||||
failures.push(format!(
|
||||
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {}",
|
||||
group.description, test.description, test.valid, got_valid, error_msg
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
return Err(failures.join("\n"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
62
test_err.log
Normal file
62
test_err.log
Normal file
@ -0,0 +1,62 @@
|
||||
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
|
||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 26.14s
|
||||
Running unittests src/lib.rs (target/debug/deps/jspg-99ace086c3537f5a)
|
||||
|
||||
running 1 test
|
||||
[32m[1m Using[0m[39m [37m[1mPgConfig("pg18")[0m[39m and `pg_config` from [36m/opt/homebrew/opt/postgresql@18/bin/pg_config[39m
|
||||
[32m[1m Building[0m[39m extension with features [36mpg_test pg18[39m
|
||||
[32m[1m Running[0m[39m command [36m"/opt/homebrew/bin/cargo" "build" "--lib" "--features" "pg_test pg18" "--no-default-features" "--message-format=json-render-diagnostics"[39m
|
||||
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.10s
|
||||
[32m[1m Installing[0m[39m extension
|
||||
[32m[1m Copying[0m[39m control file to [36m/opt/homebrew/share/postgresql@18/extension/jspg.control[39m
|
||||
[32m[1m Copying[0m[39m shared library to [36m/opt/homebrew/lib/postgresql@18/jspg.dylib[39m
|
||||
[32m[1m Discovered[0m[39m [36m[1m351[0m[39m SQL entities: [36m[1m1[0m[39m schemas ([36m[1m1[0m[39m unique), [36m[1m350[0m[39m functions, [36m[1m0[0m[39m types, [36m[1m0[0m[39m enums, [36m[1m0[0m[39m sqls, [36m[1m0[0m[39m ords, [36m[1m0[0m[39m hashes, [36m[1m0[0m[39m aggregates, [36m[1m0[0m[39m triggers
|
||||
[32m[1m Rebuilding[0m[39m [36mpgrx_embed[39m, in debug mode, for SQL generation with features [36mpg_test pg18[39m
|
||||
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.63s
|
||||
[32m[1m Writing[0m[39m SQL entities to /opt/homebrew/share/postgresql@18/extension/jspg--0.1.0.sql
|
||||
[32m[1m Finished[0m[39m installing jspg
|
||||
[36m[2026-03-01 22:54:19.068 EST] [82952] [69a509eb.14408]: LOG: starting PostgreSQL 18.1 (Homebrew) on aarch64-apple-darwin25.2.0, compiled by Apple clang version 17.0.0 (clang-1700.6.3.2), 64-bit[39m
|
||||
[36m[2026-03-01 22:54:19.070 EST] [82952] [69a509eb.14408]: LOG: listening on IPv6 address "::1", port 32218[39m
|
||||
[36m[2026-03-01 22:54:19.070 EST] [82952] [69a509eb.14408]: LOG: listening on IPv4 address "127.0.0.1", port 32218[39m
|
||||
[36m[2026-03-01 22:54:19.071 EST] [82952] [69a509eb.14408]: LOG: listening on Unix socket "/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/target/test-pgdata/.s.PGSQL.32218"[39m
|
||||
[36m[2026-03-01 22:54:19.077 EST] [82958] [69a509eb.1440e]: LOG: database system was shut down at 2026-03-01 22:49:02 EST[39m
|
||||
[32m[1m Creating[0m[39m database [36m[1mpgrx_tests[0m[39m
|
||||
|
||||
thread 'tests::pg_test_typed_refs_0' (29092254) panicked at /Users/awgneo/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pgrx-tests-0.16.1/src/framework.rs:166:9:
|
||||
|
||||
|
||||
Postgres Messages:
|
||||
[37m[2m[2026-03-01 22:54:19.068 EST] [82952] [69a509eb.14408]: LOG: starting PostgreSQL 18.1 (Homebrew) on aarch64-apple-darwin25.2.0, compiled by Apple clang version 17.0.0 (clang-1700.6.3.2), 64-bit
|
||||
[2026-03-01 22:54:19.070 EST] [82952] [69a509eb.14408]: LOG: listening on IPv6 address "::1", port 32218
|
||||
[2026-03-01 22:54:19.070 EST] [82952] [69a509eb.14408]: LOG: listening on IPv4 address "127.0.0.1", port 32218
|
||||
[2026-03-01 22:54:19.071 EST] [82952] [69a509eb.14408]: LOG: listening on Unix socket "/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/target/test-pgdata/.s.PGSQL.32218"
|
||||
[2026-03-01 22:54:19.081 EST] [82952] [69a509eb.14408]: LOG: database system is ready to accept connections
|
||||
[0m[39m
|
||||
|
||||
Test Function Messages:
|
||||
[36m[2026-03-01 22:54:20.058 EST] [82982] [69a509ec.14426]: LOG: statement: START TRANSACTION
|
||||
[2026-03-01 22:54:20.058 EST] [82982] [69a509ec.14426]: LOG: statement: SELECT "tests"."test_typed_refs_0"();
|
||||
[2026-03-01 22:54:20.062 EST] [82982] [69a509ec.14426]: ERROR: called `Result::unwrap()` on an `Err` value: "[Entity inheritance and native type discrimination] Test 'Valid person against organization schema (implicit type allowance)' failed. Expected: true, Got: false. Errors: [ValidationError { code: \"CONST_VIOLATED\", message: \"Value does not match const\", path: \"/type\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }]\n[Entity inheritance and native type discrimination] Test 'Valid organization against organization schema' failed. Expected: true, Got: false. Errors: [ValidationError { code: \"CONST_VIOLATED\", message: \"Value does not match const\", path: \"/type\" }]\n[Entity inheritance and native type discrimination] Test 'Invalid entity against organization schema (ancestor not allowed)' failed. Expected: false, Got: true. Errors: []"
|
||||
[2026-03-01 22:54:20.062 EST] [82982] [69a509ec.14426]: STATEMENT: SELECT "tests"."test_typed_refs_0"();
|
||||
[2026-03-01 22:54:20.062 EST] [82982] [69a509ec.14426]: LOG: statement: ROLLBACK
|
||||
[39m
|
||||
|
||||
Client Error:
|
||||
[31m[1mcalled `Result::unwrap()` on an `Err` value: "[Entity inheritance and native type discrimination] Test 'Valid person against organization schema (implicit type allowance)' failed. Expected: true, Got: false. Errors: [ValidationError { code: \"CONST_VIOLATED\", message: \"Value does not match const\", path: \"/type\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }, ValidationError { code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", path: \"/first_name\" }]\n[Entity inheritance and native type discrimination] Test 'Valid organization against organization schema' failed. Expected: true, Got: false. Errors: [ValidationError { code: \"CONST_VIOLATED\", message: \"Value does not match const\", path: \"/type\" }]\n[Entity inheritance and native type discrimination] Test 'Invalid entity against organization schema (ancestor not allowed)' failed. Expected: false, Got: true. Errors: []"[0m[39m
|
||||
postgres location: [37m[2mfixtures.rs[0m[39m
|
||||
rust location: [33m<unknown>[39m
|
||||
|
||||
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
test tests::pg_test_typed_refs_0 ... FAILED
|
||||
|
||||
failures:
|
||||
|
||||
failures:
|
||||
tests::pg_test_typed_refs_0
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 343 filtered out; finished in 21.82s
|
||||
|
||||
error: test failed, to rerun pass `--lib`
|
||||
1669
tests/fixtures.rs
Normal file
1669
tests/fixtures.rs
Normal file
File diff suppressed because it is too large
Load Diff
152
tests/fixtures/additionalProperties.json
vendored
Normal file
152
tests/fixtures/additionalProperties.json
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "additional property matching schema is valid",
|
||||
"data": {
|
||||
"foo": "value",
|
||||
"is_active": true,
|
||||
"hidden": false
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "additional property not matching schema is invalid",
|
||||
"data": {
|
||||
"foo": "value",
|
||||
"is_active": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "additionalProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "additional property not matching schema is invalid despite extensible: true",
|
||||
"data": {
|
||||
"foo": "hello",
|
||||
"count": "five"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "additionalProperties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "invalid array of integers",
|
||||
"data": {
|
||||
"type": "my_type",
|
||||
"group_a": [
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "invalid non-array type",
|
||||
"data": {
|
||||
"type": "my_type",
|
||||
"group_a": "field1"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema3"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
590
tests/fixtures/allOf.json
vendored
Normal file
590
tests/fixtures/allOf.json
vendored
Normal file
@ -0,0 +1,590 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch second",
|
||||
"data": {
|
||||
"foo": "baz"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch first",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong type",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": "quux"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"baz": null
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch first allOf",
|
||||
"data": {
|
||||
"bar": 2,
|
||||
"baz": null
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch second allOf",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch both",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf simple types",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"maximum": 30
|
||||
},
|
||||
{
|
||||
"minimum": 20
|
||||
}
|
||||
],
|
||||
"$id": "allOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": 25,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_2_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch one",
|
||||
"data": 35,
|
||||
"valid": false,
|
||||
"schema_id": "allOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"$id": "allOf_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "allOf_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, some false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"$id": "allOf_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "allOf_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"$id": "allOf_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "allOf_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with one empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with two empty schemas",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the first empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"$id": "allOf_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_8_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "allOf_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the last empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
],
|
||||
"$id": "allOf_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_9_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "allOf_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested allOf, to check validation semantics",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "allOf_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "allOf_10_0"
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "allOf_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_13_0"
|
||||
},
|
||||
{
|
||||
"description": "fails on extra property z explicitly",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"z": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "allOf_15_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property is invalid (root is strict)",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"name": "Me",
|
||||
"extra": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_15_0"
|
||||
},
|
||||
{
|
||||
"description": "partA mismatch is invalid",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "Me"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "allOf_15_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
143
tests/fixtures/booleanSchema.json
vendored
Normal file
143
tests/fixtures/booleanSchema.json
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
[
|
||||
{
|
||||
"description": "boolean schema 'true'",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "booleanSchema_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "string is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean true is valid",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean false is valid",
|
||||
"data": false,
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "object is valid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "array is valid",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "booleanSchema_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "boolean schema 'false'",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": {},
|
||||
"$id": "booleanSchema_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean true is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean false is invalid",
|
||||
"data": false,
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "object is invalid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "array is invalid",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "booleanSchema_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
650
tests/fixtures/const.json
vendored
Normal file
650
tests/fixtures/const.json
vendored
Normal file
@ -0,0 +1,650 @@
|
||||
[
|
||||
{
|
||||
"description": "const validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": 2,
|
||||
"$id": "const_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "same value is valid",
|
||||
"data": 2,
|
||||
"valid": true,
|
||||
"schema_id": "const_0_0"
|
||||
},
|
||||
{
|
||||
"description": "another value is invalid",
|
||||
"data": 5,
|
||||
"valid": false,
|
||||
"schema_id": "const_0_0"
|
||||
},
|
||||
{
|
||||
"description": "another type is invalid",
|
||||
"data": "a",
|
||||
"valid": false,
|
||||
"schema_id": "const_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "const_1_0"
|
||||
},
|
||||
{
|
||||
"description": "same object with different property order is valid",
|
||||
"data": {
|
||||
"baz": "bax",
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "const_1_0"
|
||||
},
|
||||
{
|
||||
"description": "another object is invalid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_1_0"
|
||||
},
|
||||
{
|
||||
"description": "another type is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with array",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": [
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
],
|
||||
"$id": "const_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "same array is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "const_2_0"
|
||||
},
|
||||
{
|
||||
"description": "another array item is invalid",
|
||||
"data": [
|
||||
2
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_2_0"
|
||||
},
|
||||
{
|
||||
"description": "array with additional items is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with null",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": null,
|
||||
"$id": "const_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "const_3_0"
|
||||
},
|
||||
{
|
||||
"description": "not null is invalid",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "const_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with false does not match 0",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": false,
|
||||
"$id": "const_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "false is valid",
|
||||
"data": false,
|
||||
"valid": true,
|
||||
"schema_id": "const_4_0"
|
||||
},
|
||||
{
|
||||
"description": "integer zero is invalid",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "const_4_0"
|
||||
},
|
||||
{
|
||||
"description": "float zero is invalid",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "const_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with true does not match 1",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": true,
|
||||
"$id": "const_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "true is valid",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "const_5_0"
|
||||
},
|
||||
{
|
||||
"description": "integer one is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "const_5_0"
|
||||
},
|
||||
{
|
||||
"description": "float one is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "const_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with [false] does not match [0]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": [
|
||||
false
|
||||
],
|
||||
"$id": "const_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false] is valid",
|
||||
"data": [
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "const_6_0"
|
||||
},
|
||||
{
|
||||
"description": "[0] is invalid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_6_0"
|
||||
},
|
||||
{
|
||||
"description": "[0.0] is invalid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with [true] does not match [1]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": [
|
||||
true
|
||||
],
|
||||
"$id": "const_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[true] is valid",
|
||||
"data": [
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "const_7_0"
|
||||
},
|
||||
{
|
||||
"description": "[1] is invalid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_7_0"
|
||||
},
|
||||
{
|
||||
"description": "[1.0] is invalid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "const_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "const_8_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": 0} is invalid",
|
||||
"data": {
|
||||
"a": 0
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_8_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": 0.0} is invalid",
|
||||
"data": {
|
||||
"a": 0
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "const_9_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": 1} is invalid",
|
||||
"data": {
|
||||
"a": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_9_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": 1.0} is invalid",
|
||||
"data": {
|
||||
"a": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": false,
|
||||
"schema_id": "const_10_0"
|
||||
},
|
||||
{
|
||||
"description": "integer zero is valid",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "const_10_0"
|
||||
},
|
||||
{
|
||||
"description": "float zero is valid",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "const_10_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "const_10_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "const_10_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string is invalid",
|
||||
"data": "",
|
||||
"valid": false,
|
||||
"schema_id": "const_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "const with 1 does not match true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": 1,
|
||||
"$id": "const_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "true is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "const_11_0"
|
||||
},
|
||||
{
|
||||
"description": "integer one is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "const_11_0"
|
||||
},
|
||||
{
|
||||
"description": "float one is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "const_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "const_12_0"
|
||||
},
|
||||
{
|
||||
"description": "integer 2 is invalid",
|
||||
"data": 2,
|
||||
"valid": false,
|
||||
"schema_id": "const_12_0"
|
||||
},
|
||||
{
|
||||
"description": "float -2.0 is valid",
|
||||
"data": -2,
|
||||
"valid": true,
|
||||
"schema_id": "const_12_0"
|
||||
},
|
||||
{
|
||||
"description": "float 2.0 is invalid",
|
||||
"data": 2,
|
||||
"valid": false,
|
||||
"schema_id": "const_12_0"
|
||||
},
|
||||
{
|
||||
"description": "float -2.00001 is invalid",
|
||||
"data": -2.00001,
|
||||
"valid": false,
|
||||
"schema_id": "const_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "const_13_0"
|
||||
},
|
||||
{
|
||||
"description": "integer minus one is invalid",
|
||||
"data": 9007199254740991,
|
||||
"valid": false,
|
||||
"schema_id": "const_13_0"
|
||||
},
|
||||
{
|
||||
"description": "float is valid",
|
||||
"data": 9007199254740992,
|
||||
"valid": true,
|
||||
"schema_id": "const_13_0"
|
||||
},
|
||||
{
|
||||
"description": "float minus one is invalid",
|
||||
"data": 9007199254740991,
|
||||
"valid": false,
|
||||
"schema_id": "const_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nul characters in strings",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"const": "hello\u0000there",
|
||||
"$id": "const_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "match string with nul",
|
||||
"data": "hello\u0000there",
|
||||
"valid": true,
|
||||
"schema_id": "const_14_0"
|
||||
},
|
||||
{
|
||||
"description": "do not match string lacking nul",
|
||||
"data": "hellothere",
|
||||
"valid": false,
|
||||
"schema_id": "const_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "const_15_0"
|
||||
},
|
||||
{
|
||||
"description": "character looks the same but uses a different codepoint",
|
||||
"data": "µ",
|
||||
"comment": "U+00B5",
|
||||
"valid": false,
|
||||
"schema_id": "const_15_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "const_16_0"
|
||||
},
|
||||
{
|
||||
"description": "character looks the same but uses combining marks",
|
||||
"data": "ä",
|
||||
"comment": "a, U+0308",
|
||||
"valid": false,
|
||||
"schema_id": "const_16_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "const_17_0"
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "const_17_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
346
tests/fixtures/contains.json
vendored
Normal file
346
tests/fixtures/contains.json
vendored
Normal file
@ -0,0 +1,346 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "array with item matching schema (6) is valid (items: true)",
|
||||
"data": [
|
||||
3,
|
||||
4,
|
||||
6
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "array with two items matching schema (5, 6) is valid (items: true)",
|
||||
"data": [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "array without items matching schema is invalid",
|
||||
"data": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "contains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "not array is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "contains_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "array with two items 5 is valid (items: true)",
|
||||
"data": [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "array without item 5 is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "contains_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_3_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "contains_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-arrays are valid",
|
||||
"data": "contains does not apply to strings",
|
||||
"valid": true,
|
||||
"schema_id": "contains_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "does not match items, matches contains",
|
||||
"data": [
|
||||
3,
|
||||
6,
|
||||
9
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "matches both items and contains",
|
||||
"data": [
|
||||
6,
|
||||
12
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "matches neither items nor contains",
|
||||
"data": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_5_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "contains_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "contains with null instance elements",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"type": "null"
|
||||
},
|
||||
"$id": "contains_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allows null items",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "contains_8_0"
|
||||
},
|
||||
{
|
||||
"description": "only matching items is valid",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "contains_8_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
178
tests/fixtures/content.json
vendored
Normal file
178
tests/fixtures/content.json
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
[
|
||||
{
|
||||
"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\"}",
|
||||
"valid": true,
|
||||
"schema_id": "content_0_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid JSON document; validates true",
|
||||
"data": "{:}",
|
||||
"valid": true,
|
||||
"schema_id": "content_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 100,
|
||||
"valid": true,
|
||||
"schema_id": "content_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "validation of binary string-encoding",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contentEncoding": "base64",
|
||||
"$id": "content_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "a valid base64 string",
|
||||
"data": "eyJmb28iOiAiYmFyIn0K",
|
||||
"valid": true,
|
||||
"schema_id": "content_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid base64 string (% is not a valid character); validates true",
|
||||
"data": "eyJmb28iOi%iYmFyIn0K",
|
||||
"valid": true,
|
||||
"schema_id": "content_1_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 100,
|
||||
"valid": true,
|
||||
"schema_id": "content_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "content_2_0"
|
||||
},
|
||||
{
|
||||
"description": "a validly-encoded invalid JSON document; validates true",
|
||||
"data": "ezp9Cg==",
|
||||
"valid": true,
|
||||
"schema_id": "content_2_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid base64 string that is valid JSON; validates true",
|
||||
"data": "{}",
|
||||
"valid": true,
|
||||
"schema_id": "content_2_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 100,
|
||||
"valid": true,
|
||||
"schema_id": "content_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "another valid base64-encoded JSON document",
|
||||
"data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid base64-encoded JSON document; validates true",
|
||||
"data": "eyJib28iOiAyMH0=",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an empty object as a base64-encoded JSON document; validates true",
|
||||
"data": "e30=",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an empty array as a base64-encoded JSON document",
|
||||
"data": "W10=",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "a validly-encoded invalid JSON document; validates true",
|
||||
"data": "ezp9Cg==",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid base64 string that is valid JSON; validates true",
|
||||
"data": "{}",
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 100,
|
||||
"valid": true,
|
||||
"schema_id": "content_3_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
619
tests/fixtures/dependencies.json
vendored
Normal file
619
tests/fixtures/dependencies.json
vendored
Normal file
@ -0,0 +1,619 @@
|
||||
[
|
||||
{
|
||||
"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": {},
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "nondependant",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "with dependency",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "missing dependency",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [
|
||||
"bar"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "schema1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": {},
|
||||
"valid": true,
|
||||
"schema_id": "schema2"
|
||||
},
|
||||
{
|
||||
"description": "object with one property",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema2"
|
||||
},
|
||||
{
|
||||
"description": "non-object is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "schema2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": {},
|
||||
"valid": true,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "nondependants",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "with dependencies",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"quux": 3
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "missing dependency",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"quux": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "missing other dependency",
|
||||
"data": {
|
||||
"bar": 1,
|
||||
"quux": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema3"
|
||||
},
|
||||
{
|
||||
"description": "missing both dependencies",
|
||||
"data": {
|
||||
"quux": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema4"
|
||||
},
|
||||
{
|
||||
"description": "quoted quotes",
|
||||
"data": {
|
||||
"foo'bar": 1,
|
||||
"foo\"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema4"
|
||||
},
|
||||
{
|
||||
"description": "CRLF missing dependent",
|
||||
"data": {
|
||||
"foo\nbar": 1,
|
||||
"foo": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema4"
|
||||
},
|
||||
{
|
||||
"description": "quoted quotes missing dependent",
|
||||
"data": {
|
||||
"foo\"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "no dependency",
|
||||
"data": {
|
||||
"foo": "quux"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "wrong type",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "wrong type other",
|
||||
"data": {
|
||||
"foo": 2,
|
||||
"bar": "quux"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "wrong type both",
|
||||
"data": {
|
||||
"foo": "quux",
|
||||
"bar": "quux"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays (invalid in strict mode)",
|
||||
"data": [
|
||||
"bar"
|
||||
],
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "STRICT_ITEM_VIOLATION"
|
||||
}
|
||||
],
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema1"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema3"
|
||||
},
|
||||
{
|
||||
"description": "object with property having schema false is invalid",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema3"
|
||||
},
|
||||
{
|
||||
"description": "object with both properties is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema3"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema4"
|
||||
},
|
||||
{
|
||||
"description": "quoted quote",
|
||||
"data": {
|
||||
"foo'bar": {
|
||||
"foo\"bar": 1
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema4"
|
||||
},
|
||||
{
|
||||
"description": "quoted tab invalid under dependent schema",
|
||||
"data": {
|
||||
"foo\tbar": 1,
|
||||
"a": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema4"
|
||||
},
|
||||
{
|
||||
"description": "quoted quote invalid under dependent schema",
|
||||
"data": {
|
||||
"foo'bar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema5"
|
||||
},
|
||||
{
|
||||
"description": "matches dependency (invalid in strict mode - bar not allowed if foo missing)",
|
||||
"data": {
|
||||
"bar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "STRICT_PROPERTY_VIOLATION"
|
||||
}
|
||||
],
|
||||
"schema_id": "schema_schema5"
|
||||
},
|
||||
{
|
||||
"description": "matches both",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "schema_schema5"
|
||||
},
|
||||
{
|
||||
"description": "no dependency",
|
||||
"data": {
|
||||
"baz": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "schema_schema6"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
133
tests/fixtures/emptyString.json
vendored
Normal file
133
tests/fixtures/emptyString.json
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
[
|
||||
{
|
||||
"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": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for array",
|
||||
"data": {
|
||||
"arr": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for string",
|
||||
"data": {
|
||||
"str": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for integer",
|
||||
"data": {
|
||||
"int": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for number",
|
||||
"data": {
|
||||
"num": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for boolean",
|
||||
"data": {
|
||||
"bool": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for null",
|
||||
"data": {
|
||||
"nul": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string valid for format",
|
||||
"data": {
|
||||
"fmt": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string INVALID for const (unless const is empty string)",
|
||||
"data": {
|
||||
"con": ""
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "CONST_VIOLATED",
|
||||
"path": "/con"
|
||||
}
|
||||
],
|
||||
"schema_id": "emptyString_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty string VALID for const if const IS empty string",
|
||||
"data": {
|
||||
"con_empty": ""
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "emptyString_0_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
595
tests/fixtures/enum.json
vendored
Normal file
595
tests/fixtures/enum.json
vendored
Normal file
@ -0,0 +1,595 @@
|
||||
[
|
||||
{
|
||||
"description": "simple enum validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"$id": "enum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "one of the enum is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "enum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "something else is invalid",
|
||||
"data": 4,
|
||||
"valid": false,
|
||||
"schema_id": "enum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": [],
|
||||
"valid": true,
|
||||
"schema_id": "enum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "something else is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "enum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "objects are deep compared",
|
||||
"data": {
|
||||
"foo": false
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "valid object matches",
|
||||
"data": {
|
||||
"foo": 12
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "enum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "extra properties in object is invalid",
|
||||
"data": {
|
||||
"foo": 12,
|
||||
"boo": 42
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "heterogeneous enum-with-null validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
6,
|
||||
null
|
||||
],
|
||||
"$id": "enum_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "enum_2_0"
|
||||
},
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 6,
|
||||
"valid": true,
|
||||
"schema_id": "enum_2_0"
|
||||
},
|
||||
{
|
||||
"description": "something else is invalid",
|
||||
"data": "test",
|
||||
"valid": false,
|
||||
"schema_id": "enum_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "enum_3_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong foo value",
|
||||
"data": {
|
||||
"foo": "foot",
|
||||
"bar": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_3_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong bar value",
|
||||
"data": {
|
||||
"foo": "foo",
|
||||
"bar": "bart"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_3_0"
|
||||
},
|
||||
{
|
||||
"description": "missing optional property is valid",
|
||||
"data": {
|
||||
"bar": "bar"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "enum_3_0"
|
||||
},
|
||||
{
|
||||
"description": "missing required property is invalid",
|
||||
"data": {
|
||||
"foo": "foo"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_3_0"
|
||||
},
|
||||
{
|
||||
"description": "missing all properties is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "enum_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "enum_4_0"
|
||||
},
|
||||
{
|
||||
"description": "member 2 is valid",
|
||||
"data": "foo\rbar",
|
||||
"valid": true,
|
||||
"schema_id": "enum_4_0"
|
||||
},
|
||||
{
|
||||
"description": "another string is invalid",
|
||||
"data": "abc",
|
||||
"valid": false,
|
||||
"schema_id": "enum_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with false does not match 0",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
false
|
||||
],
|
||||
"$id": "enum_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "false is valid",
|
||||
"data": false,
|
||||
"valid": true,
|
||||
"schema_id": "enum_5_0"
|
||||
},
|
||||
{
|
||||
"description": "integer zero is invalid",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "enum_5_0"
|
||||
},
|
||||
{
|
||||
"description": "float zero is invalid",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "enum_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with [false] does not match [0]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
[
|
||||
false
|
||||
]
|
||||
],
|
||||
"$id": "enum_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false] is valid",
|
||||
"data": [
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_6_0"
|
||||
},
|
||||
{
|
||||
"description": "[0] is invalid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_6_0"
|
||||
},
|
||||
{
|
||||
"description": "[0.0] is invalid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with true does not match 1",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
true
|
||||
],
|
||||
"$id": "enum_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "true is valid",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "enum_7_0"
|
||||
},
|
||||
{
|
||||
"description": "integer one is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "enum_7_0"
|
||||
},
|
||||
{
|
||||
"description": "float one is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "enum_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with [true] does not match [1]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
[
|
||||
true
|
||||
]
|
||||
],
|
||||
"$id": "enum_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[true] is valid",
|
||||
"data": [
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_8_0"
|
||||
},
|
||||
{
|
||||
"description": "[1] is invalid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_8_0"
|
||||
},
|
||||
{
|
||||
"description": "[1.0] is invalid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with 0 does not match false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
0
|
||||
],
|
||||
"$id": "enum_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "false is invalid",
|
||||
"data": false,
|
||||
"valid": false,
|
||||
"schema_id": "enum_9_0"
|
||||
},
|
||||
{
|
||||
"description": "integer zero is valid",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "enum_9_0"
|
||||
},
|
||||
{
|
||||
"description": "float zero is valid",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "enum_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with [0] does not match [false]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
[
|
||||
0
|
||||
]
|
||||
],
|
||||
"$id": "enum_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false] is invalid",
|
||||
"data": [
|
||||
false
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_10_0"
|
||||
},
|
||||
{
|
||||
"description": "[0] is valid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_10_0"
|
||||
},
|
||||
{
|
||||
"description": "[0.0] is valid",
|
||||
"data": [
|
||||
0
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with 1 does not match true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
1
|
||||
],
|
||||
"$id": "enum_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "true is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "enum_11_0"
|
||||
},
|
||||
{
|
||||
"description": "integer one is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "enum_11_0"
|
||||
},
|
||||
{
|
||||
"description": "float one is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "enum_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "enum with [1] does not match [true]",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
[
|
||||
1
|
||||
]
|
||||
],
|
||||
"$id": "enum_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[true] is invalid",
|
||||
"data": [
|
||||
true
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "enum_12_0"
|
||||
},
|
||||
{
|
||||
"description": "[1] is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_12_0"
|
||||
},
|
||||
{
|
||||
"description": "[1.0] is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "enum_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nul characters in strings",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"enum": [
|
||||
"hello\u0000there"
|
||||
],
|
||||
"$id": "enum_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "match string with nul",
|
||||
"data": "hello\u0000there",
|
||||
"valid": true,
|
||||
"schema_id": "enum_13_0"
|
||||
},
|
||||
{
|
||||
"description": "do not match string lacking nul",
|
||||
"data": "hellothere",
|
||||
"valid": false,
|
||||
"schema_id": "enum_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "enum_14_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property ignored during strict check, enum match succeeds",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "enum_14_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
39
tests/fixtures/exclusiveMaximum.json
vendored
Normal file
39
tests/fixtures/exclusiveMaximum.json
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
[
|
||||
{
|
||||
"description": "exclusiveMaximum validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"exclusiveMaximum": 3,
|
||||
"$id": "exclusiveMaximum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "below the exclusiveMaximum is valid",
|
||||
"data": 2.2,
|
||||
"valid": true,
|
||||
"schema_id": "exclusiveMaximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point is invalid",
|
||||
"data": 3,
|
||||
"valid": false,
|
||||
"schema_id": "exclusiveMaximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "above the exclusiveMaximum is invalid",
|
||||
"data": 3.5,
|
||||
"valid": false,
|
||||
"schema_id": "exclusiveMaximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "x",
|
||||
"valid": true,
|
||||
"schema_id": "exclusiveMaximum_0_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
39
tests/fixtures/exclusiveMinimum.json
vendored
Normal file
39
tests/fixtures/exclusiveMinimum.json
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
[
|
||||
{
|
||||
"description": "exclusiveMinimum validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"exclusiveMinimum": 1.1,
|
||||
"$id": "exclusiveMinimum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "above the exclusiveMinimum is valid",
|
||||
"data": 1.2,
|
||||
"valid": true,
|
||||
"schema_id": "exclusiveMinimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point is invalid",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "exclusiveMinimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "below the exclusiveMinimum is invalid",
|
||||
"data": 0.6,
|
||||
"valid": false,
|
||||
"schema_id": "exclusiveMinimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "x",
|
||||
"valid": true,
|
||||
"schema_id": "exclusiveMinimum_0_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
199
tests/fixtures/families.json
vendored
Normal file
199
tests/fixtures/families.json
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Family matches descendant person",
|
||||
"schema_id": "get_entities.response",
|
||||
"data": {
|
||||
"id": "2",
|
||||
"type": "person",
|
||||
"name": "ACME",
|
||||
"first_name": "John"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Graph family matches light.entity",
|
||||
"schema_id": "get_light_entities.response",
|
||||
"data": {
|
||||
"id": "3",
|
||||
"type": "entity"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Graph family matches light.person (because it $refs light.entity)",
|
||||
"schema_id": "get_light_entities.response",
|
||||
"data": {
|
||||
"id": "4",
|
||||
"type": "person"
|
||||
},
|
||||
"valid": 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"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_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"
|
||||
},
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
3770
tests/fixtures/format.json
vendored
Normal file
3770
tests/fixtures/format.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
495
tests/fixtures/if-then-else.json
vendored
Normal file
495
tests/fixtures/if-then-else.json
vendored
Normal file
@ -0,0 +1,495 @@
|
||||
[
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_0_0"
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone if",
|
||||
"data": "hello",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_1_0"
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone then",
|
||||
"data": "hello",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_2_0"
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone else",
|
||||
"data": "hello",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_3_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_3_0"
|
||||
},
|
||||
{
|
||||
"description": "valid when if test fails",
|
||||
"data": 3,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_4_0"
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_4_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_5_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_5_0"
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_5_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_6_0"
|
||||
},
|
||||
{
|
||||
"description": "valid, but would have been invalid through else",
|
||||
"data": 3,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_7_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean schema true in if always chooses the then path (invalid)",
|
||||
"data": "else",
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_8_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean schema false in if always chooses the else path (valid)",
|
||||
"data": "else",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_9_0"
|
||||
},
|
||||
{
|
||||
"description": "other redirects to else and passes",
|
||||
"data": "other",
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_9_0"
|
||||
},
|
||||
{
|
||||
"description": "no redirects to then and fails",
|
||||
"data": "no",
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_9_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid redirects to else and fails",
|
||||
"data": "invalid",
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_10_0"
|
||||
},
|
||||
{
|
||||
"description": "does not match if → then ignored → valid",
|
||||
"data": 2,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_11_0"
|
||||
},
|
||||
{
|
||||
"description": "does not match if → else executes → invalid",
|
||||
"data": 2,
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "if-then-else_13_0"
|
||||
},
|
||||
{
|
||||
"description": "fails on extra property z explicitly",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"z": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "if-then-else_13_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
843
tests/fixtures/items.json
vendored
Normal file
843
tests/fixtures/items.json
vendored
Normal file
@ -0,0 +1,843 @@
|
||||
[
|
||||
{
|
||||
"description": "a schema given for items",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"$id": "items_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid items",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_0_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong type of items",
|
||||
"data": [
|
||||
1,
|
||||
"x"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-arrays are invalid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "items_0_0"
|
||||
},
|
||||
{
|
||||
"description": "JavaScript pseudo-arrays are invalid",
|
||||
"data": {
|
||||
"0": "invalid",
|
||||
"length": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "items_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "items with boolean schema (true)",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"items": true,
|
||||
"$id": "items_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any array is valid",
|
||||
"data": [
|
||||
1,
|
||||
"foo",
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_1_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_2_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
},
|
||||
{
|
||||
"description": "too many items",
|
||||
"data": [
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
},
|
||||
{
|
||||
"description": "too many sub-items",
|
||||
"data": [
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong item",
|
||||
"data": [
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong sub-item",
|
||||
"data": [
|
||||
[
|
||||
{},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
},
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
},
|
||||
{
|
||||
"description": "fewer items is invalid",
|
||||
"data": [
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"foo": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_4_0"
|
||||
},
|
||||
{
|
||||
"description": "nested array with invalid type",
|
||||
"data": [
|
||||
[
|
||||
[
|
||||
[
|
||||
"1"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
2
|
||||
],
|
||||
[
|
||||
3
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[
|
||||
4
|
||||
],
|
||||
[
|
||||
5
|
||||
],
|
||||
[
|
||||
6
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_4_0"
|
||||
},
|
||||
{
|
||||
"description": "not deep enough",
|
||||
"data": [
|
||||
[
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
2
|
||||
],
|
||||
[
|
||||
3
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
4
|
||||
],
|
||||
[
|
||||
5
|
||||
],
|
||||
[
|
||||
6
|
||||
]
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "prefixItems with no additional items allowed",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"items": false,
|
||||
"$id": "items_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty array",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_5_0"
|
||||
},
|
||||
{
|
||||
"description": "fewer number of items present (1)",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_5_0"
|
||||
},
|
||||
{
|
||||
"description": "fewer number of items present (2)",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_5_0"
|
||||
},
|
||||
{
|
||||
"description": "equal number of items present",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_5_0"
|
||||
},
|
||||
{
|
||||
"description": "additional items are not permitted",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_6_0"
|
||||
},
|
||||
{
|
||||
"description": "prefixItems in allOf does not constrain items, valid case",
|
||||
"data": [
|
||||
5,
|
||||
5
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_7_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong type of second item",
|
||||
"data": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "items with heterogeneous array",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{}
|
||||
],
|
||||
"items": false,
|
||||
"$id": "items_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "heterogeneous invalid instance",
|
||||
"data": [
|
||||
"foo",
|
||||
"bar",
|
||||
37
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_8_0"
|
||||
},
|
||||
{
|
||||
"description": "valid instance",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "items with null instance elements",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"items": {
|
||||
"type": "null"
|
||||
},
|
||||
"$id": "items_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allows null elements",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_11_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid item (less than min) is invalid even with extensible: true",
|
||||
"data": [
|
||||
4
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "array: simple extensible array",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "array",
|
||||
"extensible": true,
|
||||
"$id": "items_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_12_0"
|
||||
},
|
||||
{
|
||||
"description": "array with items is valid (extensible)",
|
||||
"data": [
|
||||
1,
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_12_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "array: strict array",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "array",
|
||||
"extensible": false,
|
||||
"$id": "items_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_13_0"
|
||||
},
|
||||
{
|
||||
"description": "array with items is invalid (strict)",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_13_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "array: items extensible",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"extensible": true
|
||||
},
|
||||
"$id": "items_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "items_14_0"
|
||||
},
|
||||
{
|
||||
"description": "array with items is valid (items explicitly allowed to be anything extensible)",
|
||||
"data": [
|
||||
1,
|
||||
"foo",
|
||||
{}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_14_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_15_0"
|
||||
},
|
||||
{
|
||||
"description": "array with strict object items is valid",
|
||||
"data": [
|
||||
{}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "items_15_0"
|
||||
},
|
||||
{
|
||||
"description": "array with invalid strict object items (extra property)",
|
||||
"data": [
|
||||
{
|
||||
"extra": 1
|
||||
}
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "items_15_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
196
tests/fixtures/maxContains.json
vendored
Normal file
196
tests/fixtures/maxContains.json
vendored
Normal file
@ -0,0 +1,196 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "two items still valid against lone maxContains",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxContains with contains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"maxContains": 1,
|
||||
"extensible": true,
|
||||
"$id": "maxContains_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, valid maxContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, invalid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "some elements match, valid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "some elements match, invalid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "too many elements match, invalid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minContains < maxContains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"minContains": 1,
|
||||
"maxContains": 3,
|
||||
"extensible": true,
|
||||
"$id": "maxContains_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "actual < minContains < maxContains",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_3_0"
|
||||
},
|
||||
{
|
||||
"description": "minContains < actual < maxContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_3_0"
|
||||
},
|
||||
{
|
||||
"description": "minContains < maxContains < actual",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxContains_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxContains_4_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
105
tests/fixtures/maxItems.json
vendored
Normal file
105
tests/fixtures/maxItems.json
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
[
|
||||
{
|
||||
"description": "maxItems validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxItems": 2,
|
||||
"extensible": true,
|
||||
"$id": "maxItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-arrays",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "maxItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxItems validation with a decimal",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxItems": 2,
|
||||
"extensible": true,
|
||||
"$id": "maxItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "maxItems_2_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
70
tests/fixtures/maxLength.json
vendored
Normal file
70
tests/fixtures/maxLength.json
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
[
|
||||
{
|
||||
"description": "maxLength validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxLength": 2,
|
||||
"$id": "maxLength_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": "f",
|
||||
"valid": true,
|
||||
"schema_id": "maxLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": "fo",
|
||||
"valid": true,
|
||||
"schema_id": "maxLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "maxLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 100,
|
||||
"valid": true,
|
||||
"schema_id": "maxLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "two graphemes is long enough",
|
||||
"data": "💩💩",
|
||||
"valid": true,
|
||||
"schema_id": "maxLength_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxLength validation with a decimal",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxLength": 2,
|
||||
"$id": "maxLength_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": "f",
|
||||
"valid": true,
|
||||
"schema_id": "maxLength_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "maxLength_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
157
tests/fixtures/maxProperties.json
vendored
Normal file
157
tests/fixtures/maxProperties.json
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
[
|
||||
{
|
||||
"description": "maxProperties validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxProperties": 2,
|
||||
"extensible": true,
|
||||
"$id": "maxProperties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxProperties validation with a decimal",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maxProperties": 2,
|
||||
"extensible": true,
|
||||
"$id": "maxProperties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "shorter is valid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too long is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "maxProperties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": {},
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_2_0"
|
||||
},
|
||||
{
|
||||
"description": "one property is invalid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "maxProperties_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "maxProperties_3_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property is valid if below maxProperties",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "maxProperties_3_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
76
tests/fixtures/maximum.json
vendored
Normal file
76
tests/fixtures/maximum.json
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"description": "maximum validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maximum": 3,
|
||||
"$id": "maximum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "below the maximum is valid",
|
||||
"data": 2.6,
|
||||
"valid": true,
|
||||
"schema_id": "maximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point is valid",
|
||||
"data": 3,
|
||||
"valid": true,
|
||||
"schema_id": "maximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "above the maximum is invalid",
|
||||
"data": 3.5,
|
||||
"valid": false,
|
||||
"schema_id": "maximum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "x",
|
||||
"valid": true,
|
||||
"schema_id": "maximum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maximum validation with unsigned integer",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"maximum": 300,
|
||||
"$id": "maximum_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "below the maximum is invalid",
|
||||
"data": 299.97,
|
||||
"valid": true,
|
||||
"schema_id": "maximum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point integer is valid",
|
||||
"data": 300,
|
||||
"valid": true,
|
||||
"schema_id": "maximum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point float is valid",
|
||||
"data": 300,
|
||||
"valid": true,
|
||||
"schema_id": "maximum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "above the maximum is invalid",
|
||||
"data": 300.5,
|
||||
"valid": false,
|
||||
"schema_id": "maximum_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
247
tests/fixtures/merge.json
vendored
Normal file
247
tests/fixtures/merge.json
vendored
Normal file
@ -0,0 +1,247 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "merge_0_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid when base property has wrong type",
|
||||
"data": {
|
||||
"base_prop": 1,
|
||||
"child_prop": "b"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "TYPE_MISMATCH",
|
||||
"path": "/base_prop"
|
||||
}
|
||||
],
|
||||
"schema_id": "merge_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "merge_1_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid when base required missing",
|
||||
"data": {
|
||||
"b": "ok"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "/a"
|
||||
}
|
||||
],
|
||||
"schema_id": "merge_1_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid when child required missing",
|
||||
"data": {
|
||||
"a": "ok"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "/b"
|
||||
}
|
||||
],
|
||||
"schema_id": "merge_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "merge_2_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid missing base dep",
|
||||
"data": {
|
||||
"trigger": "go",
|
||||
"child_dep": "ok"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "DEPENDENCY_FAILED",
|
||||
"path": "/base_dep"
|
||||
}
|
||||
],
|
||||
"schema_id": "merge_2_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid missing child dep",
|
||||
"data": {
|
||||
"trigger": "go",
|
||||
"base_dep": "ok"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_errors": [
|
||||
{
|
||||
"code": "DEPENDENCY_FAILED",
|
||||
"path": "/child_dep"
|
||||
}
|
||||
],
|
||||
"schema_id": "merge_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"comment": "Verifies validator handles the unmerged metadata correctly (ignores it or handles replacement)",
|
||||
"schema_id": "merge_3_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
390
tests/fixtures/minContains.json
vendored
Normal file
390
tests/fixtures/minContains.json
vendored
Normal file
@ -0,0 +1,390 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_0_0"
|
||||
},
|
||||
{
|
||||
"description": "zero items still valid against lone minContains",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minContains=1 with contains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"minContains": 1,
|
||||
"extensible": true,
|
||||
"$id": "minContains_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "no elements match",
|
||||
"data": [
|
||||
2
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "single element matches, valid minContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "some elements match, valid minContains",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_1_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, valid minContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minContains=2 with contains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"minContains": 2,
|
||||
"extensible": true,
|
||||
"$id": "minContains_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, invalid minContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "some elements match, invalid minContains",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, valid minContains (exactly as needed)",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, valid minContains (more than needed)",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_2_0"
|
||||
},
|
||||
{
|
||||
"description": "some elements match, valid minContains",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_3_0"
|
||||
},
|
||||
{
|
||||
"description": "both elements match, valid minContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxContains = minContains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"maxContains": 2,
|
||||
"minContains": 2,
|
||||
"extensible": true,
|
||||
"$id": "minContains_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, invalid minContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, invalid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_4_0"
|
||||
},
|
||||
{
|
||||
"description": "all elements match, valid maxContains and minContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "maxContains < minContains",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"maxContains": 1,
|
||||
"minContains": 3,
|
||||
"extensible": true,
|
||||
"$id": "minContains_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_5_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid minContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_5_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid maxContains",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_5_0"
|
||||
},
|
||||
{
|
||||
"description": "invalid maxContains and minContains",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minContains = 0",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"contains": {
|
||||
"const": 1
|
||||
},
|
||||
"minContains": 0,
|
||||
"extensible": true,
|
||||
"$id": "minContains_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "empty data",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_6_0"
|
||||
},
|
||||
{
|
||||
"description": "minContains = 0 makes contains always pass",
|
||||
"data": [
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": [],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_7_0"
|
||||
},
|
||||
{
|
||||
"description": "not more than maxContains",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_7_0"
|
||||
},
|
||||
{
|
||||
"description": "too many",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "minContains_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minContains_8_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
96
tests/fixtures/minItems.json
vendored
Normal file
96
tests/fixtures/minItems.json
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
[
|
||||
{
|
||||
"description": "minItems validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minItems": 1,
|
||||
"extensible": true,
|
||||
"$id": "minItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "longer is valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-arrays",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "minItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minItems validation with a decimal",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minItems": 1,
|
||||
"extensible": true,
|
||||
"$id": "minItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "longer is valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "minItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "minItems_2_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
70
tests/fixtures/minLength.json
vendored
Normal file
70
tests/fixtures/minLength.json
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
[
|
||||
{
|
||||
"description": "minLength validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minLength": 2,
|
||||
"$id": "minLength_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "longer is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "minLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": "fo",
|
||||
"valid": true,
|
||||
"schema_id": "minLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": "f",
|
||||
"valid": false,
|
||||
"schema_id": "minLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-strings",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "minLength_0_0"
|
||||
},
|
||||
{
|
||||
"description": "one grapheme is not long enough",
|
||||
"data": "💩",
|
||||
"valid": false,
|
||||
"schema_id": "minLength_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minLength validation with a decimal",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minLength": 2,
|
||||
"$id": "minLength_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "longer is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "minLength_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": "f",
|
||||
"valid": false,
|
||||
"schema_id": "minLength_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
108
tests/fixtures/minProperties.json
vendored
Normal file
108
tests/fixtures/minProperties.json
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
[
|
||||
{
|
||||
"description": "minProperties validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minProperties": 1,
|
||||
"extensible": true,
|
||||
"$id": "minProperties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "longer is valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "exact length is valid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "minProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "too short is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "minProperties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "minProperties_2_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
94
tests/fixtures/minimum.json
vendored
Normal file
94
tests/fixtures/minimum.json
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"description": "minimum validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minimum": 1.1,
|
||||
"$id": "minimum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "above the minimum is valid",
|
||||
"data": 2.6,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point is valid",
|
||||
"data": 1.1,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "below the minimum is invalid",
|
||||
"data": 0.6,
|
||||
"valid": false,
|
||||
"schema_id": "minimum_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "x",
|
||||
"valid": true,
|
||||
"schema_id": "minimum_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "minimum validation with signed integer",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"minimum": -2,
|
||||
"$id": "minimum_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "negative above the minimum is valid",
|
||||
"data": -1,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "positive above the minimum is valid",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point is valid",
|
||||
"data": -2,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "boundary point with float is valid",
|
||||
"data": -2,
|
||||
"valid": true,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "float below the minimum is invalid",
|
||||
"data": -2.0001,
|
||||
"valid": false,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "int below the minimum is invalid",
|
||||
"data": -3,
|
||||
"valid": false,
|
||||
"schema_id": "minimum_1_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "x",
|
||||
"valid": true,
|
||||
"schema_id": "minimum_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
109
tests/fixtures/multipleOf.json
vendored
Normal file
109
tests/fixtures/multipleOf.json
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
[
|
||||
{
|
||||
"description": "by int",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"multipleOf": 2,
|
||||
"$id": "multipleOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "int by int",
|
||||
"data": 10,
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "int by int fail",
|
||||
"data": 7,
|
||||
"valid": false,
|
||||
"schema_id": "multipleOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores non-numbers",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "by number",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"multipleOf": 1.5,
|
||||
"$id": "multipleOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "zero is multiple of anything",
|
||||
"data": 0,
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "4.5 is multiple of 1.5",
|
||||
"data": 4.5,
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "35 is not multiple of 1.5",
|
||||
"data": 35,
|
||||
"valid": false,
|
||||
"schema_id": "multipleOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_2_0"
|
||||
},
|
||||
{
|
||||
"description": "0.00751 is not multiple of 0.0001",
|
||||
"data": 0.00751,
|
||||
"valid": false,
|
||||
"schema_id": "multipleOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "multipleOf_3_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
489
tests/fixtures/not.json
vendored
Normal file
489
tests/fixtures/not.json
vendored
Normal file
@ -0,0 +1,489 @@
|
||||
[
|
||||
{
|
||||
"description": "not",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": {
|
||||
"type": "integer"
|
||||
},
|
||||
"$id": "not_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allowed",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "not_0_0"
|
||||
},
|
||||
{
|
||||
"description": "disallowed",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "not_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "not multiple types",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": {
|
||||
"type": [
|
||||
"integer",
|
||||
"boolean"
|
||||
]
|
||||
},
|
||||
"$id": "not_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "not_1_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "not_1_0"
|
||||
},
|
||||
{
|
||||
"description": "other mismatch",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "not_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "not_2_0"
|
||||
},
|
||||
{
|
||||
"description": "other match",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "not_2_0"
|
||||
},
|
||||
{
|
||||
"description": "mismatch",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbidden property",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {
|
||||
"not": {}
|
||||
}
|
||||
},
|
||||
"$id": "not_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "property present",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_3_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "not_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbid everything with empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": {},
|
||||
"$id": "not_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean true is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean false is invalid",
|
||||
"data": false,
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "object is invalid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "array is invalid",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "not_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbid everything with boolean schema true",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": true,
|
||||
"$id": "not_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean true is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean false is invalid",
|
||||
"data": false,
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "object is invalid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "array is invalid",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "not_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allow everything with boolean schema false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": false,
|
||||
"extensible": true,
|
||||
"$id": "not_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "string is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean true is valid",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "boolean false is valid",
|
||||
"data": false,
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "object is valid",
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "array is valid",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "not_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "double negation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"not": {
|
||||
"not": {}
|
||||
},
|
||||
"$id": "not_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "not_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "not_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "not_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "not_11_0"
|
||||
},
|
||||
{
|
||||
"description": "defined property allowed",
|
||||
"data": {
|
||||
"bar": "baz"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "not_11_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
568
tests/fixtures/oneOf.json
vendored
Normal file
568
tests/fixtures/oneOf.json
vendored
Normal file
@ -0,0 +1,568 @@
|
||||
[
|
||||
{
|
||||
"description": "oneOf",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"minimum": 2
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": 2.5,
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": 3,
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_0_0"
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": 1.5,
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "one oneOf valid",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_1_0"
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_6_0"
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": "baz"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_6_0"
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": "baz",
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_6_0"
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid (complex)",
|
||||
"data": {
|
||||
"foo": 2,
|
||||
"bar": "quux"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with empty schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
],
|
||||
"$id": "oneOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "one valid - valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_7_0"
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_8_0"
|
||||
},
|
||||
{
|
||||
"description": "first valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_8_0"
|
||||
},
|
||||
{
|
||||
"description": "second valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_8_0"
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_8_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property invalid (strict)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"extra": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_9_0"
|
||||
},
|
||||
{
|
||||
"description": "first valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_9_0"
|
||||
},
|
||||
{
|
||||
"description": "second valid - valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_9_0"
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"baz": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_9_0"
|
||||
},
|
||||
{
|
||||
"description": "extra properties are valid (extensible)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2,
|
||||
"extra": "value"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_10_0"
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": {
|
||||
"foo": "foo"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_10_0"
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": {
|
||||
"foo": "foo",
|
||||
"bar": 8
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_10_0"
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": {
|
||||
"baz": "quux"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested oneOf, to check validation semantics",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$id": "oneOf_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_11_0"
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "oneOf_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "oneOf_12_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
82
tests/fixtures/pattern.json
vendored
Normal file
82
tests/fixtures/pattern.json
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"description": "pattern validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"pattern": "^a*$",
|
||||
"$id": "pattern_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "a matching pattern is valid",
|
||||
"data": "aaa",
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a non-matching pattern is invalid",
|
||||
"data": "abc",
|
||||
"valid": false,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores booleans",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores integers",
|
||||
"data": 123,
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores floats",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores objects",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores null",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "pattern_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "pattern is not anchored",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"pattern": "a+",
|
||||
"$id": "pattern_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "matches a substring",
|
||||
"data": "xxaayy",
|
||||
"valid": true,
|
||||
"schema_id": "pattern_1_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
321
tests/fixtures/patternProperties.json
vendored
Normal file
321
tests/fixtures/patternProperties.json
vendored
Normal file
@ -0,0 +1,321 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "multiple valid matches is valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"foooooo": 2
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a single invalid match is invalid",
|
||||
"data": {
|
||||
"foo": "bar",
|
||||
"fooooo": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "multiple invalid matches is invalid",
|
||||
"data": {
|
||||
"foo": "bar",
|
||||
"foooooo": "baz"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property not matching pattern is INVALID (strict by default)",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"extra": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a simultaneous match is valid",
|
||||
"data": {
|
||||
"aaaa": 18
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "multiple matches is valid",
|
||||
"data": {
|
||||
"a": 21,
|
||||
"aaaa": 18
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid due to one is invalid",
|
||||
"data": {
|
||||
"a": "bar"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid due to the other is invalid",
|
||||
"data": {
|
||||
"aaaa": 31
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an invalid due to both is invalid",
|
||||
"data": {
|
||||
"aaa": "foo",
|
||||
"aaaa": 31
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_2_0"
|
||||
},
|
||||
{
|
||||
"description": "recognized members are accounted for",
|
||||
"data": {
|
||||
"a31b": null
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_2_0"
|
||||
},
|
||||
{
|
||||
"description": "regexes are case sensitive",
|
||||
"data": {
|
||||
"a_x_3": 3
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_2_0"
|
||||
},
|
||||
{
|
||||
"description": "regexes are case sensitive, 2",
|
||||
"data": {
|
||||
"a_X_3": 3
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_3_0"
|
||||
},
|
||||
{
|
||||
"description": "object with property matching schema false is invalid",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_3_0"
|
||||
},
|
||||
{
|
||||
"description": "object with both properties is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_3_0"
|
||||
},
|
||||
{
|
||||
"description": "object with a property matching both true and false is invalid",
|
||||
"data": {
|
||||
"foobar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_3_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "patternProperties_5_0"
|
||||
},
|
||||
{
|
||||
"description": "property matching pattern MUST still be valid",
|
||||
"data": {
|
||||
"foo": "invalid string"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "patternProperties_5_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
193
tests/fixtures/prefixItems.json
vendored
Normal file
193
tests/fixtures/prefixItems.json
vendored
Normal file
@ -0,0 +1,193 @@
|
||||
[
|
||||
{
|
||||
"description": "a schema given for prefixItems",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"$id": "prefixItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "correct types",
|
||||
"data": [
|
||||
1,
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "wrong types",
|
||||
"data": [
|
||||
"foo",
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "incomplete array of items",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "array with additional items (invalid due to strictness)",
|
||||
"data": [
|
||||
1,
|
||||
"foo",
|
||||
true
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "JavaScript pseudo-array is valid (invalid due to strict object validation)",
|
||||
"data": {
|
||||
"0": "invalid",
|
||||
"1": "valid",
|
||||
"length": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "prefixItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "prefixItems with boolean schemas",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"$id": "prefixItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "array with one item is valid",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "array with two items is invalid",
|
||||
"data": [
|
||||
1,
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "prefixItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "prefixItems with null instance elements",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"$id": "prefixItems_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allows null elements",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "prefixItems_4_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
543
tests/fixtures/properties.json
vendored
Normal file
543
tests/fixtures/properties.json
vendored
Normal file
@ -0,0 +1,543 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "one property invalid is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": {}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "both properties invalid is invalid",
|
||||
"data": {
|
||||
"foo": [],
|
||||
"bar": {}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "doesn't invalidate other properties",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "properties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "properties_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "properties_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "properties with boolean schema",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": true,
|
||||
"bar": false
|
||||
},
|
||||
"$id": "properties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "no property present is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "properties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "only 'true' property present is valid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "only 'false' property present is invalid",
|
||||
"data": {
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_1_0"
|
||||
},
|
||||
{
|
||||
"description": "both properties present is invalid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_2_0"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": [],
|
||||
"valid": true,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "none of the properties mentioned",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "__proto__ not valid",
|
||||
"data": {
|
||||
"__proto__": "foo"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "toString not valid",
|
||||
"data": {
|
||||
"toString": {
|
||||
"length": 37
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "constructor not valid",
|
||||
"data": {
|
||||
"constructor": {
|
||||
"length": 37
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_4_0"
|
||||
},
|
||||
{
|
||||
"description": "all present and valid",
|
||||
"data": {
|
||||
"__proto__": 12,
|
||||
"toString": {
|
||||
"length": "foo"
|
||||
},
|
||||
"constructor": 37
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "properties_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "properties_12_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
281
tests/fixtures/propertyNames.json
vendored
Normal file
281
tests/fixtures/propertyNames.json
vendored
Normal file
@ -0,0 +1,281 @@
|
||||
[
|
||||
{
|
||||
"description": "propertyNames validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"propertyNames": {
|
||||
"maxLength": 3
|
||||
},
|
||||
"extensible": true,
|
||||
"$id": "propertyNames_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "all property names valid",
|
||||
"data": {
|
||||
"f": {},
|
||||
"foo": {}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
},
|
||||
{
|
||||
"description": "some property names invalid",
|
||||
"data": {
|
||||
"foo": {},
|
||||
"foobar": {}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
},
|
||||
{
|
||||
"description": "object without properties is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "foobar",
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": {}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_1_0"
|
||||
},
|
||||
{
|
||||
"description": "non-matching property name is invalid",
|
||||
"data": {
|
||||
"aaA": {}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_1_0"
|
||||
},
|
||||
{
|
||||
"description": "object without properties is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_2_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_3_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_4_0"
|
||||
},
|
||||
{
|
||||
"description": "object with any other property is invalid",
|
||||
"data": {
|
||||
"bar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_4_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_5_0"
|
||||
},
|
||||
{
|
||||
"description": "object with property foo and bar is valid",
|
||||
"data": {
|
||||
"foo": 1,
|
||||
"bar": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_5_0"
|
||||
},
|
||||
{
|
||||
"description": "object with any other property is invalid",
|
||||
"data": {
|
||||
"baz": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_5_0"
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "propertyNames_6_0"
|
||||
},
|
||||
{
|
||||
"description": "extra property with invalid name is invalid",
|
||||
"data": {
|
||||
"foobar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "propertyNames_6_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
833
tests/fixtures/ref.json
vendored
Normal file
833
tests/fixtures/ref.json
vendored
Normal file
@ -0,0 +1,833 @@
|
||||
[
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "ref_4_0"
|
||||
},
|
||||
{
|
||||
"description": "nested ref invalid",
|
||||
"data": "a",
|
||||
"valid": false,
|
||||
"schema_id": "ref_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": []
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_5_0"
|
||||
},
|
||||
{
|
||||
"description": "ref valid, maxItems invalid",
|
||||
"data": {
|
||||
"foo": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_5_0"
|
||||
},
|
||||
{
|
||||
"description": "ref invalid",
|
||||
"data": {
|
||||
"foo": "string"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_6_0"
|
||||
},
|
||||
{
|
||||
"description": "property named $ref invalid",
|
||||
"data": {
|
||||
"$ref": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_7_0"
|
||||
},
|
||||
{
|
||||
"description": "property named $ref invalid",
|
||||
"data": {
|
||||
"$ref": 2
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": true,
|
||||
"schema_id": "ref_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"valid": false,
|
||||
"schema_id": "ref_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_11_0"
|
||||
},
|
||||
{
|
||||
"description": "object with strings is invalid",
|
||||
"data": {
|
||||
"foo\"bar": "1"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_11_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_35_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_36_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_37_0"
|
||||
},
|
||||
{
|
||||
"description": "ref child resets to strict (default)",
|
||||
"data": {
|
||||
"ref_child": {
|
||||
"b": 1,
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_37_0"
|
||||
},
|
||||
{
|
||||
"description": "ref child with explicit extensible=true is loose",
|
||||
"data": {
|
||||
"extensible_ref_child": {
|
||||
"c": 1,
|
||||
"extra": 2
|
||||
}
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_37_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_38_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_39_0"
|
||||
},
|
||||
{
|
||||
"description": "parent type is now invalid (shadowed)",
|
||||
"data": {
|
||||
"type": "parent",
|
||||
"age": 15
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_39_0"
|
||||
},
|
||||
{
|
||||
"description": "child min age (15) is enforced",
|
||||
"data": {
|
||||
"type": "child",
|
||||
"age": 12
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "ref_39_0"
|
||||
},
|
||||
{
|
||||
"description": "parent max age (20) is shadowed (replaced) by child definition",
|
||||
"data": {
|
||||
"type": "child",
|
||||
"age": 21
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "ref_39_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Valid organization against organization schema",
|
||||
"schema_id": "save_org.request",
|
||||
"data": {
|
||||
"id": "2",
|
||||
"type": "organization",
|
||||
"name": "ACME"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Invalid entity against organization schema (ancestor not allowed)",
|
||||
"schema_id": "save_org.request",
|
||||
"data": {
|
||||
"id": "3",
|
||||
"type": "entity"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_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"
|
||||
},
|
||||
"valid": 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"
|
||||
},
|
||||
"valid": false,
|
||||
"expect_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"
|
||||
},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "Invalid base address against us_address",
|
||||
"schema_id": "save_address.request",
|
||||
"data": {
|
||||
"street": "123 Main",
|
||||
"city": "Anytown"
|
||||
},
|
||||
"valid": 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"
|
||||
},
|
||||
"valid": 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"
|
||||
},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
255
tests/fixtures/required.json
vendored
Normal file
255
tests/fixtures/required.json
vendored
Normal file
@ -0,0 +1,255 @@
|
||||
[
|
||||
{
|
||||
"description": "required validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {},
|
||||
"bar": {}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
],
|
||||
"$id": "required_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "present required property is valid",
|
||||
"data": {
|
||||
"foo": 1
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-present required property is invalid",
|
||||
"data": {
|
||||
"bar": 1
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores arrays",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores strings",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores null",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores boolean",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "required_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "required default validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {}
|
||||
},
|
||||
"$id": "required_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "not required by default",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "required_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "required with empty array",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {}
|
||||
},
|
||||
"required": [],
|
||||
"$id": "required_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "property not required",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "required_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "required_3_0"
|
||||
},
|
||||
{
|
||||
"description": "object with some properties missing is invalid",
|
||||
"data": {
|
||||
"foo\nbar": "1",
|
||||
"foo\"bar": "1"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "required_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": [],
|
||||
"valid": true,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "ignores other non-objects",
|
||||
"data": 12,
|
||||
"valid": true,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "none of the properties mentioned",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "__proto__ present",
|
||||
"data": {
|
||||
"__proto__": "foo"
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "toString present",
|
||||
"data": {
|
||||
"toString": {
|
||||
"length": 37
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "constructor present",
|
||||
"data": {
|
||||
"constructor": {
|
||||
"length": 37
|
||||
}
|
||||
},
|
||||
"valid": false,
|
||||
"schema_id": "required_4_0"
|
||||
},
|
||||
{
|
||||
"description": "all present",
|
||||
"data": {
|
||||
"__proto__": 12,
|
||||
"toString": {
|
||||
"length": "foo"
|
||||
},
|
||||
"constructor": 37
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "required_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "required_5_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
669
tests/fixtures/type.json
vendored
Normal file
669
tests/fixtures/type.json
vendored
Normal file
@ -0,0 +1,669 @@
|
||||
[
|
||||
{
|
||||
"description": "integer type matches integers",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "integer",
|
||||
"$id": "type_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "an integer is an integer",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a float with zero fractional part is an integer",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not an integer",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not an integer",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is still not an integer, even if it looks like one",
|
||||
"data": "1",
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not an integer",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not an integer",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is not an integer",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not an integer",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "number type matches numbers",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "number",
|
||||
"$id": "type_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "an integer is a number",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a float with zero fractional part is a number (and an integer)",
|
||||
"data": 1,
|
||||
"valid": true,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is a number",
|
||||
"data": 1.1,
|
||||
"valid": true,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not a number",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is still not a number, even if it looks like one",
|
||||
"data": "1",
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not a number",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not a number",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is not a number",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not a number",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "string type matches strings",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "string",
|
||||
"$id": "type_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "1 is not a string",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not a string",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is a string",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is still a string, even if it looks like a number",
|
||||
"data": "1",
|
||||
"valid": true,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "an empty string is still a string",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not a string",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not a string",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is not a string",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not a string",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "object type matches objects",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "object",
|
||||
"$id": "type_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "an integer is not an object",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not an object",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not an object",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is an object",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not an object",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is not an object",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not an object",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "array type matches arrays",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "array",
|
||||
"$id": "type_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "an integer is not an array",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not an array",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not an array",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not an array",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is an array",
|
||||
"data": [],
|
||||
"valid": true,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is not an array",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not an array",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "boolean type matches booleans",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"$id": "type_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "an integer is not a boolean",
|
||||
"data": 1,
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "zero is not a boolean",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not a boolean",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not a boolean",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "an empty string is a null",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not a boolean",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not a boolean",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "true is a boolean",
|
||||
"data": true,
|
||||
"valid": true,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "false is a boolean",
|
||||
"data": false,
|
||||
"valid": true,
|
||||
"schema_id": "type_5_0"
|
||||
},
|
||||
{
|
||||
"description": "null is not a boolean",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is not null",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "zero is not null",
|
||||
"data": 0,
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is not null",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "an empty string is null",
|
||||
"data": "",
|
||||
"valid": true,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is not null",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is not null",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "true is not null",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "false is not null",
|
||||
"data": false,
|
||||
"valid": false,
|
||||
"schema_id": "type_6_0"
|
||||
},
|
||||
{
|
||||
"description": "null is null",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "type_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"valid": true,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "a string is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "a float is invalid",
|
||||
"data": 1.1,
|
||||
"valid": false,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "an object is invalid",
|
||||
"data": {},
|
||||
"valid": false,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "an array is invalid",
|
||||
"data": [],
|
||||
"valid": false,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "a boolean is invalid",
|
||||
"data": true,
|
||||
"valid": false,
|
||||
"schema_id": "type_7_0"
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_7_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "type as array with one item",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": [
|
||||
"string"
|
||||
],
|
||||
"$id": "type_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "string is valid",
|
||||
"data": "foo",
|
||||
"valid": true,
|
||||
"schema_id": "type_8_0"
|
||||
},
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "type_8_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "type: array or object",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"type": [
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"items": {},
|
||||
"$id": "type_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "array is valid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "type_9_0"
|
||||
},
|
||||
{
|
||||
"description": "object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "type_9_0"
|
||||
},
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "type_9_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_9_0"
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false,
|
||||
"schema_id": "type_9_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "type_10_0"
|
||||
},
|
||||
{
|
||||
"description": "object is valid",
|
||||
"data": {},
|
||||
"valid": true,
|
||||
"schema_id": "type_10_0"
|
||||
},
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true,
|
||||
"schema_id": "type_10_0"
|
||||
},
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 123,
|
||||
"valid": false,
|
||||
"schema_id": "type_10_0"
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false,
|
||||
"schema_id": "type_10_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"valid": true,
|
||||
"schema_id": "type_11_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
958
tests/fixtures/uniqueItems.json
vendored
Normal file
958
tests/fixtures/uniqueItems.json
vendored
Normal file
@ -0,0 +1,958 @@
|
||||
[
|
||||
{
|
||||
"description": "uniqueItems validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"extensible": true,
|
||||
"$id": "uniqueItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "unique array of integers is valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of integers is invalid",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of more than two integers is invalid",
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "numbers are unique if mathematically unequal",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "false is not equal to zero",
|
||||
"data": [
|
||||
0,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "true is not equal to one",
|
||||
"data": [
|
||||
1,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of strings is valid",
|
||||
"data": [
|
||||
"foo",
|
||||
"bar",
|
||||
"baz"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of strings is invalid",
|
||||
"data": [
|
||||
"foo",
|
||||
"bar",
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
{
|
||||
"foo": "baz"
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of objects is invalid",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "property order of array of objects is ignored",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar",
|
||||
"bar": "foo"
|
||||
},
|
||||
{
|
||||
"bar": "foo",
|
||||
"foo": "bar"
|
||||
}
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of nested objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of nested objects is invalid",
|
||||
"data": [
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of arrays is valid",
|
||||
"data": [
|
||||
[
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"bar"
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of arrays is invalid",
|
||||
"data": [
|
||||
[
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of more than two arrays is invalid",
|
||||
"data": [
|
||||
[
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"bar"
|
||||
],
|
||||
[
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "1 and true are unique",
|
||||
"data": [
|
||||
1,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "0 and false are unique",
|
||||
"data": [
|
||||
0,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "[1] and [true] are unique",
|
||||
"data": [
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
true
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "[0] and [false] are unique",
|
||||
"data": [
|
||||
[
|
||||
0
|
||||
],
|
||||
[
|
||||
false
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "nested [1] and [true] are unique",
|
||||
"data": [
|
||||
[
|
||||
[
|
||||
1
|
||||
],
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
[
|
||||
true
|
||||
],
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "nested [0] and [false] are unique",
|
||||
"data": [
|
||||
[
|
||||
[
|
||||
0
|
||||
],
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
[
|
||||
false
|
||||
],
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "unique heterogeneous types are valid",
|
||||
"data": [
|
||||
{},
|
||||
[
|
||||
1
|
||||
],
|
||||
true,
|
||||
null,
|
||||
1,
|
||||
"{}"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique heterogeneous types are invalid",
|
||||
"data": [
|
||||
{},
|
||||
[
|
||||
1
|
||||
],
|
||||
true,
|
||||
null,
|
||||
{},
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "different objects are unique",
|
||||
"data": [
|
||||
{
|
||||
"a": 1,
|
||||
"b": 2
|
||||
},
|
||||
{
|
||||
"a": 2,
|
||||
"b": 1
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "objects are non-unique despite key order",
|
||||
"data": [
|
||||
{
|
||||
"a": 1,
|
||||
"b": 2
|
||||
},
|
||||
{
|
||||
"b": 2,
|
||||
"a": 1
|
||||
}
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": false} and {\"a\": 0} are unique",
|
||||
"data": [
|
||||
{
|
||||
"a": false
|
||||
},
|
||||
{
|
||||
"a": 0
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
},
|
||||
{
|
||||
"description": "{\"a\": true} and {\"a\": 1} are unique",
|
||||
"data": [
|
||||
{
|
||||
"a": true
|
||||
},
|
||||
{
|
||||
"a": 1
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_0_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "uniqueItems with an array of items",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"uniqueItems": true,
|
||||
"extensible": true,
|
||||
"$id": "uniqueItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false, true] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, false] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "[false, false] from items array is not valid",
|
||||
"data": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, true] from items array is not valid",
|
||||
"data": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array extended from [false, true] is valid",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
"foo",
|
||||
"bar"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array extended from [true, false] is valid",
|
||||
"data": [
|
||||
true,
|
||||
false,
|
||||
"foo",
|
||||
"bar"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array extended from [false, true] is not valid",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
"foo",
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array extended from [true, false] is not valid",
|
||||
"data": [
|
||||
true,
|
||||
false,
|
||||
"foo",
|
||||
"foo"
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_1_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "uniqueItems with an array of items and additionalItems=false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"uniqueItems": true,
|
||||
"items": false,
|
||||
"$id": "uniqueItems_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false, true] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_2_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, false] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_2_0"
|
||||
},
|
||||
{
|
||||
"description": "[false, false] from items array is not valid",
|
||||
"data": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_2_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, true] from items array is not valid",
|
||||
"data": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_2_0"
|
||||
},
|
||||
{
|
||||
"description": "extra items are invalid even if unique",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_2_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "uniqueItems=false validation",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"uniqueItems": false,
|
||||
"extensible": true,
|
||||
"$id": "uniqueItems_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "unique array of integers is valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of integers is valid",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "numbers are unique if mathematically unequal",
|
||||
"data": [
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "false is not equal to zero",
|
||||
"data": [
|
||||
0,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "true is not equal to one",
|
||||
"data": [
|
||||
1,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
{
|
||||
"foo": "baz"
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of nested objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of nested objects is valid",
|
||||
"data": [
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array of arrays is valid",
|
||||
"data": [
|
||||
[
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"bar"
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array of arrays is valid",
|
||||
"data": [
|
||||
[
|
||||
"foo"
|
||||
],
|
||||
[
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "1 and true are unique",
|
||||
"data": [
|
||||
1,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "0 and false are unique",
|
||||
"data": [
|
||||
0,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "unique heterogeneous types are valid",
|
||||
"data": [
|
||||
{},
|
||||
[
|
||||
1
|
||||
],
|
||||
true,
|
||||
null,
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique heterogeneous types are valid",
|
||||
"data": [
|
||||
{},
|
||||
[
|
||||
1
|
||||
],
|
||||
true,
|
||||
null,
|
||||
{},
|
||||
1
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_3_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "uniqueItems=false with an array of items",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"uniqueItems": false,
|
||||
"extensible": true,
|
||||
"$id": "uniqueItems_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false, true] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, false] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "[false, false] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, true] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array extended from [false, true] is valid",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
"foo",
|
||||
"bar"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "unique array extended from [true, false] is valid",
|
||||
"data": [
|
||||
true,
|
||||
false,
|
||||
"foo",
|
||||
"bar"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array extended from [false, true] is valid",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
"foo",
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
},
|
||||
{
|
||||
"description": "non-unique array extended from [true, false] is valid",
|
||||
"data": [
|
||||
true,
|
||||
false,
|
||||
"foo",
|
||||
"foo"
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_4_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "uniqueItems=false with an array of items and additionalItems=false",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"uniqueItems": false,
|
||||
"items": false,
|
||||
"$id": "uniqueItems_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "[false, true] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_5_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, false] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_5_0"
|
||||
},
|
||||
{
|
||||
"description": "[false, false] from items array is valid",
|
||||
"data": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_5_0"
|
||||
},
|
||||
{
|
||||
"description": "[true, true] from items array is valid",
|
||||
"data": [
|
||||
true,
|
||||
true
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_5_0"
|
||||
},
|
||||
{
|
||||
"description": "extra items are invalid even if unique",
|
||||
"data": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_5_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "extensible: true allows extra items in uniqueItems",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"extensible": true,
|
||||
"$id": "uniqueItems_6_0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "extra items must be unique",
|
||||
"data": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"valid": false,
|
||||
"schema_id": "uniqueItems_6_0"
|
||||
},
|
||||
{
|
||||
"description": "extra unique items valid",
|
||||
"data": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"valid": true,
|
||||
"schema_id": "uniqueItems_6_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
102
tests/lib.rs
Normal file
102
tests/lib.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use ::jspg::*;
|
||||
use pgrx::JsonB;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_library_api() {
|
||||
// 1. Initially, schemas are not cached.
|
||||
assert!(!json_schema_cached("test_schema"));
|
||||
|
||||
// Expected uninitialized drop format: errors + null response
|
||||
let uninitialized_drop = validate_json_schema("test_schema", JsonB(json!({})));
|
||||
assert_eq!(
|
||||
uninitialized_drop.0,
|
||||
json!({
|
||||
"type": "drop",
|
||||
"errors": [{
|
||||
"code": "VALIDATOR_NOT_INITIALIZED",
|
||||
"message": "JSON Schemas have not been cached yet. Run cache_json_schemas()",
|
||||
"details": { "path": "" }
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
// 2. Cache schemas
|
||||
let db_json = json!({
|
||||
"puncs": [],
|
||||
"enums": [],
|
||||
"types": [{
|
||||
"schemas": [{
|
||||
"$id": "test_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" }
|
||||
},
|
||||
"required": ["name"]
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
let cache_drop = jspg_cache_database(JsonB(db_json));
|
||||
assert_eq!(
|
||||
cache_drop.0,
|
||||
json!({
|
||||
"type": "drop",
|
||||
"response": "success"
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Check schemas are cached
|
||||
assert!(json_schema_cached("test_schema"));
|
||||
|
||||
let show_drop = show_json_schemas();
|
||||
assert_eq!(
|
||||
show_drop.0,
|
||||
json!({
|
||||
"type": "drop",
|
||||
"response": ["test_schema"]
|
||||
})
|
||||
);
|
||||
|
||||
// 4. Validate Happy Path
|
||||
let happy_drop = validate_json_schema("test_schema", JsonB(json!({"name": "Neo"})));
|
||||
assert_eq!(
|
||||
happy_drop.0,
|
||||
json!({
|
||||
"type": "drop",
|
||||
"response": "success"
|
||||
})
|
||||
);
|
||||
|
||||
// 5. Validate Unhappy Path
|
||||
let unhappy_drop = validate_json_schema("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 = clear_json_schemas();
|
||||
assert_eq!(
|
||||
clear_drop.0,
|
||||
json!({
|
||||
"type": "drop",
|
||||
"response": "success"
|
||||
})
|
||||
);
|
||||
assert!(!json_schema_cached("test_schema"));
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
- validator: ensure `uneval` state is propagated when `$ref` validation fails
|
||||
|
||||
## [0.6.1] - 2025-01-07
|
||||
|
||||
### Bug Fixes
|
||||
- fix: FileLoader should not be used in wasm
|
||||
|
||||
## [0.6.0] - 2024-05-30
|
||||
|
||||
### Braking Changes
|
||||
- loader: Allow to replace entirely
|
||||
|
||||
### Bug Fixes
|
||||
- seperate doc loading from root creation
|
||||
- validator: if contentEncoding fails, skip contentMediaType
|
||||
- loader: should load latest from metaschemas dir
|
||||
- fix: hash for json numbers with zero fractions
|
||||
- fix: resources/anchors in non-std schema loc not supported
|
||||
|
||||
### Changes
|
||||
- boon binary artificats under github release
|
||||
- boon binary `--cacert` option
|
||||
- boon binary `--insecure` flag
|
||||
|
||||
## [0.5.3] - 2024-01-27
|
||||
|
||||
### Changes
|
||||
- updated dependencies
|
||||
|
||||
## [0.5.2] - 2024-01-27
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Error message for failed const validation is wrong
|
||||
|
||||
## [0.5.1] - 2023-07-13
|
||||
|
||||
### Changes
|
||||
|
||||
- WASM compatibility
|
||||
- minor performance improvements
|
||||
|
||||
## [0.5.0] - 2023-03-29
|
||||
|
||||
### Breaking Changes
|
||||
- chages to error api
|
||||
|
||||
### Performance
|
||||
- minor improvements in validation
|
||||
|
||||
## [0.4.0] - 2023-03-24
|
||||
|
||||
### Breaking Changes
|
||||
- chages to error api
|
||||
|
||||
### Fixed
|
||||
- Compler.add_resource should not check file exists
|
||||
|
||||
### Added
|
||||
- implement `contentSchema` keyword
|
||||
- ECMA-262 regex compatibility
|
||||
- add example_custom_content_encoding
|
||||
- add example_custom_content_media_type
|
||||
|
||||
### Performance
|
||||
- significant improvement in validation
|
||||
|
||||
## [0.3.1] - 2023-03-07
|
||||
|
||||
### Added
|
||||
- add example_from_yaml_files
|
||||
- cli: support yaml files
|
||||
|
||||
### Fixed
|
||||
- ensure fragment decoded before use
|
||||
- $dynamicRef w/o anchor is same as $ref
|
||||
1441
validator/Cargo.lock
generated
1441
validator/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "boon"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
description = "JSONSchema (draft 2020-12, draft 2019-09, draft-7, draft-6, draft-4) Validation"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/santhosh-tekuri/boon"
|
||||
authors = ["santhosh kumar tekuri <santhosh.tekuri@gmail.com>"]
|
||||
keywords = ["jsonschema", "validation"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
categories = ["web-programming"]
|
||||
exclude = [ "tests", ".github", ".gitmodules" ]
|
||||
|
||||
[dependencies]
|
||||
pgrx = "0.16.1"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
regex = "1.12.2"
|
||||
regex-syntax = "0.8.8"
|
||||
url = "2"
|
||||
fluent-uri = "0.4.1"
|
||||
idna = "1.1"
|
||||
percent-encoding = "2"
|
||||
once_cell = "1"
|
||||
base64 = "0.22"
|
||||
ahash = "0.8.12"
|
||||
appendlist = "1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
pgrx-tests = "0.16.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
ureq = "3.1"
|
||||
rustls = "0.23"
|
||||
criterion = "0.7"
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
harness = false
|
||||
@ -1,177 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@ -1,18 +0,0 @@
|
||||
Copyright 2023 Santhosh Kumar Tekuri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -1,88 +0,0 @@
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://crates.io/crates/boon)
|
||||
[](https://docs.rs/boon/)
|
||||
[](https://github.com/santhosh-tekuri/boon/actions/workflows/rust.yml)
|
||||
[](https://codecov.io/gh/santhosh-tekuri/boon)
|
||||
[](https://deps.rs/repo/github/Santhosh-tekuri/boon)
|
||||
|
||||
[Examples](https://github.com/santhosh-tekuri/boon/blob/main/tests/examples.rs)
|
||||
[Changelog](https://github.com/santhosh-tekuri/boon/blob/main/CHANGELOG.md)
|
||||
|
||||
## Library Features
|
||||
|
||||
- [x] pass [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) excluding optional(compare with other impls at [bowtie](https://bowtie-json-schema.github.io/bowtie/#))
|
||||
- [x] [](https://bowtie.report/#/dialects/draft4)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft6)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft7)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft2019-09)
|
||||
- [x] [](https://bowtie.report/#/dialects/draft2020-12)
|
||||
- [x] detect infinite loop traps
|
||||
- [x] `$schema` cycle
|
||||
- [x] validation cycle
|
||||
- [x] custom `$schema` url
|
||||
- [x] vocabulary based validation
|
||||
- [x] ECMA-262 regex compatibility (pass tests from `optional/ecmascript-regex.json`)
|
||||
- [x] format assertions
|
||||
- [x] flag to enable in draft >= 2019-09
|
||||
- [x] custom format registration
|
||||
- [x] built-in formats
|
||||
- [x] regex, uuid
|
||||
- [x] ipv4, ipv6
|
||||
- [x] hostname, email
|
||||
- [x] idn-hostname, idn-email
|
||||
- [x] date, time, date-time, duration
|
||||
- [x] json-pointer, relative-json-pointer
|
||||
- [x] uri, uri-reference, uri-template
|
||||
- [x] iri, iri-reference
|
||||
- [x] period
|
||||
- [x] content assertions
|
||||
- [x] flag to enable in draft >= 7
|
||||
- [x] contentEncoding
|
||||
- [x] base64
|
||||
- [x] custom
|
||||
- [x] contentMediaType
|
||||
- [x] application/json
|
||||
- [x] custom
|
||||
- [x] contentSchema
|
||||
- [x] errors
|
||||
- [x] introspectable
|
||||
- [x] hierarchy
|
||||
- [x] alternative display with `#`
|
||||
- [x] output
|
||||
- [x] flag
|
||||
- [x] basic
|
||||
- [x] detailed
|
||||
- [ ] custom vocabulary
|
||||
|
||||
## CLI
|
||||
|
||||
to install: `cargo install boon-cli --locked`
|
||||
|
||||
or download it from [releases](https://github.com/santhosh-tekuri/boon/releases)
|
||||
|
||||
```
|
||||
Usage: boon [OPTIONS] SCHEMA [INSTANCE...]
|
||||
|
||||
Options:
|
||||
-h, --help Print help information
|
||||
-q, --quiet Do not print errors
|
||||
-d, --draft <VER> Draft used when '$schema' is missing. Valid values 4,
|
||||
6, 7, 2019, 2020 (default 2020)
|
||||
-o, --output <FMT> Output format. Valid values simple, alt, flag, basic,
|
||||
detailed (default simple)
|
||||
-f, --assert-format
|
||||
Enable format assertions with draft >= 2019
|
||||
-c, --assert-content
|
||||
Enable content assertions with draft >= 7
|
||||
--cacert <FILE> Use the specified PEM certificate file to verify the
|
||||
peer. The file may contain multiple CA certificates
|
||||
-k, --insecure Use insecure TLS connection
|
||||
```
|
||||
|
||||
This cli can validate both schema and multiple instances.
|
||||
|
||||
It support both json and yaml files
|
||||
|
||||
exit code is:
|
||||
- `1` if command line arguments are invalid.
|
||||
- `2` if there are errors
|
||||
@ -1,26 +0,0 @@
|
||||
use std::{env, fs::File};
|
||||
|
||||
use boon::{Compiler, Schemas};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn validate(c: &mut Criterion) {
|
||||
let (Ok(schema), Ok(instance)) = (env::var("SCHEMA"), env::var("INSTANCE")) else {
|
||||
panic!("SCHEMA, INSTANCE environment variables not set");
|
||||
};
|
||||
|
||||
let mut schemas = Schemas::new();
|
||||
let mut compiler = Compiler::new();
|
||||
compiler.enable_format_assertions();
|
||||
let sch = compiler.compile(&schema, &mut schemas).unwrap();
|
||||
let rdr = File::open(&instance).unwrap();
|
||||
let inst: Value = if instance.ends_with(".yaml") || instance.ends_with(".yml") {
|
||||
serde_yaml::from_reader(rdr).unwrap()
|
||||
} else {
|
||||
serde_json::from_reader(rdr).unwrap()
|
||||
};
|
||||
c.bench_function("boon", |b| b.iter(|| schemas.validate(&inst, sch).unwrap()));
|
||||
}
|
||||
|
||||
criterion_group!(benches, validate);
|
||||
criterion_main!(benches);
|
||||
1156
validator/cli/Cargo.lock
generated
1156
validator/cli/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "boon-cli"
|
||||
version = "0.6.2"
|
||||
edition = "2021"
|
||||
description = "cli for JSONSchema (draft 2020-12, draft 2019-09, draft-7, draft-6, draft-4) Validation"
|
||||
repository = "https://github.com/santhosh-tekuri/boon/cli"
|
||||
authors = ["santhosh kumar tekuri <santhosh.tekuri@gmail.com>"]
|
||||
keywords = ["jsonschema", "validation"]
|
||||
categories = ["web-programming"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
boon = { version = "0.6.1", path = ".."}
|
||||
url = "2"
|
||||
getopts = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
ureq = "2.12"
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
rustls-pemfile = "2.1"
|
||||
|
||||
[[bin]]
|
||||
name = "boon"
|
||||
path = "src/main.rs"
|
||||
@ -1,316 +0,0 @@
|
||||
use core::panic;
|
||||
use std::{env, error::Error, fs::File, io::BufReader, process, str::FromStr, sync::Arc};
|
||||
|
||||
use boon::{Compiler, Draft, Schemas, SchemeUrlLoader, UrlLoader};
|
||||
use getopts::Options;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use serde_json::Value;
|
||||
use ureq::Agent;
|
||||
use url::Url;
|
||||
|
||||
fn main() {
|
||||
let opts = options();
|
||||
let matches = match opts.parse(env::args().skip(1)) {
|
||||
Ok(m) => m,
|
||||
Err(f) => {
|
||||
eprintln!("{f}");
|
||||
eprintln!();
|
||||
eprintln!("{}", opts.usage(BRIEF));
|
||||
process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
if matches.opt_present("version") {
|
||||
println!("{}", env!("CARGO_PKG_VERSION"));
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
if matches.opt_present("help") {
|
||||
println!("{}", opts.usage(BRIEF));
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
// draft --
|
||||
let mut draft = Draft::default();
|
||||
if let Some(v) = matches.opt_str("draft") {
|
||||
let Ok(v) = usize::from_str(&v) else {
|
||||
eprintln!("invalid draft: {v}");
|
||||
eprintln!();
|
||||
eprintln!("{}", opts.usage(BRIEF));
|
||||
process::exit(1);
|
||||
};
|
||||
draft = match v {
|
||||
4 => Draft::V4,
|
||||
6 => Draft::V6,
|
||||
7 => Draft::V7,
|
||||
2019 => Draft::V2019_09,
|
||||
2020 => Draft::V2020_12,
|
||||
_ => {
|
||||
eprintln!("invalid draft: {v}");
|
||||
eprintln!();
|
||||
eprintln!("{}", opts.usage(BRIEF));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// output --
|
||||
let output = matches.opt_str("output");
|
||||
if let Some(o) = &output {
|
||||
if !matches!(o.as_str(), "simple" | "alt" | "flag" | "basic" | "detailed") {
|
||||
eprintln!("invalid output: {o}");
|
||||
eprintln!();
|
||||
eprintln!("{}", opts.usage(BRIEF));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// flags --
|
||||
let quiet = matches.opt_present("quiet");
|
||||
let assert_format = matches.opt_present("assert-format");
|
||||
let assert_content = matches.opt_present("assert-content");
|
||||
let insecure = matches.opt_present("insecure");
|
||||
|
||||
// schema --
|
||||
let Some(schema) = matches.free.first() else {
|
||||
eprintln!("missing SCHEMA");
|
||||
eprintln!();
|
||||
eprintln!("{}", opts.usage(BRIEF));
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
// compile --
|
||||
let mut schemas = Schemas::new();
|
||||
let mut compiler = Compiler::new();
|
||||
let mut loader = SchemeUrlLoader::new();
|
||||
loader.register("file", Box::new(FileUrlLoader));
|
||||
let cacert = matches.opt_str("cacert");
|
||||
let cacert = cacert.as_deref();
|
||||
loader.register("http", Box::new(HttpUrlLoader::new(cacert, insecure)));
|
||||
loader.register("https", Box::new(HttpUrlLoader::new(cacert, insecure)));
|
||||
compiler.use_loader(Box::new(loader));
|
||||
compiler.set_default_draft(draft);
|
||||
if assert_format {
|
||||
compiler.enable_format_assertions();
|
||||
}
|
||||
if assert_content {
|
||||
compiler.enable_content_assertions();
|
||||
}
|
||||
let sch = match compiler.compile(schema, &mut schemas) {
|
||||
Ok(sch) => {
|
||||
println!("schema {schema}: ok");
|
||||
sch
|
||||
}
|
||||
Err(e) => {
|
||||
println!("schema {schema}: failed");
|
||||
if !quiet {
|
||||
println!("{e:#}");
|
||||
}
|
||||
process::exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
// validate --
|
||||
let mut all_valid = true;
|
||||
for instance in &matches.free[1..] {
|
||||
if !quiet {
|
||||
println!();
|
||||
}
|
||||
let rdr = match File::open(instance) {
|
||||
Ok(rdr) => BufReader::new(rdr),
|
||||
Err(e) => {
|
||||
println!("instance {instance}: failed");
|
||||
if !quiet {
|
||||
println!("error reading file {instance}: {e}");
|
||||
}
|
||||
all_valid = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let value: Result<Value, String> =
|
||||
if instance.ends_with(".yaml") || instance.ends_with(".yml") {
|
||||
serde_yaml::from_reader(rdr).map_err(|e| e.to_string())
|
||||
} else {
|
||||
serde_json::from_reader(rdr).map_err(|e| e.to_string())
|
||||
};
|
||||
let value = match value {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("instance {instance}: failed");
|
||||
if !quiet {
|
||||
println!("error parsing file {instance}: {e}");
|
||||
}
|
||||
all_valid = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match schemas.validate(&value, sch) {
|
||||
Ok(_) => println!("instance {instance}: ok"),
|
||||
Err(e) => {
|
||||
println!("instance {instance}: failed");
|
||||
if !quiet {
|
||||
match &output {
|
||||
Some(out) => match out.as_str() {
|
||||
"simple" => println!("{e}"),
|
||||
"alt" => println!("{e:#}"),
|
||||
"flag" => println!("{:#}", e.flag_output()),
|
||||
"basic" => println!("{:#}", e.basic_output()),
|
||||
"detailed" => println!("{:#}", e.detailed_output()),
|
||||
_ => (),
|
||||
},
|
||||
None => println!("{e}"),
|
||||
}
|
||||
}
|
||||
all_valid = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
if !all_valid {
|
||||
process::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
const BRIEF: &str = "Usage: boon [OPTIONS] SCHEMA [INSTANCE...]";
|
||||
|
||||
fn options() -> Options {
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("v", "version", "Print version and exit");
|
||||
opts.optflag("h", "help", "Print help information");
|
||||
opts.optflag("q", "quiet", "Do not print errors");
|
||||
opts.optopt(
|
||||
"d",
|
||||
"draft",
|
||||
"Draft used when '$schema' is missing. Valid values 4, 6, 7, 2019, 2020 (default 2020)",
|
||||
"<VER>",
|
||||
);
|
||||
opts.optopt(
|
||||
"o",
|
||||
"output",
|
||||
"Output format. Valid values simple, alt, flag, basic, detailed (default simple)",
|
||||
"<FMT>",
|
||||
);
|
||||
opts.optflag(
|
||||
"f",
|
||||
"assert-format",
|
||||
"Enable format assertions with draft >= 2019",
|
||||
);
|
||||
opts.optflag(
|
||||
"c",
|
||||
"assert-content",
|
||||
"Enable content assertions with draft >= 7",
|
||||
);
|
||||
opts.optopt(
|
||||
"",
|
||||
"cacert",
|
||||
"Use the specified PEM certificate file to verify the peer. The file may contain multiple CA certificates",
|
||||
"<FILE>",
|
||||
);
|
||||
opts.optflag("k", "insecure", "Use insecure TLS connection");
|
||||
opts
|
||||
}
|
||||
|
||||
struct FileUrlLoader;
|
||||
impl UrlLoader for FileUrlLoader {
|
||||
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
|
||||
let url = Url::parse(url)?;
|
||||
let path = url.to_file_path().map_err(|_| "invalid file path")?;
|
||||
let file = File::open(&path)?;
|
||||
if path
|
||||
.extension()
|
||||
.filter(|&ext| ext == "yaml" || ext == "yml")
|
||||
.is_some()
|
||||
{
|
||||
Ok(serde_yaml::from_reader(file)?)
|
||||
} else {
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpUrlLoader(Agent);
|
||||
|
||||
impl HttpUrlLoader {
|
||||
fn new(cacert: Option<&str>, insecure: bool) -> Self {
|
||||
let mut builder = ureq::builder();
|
||||
if let Some(cacert) = cacert {
|
||||
let file = File::open(cacert).unwrap_or_else(|e| panic!("error opening {cacert}: {e}"));
|
||||
let certs: Result<Vec<_>, _> =
|
||||
rustls_pemfile::certs(&mut BufReader::new(file)).collect();
|
||||
let certs = certs.unwrap_or_else(|e| panic!("error reading cacert: {e}"));
|
||||
assert!(!certs.is_empty(), "no certs in cacert");
|
||||
let mut store = rustls::RootCertStore::empty();
|
||||
for cert in certs {
|
||||
store
|
||||
.add(cert)
|
||||
.unwrap_or_else(|e| panic!("error adding cert: {e}"))
|
||||
}
|
||||
let tls_config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(store)
|
||||
.with_no_client_auth();
|
||||
builder = builder.tls_config(tls_config.into());
|
||||
} else if insecure {
|
||||
let tls_config = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
|
||||
.with_no_client_auth();
|
||||
builder = builder.tls_config(tls_config.into());
|
||||
}
|
||||
Self(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
impl UrlLoader for HttpUrlLoader {
|
||||
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
|
||||
let response = self.0.get(url).call()?;
|
||||
let is_yaml = url.ends_with(".yaml") || url.ends_with(".yml") || {
|
||||
let ctype = response.content_type();
|
||||
ctype.ends_with("/yaml") || ctype.ends_with("-yaml")
|
||||
};
|
||||
if is_yaml {
|
||||
Ok(serde_yaml::from_reader(response.into_reader())?)
|
||||
} else {
|
||||
Ok(serde_json::from_reader(response.into_reader())?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InsecureVerifier;
|
||||
|
||||
impl ServerCertVerifier for InsecureVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
@ -1,999 +0,0 @@
|
||||
use std::{cmp::Ordering, collections::HashMap, error::Error, fmt::Display};
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::{Map, Value};
|
||||
use url::Url;
|
||||
|
||||
use crate::{content::*, draft::*, ecma, formats::*, root::*, roots::*, util::*, *};
|
||||
|
||||
/// Supported draft versions
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Draft {
|
||||
/// Draft for `http://json-schema.org/draft-04/schema`
|
||||
V4,
|
||||
/// Draft for `http://json-schema.org/draft-06/schema`
|
||||
V6,
|
||||
/// Draft for `http://json-schema.org/draft-07/schema`
|
||||
V7,
|
||||
/// Draft for `https://json-schema.org/draft/2019-09/schema`
|
||||
V2019_09,
|
||||
/// Draft for `https://json-schema.org/draft/2020-12/schema`
|
||||
V2020_12,
|
||||
}
|
||||
|
||||
impl Draft {
|
||||
/**
|
||||
Get [`Draft`] for given `url`
|
||||
|
||||
# Arguments
|
||||
|
||||
* `url` - accepts both `http` and `https` and ignores any fragments in url
|
||||
|
||||
# Examples
|
||||
|
||||
```
|
||||
# use boon::*;
|
||||
assert_eq!(Draft::from_url("https://json-schema.org/draft/2020-12/schema"), Some(Draft::V2020_12));
|
||||
assert_eq!(Draft::from_url("http://json-schema.org/draft-07/schema#"), Some(Draft::V7));
|
||||
```
|
||||
*/
|
||||
pub fn from_url(url: &str) -> Option<Draft> {
|
||||
match crate::draft::Draft::from_url(url) {
|
||||
Some(draft) => match draft.version {
|
||||
4 => Some(Draft::V4),
|
||||
6 => Some(Draft::V6),
|
||||
7 => Some(Draft::V7),
|
||||
2019 => Some(Draft::V2019_09),
|
||||
2020 => Some(Draft::V2020_12),
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn internal(&self) -> &'static crate::draft::Draft {
|
||||
match self {
|
||||
Draft::V4 => &DRAFT4,
|
||||
Draft::V6 => &DRAFT6,
|
||||
Draft::V7 => &DRAFT7,
|
||||
Draft::V2019_09 => &DRAFT2019,
|
||||
Draft::V2020_12 => &DRAFT2020,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns latest draft supported
|
||||
impl Default for Draft {
|
||||
fn default() -> Self {
|
||||
Draft::V2020_12
|
||||
}
|
||||
}
|
||||
|
||||
/// JsonSchema compiler.
|
||||
#[derive(Default)]
|
||||
pub struct Compiler {
|
||||
roots: Roots,
|
||||
assert_format: bool,
|
||||
assert_content: bool,
|
||||
formats: HashMap<&'static str, Format>,
|
||||
decoders: HashMap<&'static str, Decoder>,
|
||||
media_types: HashMap<&'static str, MediaType>,
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/**
|
||||
Overrides the draft used to compile schemas without
|
||||
explicit `$schema` field.
|
||||
|
||||
By default this library uses latest draft supported.
|
||||
|
||||
The use of this option is HIGHLY encouraged to ensure
|
||||
continued correct operation of your schema. The current
|
||||
default value will not stay the same over time.
|
||||
*/
|
||||
pub fn set_default_draft(&mut self, d: Draft) {
|
||||
self.roots.default_draft = d.internal()
|
||||
}
|
||||
|
||||
/**
|
||||
Always enable format assertions.
|
||||
|
||||
# Default Behavior
|
||||
|
||||
- for draft-07 and earlier: enabled
|
||||
- for draft/2019-09: disabled, unless
|
||||
metaschema says `format` vocabulary is required
|
||||
- for draft/2020-12: disabled, unless
|
||||
metaschema says `format-assertion` vocabulary is required
|
||||
*/
|
||||
pub fn enable_format_assertions(&mut self) {
|
||||
self.assert_format = true;
|
||||
}
|
||||
|
||||
/**
|
||||
Always enable content assertions.
|
||||
|
||||
content assertions include keywords:
|
||||
- contentEncoding
|
||||
- contentMediaType
|
||||
- contentSchema
|
||||
|
||||
Default Behavior is always disabled.
|
||||
*/
|
||||
pub fn enable_content_assertions(&mut self) {
|
||||
self.assert_content = true;
|
||||
}
|
||||
|
||||
/// Overrides default [`UrlLoader`] used to load schema resources
|
||||
pub fn use_loader(&mut self, url_loader: Box<dyn UrlLoader>) {
|
||||
self.roots.loader.use_loader(url_loader);
|
||||
}
|
||||
|
||||
/**
|
||||
Registers custom `format`
|
||||
|
||||
# Note
|
||||
|
||||
- `regex` format cannot be overridden
|
||||
- format assertions are disabled for draft >= 2019-09.
|
||||
see [`Compiler::enable_format_assertions`]
|
||||
*/
|
||||
pub fn register_format(&mut self, format: Format) {
|
||||
if format.name != "regex" {
|
||||
self.formats.insert(format.name, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Registers custom `contentEncoding`
|
||||
|
||||
Note that content assertions are disabled by default.
|
||||
see [`Compiler::enable_content_assertions`]
|
||||
*/
|
||||
pub fn register_content_encoding(&mut self, decoder: Decoder) {
|
||||
self.decoders.insert(decoder.name, decoder);
|
||||
}
|
||||
|
||||
/**
|
||||
Registers custom `contentMediaType`
|
||||
|
||||
Note that content assertions are disabled by default.
|
||||
see [`Compiler::enable_content_assertions`]
|
||||
*/
|
||||
pub fn register_content_media_type(&mut self, media_type: MediaType) {
|
||||
self.media_types.insert(media_type.name, media_type);
|
||||
}
|
||||
|
||||
/**
|
||||
Adds schema resource which used later in reference resoltion
|
||||
If you do not know which schema resources required, then use [`UrlLoader`].
|
||||
|
||||
The argument `loc` can be file path or url. any fragment in `loc` is ignored.
|
||||
|
||||
# Errors
|
||||
|
||||
returns [`CompileError`] if url parsing failed.
|
||||
*/
|
||||
pub fn add_resource(&mut self, loc: &str, json: Value) -> Result<(), CompileError> {
|
||||
let uf = UrlFrag::absolute(loc)?;
|
||||
self.roots.loader.add_doc(uf.url, json);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Compile given `loc` into `target` and return an identifier to the compiled
|
||||
schema.
|
||||
|
||||
the argument `loc` can be file path or url with optional fragment.
|
||||
examples: `http://example.com/schema.json#/defs/address`,
|
||||
`samples/schema_file.json#defs/address`
|
||||
|
||||
if `loc` is already compiled, it simply returns the same [`SchemaIndex`]
|
||||
*/
|
||||
pub fn compile(
|
||||
&mut self,
|
||||
loc: &str,
|
||||
target: &mut Schemas,
|
||||
) -> Result<SchemaIndex, CompileError> {
|
||||
let uf = UrlFrag::absolute(loc)?;
|
||||
// resolve anchor
|
||||
let up = self.roots.resolve_fragment(uf)?;
|
||||
|
||||
let result = self.do_compile(up, target);
|
||||
if let Err(bug @ CompileError::Bug(_)) = &result {
|
||||
debug_assert!(false, "{bug}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn do_compile(
|
||||
&mut self,
|
||||
up: UrlPtr,
|
||||
target: &mut Schemas,
|
||||
) -> Result<SchemaIndex, CompileError> {
|
||||
let mut queue = Queue::new();
|
||||
let mut compiled = Vec::new();
|
||||
|
||||
let index = queue.enqueue_schema(target, up);
|
||||
if queue.schemas.is_empty() {
|
||||
// already got compiled
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
while queue.schemas.len() > compiled.len() {
|
||||
let up = &queue.schemas[compiled.len()];
|
||||
self.roots.ensure_subschema(up)?;
|
||||
let Some(root) = self.roots.get(&up.url) else {
|
||||
return Err(CompileError::Bug("or_load didn't add".into()));
|
||||
};
|
||||
let doc = self.roots.loader.load(&root.url)?;
|
||||
let v = up.lookup(doc)?;
|
||||
let sch = self.compile_value(target, v, &up.clone(), root, &mut queue)?;
|
||||
compiled.push(sch);
|
||||
self.roots.insert(&mut queue.roots);
|
||||
}
|
||||
|
||||
target.insert(queue.schemas, compiled);
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
fn compile_value(
|
||||
&self,
|
||||
schemas: &Schemas,
|
||||
v: &Value,
|
||||
up: &UrlPtr,
|
||||
root: &Root,
|
||||
queue: &mut Queue,
|
||||
) -> Result<Schema, CompileError> {
|
||||
let mut s = Schema::new(up.to_string());
|
||||
s.draft_version = root.draft.version;
|
||||
|
||||
// we know it is already in queue, we just want to get its index
|
||||
let len = queue.schemas.len();
|
||||
s.idx = queue.enqueue_schema(schemas, up.to_owned());
|
||||
debug_assert_eq!(queue.schemas.len(), len, "{up} should already be in queue");
|
||||
|
||||
s.resource = {
|
||||
let base = UrlPtr {
|
||||
url: up.url.clone(),
|
||||
ptr: root.resource(&up.ptr).ptr.clone(),
|
||||
};
|
||||
queue.enqueue_schema(schemas, base)
|
||||
};
|
||||
|
||||
// if resource, enqueue dynamicAnchors for compilation
|
||||
if s.idx == s.resource && root.draft.version >= 2020 {
|
||||
let res = root.resource(&up.ptr);
|
||||
for (anchor, anchor_ptr) in &res.anchors {
|
||||
if res.dynamic_anchors.contains(anchor) {
|
||||
let up = UrlPtr {
|
||||
url: up.url.clone(),
|
||||
ptr: anchor_ptr.clone(),
|
||||
};
|
||||
let danchor_sch = queue.enqueue_schema(schemas, up);
|
||||
s.dynamic_anchors.insert(anchor.to_string(), danchor_sch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match v {
|
||||
Value::Object(obj) => {
|
||||
if obj.is_empty() {
|
||||
s.boolean = Some(true);
|
||||
} else {
|
||||
ObjCompiler {
|
||||
c: self,
|
||||
obj,
|
||||
up,
|
||||
schemas,
|
||||
root,
|
||||
queue,
|
||||
}
|
||||
.compile_obj(&mut s)?;
|
||||
}
|
||||
}
|
||||
Value::Bool(b) => s.boolean = Some(*b),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
s.all_props_evaluated = s.additional_properties.is_some();
|
||||
s.all_items_evaluated = if s.draft_version < 2020 {
|
||||
s.additional_items.is_some() || matches!(s.items, Some(Items::SchemaRef(_)))
|
||||
} else {
|
||||
s.items2020.is_some()
|
||||
};
|
||||
s.num_items_evaluated = if let Some(Items::SchemaRefs(list)) = &s.items {
|
||||
list.len()
|
||||
} else {
|
||||
s.prefix_items.len()
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
struct ObjCompiler<'c, 'v, 'l, 's, 'r, 'q> {
|
||||
c: &'c Compiler,
|
||||
obj: &'v Map<String, Value>,
|
||||
up: &'l UrlPtr,
|
||||
schemas: &'s Schemas,
|
||||
root: &'r Root,
|
||||
queue: &'q mut Queue,
|
||||
}
|
||||
|
||||
// compile supported drafts
|
||||
impl ObjCompiler<'_, '_, '_, '_, '_, '_> {
|
||||
fn compile_obj(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
self.compile_draft4(s)?;
|
||||
if self.draft_version() >= 6 {
|
||||
self.compile_draft6(s)?;
|
||||
}
|
||||
if self.draft_version() >= 7 {
|
||||
self.compile_draft7(s)?;
|
||||
}
|
||||
if self.draft_version() >= 2019 {
|
||||
self.compile_draft2019(s)?;
|
||||
}
|
||||
if self.draft_version() >= 2020 {
|
||||
self.compile_draft2020(s)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_draft4(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
if self.has_vocab("core") {
|
||||
s.ref_ = self.enqueue_ref("$ref")?;
|
||||
if s.ref_.is_some() && self.draft_version() < 2019 {
|
||||
// All other properties in a "$ref" object MUST be ignored
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_vocab("applicator") {
|
||||
s.all_of = self.enqueue_arr("allOf");
|
||||
s.any_of = self.enqueue_arr("anyOf");
|
||||
s.one_of = self.enqueue_arr("oneOf");
|
||||
s.not = self.enqueue_prop("not");
|
||||
|
||||
if self.draft_version() < 2020 {
|
||||
match self.value("items") {
|
||||
Some(Value::Array(_)) => {
|
||||
s.items = Some(Items::SchemaRefs(self.enqueue_arr("items")));
|
||||
s.additional_items = self.enquue_additional("additionalItems");
|
||||
}
|
||||
_ => s.items = self.enqueue_prop("items").map(Items::SchemaRef),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Object(props_obj)) = self.value("properties") {
|
||||
let mut properties = AHashMap::with_capacity(props_obj.len());
|
||||
for (pname, pvalue) in props_obj {
|
||||
let ptr = self.up.ptr.append2("properties", pname);
|
||||
let sch_idx = self.enqueue_schema(ptr);
|
||||
properties.insert(pname.clone(), sch_idx);
|
||||
|
||||
if let Some(prop_schema_obj) = pvalue.as_object() {
|
||||
if let Some(Value::Bool(true)) = prop_schema_obj.get("override") {
|
||||
s.override_properties.insert(pname.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
s.properties = properties;
|
||||
}
|
||||
s.pattern_properties = {
|
||||
let mut v = vec![];
|
||||
if let Some(Value::Object(obj)) = self.value("patternProperties") {
|
||||
for pname in obj.keys() {
|
||||
let ecma =
|
||||
ecma::convert(pname).map_err(|src| CompileError::InvalidRegex {
|
||||
url: self.up.format("patternProperties"),
|
||||
regex: pname.to_owned(),
|
||||
src,
|
||||
})?;
|
||||
let regex =
|
||||
Regex::new(ecma.as_ref()).map_err(|e| CompileError::InvalidRegex {
|
||||
url: self.up.format("patternProperties"),
|
||||
regex: ecma.into_owned(),
|
||||
src: e.into(),
|
||||
})?;
|
||||
let ptr = self.up.ptr.append2("patternProperties", pname);
|
||||
let sch = self.enqueue_schema(ptr);
|
||||
v.push((regex, sch));
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
s.additional_properties = self.enquue_additional("additionalProperties");
|
||||
|
||||
if let Some(Value::Object(deps)) = self.value("dependencies") {
|
||||
s.dependencies = deps
|
||||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
let v = match v {
|
||||
Value::Array(_) => Some(Dependency::Props(to_strings(v))),
|
||||
_ => {
|
||||
let ptr = self.up.ptr.append2("dependencies", k);
|
||||
Some(Dependency::SchemaRef(self.enqueue_schema(ptr)))
|
||||
}
|
||||
};
|
||||
v.map(|v| (k.clone(), v))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_vocab("validation") {
|
||||
match self.value("type") {
|
||||
Some(Value::String(t)) => {
|
||||
if let Some(t) = Type::from_str(t) {
|
||||
s.types.add(t)
|
||||
}
|
||||
}
|
||||
Some(Value::Array(arr)) => {
|
||||
for t in arr {
|
||||
if let Value::String(t) = t {
|
||||
if let Some(t) = Type::from_str(t) {
|
||||
s.types.add(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(Value::Array(e)) = self.value("enum") {
|
||||
let mut types = Types::default();
|
||||
for item in e {
|
||||
types.add(Type::of(item));
|
||||
}
|
||||
s.enum_ = Some(Enum {
|
||||
types,
|
||||
values: e.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
s.multiple_of = self.num("multipleOf");
|
||||
|
||||
s.maximum = self.num("maximum");
|
||||
if let Some(Value::Bool(exclusive)) = self.value("exclusiveMaximum") {
|
||||
if *exclusive {
|
||||
s.exclusive_maximum = s.maximum.take();
|
||||
}
|
||||
} else {
|
||||
s.exclusive_maximum = self.num("exclusiveMaximum");
|
||||
}
|
||||
|
||||
s.minimum = self.num("minimum");
|
||||
if let Some(Value::Bool(exclusive)) = self.value("exclusiveMinimum") {
|
||||
if *exclusive {
|
||||
s.exclusive_minimum = s.minimum.take();
|
||||
}
|
||||
} else {
|
||||
s.exclusive_minimum = self.num("exclusiveMinimum");
|
||||
}
|
||||
|
||||
s.max_length = self.usize("maxLength");
|
||||
s.min_length = self.usize("minLength");
|
||||
|
||||
if let Some(Value::String(p)) = self.value("pattern") {
|
||||
let p = ecma::convert(p).map_err(CompileError::Bug)?;
|
||||
s.pattern = Some(Regex::new(p.as_ref()).map_err(|e| CompileError::Bug(e.into()))?);
|
||||
}
|
||||
|
||||
s.max_items = self.usize("maxItems");
|
||||
s.min_items = self.usize("minItems");
|
||||
s.unique_items = self.bool("uniqueItems");
|
||||
|
||||
s.max_properties = self.usize("maxProperties");
|
||||
s.min_properties = self.usize("minProperties");
|
||||
|
||||
if let Some(req) = self.value("required") {
|
||||
s.required = to_strings(req);
|
||||
}
|
||||
}
|
||||
|
||||
// format --
|
||||
if self.c.assert_format
|
||||
|| self.has_vocab(match self.draft_version().cmp(&2019) {
|
||||
Ordering::Less => "core",
|
||||
Ordering::Equal => "format",
|
||||
Ordering::Greater => "format-assertion",
|
||||
})
|
||||
{
|
||||
if let Some(Value::String(format)) = self.value("format") {
|
||||
s.format = self
|
||||
.c
|
||||
.formats
|
||||
.get(format.as_str())
|
||||
.or_else(|| FORMATS.get(format.as_str()))
|
||||
.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_draft6(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
if self.has_vocab("applicator") {
|
||||
s.contains = self.enqueue_prop("contains");
|
||||
s.property_names = self.enqueue_prop("propertyNames");
|
||||
}
|
||||
|
||||
if self.has_vocab("validation") {
|
||||
s.constant = self.value("const").cloned();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_draft7(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
if self.has_vocab("applicator") {
|
||||
s.if_ = self.enqueue_prop("if");
|
||||
if s.if_.is_some() {
|
||||
if !self.bool_schema("if", false) {
|
||||
s.then = self.enqueue_prop("then");
|
||||
}
|
||||
if !self.bool_schema("if", true) {
|
||||
s.else_ = self.enqueue_prop("else");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.c.assert_content {
|
||||
if let Some(Value::String(encoding)) = self.value("contentEncoding") {
|
||||
s.content_encoding = self
|
||||
.c
|
||||
.decoders
|
||||
.get(encoding.as_str())
|
||||
.or_else(|| DECODERS.get(encoding.as_str()))
|
||||
.cloned();
|
||||
}
|
||||
|
||||
if let Some(Value::String(media_type)) = self.value("contentMediaType") {
|
||||
s.content_media_type = self
|
||||
.c
|
||||
.media_types
|
||||
.get(media_type.as_str())
|
||||
.or_else(|| MEDIA_TYPES.get(media_type.as_str()))
|
||||
.cloned();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_draft2019(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
if self.has_vocab("core") {
|
||||
s.recursive_ref = self.enqueue_ref("$recursiveRef")?;
|
||||
s.recursive_anchor = self.bool("$recursiveAnchor");
|
||||
}
|
||||
|
||||
if self.has_vocab("validation") {
|
||||
if s.contains.is_some() {
|
||||
s.max_contains = self.usize("maxContains");
|
||||
s.min_contains = self.usize("minContains");
|
||||
}
|
||||
|
||||
if let Some(Value::Object(dep_req)) = self.value("dependentRequired") {
|
||||
for (pname, pvalue) in dep_req {
|
||||
s.dependent_required
|
||||
.push((pname.clone(), to_strings(pvalue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_vocab("applicator") {
|
||||
s.dependent_schemas = self.enqueue_map("dependentSchemas");
|
||||
}
|
||||
|
||||
if self.has_vocab(match self.draft_version() {
|
||||
2019 => "applicator",
|
||||
_ => "unevaluated",
|
||||
}) {
|
||||
s.unevaluated_items = self.enqueue_prop("unevaluatedItems");
|
||||
s.unevaluated_properties = self.enqueue_prop("unevaluatedProperties");
|
||||
}
|
||||
|
||||
if self.c.assert_content
|
||||
&& s.content_media_type
|
||||
.map(|mt| mt.json_compatible)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
s.content_schema = self.enqueue_prop("contentSchema");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_draft2020(&mut self, s: &mut Schema) -> Result<(), CompileError> {
|
||||
if self.has_vocab("core") {
|
||||
if let Some(sch) = self.enqueue_ref("$dynamicRef")? {
|
||||
if let Some(Value::String(dref)) = self.value("$dynamicRef") {
|
||||
let Ok((_, frag)) = Fragment::split(dref) else {
|
||||
let loc = self.up.format("$dynamicRef");
|
||||
return Err(CompileError::ParseAnchorError { loc });
|
||||
};
|
||||
let anchor = match frag {
|
||||
Fragment::Anchor(Anchor(s)) => Some(s),
|
||||
Fragment::JsonPointer(_) => None,
|
||||
};
|
||||
s.dynamic_ref = Some(DynamicRef { sch, anchor });
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(Value::String(anchor)) = self.value("$dynamicAnchor") {
|
||||
s.dynamic_anchor = Some(anchor.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_vocab("applicator") {
|
||||
s.prefix_items = self.enqueue_arr("prefixItems");
|
||||
s.items2020 = self.enqueue_prop("items");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// enqueue helpers
|
||||
impl ObjCompiler<'_, '_, '_, '_, '_, '_> {
|
||||
fn enqueue_schema(&mut self, ptr: JsonPointer) -> SchemaIndex {
|
||||
let up = UrlPtr {
|
||||
url: self.up.url.clone(),
|
||||
ptr,
|
||||
};
|
||||
self.queue.enqueue_schema(self.schemas, up)
|
||||
}
|
||||
|
||||
fn enqueue_prop(&mut self, pname: &'static str) -> Option<SchemaIndex> {
|
||||
if self.obj.contains_key(pname) {
|
||||
let ptr = self.up.ptr.append(pname);
|
||||
Some(self.enqueue_schema(ptr))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_arr(&mut self, pname: &'static str) -> Vec<SchemaIndex> {
|
||||
if let Some(Value::Array(arr)) = self.obj.get(pname) {
|
||||
(0..arr.len())
|
||||
.map(|i| {
|
||||
let ptr = self.up.ptr.append2(pname, &i.to_string());
|
||||
self.enqueue_schema(ptr)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_map<T>(&mut self, pname: &'static str) -> T
|
||||
where
|
||||
T: Default,
|
||||
T: FromIterator<(String, SchemaIndex)>,
|
||||
{
|
||||
if let Some(Value::Object(obj)) = self.obj.get(pname) {
|
||||
obj.keys()
|
||||
.map(|k| {
|
||||
let ptr = self.up.ptr.append2(pname, k);
|
||||
(k.clone(), self.enqueue_schema(ptr))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
T::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_ref(&mut self, pname: &str) -> Result<Option<SchemaIndex>, CompileError> {
|
||||
let Some(Value::String(ref_)) = self.obj.get(pname) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let base_url = self.root.base_url(&self.up.ptr);
|
||||
let abs_ref = UrlFrag::join(base_url, ref_)?;
|
||||
if let Some(resolved_ref) = self.root.resolve(&abs_ref)? {
|
||||
// local ref
|
||||
return Ok(Some(self.enqueue_schema(resolved_ref.ptr)));
|
||||
}
|
||||
// remote ref
|
||||
let up = self.queue.resolve_anchor(abs_ref, &self.c.roots)?;
|
||||
Ok(Some(self.queue.enqueue_schema(self.schemas, up)))
|
||||
}
|
||||
|
||||
fn enquue_additional(&mut self, pname: &'static str) -> Option<Additional> {
|
||||
if let Some(Value::Bool(b)) = self.obj.get(pname) {
|
||||
Some(Additional::Bool(*b))
|
||||
} else {
|
||||
self.enqueue_prop(pname).map(Additional::SchemaRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// query helpers
|
||||
impl<'v> ObjCompiler<'_, 'v, '_, '_, '_, '_> {
|
||||
fn draft_version(&self) -> usize {
|
||||
self.root.draft.version
|
||||
}
|
||||
|
||||
fn has_vocab(&self, name: &str) -> bool {
|
||||
self.root.has_vocab(name)
|
||||
}
|
||||
|
||||
fn value(&self, pname: &str) -> Option<&'v Value> {
|
||||
self.obj.get(pname)
|
||||
}
|
||||
|
||||
fn bool(&self, pname: &str) -> bool {
|
||||
matches!(self.obj.get(pname), Some(Value::Bool(true)))
|
||||
}
|
||||
|
||||
fn usize(&self, pname: &str) -> Option<usize> {
|
||||
let Some(Value::Number(n)) = self.obj.get(pname) else {
|
||||
return None;
|
||||
};
|
||||
if n.is_u64() {
|
||||
n.as_u64().map(|n| n as usize)
|
||||
} else {
|
||||
n.as_f64()
|
||||
.filter(|n| n.is_sign_positive() && n.fract() == 0.0)
|
||||
.map(|n| n as usize)
|
||||
}
|
||||
}
|
||||
|
||||
fn num(&self, pname: &str) -> Option<Number> {
|
||||
if let Some(Value::Number(n)) = self.obj.get(pname) {
|
||||
Some(n.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_schema(&self, pname: &str, b: bool) -> bool {
|
||||
if let Some(Value::Bool(v)) = self.obj.get(pname) {
|
||||
return *v == b;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for compilation failures.
|
||||
#[derive(Debug)]
|
||||
pub enum CompileError {
|
||||
/// Error in parsing `url`.
|
||||
ParseUrlError { url: String, src: Box<dyn Error> },
|
||||
|
||||
/// Failed loading `url`.
|
||||
LoadUrlError { url: String, src: Box<dyn Error> },
|
||||
|
||||
/// no [`UrlLoader`] registered for the `url`
|
||||
UnsupportedUrlScheme { url: String },
|
||||
|
||||
/// Error in parsing `$schema` url.
|
||||
InvalidMetaSchemaUrl { url: String, src: Box<dyn Error> },
|
||||
|
||||
/// draft `url` is not supported
|
||||
UnsupportedDraft { url: String },
|
||||
|
||||
/// Cycle in resolving `$schema` in `url`.
|
||||
MetaSchemaCycle { url: String },
|
||||
|
||||
/// `url` is not valid against metaschema.
|
||||
ValidationError {
|
||||
url: String,
|
||||
src: ValidationError<'static, 'static>,
|
||||
},
|
||||
|
||||
/// Error in parsing id at `loc`
|
||||
ParseIdError { loc: String },
|
||||
|
||||
/// Error in parsing anchor at `loc`
|
||||
ParseAnchorError { loc: String },
|
||||
|
||||
/// Duplicate id `id` in `url` at `ptr1` and `ptr2`.
|
||||
DuplicateId {
|
||||
url: String,
|
||||
id: String,
|
||||
ptr1: String,
|
||||
ptr2: String,
|
||||
},
|
||||
|
||||
/// Duplicate anchor `anchor` in `url` at `ptr1` and `ptr2`.
|
||||
DuplicateAnchor {
|
||||
anchor: String,
|
||||
url: String,
|
||||
ptr1: String,
|
||||
ptr2: String,
|
||||
},
|
||||
|
||||
/// Not a valid json pointer.
|
||||
InvalidJsonPointer(String),
|
||||
|
||||
/// JsonPointer evaluated to nothing.
|
||||
JsonPointerNotFound(String),
|
||||
|
||||
/// anchor in `reference` not found in `url`.
|
||||
AnchorNotFound { url: String, reference: String },
|
||||
|
||||
/// Unsupported vocabulary `vocabulary` in `url`.
|
||||
UnsupportedVocabulary { url: String, vocabulary: String },
|
||||
|
||||
/// Invalid Regex `regex` at `url`.
|
||||
InvalidRegex {
|
||||
url: String,
|
||||
regex: String,
|
||||
src: Box<dyn Error>,
|
||||
},
|
||||
|
||||
/// Encountered bug in compiler implementation. Please report
|
||||
/// this as an issue for this crate.
|
||||
Bug(Box<dyn Error>),
|
||||
}
|
||||
|
||||
impl Error for CompileError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::ParseUrlError { src, .. } => Some(src.as_ref()),
|
||||
Self::LoadUrlError { src, .. } => Some(src.as_ref()),
|
||||
Self::InvalidMetaSchemaUrl { src, .. } => Some(src.as_ref()),
|
||||
Self::ValidationError { src, .. } => Some(src),
|
||||
Self::Bug(src) => Some(src.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CompileError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ParseUrlError { url, src } => {
|
||||
if f.alternate() {
|
||||
write!(f, "error parsing url {url}: {src}")
|
||||
} else {
|
||||
write!(f, "error parsing {url}")
|
||||
}
|
||||
}
|
||||
Self::LoadUrlError { url, src } => {
|
||||
if f.alternate() {
|
||||
write!(f, "error loading {url}: {src}")
|
||||
} else {
|
||||
write!(f, "error loading {url}")
|
||||
}
|
||||
}
|
||||
Self::UnsupportedUrlScheme { url } => write!(f, "unsupported scheme in {url}"),
|
||||
Self::InvalidMetaSchemaUrl { url, src } => {
|
||||
if f.alternate() {
|
||||
write!(f, "invalid $schema in {url}: {src}")
|
||||
} else {
|
||||
write!(f, "invalid $schema in {url}")
|
||||
}
|
||||
}
|
||||
Self::UnsupportedDraft { url } => write!(f, "draft {url} is not supported"),
|
||||
Self::MetaSchemaCycle { url } => {
|
||||
write!(f, "cycle in resolving $schema in {url}")
|
||||
}
|
||||
Self::ValidationError { url, src } => {
|
||||
if f.alternate() {
|
||||
write!(f, "{url} is not valid against metaschema: {src}")
|
||||
} else {
|
||||
write!(f, "{url} is not valid against metaschema")
|
||||
}
|
||||
}
|
||||
Self::ParseIdError { loc } => write!(f, "error in parsing id at {loc}"),
|
||||
Self::ParseAnchorError { loc } => write!(f, "error in parsing anchor at {loc}"),
|
||||
Self::DuplicateId {
|
||||
url,
|
||||
id,
|
||||
ptr1,
|
||||
ptr2,
|
||||
} => write!(f, "duplicate $id {id} in {url} at {ptr1:?} and {ptr2:?}"),
|
||||
Self::DuplicateAnchor {
|
||||
anchor,
|
||||
url,
|
||||
ptr1,
|
||||
ptr2,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"duplicate anchor {anchor:?} in {url} at {ptr1:?} and {ptr2:?}"
|
||||
)
|
||||
}
|
||||
Self::InvalidJsonPointer(loc) => write!(f, "invalid json-pointer {loc}"),
|
||||
Self::JsonPointerNotFound(loc) => write!(f, "json-pointer in {loc} not found"),
|
||||
Self::AnchorNotFound { url, reference } => {
|
||||
write!(
|
||||
f,
|
||||
"anchor in reference {reference} is not found in schema {url}"
|
||||
)
|
||||
}
|
||||
Self::UnsupportedVocabulary { url, vocabulary } => {
|
||||
write!(f, "unsupported vocabulary {vocabulary} in {url}")
|
||||
}
|
||||
Self::InvalidRegex { url, regex, src } => {
|
||||
if f.alternate() {
|
||||
write!(f, "invalid regex {} at {url}: {src}", quote(regex))
|
||||
} else {
|
||||
write!(f, "invalid regex {} at {url}", quote(regex))
|
||||
}
|
||||
}
|
||||
Self::Bug(src) => {
|
||||
write!(
|
||||
f,
|
||||
"encountered bug in jsonschema compiler. please report: {src}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helpers --
|
||||
|
||||
fn to_strings(v: &Value) -> Vec<String> {
|
||||
if let Value::Array(a) = v {
|
||||
a.iter()
|
||||
.filter_map(|t| {
|
||||
if let Value::String(t) = t {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Queue {
|
||||
pub(crate) schemas: Vec<UrlPtr>,
|
||||
pub(crate) roots: HashMap<Url, Root>,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
schemas: vec![],
|
||||
roots: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_anchor(
|
||||
&mut self,
|
||||
uf: UrlFrag,
|
||||
roots: &Roots,
|
||||
) -> Result<UrlPtr, CompileError> {
|
||||
match uf.frag {
|
||||
Fragment::JsonPointer(ptr) => Ok(UrlPtr { url: uf.url, ptr }),
|
||||
Fragment::Anchor(_) => {
|
||||
let root = match roots.get(&uf.url).or_else(|| self.roots.get(&uf.url)) {
|
||||
Some(root) => root,
|
||||
None => {
|
||||
let doc = roots.loader.load(&uf.url)?;
|
||||
let r = roots.create_root(uf.url.clone(), doc)?;
|
||||
self.roots.entry(uf.url).or_insert(r)
|
||||
}
|
||||
};
|
||||
root.resolve_fragment(&uf.frag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enqueue_schema(&mut self, schemas: &Schemas, up: UrlPtr) -> SchemaIndex {
|
||||
if let Some(sch) = schemas.get_by_loc(&up) {
|
||||
// already got compiled
|
||||
return sch.idx;
|
||||
}
|
||||
if let Some(qindex) = self.schemas.iter().position(|e| *e == up) {
|
||||
// already queued for compilation
|
||||
return SchemaIndex(schemas.size() + qindex);
|
||||
}
|
||||
|
||||
// new compilation request
|
||||
self.schemas.push(up);
|
||||
SchemaIndex(schemas.size() + self.schemas.len() - 1)
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
use base64::Engine;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::de::IgnoredAny;
|
||||
use serde_json::Value;
|
||||
|
||||
// decoders --
|
||||
|
||||
/// Defines Decoder for `contentEncoding`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Decoder {
|
||||
/// Name of the encoding
|
||||
pub name: &'static str,
|
||||
|
||||
/// Decodes given string to bytes
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub func: fn(s: &str) -> Result<Vec<u8>, Box<dyn Error>>,
|
||||
}
|
||||
|
||||
pub(crate) static DECODERS: Lazy<HashMap<&'static str, Decoder>> = Lazy::new(|| {
|
||||
let mut m = HashMap::<&'static str, Decoder>::new();
|
||||
m.insert(
|
||||
"base64",
|
||||
Decoder {
|
||||
name: "base64",
|
||||
func: decode_base64,
|
||||
},
|
||||
);
|
||||
m
|
||||
});
|
||||
|
||||
fn decode_base64(s: &str) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
Ok(base64::engine::general_purpose::STANDARD.decode(s)?)
|
||||
}
|
||||
|
||||
// mediatypes --
|
||||
|
||||
/// Defines Mediatype for `contentMediaType`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MediaType {
|
||||
/// Name of this media-type as defined in RFC 2046.
|
||||
/// Example: `application/json`
|
||||
pub name: &'static str,
|
||||
|
||||
/// whether this media type can be deserialized to json. If so it can
|
||||
/// be validated by `contentSchema` keyword.
|
||||
pub json_compatible: bool,
|
||||
|
||||
/**
|
||||
Check whether `bytes` conforms to this media-type.
|
||||
|
||||
Should return `Ok(Some(Value))` if `deserialize` is `true`, otherwise it can return `Ok(None)`.
|
||||
Ideally you could deserialize to `serde::de::IgnoredAny` if `deserialize` is `false` to gain
|
||||
some performance.
|
||||
|
||||
`deserialize` is always `false` if `json_compatible` is `false`.
|
||||
*/
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub func: fn(bytes: &[u8], deserialize: bool) -> Result<Option<Value>, Box<dyn Error>>,
|
||||
}
|
||||
|
||||
pub(crate) static MEDIA_TYPES: Lazy<HashMap<&'static str, MediaType>> = Lazy::new(|| {
|
||||
let mut m = HashMap::<&'static str, MediaType>::new();
|
||||
m.insert(
|
||||
"application/json",
|
||||
MediaType {
|
||||
name: "application/json",
|
||||
json_compatible: true,
|
||||
func: check_json,
|
||||
},
|
||||
);
|
||||
m
|
||||
});
|
||||
|
||||
fn check_json(bytes: &[u8], deserialize: bool) -> Result<Option<Value>, Box<dyn Error>> {
|
||||
if deserialize {
|
||||
return Ok(Some(serde_json::from_slice(bytes)?));
|
||||
}
|
||||
serde_json::from_slice::<IgnoredAny>(bytes)?;
|
||||
Ok(None)
|
||||
}
|
||||
@ -1,576 +0,0 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::{Map, Value};
|
||||
use url::Url;
|
||||
|
||||
use crate::{compiler::*, root::Resource, util::*, SchemaIndex, Schemas};
|
||||
|
||||
const POS_SELF: u8 = 1 << 0;
|
||||
const POS_PROP: u8 = 1 << 1;
|
||||
const POS_ITEM: u8 = 1 << 2;
|
||||
|
||||
pub(crate) static DRAFT4: Lazy<Draft> = Lazy::new(|| Draft {
|
||||
version: 4,
|
||||
id: "id",
|
||||
url: "http://json-schema.org/draft-04/schema",
|
||||
subschemas: HashMap::from([
|
||||
// type agnostic
|
||||
("definitions", POS_PROP),
|
||||
("not", POS_SELF),
|
||||
("allOf", POS_ITEM),
|
||||
("anyOf", POS_ITEM),
|
||||
("oneOf", POS_ITEM),
|
||||
// object
|
||||
("properties", POS_PROP),
|
||||
("additionalProperties", POS_SELF),
|
||||
("patternProperties", POS_PROP),
|
||||
// array
|
||||
("items", POS_SELF | POS_ITEM),
|
||||
("additionalItems", POS_SELF),
|
||||
("dependencies", POS_PROP),
|
||||
]),
|
||||
vocab_prefix: "",
|
||||
all_vocabs: vec![],
|
||||
default_vocabs: vec![],
|
||||
});
|
||||
|
||||
pub(crate) static DRAFT6: Lazy<Draft> = Lazy::new(|| {
|
||||
let mut subschemas = DRAFT4.subschemas.clone();
|
||||
subschemas.extend([("propertyNames", POS_SELF), ("contains", POS_SELF)]);
|
||||
Draft {
|
||||
version: 6,
|
||||
id: "$id",
|
||||
url: "http://json-schema.org/draft-06/schema",
|
||||
subschemas,
|
||||
vocab_prefix: "",
|
||||
all_vocabs: vec![],
|
||||
default_vocabs: vec![],
|
||||
}
|
||||
});
|
||||
|
||||
pub(crate) static DRAFT7: Lazy<Draft> = Lazy::new(|| {
|
||||
let mut subschemas = DRAFT6.subschemas.clone();
|
||||
subschemas.extend([("if", POS_SELF), ("then", POS_SELF), ("else", POS_SELF)]);
|
||||
Draft {
|
||||
version: 7,
|
||||
id: "$id",
|
||||
url: "http://json-schema.org/draft-07/schema",
|
||||
subschemas,
|
||||
vocab_prefix: "",
|
||||
all_vocabs: vec![],
|
||||
default_vocabs: vec![],
|
||||
}
|
||||
});
|
||||
|
||||
pub(crate) static DRAFT2019: Lazy<Draft> = Lazy::new(|| {
|
||||
let mut subschemas = DRAFT7.subschemas.clone();
|
||||
subschemas.extend([
|
||||
("$defs", POS_PROP),
|
||||
("dependentSchemas", POS_PROP),
|
||||
("unevaluatedProperties", POS_SELF),
|
||||
("unevaluatedItems", POS_SELF),
|
||||
("contentSchema", POS_SELF),
|
||||
]);
|
||||
Draft {
|
||||
version: 2019,
|
||||
id: "$id",
|
||||
url: "https://json-schema.org/draft/2019-09/schema",
|
||||
subschemas,
|
||||
vocab_prefix: "https://json-schema.org/draft/2019-09/vocab/",
|
||||
all_vocabs: vec![
|
||||
"core",
|
||||
"applicator",
|
||||
"validation",
|
||||
"meta-data",
|
||||
"format",
|
||||
"content",
|
||||
],
|
||||
default_vocabs: vec!["core", "applicator", "validation"],
|
||||
}
|
||||
});
|
||||
|
||||
pub(crate) static DRAFT2020: Lazy<Draft> = Lazy::new(|| {
|
||||
let mut subschemas = DRAFT2019.subschemas.clone();
|
||||
subschemas.extend([("prefixItems", POS_ITEM)]);
|
||||
Draft {
|
||||
version: 2020,
|
||||
id: "$id",
|
||||
url: "https://json-schema.org/draft/2020-12/schema",
|
||||
subschemas,
|
||||
vocab_prefix: "https://json-schema.org/draft/2020-12/vocab/",
|
||||
all_vocabs: vec![
|
||||
"core",
|
||||
"applicator",
|
||||
"unevaluated",
|
||||
"validation",
|
||||
"meta-data",
|
||||
"format-annotation",
|
||||
"format-assertion",
|
||||
"content",
|
||||
],
|
||||
default_vocabs: vec!["core", "applicator", "unevaluated", "validation"],
|
||||
}
|
||||
});
|
||||
|
||||
pub(crate) static STD_METASCHEMAS: Lazy<Schemas> =
|
||||
Lazy::new(|| load_std_metaschemas().expect("std metaschemas must be compilable"));
|
||||
|
||||
pub(crate) fn latest() -> &'static Draft {
|
||||
crate::Draft::default().internal()
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
pub(crate) struct Draft {
|
||||
pub(crate) version: usize,
|
||||
pub(crate) url: &'static str,
|
||||
id: &'static str, // property name used to represent id
|
||||
subschemas: HashMap<&'static str, u8>, // location of subschemas
|
||||
pub(crate) vocab_prefix: &'static str, // prefix used for vocabulary
|
||||
pub(crate) all_vocabs: Vec<&'static str>, // names of supported vocabs
|
||||
pub(crate) default_vocabs: Vec<&'static str>, // names of default vocabs
|
||||
}
|
||||
|
||||
impl Draft {
|
||||
pub(crate) fn from_url(url: &str) -> Option<&'static Draft> {
|
||||
let (mut url, frag) = split(url);
|
||||
if !frag.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(s) = url.strip_prefix("http://") {
|
||||
url = s;
|
||||
}
|
||||
if let Some(s) = url.strip_prefix("https://") {
|
||||
url = s;
|
||||
}
|
||||
match url {
|
||||
"json-schema.org/schema" => Some(latest()),
|
||||
"json-schema.org/draft/2020-12/schema" => Some(&DRAFT2020),
|
||||
"json-schema.org/draft/2019-09/schema" => Some(&DRAFT2019),
|
||||
"json-schema.org/draft-07/schema" => Some(&DRAFT7),
|
||||
"json-schema.org/draft-06/schema" => Some(&DRAFT6),
|
||||
"json-schema.org/draft-04/schema" => Some(&DRAFT4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_schema(&self) -> Option<SchemaIndex> {
|
||||
let url = match self.version {
|
||||
2020 => "https://json-schema.org/draft/2020-12/schema",
|
||||
2019 => "https://json-schema.org/draft/2019-09/schema",
|
||||
7 => "http://json-schema.org/draft-07/schema",
|
||||
6 => "http://json-schema.org/draft-06/schema",
|
||||
4 => "http://json-schema.org/draft-04/schema",
|
||||
_ => return None,
|
||||
};
|
||||
let up = UrlPtr {
|
||||
url: Url::parse(url).unwrap_or_else(|_| panic!("{url} should be valid url")),
|
||||
ptr: "".into(),
|
||||
};
|
||||
STD_METASCHEMAS.get_by_loc(&up).map(|s| s.idx)
|
||||
}
|
||||
|
||||
pub(crate) fn validate(&self, up: &UrlPtr, v: &Value) -> Result<(), CompileError> {
|
||||
let Some(sch) = self.get_schema() else {
|
||||
return Err(CompileError::Bug(
|
||||
format!("no metaschema preloaded for draft {}", self.version).into(),
|
||||
));
|
||||
};
|
||||
STD_METASCHEMAS
|
||||
.validate(v, sch, None)
|
||||
.map_err(|src| CompileError::ValidationError {
|
||||
url: up.to_string(),
|
||||
src: src.clone_static(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_id<'a>(&self, obj: &'a Map<String, Value>) -> Option<&'a str> {
|
||||
if self.version < 2019 && obj.contains_key("$ref") {
|
||||
return None; // All other properties in a "$ref" object MUST be ignored
|
||||
}
|
||||
let Some(Value::String(id)) = obj.get(self.id) else {
|
||||
return None;
|
||||
};
|
||||
let (id, _) = split(id); // ignore fragment
|
||||
Some(id).filter(|id| !id.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn get_vocabs(
|
||||
&self,
|
||||
url: &Url,
|
||||
doc: &Value,
|
||||
) -> Result<Option<Vec<String>>, CompileError> {
|
||||
if self.version < 2019 {
|
||||
return Ok(None);
|
||||
}
|
||||
let Value::Object(obj) = doc else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(Value::Object(obj)) = obj.get("$vocabulary") else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut vocabs = vec![];
|
||||
for (vocab, reqd) in obj {
|
||||
if let Value::Bool(true) = reqd {
|
||||
let name = vocab
|
||||
.strip_prefix(self.vocab_prefix)
|
||||
.filter(|name| self.all_vocabs.contains(name));
|
||||
if let Some(name) = name {
|
||||
vocabs.push(name.to_owned()); // todo: avoid alloc
|
||||
} else {
|
||||
return Err(CompileError::UnsupportedVocabulary {
|
||||
url: url.as_str().to_owned(),
|
||||
vocabulary: vocab.to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(vocabs))
|
||||
}
|
||||
|
||||
// collects anchors/dynamic_achors from `sch` into `res`.
|
||||
// note this does not collect from subschemas in sch.
|
||||
pub(crate) fn collect_anchors(
|
||||
&self,
|
||||
sch: &Value,
|
||||
sch_ptr: &JsonPointer,
|
||||
res: &mut Resource,
|
||||
url: &Url,
|
||||
) -> Result<(), CompileError> {
|
||||
let Value::Object(obj) = sch else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut add_anchor = |anchor: Anchor| match res.anchors.entry(anchor) {
|
||||
Entry::Occupied(entry) => {
|
||||
if entry.get() == sch_ptr {
|
||||
// anchor with same root_ptr already exists
|
||||
return Ok(());
|
||||
}
|
||||
Err(CompileError::DuplicateAnchor {
|
||||
url: url.as_str().to_owned(),
|
||||
anchor: entry.key().to_string(),
|
||||
ptr1: entry.get().to_string(),
|
||||
ptr2: sch_ptr.to_string(),
|
||||
})
|
||||
}
|
||||
entry => {
|
||||
entry.or_insert(sch_ptr.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
if self.version < 2019 {
|
||||
if obj.contains_key("$ref") {
|
||||
return Ok(()); // All other properties in a "$ref" object MUST be ignored
|
||||
}
|
||||
// anchor is specified in id
|
||||
if let Some(Value::String(id)) = obj.get(self.id) {
|
||||
let Ok((_, frag)) = Fragment::split(id) else {
|
||||
let loc = UrlFrag::format(url, sch_ptr.as_str());
|
||||
return Err(CompileError::ParseAnchorError { loc });
|
||||
};
|
||||
if let Fragment::Anchor(anchor) = frag {
|
||||
add_anchor(anchor)?;
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if self.version >= 2019 {
|
||||
if let Some(Value::String(anchor)) = obj.get("$anchor") {
|
||||
add_anchor(anchor.as_str().into())?;
|
||||
}
|
||||
}
|
||||
if self.version >= 2020 {
|
||||
if let Some(Value::String(anchor)) = obj.get("$dynamicAnchor") {
|
||||
add_anchor(anchor.as_str().into())?;
|
||||
res.dynamic_anchors.insert(anchor.as_str().into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// error is json-ptr to invalid id
|
||||
pub(crate) fn collect_resources(
|
||||
&self,
|
||||
sch: &Value,
|
||||
base: &Url, // base of json
|
||||
sch_ptr: JsonPointer, // ptr of json
|
||||
url: &Url,
|
||||
resources: &mut HashMap<JsonPointer, Resource>,
|
||||
) -> Result<(), CompileError> {
|
||||
if resources.contains_key(&sch_ptr) {
|
||||
// resources are already collected
|
||||
return Ok(());
|
||||
}
|
||||
if let Value::Bool(_) = sch {
|
||||
if sch_ptr.is_empty() {
|
||||
// root resource
|
||||
resources.insert(sch_ptr.clone(), Resource::new(sch_ptr, base.clone()));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Value::Object(obj) = sch else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut base = base;
|
||||
let tmp;
|
||||
let res = if let Some(id) = self.get_id(obj) {
|
||||
let Ok(id) = UrlFrag::join(base, id) else {
|
||||
let loc = UrlFrag::format(url, sch_ptr.as_str());
|
||||
return Err(CompileError::ParseIdError { loc });
|
||||
};
|
||||
tmp = id.url;
|
||||
base = &tmp;
|
||||
Some(Resource::new(sch_ptr.clone(), base.clone()))
|
||||
} else if sch_ptr.is_empty() {
|
||||
// root resource
|
||||
Some(Resource::new(sch_ptr.clone(), base.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(res) = res {
|
||||
if let Some(dup) = resources.values_mut().find(|res| res.id == *base) {
|
||||
return Err(CompileError::DuplicateId {
|
||||
url: url.to_string(),
|
||||
id: base.to_string(),
|
||||
ptr1: res.ptr.to_string(),
|
||||
ptr2: dup.ptr.to_string(),
|
||||
});
|
||||
}
|
||||
resources.insert(sch_ptr.clone(), res);
|
||||
}
|
||||
|
||||
// collect anchors into base resource
|
||||
if let Some(res) = resources.values_mut().find(|res| res.id == *base) {
|
||||
self.collect_anchors(sch, &sch_ptr, res, url)?;
|
||||
} else {
|
||||
debug_assert!(false, "base resource must exist");
|
||||
}
|
||||
|
||||
for (&kw, &pos) in &self.subschemas {
|
||||
let Some(v) = obj.get(kw) else {
|
||||
continue;
|
||||
};
|
||||
if pos & POS_SELF != 0 {
|
||||
let ptr = sch_ptr.append(kw);
|
||||
self.collect_resources(v, base, ptr, url, resources)?;
|
||||
}
|
||||
if pos & POS_ITEM != 0 {
|
||||
if let Value::Array(arr) = v {
|
||||
for (i, item) in arr.iter().enumerate() {
|
||||
let ptr = sch_ptr.append2(kw, &i.to_string());
|
||||
self.collect_resources(item, base, ptr, url, resources)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if pos & POS_PROP != 0 {
|
||||
if let Value::Object(obj) = v {
|
||||
for (pname, pvalue) in obj {
|
||||
let ptr = sch_ptr.append2(kw, pname);
|
||||
self.collect_resources(pvalue, base, ptr, url, resources)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_subschema(&self, ptr: &str) -> bool {
|
||||
if ptr.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn split(mut ptr: &str) -> (&str, &str) {
|
||||
ptr = &ptr[1..]; // rm `/` prefix
|
||||
if let Some(i) = ptr.find('/') {
|
||||
(&ptr[..i], &ptr[i..])
|
||||
} else {
|
||||
(ptr, "")
|
||||
}
|
||||
}
|
||||
|
||||
let (tok, ptr) = split(ptr);
|
||||
|
||||
if let Some(&pos) = self.subschemas.get(tok) {
|
||||
if pos & POS_SELF != 0 && self.is_subschema(ptr) {
|
||||
return true;
|
||||
}
|
||||
if !ptr.is_empty() {
|
||||
if pos & POS_PROP != 0 {
|
||||
let (_, ptr) = split(ptr);
|
||||
if self.is_subschema(ptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if pos & POS_ITEM != 0 {
|
||||
let (tok, ptr) = split(ptr);
|
||||
if usize::from_str(tok).is_ok() && self.is_subschema(ptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn load_std_metaschemas() -> Result<Schemas, CompileError> {
|
||||
let mut schemas = Schemas::new();
|
||||
let mut compiler = Compiler::new();
|
||||
compiler.enable_format_assertions();
|
||||
compiler.compile("https://json-schema.org/draft/2020-12/schema", &mut schemas)?;
|
||||
compiler.compile("https://json-schema.org/draft/2019-09/schema", &mut schemas)?;
|
||||
compiler.compile("http://json-schema.org/draft-07/schema", &mut schemas)?;
|
||||
compiler.compile("http://json-schema.org/draft-06/schema", &mut schemas)?;
|
||||
compiler.compile("http://json-schema.org/draft-04/schema", &mut schemas)?;
|
||||
Ok(schemas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Compiler, Schemas};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_meta() {
|
||||
let mut schemas = Schemas::default();
|
||||
let mut compiler = Compiler::default();
|
||||
let v: Value = serde_json::from_str(include_str!("metaschemas/draft-04/schema")).unwrap();
|
||||
let url = "https://json-schema.org/draft-04/schema";
|
||||
compiler.add_resource(url, v).unwrap();
|
||||
compiler.compile(url, &mut schemas).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_url() {
|
||||
let tests = [
|
||||
("http://json-schema.org/draft/2020-12/schema", Some(2020)), // http url
|
||||
("https://json-schema.org/draft/2020-12/schema", Some(2020)), // https url
|
||||
("https://json-schema.org/schema", Some(latest().version)), // latest
|
||||
("https://json-schema.org/draft-04/schema", Some(4)),
|
||||
];
|
||||
for (url, version) in tests {
|
||||
let got = Draft::from_url(url).map(|d| d.version);
|
||||
assert_eq!(got, version, "for {url}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_ids() {
|
||||
let url = Url::parse("http://a.com/schema.json").unwrap();
|
||||
let json: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"id": "http://a.com/schemas/schema.json",
|
||||
"definitions": {
|
||||
"s1": { "id": "http://a.com/definitions/s1" },
|
||||
"s2": {
|
||||
"id": "../s2",
|
||||
"items": [
|
||||
{ "id": "http://c.com/item" },
|
||||
{ "id": "http://d.com/item" }
|
||||
]
|
||||
},
|
||||
"s3": {
|
||||
"definitions": {
|
||||
"s1": {
|
||||
"id": "s3",
|
||||
"items": {
|
||||
"id": "http://b.com/item"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"s4": { "id": "http://e.com/def#abcd" }
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let want = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("", "http://a.com/schemas/schema.json"); // root with id
|
||||
m.insert("/definitions/s1", "http://a.com/definitions/s1");
|
||||
m.insert("/definitions/s2", "http://a.com/s2"); // relative id
|
||||
m.insert("/definitions/s3/definitions/s1", "http://a.com/schemas/s3");
|
||||
m.insert("/definitions/s3/definitions/s1/items", "http://b.com/item");
|
||||
m.insert("/definitions/s2/items/0", "http://c.com/item");
|
||||
m.insert("/definitions/s2/items/1", "http://d.com/item");
|
||||
m.insert("/definitions/s4", "http://e.com/def"); // id with fragments
|
||||
m
|
||||
};
|
||||
let mut got = HashMap::new();
|
||||
DRAFT4
|
||||
.collect_resources(&json, &url, "".into(), &url, &mut got)
|
||||
.unwrap();
|
||||
let got = got
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.id.as_str()))
|
||||
.collect::<HashMap<&str, &str>>();
|
||||
assert_eq!(got, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_anchors() {
|
||||
let url = Url::parse("http://a.com/schema.json").unwrap();
|
||||
let json: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"$defs": {
|
||||
"s2": {
|
||||
"$id": "http://b.com",
|
||||
"$anchor": "b1",
|
||||
"items": [
|
||||
{ "$anchor": "b2" },
|
||||
{
|
||||
"$id": "http//c.com",
|
||||
"items": [
|
||||
{"$anchor": "c1"},
|
||||
{"$dynamicAnchor": "c2"}
|
||||
]
|
||||
},
|
||||
{ "$dynamicAnchor": "b3" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut resources = HashMap::new();
|
||||
DRAFT2020
|
||||
.collect_resources(&json, &url, "".into(), &url, &mut resources)
|
||||
.unwrap();
|
||||
assert!(resources.get("").unwrap().anchors.is_empty());
|
||||
assert_eq!(resources.get("/$defs/s2").unwrap().anchors, {
|
||||
let mut want = HashMap::new();
|
||||
want.insert("b1".into(), "/$defs/s2".into());
|
||||
want.insert("b2".into(), "/$defs/s2/items/0".into());
|
||||
want.insert("b3".into(), "/$defs/s2/items/2".into());
|
||||
want
|
||||
});
|
||||
assert_eq!(resources.get("/$defs/s2/items/1").unwrap().anchors, {
|
||||
let mut want = HashMap::new();
|
||||
want.insert("c1".into(), "/$defs/s2/items/1/items/0".into());
|
||||
want.insert("c2".into(), "/$defs/s2/items/1/items/1".into());
|
||||
want
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_subschema() {
|
||||
let tests = vec![("/allOf/0", true), ("/allOf/$defs", false)];
|
||||
for test in tests {
|
||||
let got = DRAFT2020.is_subschema(test.0);
|
||||
assert_eq!(got, test.1, "{}", test.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use regex_syntax::ast::parse::Parser;
|
||||
use regex_syntax::ast::{self, *};
|
||||
|
||||
// covert ecma regex to rust regex if possible
|
||||
// see https://262.ecma-international.org/11.0/#sec-regexp-regular-expression-objects
|
||||
pub(crate) fn convert(pattern: &str) -> Result<Cow<'_, str>, Box<dyn std::error::Error>> {
|
||||
let mut pattern = Cow::Borrowed(pattern);
|
||||
|
||||
let mut ast = loop {
|
||||
match Parser::new().parse(pattern.as_ref()) {
|
||||
Ok(ast) => break ast,
|
||||
Err(e) => {
|
||||
if let Some(s) = fix_error(&e) {
|
||||
pattern = Cow::Owned(s);
|
||||
} else {
|
||||
Err(e)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let translator = Translator {
|
||||
pat: pattern.as_ref(),
|
||||
out: None,
|
||||
};
|
||||
if let Some(updated_pattern) = ast::visit(&ast, translator)? {
|
||||
match Parser::new().parse(&updated_pattern) {
|
||||
Ok(updated_ast) => {
|
||||
pattern = Cow::Owned(updated_pattern);
|
||||
ast = updated_ast;
|
||||
}
|
||||
Err(e) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"ecma::translate changed {:?} to {:?}: {e}",
|
||||
pattern, updated_pattern
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
fn fix_error(e: &Error) -> Option<String> {
|
||||
if let ErrorKind::EscapeUnrecognized = e.kind() {
|
||||
let (start, end) = (e.span().start.offset, e.span().end.offset);
|
||||
let s = &e.pattern()[start..end];
|
||||
if let r"\c" = s {
|
||||
// handle \c{control_letter}
|
||||
if let Some(control_letter) = e.pattern()[end..].chars().next() {
|
||||
if control_letter.is_ascii_alphabetic() {
|
||||
return Some(format!(
|
||||
"{}{}{}",
|
||||
&e.pattern()[..start],
|
||||
((control_letter as u8) % 32) as char,
|
||||
&e.pattern()[end + 1..],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
handles following translations:
|
||||
- \d should ascii digits only. so replace with [0-9]
|
||||
- \D should match everything but ascii digits. so replace with [^0-9]
|
||||
- \w should match ascii letters only. so replace with [a-zA-Z0-9_]
|
||||
- \W should match everything but ascii letters. so replace with [^a-zA-Z0-9_]
|
||||
- \s and \S differences
|
||||
- \a is not an ECMA 262 control escape
|
||||
*/
|
||||
struct Translator<'a> {
|
||||
pat: &'a str,
|
||||
out: Option<String>,
|
||||
}
|
||||
|
||||
impl Translator<'_> {
|
||||
fn replace(&mut self, span: &Span, with: &str) {
|
||||
let (start, end) = (span.start.offset, span.end.offset);
|
||||
self.out = Some(format!("{}{with}{}", &self.pat[..start], &self.pat[end..]));
|
||||
}
|
||||
|
||||
fn replace_class_class(&mut self, perl: &ClassPerl) {
|
||||
match perl.kind {
|
||||
ClassPerlKind::Digit => {
|
||||
self.replace(&perl.span, if perl.negated { "[^0-9]" } else { "[0-9]" });
|
||||
}
|
||||
ClassPerlKind::Word => {
|
||||
let with = &if perl.negated {
|
||||
"[^A-Za-z0-9_]"
|
||||
} else {
|
||||
"[A-Za-z0-9_]"
|
||||
};
|
||||
self.replace(&perl.span, with);
|
||||
}
|
||||
ClassPerlKind::Space => {
|
||||
let with = &if perl.negated {
|
||||
"[^ \t\n\r\u{000b}\u{000c}\u{00a0}\u{feff}\u{2003}\u{2029}]"
|
||||
} else {
|
||||
"[ \t\n\r\u{000b}\u{000c}\u{00a0}\u{feff}\u{2003}\u{2029}]"
|
||||
};
|
||||
self.replace(&perl.span, with);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor for Translator<'_> {
|
||||
type Output = Option<String>;
|
||||
type Err = &'static str;
|
||||
|
||||
fn finish(self) -> Result<Self::Output, Self::Err> {
|
||||
Ok(self.out)
|
||||
}
|
||||
|
||||
fn visit_class_set_item_pre(&mut self, ast: &ast::ClassSetItem) -> Result<(), Self::Err> {
|
||||
if let ClassSetItem::Perl(perl) = ast {
|
||||
self.replace_class_class(perl);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_post(&mut self, ast: &Ast) -> Result<(), Self::Err> {
|
||||
if self.out.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
match ast {
|
||||
Ast::ClassPerl(perl) => {
|
||||
self.replace_class_class(perl);
|
||||
}
|
||||
Ast::Literal(literal) => {
|
||||
if let Literal {
|
||||
kind: LiteralKind::Special(SpecialLiteralKind::Bell),
|
||||
..
|
||||
} = literal.as_ref()
|
||||
{
|
||||
return Err("\\a is not an ECMA 262 control escape");
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ecma_compat_valid() {
|
||||
// println!("{:#?}", Parser::new().parse(r#"a\a"#));
|
||||
let tests = [
|
||||
(r"ab\cAcde\cBfg", "ab\u{1}cde\u{2}fg"), // \c{control_letter}
|
||||
(r"\\comment", r"\\comment"), // there is no \c
|
||||
(r"ab\def", r#"ab[0-9]ef"#), // \d
|
||||
(r"ab[a-z\d]ef", r#"ab[a-z[0-9]]ef"#), // \d inside classSet
|
||||
(r"ab\Def", r#"ab[^0-9]ef"#), // \d
|
||||
(r"ab[a-z\D]ef", r#"ab[a-z[^0-9]]ef"#), // \D inside classSet
|
||||
];
|
||||
for (input, want) in tests {
|
||||
match convert(input) {
|
||||
Ok(got) => {
|
||||
if got.as_ref() != want {
|
||||
panic!("convert({input:?}): got: {got:?}, want: {want:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("convert({input:?}) failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecma_compat_invalid() {
|
||||
// println!("{:#?}", Parser::new().parse(r#"a\a"#));
|
||||
let tests = [
|
||||
r"\c\n", // \c{invalid_char}
|
||||
r"abc\adef", // \a is not valid
|
||||
];
|
||||
for input in tests {
|
||||
if convert(input).is_ok() {
|
||||
panic!("convert({input:?}) mut fail");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user