Compare commits

...

84 Commits

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

BIN
.DS_Store vendored Normal file

Binary file not shown.

49
.agent/workflows/jspg.md Normal file
View 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.

13
.env
View File

@ -1,13 +0,0 @@
ENVIRONMENT=local
DATABASE_PASSWORD=tIr4TJ0qUwGVM0rlQSe3W7Tgpi33zPbk
DATABASE_ROLE=agreego_admin
DATABASE_HOST=127.1.27.4
DATABASE_PORT=5432
POSTGRES_PASSWORD=xzIq5JT0xY3F+2m1GtnrKDdK29sNSXVVYZHPKJVh8pI=
DATABASE_NAME=agreego
DEV_DATABASE_NAME=agreego_dev
GITEA_TOKEN=3d70c23673517330623a5122998fb304e3c73f0a
MOOV_ACCOUNT_ID=69a0d2f6-77a2-4e26-934f-d869134f87d3
MOOV_PUBLIC_KEY=9OMhK5qGnh7Tmk2Z
MOOV_SECRET_KEY=DrRox7B-YWfO9IheiUUX7lGP8-7VY-Ni
MOOV_DOMAIN=http://localhost

3
.geminiignore Normal file
View File

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

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
/target
/package
/package
.env
/src/tests.rs

3
.gitmodules vendored
View File

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

1758
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

130
GEMINI.md Normal file
View File

@ -0,0 +1,130 @@
# JSPG: JSON Schema Postgres
**JSPG** is a high-performance PostgreSQL extension for in-memory JSON Schema validation, specifically targeting **Draft 2020-12**.
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.
## 🎯 Goals
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.
## 🔌 API Reference
The extension exposes the following functions to PostgreSQL:
### `cache_json_schemas(enums jsonb, types jsonb, puncs jsonb) -> jsonb`
Loads and compiles the entire schema registry into the session's memory, atomically replacing the previous validator.
* **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.
### `mask_json_schema(schema_id text, instance jsonb) -> jsonb`
Validates a JSON instance and returns a new JSON object with unknown properties removed (pruned) based on the schema.
* **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.
### `validate_json_schema(schema_id text, instance jsonb) -> jsonb`
Validates a JSON instance against a pre-compiled schema.
* **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": [...]}`).
### `json_schema_cached(schema_id text) -> bool`
Checks if a specific schema ID is currently present in the cache.
### `clear_json_schemas() -> jsonb`
Clears the current session's schema cache, freeing memory.
### `show_json_schemas() -> jsonb`
Returns a debug dump of the currently cached schemas (for development/debugging).
## ✨ Custom Features & Deviations
JSPG implements specific extensions to the Draft 2020-12 standard to support the Punc architecture's object-oriented needs.
### 1. 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.
* **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.
### 2. Virtual Family References (`$family`)
To support polymorphic fields (e.g., a field that accepts any "User" type), JSPG generates virtual schemas representing type hierarchies.
* **Mechanism**: When caching types, if a type defines a `hierarchy` (e.g., `["entity", "organization", "person"]`), JSPG generates a virtual `oneOf` family containing refs to all valid descendants. These can be pointed to exclusively by using `{"$family": "organization"}`. Because `$family` is a macro-pointer that swaps in the virtual union, it **must** be used exclusively in its schema object; you cannot define other properties alongside it.
### 3. 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.
* **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`.
### 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".
### 5. Masking (Constructive Validation)
JSPG supports a "Constructive Validation" mode via `mask_json_schema`. This is designed for high-performance API responses where the schema dictates the exact shape of the returned data.
* **Mechanism**: The validator traverses the instance against the schema.
* **Valid Fields**: Kept in the output.
* **Unknown/Extra Fields**: Silently removed (pruned) if `extensible: false` (default).
* **Invalid Fields**: Still trigger standard validation errors.
This allows the database to return "raw" joined rows (e.g. `SELECT * FROM person JOIN organization ...`) and have JSPG automatically shape the result into the expected API response, removing any internal or unrelated columns not defined in the schema.
## 🏗️ Architecture
The extension is written in Rust using `pgrx` and structures its schema parser to mirror the Punc Generator's design:
* **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`.
### Concurrency & Threading ("Atomic Swap")
To support high-throughput validation while allowing for runtime schema updates (e.g., during development or hot-reloading), JSPG uses an **Atomic Swap** pattern.
1. **Immutable Validator**: The `Validator` struct immutably owns the `Registry`. Once created, a validator instance (and its registry) never changes.
2. **Global Pointer**: A global `RwLock<Option<Arc<Validator>>>` holds the current active validator.
3. **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.
4. **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`.

90
build.rs Normal file
View File

@ -0,0 +1,90 @@
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::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() {
// 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::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();
}
}
}
}
}
}

88
flow
View File

@ -1,53 +1,42 @@
#!/bin/bash
#!/usr/bin/env bash
# Flows
source ./flows/base
source ./flows/git
source ./flows/kube
source ./flows/packaging
source ./flows/rust
# Vars
POSTGRES_VERSION="17"
POSTGRES_VERSION="18"
POSTGRES_CONFIG_PATH="/opt/homebrew/opt/postgresql@${POSTGRES_VERSION}/bin/pg_config"
DEPENDENCIES+=(icu4c pkg-config "postgresql@${POSTGRES_VERSION}")
CARGO_DEPENDENCIES=(cargo-pgrx==0.14.0)
CARGO_DEPENDENCIES=(cargo-pgrx==0.16.1)
GITEA_ORGANIZATION="cellular"
GITEA_REPOSITORY="jspg"
env() {
# Check if GITEA_TOKEN is set
if [ -z "$GITEA_TOKEN" ]; then
# If not set, try to get it from kubectl
GITEA_TOKEN=$(kubectl get secret -n cellular gitea-git -o jsonpath='{.data.token}' | base64 --decode)
if [ -z "$GITEA_TOKEN" ]; then
error "GITEA_TOKEN is not set and couldn't be retrieved from kubectl" >&2
return 2
fi
export GITEA_TOKEN
fi
success "Environment variables set"
}
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 $?
@ -63,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() {
@ -79,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."
@ -89,31 +77,28 @@ install() {
pg_sharedir=$("$POSTGRES_CONFIG_PATH" --sharedir)
local pg_config_status=$?
if [ $pg_config_status -ne 0 ] || [ -z "$pg_sharedir" ]; then
error "Failed to determine PostgreSQL shared directory using pg_config."
return 2
abort "Failed to determine PostgreSQL shared directory using pg_config." 2
fi
local installed_control_path="${pg_sharedir}/extension/jspg.control"
# Modify the control file
if [ ! -f "$installed_control_path" ]; then
error "Installed control file not found: '$installed_control_path'"
return 2
abort "Installed control file not found: '$installed_control_path'" 2
fi
info "Modifying control file for non-superuser access: ${installed_control_path}"
# Use sed -i '' for macOS compatibility
if sed -i '' '/^superuser = false/d' "$installed_control_path" && \
echo 'trusted = true' >> "$installed_control_path"; then
success "Control file modified successfully."
else
error "Failed to modify control file: ${installed_control_path}"
return 2
success "Control file modified successfully." && return 0
fi
abort "Failed to modify control file: ${installed_control_path}" 2
}
test() {
info "Running jspg tests..."
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
cargo test --tests "$@" || return $?
}
clean() {
@ -122,26 +107,27 @@ clean() {
}
jspg-usage() {
printf "prepare\tCheck OS, Cargo, and PGRX dependencies.\n"
printf "install\tBuild and install the extension locally (after prepare).\n"
printf "reinstall\tClean, build, and install the extension locally (after prepare).\n"
printf "test\t\tRun pgrx integration tests.\n"
printf "clean\t\tRemove pgrx build artifacts.\n"
echo "up|Check OS, Cargo, and PGRX dependencies."
echo "install|Build and install the extension locally (after up)."
echo "reinstall|Clean, build, and install the extension locally (after up)."
echo "test-jspg|Run pgrx integration tests."
echo "test-validator|Run validator integration tests."
echo "clean|Remove pgrx build artifacts."
}
jspg-flow() {
case "$1" in
env) env; return $?;;
prepare) prepare && cargo-prepare && pgrx-prepare; return $?;;
up) up && rust-up && pgrx-up; return $?;;
down) pgrx-down && rust-down && down; return $?;;
build) build; return $?;;
install) install; return $?;;
reinstall) clean && install; return $?;;
test) test "${@:2}"; return $?;;
clean) clean; return $?;;
release) env; release; return $?;;
*) return 1 ;;
*) return 127 ;;
esac
}
register-flow "jspg-flow" "jspg-usage"
register-flow "jspg"
dispatch "$@"

2
flows

Submodule flows updated: 3e3954fb79...a210ac6497

1
rustfmt.toml Normal file
View File

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

394
src/compiler.rs Normal file
View File

@ -0,0 +1,394 @@
use crate::schema::Schema;
use regex::Regex;
use serde_json::Value;
// use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
/// Represents a compiled format validator
#[derive(Debug, Clone)]
pub enum CompiledFormat {
/// A simple function pointer validator
Func(fn(&Value) -> Result<(), Box<dyn Error + Send + Sync>>),
/// A regex-based validator
Regex(Regex),
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub Regex);
/// The Compiler is responsible for pre-calculating high-cost schema operations
pub struct Compiler;
impl Compiler {
/// Internal: Compiles formats and regexes in-place
fn compile_formats_and_regexes(schema: &mut Schema) {
// 1. Compile Format
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
// 2. Compile Pattern (regex)
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// 2.5 Compile Pattern Properties
if let Some(pp) = &schema.pattern_properties {
let mut compiled_pp = Vec::new();
for (pattern, sub_schema) in pp {
if let Ok(re) = Regex::new(pattern) {
compiled_pp.push((CompiledRegex(re), sub_schema.clone()));
} else {
eprintln!(
"Invalid patternProperty regex in schema (compile time): {}",
pattern
);
}
}
if !compiled_pp.is_empty() {
schema.compiled_pattern_properties = Some(compiled_pp);
}
}
// 3. Recurse
Self::compile_recursive(schema);
}
fn normalize_dependencies(schema: &mut Schema) {
if let Some(deps) = schema.dependencies.take() {
for (key, dep) in deps {
match dep {
crate::schema::Dependency::Props(props) => {
schema
.dependent_required
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, props);
}
crate::schema::Dependency::Schema(sub_schema) => {
schema
.dependent_schemas
.get_or_insert_with(std::collections::BTreeMap::new)
.insert(key, sub_schema);
}
}
}
}
}
fn compile_recursive(schema: &mut Schema) {
Self::normalize_dependencies(schema);
// Compile self
if let Some(format_str) = &schema.format {
if let Some(fmt) = crate::formats::FORMATS.get(format_str.as_str()) {
schema.compiled_format = Some(CompiledFormat::Func(fmt.func));
}
}
if let Some(pattern_str) = &schema.pattern {
if let Ok(re) = Regex::new(pattern_str) {
schema.compiled_pattern = Some(CompiledRegex(re));
}
}
// Recurse
if let Some(defs) = &mut schema.definitions {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(defs) = &mut schema.defs {
for s in defs.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(props) = &mut schema.properties {
for s in props.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(add_props) = &mut schema.additional_properties {
Self::compile_recursive(Arc::make_mut(add_props));
}
// ... Recurse logic ...
if let Some(items) = &mut schema.items {
Self::compile_recursive(Arc::make_mut(items));
}
if let Some(prefix_items) = &mut schema.prefix_items {
for s in prefix_items {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(not) = &mut schema.not {
Self::compile_recursive(Arc::make_mut(not));
}
if let Some(all_of) = &mut schema.all_of {
for s in all_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(any_of) = &mut schema.any_of {
for s in any_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(one_of) = &mut schema.one_of {
for s in one_of {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(s) = &mut schema.if_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.then_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(s) = &mut schema.else_ {
Self::compile_recursive(Arc::make_mut(s));
}
if let Some(ds) = &mut schema.dependent_schemas {
for s in ds.values_mut() {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(pn) = &mut schema.property_names {
Self::compile_recursive(Arc::make_mut(pn));
}
}
/// Recursively traverses the schema tree to build the local registry index.
fn compile_index(
schema: &Arc<Schema>,
registry: &mut crate::registry::Registry,
parent_base: Option<String>,
pointer: json_pointer::JsonPointer<String, Vec<String>>,
) {
// 1. Index using Parent Base (Path from Parent)
if let Some(base) = &parent_base {
// We use the pointer's string representation (e.g., "/definitions/foo")
// and append it to the base.
let fragment = pointer.to_string();
let ptr_uri = if fragment.is_empty() {
base.clone()
} else {
format!("{}#{}", base, fragment)
};
registry.insert(ptr_uri, schema.clone());
}
// 2. Determine Current Scope... (unchanged logic)
let mut current_base = parent_base.clone();
let mut child_pointer = pointer.clone();
if let Some(id) = &schema.obj.id {
let mut new_base = None;
if let Ok(_) = url::Url::parse(id) {
new_base = Some(id.clone());
} else if let Some(base) = &current_base {
if let Ok(base_url) = url::Url::parse(base) {
if let Ok(joined) = base_url.join(id) {
new_base = Some(joined.to_string());
}
}
} else {
new_base = Some(id.clone());
}
if let Some(base) = new_base {
// println!("DEBUG: Compiling index for path: {}", base); // Added println
registry.insert(base.clone(), schema.clone());
current_base = Some(base);
child_pointer = json_pointer::JsonPointer::new(vec![]); // Reset
}
}
// 3. Index by Anchor
if let Some(anchor) = &schema.obj.anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// Index by Dynamic Anchor
if let Some(d_anchor) = &schema.obj.dynamic_anchor {
if let Some(base) = &current_base {
let anchor_uri = format!("{}#{}", base, d_anchor);
registry.insert(anchor_uri, schema.clone());
}
}
// 4. Recurse (unchanged logic structure, just passing registry)
if let Some(defs) = schema.defs.as_ref().or(schema.definitions.as_ref()) {
let segment = if schema.defs.is_some() {
"$defs"
} else {
"definitions"
};
for (key, sub_schema) in defs {
let mut sub = child_pointer.clone();
sub.push(segment.to_string());
let decoded_key = percent_encoding::percent_decode_str(key).decode_utf8_lossy();
sub.push(decoded_key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(props) = &schema.properties {
for (key, sub_schema) in props {
let mut sub = child_pointer.clone();
sub.push("properties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(items) = &schema.items {
let mut sub = child_pointer.clone();
sub.push("items".to_string());
Self::compile_index(items, registry, current_base.clone(), sub);
}
if let Some(prefix_items) = &schema.prefix_items {
for (i, sub_schema) in prefix_items.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("prefixItems".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(all_of) = &schema.all_of {
for (i, sub_schema) in all_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("allOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(any_of) = &schema.any_of {
for (i, sub_schema) in any_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("anyOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(one_of) = &schema.one_of {
for (i, sub_schema) in one_of.iter().enumerate() {
let mut sub = child_pointer.clone();
sub.push("oneOf".to_string());
sub.push(i.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(not) = &schema.not {
let mut sub = child_pointer.clone();
sub.push("not".to_string());
Self::compile_index(not, registry, current_base.clone(), sub);
}
if let Some(if_) = &schema.if_ {
let mut sub = child_pointer.clone();
sub.push("if".to_string());
Self::compile_index(if_, registry, current_base.clone(), sub);
}
if let Some(then_) = &schema.then_ {
let mut sub = child_pointer.clone();
sub.push("then".to_string());
Self::compile_index(then_, registry, current_base.clone(), sub);
}
if let Some(else_) = &schema.else_ {
let mut sub = child_pointer.clone();
sub.push("else".to_string());
Self::compile_index(else_, registry, current_base.clone(), sub);
}
if let Some(deps) = &schema.dependent_schemas {
for (key, sub_schema) in deps {
let mut sub = child_pointer.clone();
sub.push("dependentSchemas".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(pp) = &schema.pattern_properties {
for (key, sub_schema) in pp {
let mut sub = child_pointer.clone();
sub.push("patternProperties".to_string());
sub.push(key.to_string());
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(add_props) = &schema.additional_properties {
let mut sub = child_pointer.clone();
sub.push("additionalProperties".to_string());
Self::compile_index(add_props, registry, current_base.clone(), sub);
}
if let Some(contains) = &schema.contains {
let mut sub = child_pointer.clone();
sub.push("contains".to_string());
Self::compile_index(contains, registry, current_base.clone(), sub);
}
if let Some(property_names) = &schema.property_names {
let mut sub = child_pointer.clone();
sub.push("propertyNames".to_string());
Self::compile_index(property_names, registry, current_base.clone(), sub);
}
}
pub fn compile(mut root_schema: Schema, root_id: Option<String>) -> Arc<Schema> {
// 1. Compile in-place (formats/regexes/normalization)
Self::compile_formats_and_regexes(&mut root_schema);
// Apply root_id override if schema ID is missing
if let Some(rid) = &root_id {
if root_schema.obj.id.is_none() {
root_schema.obj.id = Some(rid.clone());
}
}
// 2. Build ID/Pointer Index
let mut registry = crate::registry::Registry::new();
// We need a temporary Arc to satisfy compile_index recursion
// But we are modifying root_schema.
// This is tricky. compile_index takes &Arc<Schema>.
// We should build the index first, THEN attach it.
let root = Arc::new(root_schema);
// Default base_uri to ""
let base_uri = root_id
.clone()
.or_else(|| root.obj.id.clone())
.or(Some("".to_string()));
Self::compile_index(
&root,
&mut registry,
base_uri,
json_pointer::JsonPointer::new(vec![]),
);
// Also ensure root id is indexed if present
if let Some(rid) = root_id {
registry.insert(rid, root.clone());
}
// Now we need to attach this registry to the root schema.
// Since root is an Arc, we might need to recreate it if we can't mutate.
// Schema struct modifications require &mut.
let mut final_schema = Arc::try_unwrap(root).unwrap_or_else(|arc| (*arc).clone());
final_schema.obj.compiled_registry = Some(Arc::new(registry));
Arc::new(final_schema)
}
}

118
src/context.rs Normal file
View File

@ -0,0 +1,118 @@
use crate::error::ValidationError;
use crate::instance::ValidationInstance;
use crate::result::ValidationResult;
use crate::schema::Schema;
use crate::validator::Validator;
use std::collections::HashSet;
pub struct ValidationContext<'a, I: ValidationInstance<'a>> {
pub validator: &'a Validator,
pub root: &'a Schema,
pub schema: &'a Schema,
pub instance: I,
pub path: String,
pub depth: usize,
pub scope: Vec<String>,
pub overrides: HashSet<String>,
pub extensible: bool,
pub reporter: bool,
}
impl<'a, I: ValidationInstance<'a>> ValidationContext<'a, I> {
pub fn new(
validator: &'a Validator,
root: &'a Schema,
schema: &'a Schema,
instance: I,
scope: Vec<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator,
root,
schema,
instance,
path: String::new(),
depth: 0,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
}
pub fn derive(
&self,
schema: &'a Schema,
instance: I,
path: &str,
scope: Vec<String>,
overrides: HashSet<String>,
extensible: bool,
reporter: bool,
) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible);
Self {
validator: self.validator,
root: self.root,
schema,
instance,
path: path.to_string(),
depth: self.depth + 1,
scope,
overrides,
extensible: effective_extensible,
reporter,
}
}
pub fn derive_for_schema(&self, schema: &'a Schema, reporter: bool) -> Self {
self.derive(
schema,
self.instance,
&self.path,
self.scope.clone(),
HashSet::new(),
self.extensible,
reporter,
)
}
pub fn validate(&self) -> Result<ValidationResult, ValidationError> {
let mut effective_scope = self.scope.clone();
if let Some(id) = &self.schema.obj.id {
let current_base = self.scope.last().map(|s| s.as_str()).unwrap_or("");
let mut new_base = id.clone();
if !current_base.is_empty() {
if let Ok(base_url) = url::Url::parse(current_base) {
if let Ok(joined) = base_url.join(id) {
new_base = joined.to_string();
}
}
}
effective_scope.push(new_base);
let shadow = ValidationContext {
validator: self.validator,
root: self.root,
schema: self.schema,
instance: self.instance,
path: self.path.clone(),
depth: self.depth,
scope: effective_scope,
overrides: self.overrides.clone(),
extensible: self.extensible,
reporter: self.reporter,
};
return shadow.validate_scoped();
}
self.validate_scoped()
}
}

66
src/drop.rs Normal file
View File

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

6
src/error.rs Normal file
View File

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

875
src/formats.rs Normal file
View File

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

98
src/instance.rs Normal file
View File

@ -0,0 +1,98 @@
use serde_json::Value;
use std::collections::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
});
}
}
}
}

View File

@ -2,216 +2,211 @@ use pgrx::*;
pg_module_magic!();
use serde_json::{json, Value};
use std::{collections::HashMap, sync::RwLock};
use boon::{Compiler, Schemas, ValidationError, SchemaIndex, CompileError};
use lazy_static::lazy_static;
pub mod compiler;
pub mod drop;
pub mod formats;
struct BoonCache {
schemas: Schemas,
id_to_index: HashMap<String, SchemaIndex>,
}
pub mod registry;
mod schema;
pub mod util;
mod validator;
lazy_static! {
static ref SCHEMA_CACHE: RwLock<BoonCache> = RwLock::new(BoonCache {
schemas: Schemas::new(),
id_to_index: HashMap::new(),
});
pub mod context;
pub mod error;
pub mod instance;
pub mod result;
pub(crate) mod rules;
use serde_json::json;
use std::sync::{Arc, RwLock};
lazy_static::lazy_static! {
// Global Atomic Swap Container:
// - RwLock: To protect the SWAP of the Option.
// - Option: Because it starts empty.
// - Arc: Because multiple running threads might hold the OLD validator while we swap.
// - Validator: It immutably owns the Registry.
static ref GLOBAL_VALIDATOR: RwLock<Option<Arc<validator::Validator>>> = RwLock::new(None);
}
#[pg_extern(strict)]
fn cache_json_schema(schema_id: &str, schema: JsonB) -> JsonB {
let mut cache = SCHEMA_CACHE.write().unwrap();
let schema_value: Value = schema.0;
let schema_path = format!("urn:{}", schema_id);
pub fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
// 1 & 2. Build Registry, Families, and Wrap in Validator all in one shot
let new_validator = crate::validator::Validator::from_punc_definition(
Some(&enums.0),
Some(&types.0),
Some(&puncs.0),
);
let new_arc = Arc::new(new_validator);
let mut compiler = Compiler::new();
compiler.enable_format_assertions();
// Use schema_path when adding the resource
if let Err(e) = compiler.add_resource(&schema_path, schema_value.clone()) {
return JsonB(json!({
"success": false,
"error": {
"message": format!("Failed to add schema resource '{}': {}", schema_id, e),
"schema_path": schema_path
}
}));
// 3. ATOMIC SWAP
{
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
*lock = Some(new_arc);
}
// Use schema_path when compiling
match compiler.compile(&schema_path, &mut cache.schemas) {
Ok(sch_index) => {
// Store the index using the original schema_id as the key
cache.id_to_index.insert(schema_id.to_string(), sch_index);
JsonB(json!({ "success": true }))
}
Err(e) => {
let error = match &e {
CompileError::ValidationError { url: _url, src } => {
// Collect leaf errors from the meta-schema validation failure
let mut error_list = Vec::new();
collect_leaf_errors(src, &mut error_list);
// Filter and deduplicate errors, returning as a single JSON Value (Array)
json!(filter_boon_errors(error_list))
}
_ => {
// Keep existing handling for other compilation errors
let _error_type = format!("{:?}", e).split('(').next().unwrap_or("Unknown").to_string();
json!({
"message": format!("Schema '{}' compilation failed: {}", schema_id, e),
"schema_path": schema_path,
"detail": format!("{:?}", e),
})
}
};
// Ensure the outer structure remains { success: false, error: ... }
JsonB(json!({
"success": false,
"error": error
}))
}
}
let drop = crate::drop::Drop::success();
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();
// Lookup uses the original schema_id
match cache.id_to_index.get(schema_id) {
None => JsonB(json!({
"success": false,
"error": {
"message": format!("Schema with id '{}' not found in cache", schema_id)
}
})),
Some(sch_index) => {
let instance_value: Value = instance.0;
match cache.schemas.validate(&instance_value, *sch_index) {
Ok(_) => JsonB(json!({ "success": true })),
Err(validation_error) => {
// Directly use the result of format_validation_error
// which now includes the top-level success indicator and flat error list
let mut error_list = Vec::new();
collect_leaf_errors(&validation_error, &mut error_list);
JsonB(json!({
"success": false,
"error": filter_boon_errors(error_list) // Filter and deduplicate errors
}))
}
}
}
}
}
// Recursively collects leaf errors into a flat list
fn collect_leaf_errors(error: &ValidationError, errors_list: &mut Vec<Value>) {
if error.causes.is_empty() {
let default_message = format!("{}", error);
let message = if let Some(start_index) = default_message.find("': ") {
default_message[start_index + 3..].to_string()
} else {
default_message
};
errors_list.push(json!({
"message": message,
"schema_path": error.schema_url.to_string(),
"instance_path": error.instance_location.to_string(),
}));
} else {
for cause in &error.causes {
collect_leaf_errors(cause, errors_list);
}
}
}
// Filters collected errors, removing structural noise and then deduplicating by instance_path
fn filter_boon_errors(raw_errors: Vec<Value>) -> Vec<Value> {
use std::collections::HashMap;
use std::collections::hash_map::Entry;
// Define schema keywords that indicate structural paths, not instance paths
let structural_path_segments = [
"/allOf/", "/anyOf/", "/oneOf/",
"/if/", "/then/", "/else/",
"/not/"
// Note: "/properties/" and "/items/" are generally valid,
// but might appear spuriously in boon's paths for complex types.
// We exclude only the explicitly logical/combinatorial ones for now.
];
// 1. Filter out errors with instance_paths containing structural segments
let plausible_errors: Vec<Value> = raw_errors.into_iter().filter(|error_value| {
if let Some(instance_path_value) = error_value.get("instance_path") {
if let Some(instance_path_str) = instance_path_value.as_str() {
// Keep if NONE of the structural segments are present
!structural_path_segments.iter().any(|&segment| instance_path_str.contains(segment))
} else {
false // Invalid instance_path type, filter out
}
} else {
false // No instance_path field, filter out
}
}).collect();
// 2. Deduplicate the remaining plausible errors by instance_path
let mut unique_errors: HashMap<String, Value> = HashMap::new();
for error_value in plausible_errors {
if let Some(instance_path_value) = error_value.get("instance_path") {
if let Some(instance_path_str) = instance_path_value.as_str() {
if let Entry::Vacant(entry) = unique_errors.entry(instance_path_str.to_string()) {
entry.insert(error_value);
}
}
}
}
// Collect the unique errors
unique_errors.into_values().collect()
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
let cache = SCHEMA_CACHE.read().unwrap();
cache.id_to_index.contains_key(schema_id)
}
#[pg_extern(strict)]
fn clear_json_schemas() {
let mut cache = SCHEMA_CACHE.write().unwrap();
*cache = BoonCache {
schemas: Schemas::new(),
id_to_index: HashMap::new(),
pub fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
// We need a mutable copy of the value to mask it
let mut mutable_instance = instance.0.clone();
match validator.mask(schema_id, &mut mutable_instance) {
Ok(result) => {
// If valid, return the MASKED instance
if result.is_valid() {
let drop = crate::drop::Drop::success_with_val(mutable_instance);
JsonB(serde_json::to_value(drop).unwrap())
} else {
// If invalid, return errors (Schema Validation Errors)
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())
}
}
Err(e) => {
// Schema Not Found or other fatal error
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())
}
}
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> Vec<String> {
let cache = SCHEMA_CACHE.read().unwrap();
let ids: Vec<String> = cache.id_to_index.keys().cloned().collect();
ids
pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
match 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())
}
}
Err(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())
}
}
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
#[pg_extern(strict, parallel_safe)]
pub fn json_schema_cached(schema_id: &str) -> bool {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
match validator.validate(schema_id, &serde_json::Value::Null) {
Err(e) if e.code == "SCHEMA_NOT_FOUND" => false,
_ => true,
}
} else {
false
}
}
#[pg_extern(strict)]
pub fn clear_json_schemas() -> JsonB {
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
*lock = None;
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
}
#[pg_extern(strict, parallel_safe)]
pub fn show_json_schemas() -> JsonB {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
let mut keys = 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 {
use pgrx::prelude::*;
include!("tests/fixtures.rs");
}
/// 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
// perform any initialization common to all tests
}
#[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"))]
#[pg_schema]
mod tests {
include!("tests.rs");
}

50
src/registry.rs Normal file
View File

@ -0,0 +1,50 @@
use crate::schema::Schema;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::RwLock;
lazy_static! {
pub static ref REGISTRY: RwLock<Registry> = RwLock::new(Registry::new());
}
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct Registry {
pub schemas: HashMap<String, Arc<Schema>>,
}
impl Registry {
pub fn new() -> Self {
Registry {
schemas: HashMap::new(),
}
}
pub fn add(&mut self, schema: crate::schema::Schema) {
let id = schema
.obj
.id
.clone()
.expect("Schema must have an $id to be registered");
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
self.schemas.insert(id, compiled);
}
pub fn insert(&mut self, id: String, schema: Arc<Schema>) {
// We allow overwriting for now to support re-compilation in tests/dev
self.schemas.insert(id, schema);
}
pub fn get(&self, id: &str) -> Option<Arc<Schema>> {
self.schemas.get(id).cloned()
}
pub fn clear(&mut self) {
self.schemas.clear();
}
pub fn len(&self) -> usize {
self.schemas.len()
}
}

27
src/result.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::error::ValidationError;
use std::collections::HashSet;
#[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()
}
}

1008
src/rules.rs Normal file

File diff suppressed because it is too large Load Diff

222
src/schema.rs Normal file
View File

@ -0,0 +1,222 @@
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 ref_string: Option<String>,
#[serde(rename = "$anchor")]
pub anchor: Option<String>,
#[serde(rename = "$dynamicAnchor")]
pub dynamic_anchor: Option<String>,
#[serde(rename = "$dynamicRef")]
pub dynamic_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>>,
// Definitions (for $ref resolution)
#[serde(rename = "$defs")]
pub defs: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "definitions")]
pub definitions: Option<BTreeMap<String, Arc<Schema>>>,
// 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>>,
#[serde(rename = "dependentRequired")]
pub dependent_required: Option<BTreeMap<String, Vec<String>>>,
#[serde(rename = "dependentSchemas")]
pub dependent_schemas: Option<BTreeMap<String, 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::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>,
// Compiled Fields (Hidden from JSON/Serde)
#[serde(skip)]
pub compiled_format: Option<crate::compiler::CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: Option<crate::compiler::CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(crate::compiler::CompiledRegex, Arc<Schema>)>>,
#[serde(skip)]
pub compiled_registry: Option<Arc<crate::registry::Registry>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Schema {
#[serde(flatten)]
pub obj: SchemaObject,
#[serde(skip)]
pub always_fail: bool,
}
impl Default for Schema {
fn default() -> Self {
Schema {
obj: SchemaObject::default(),
always_fail: false,
}
}
}
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<'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 obj: SchemaObject = serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
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>),
}

View File

@ -1,415 +0,0 @@
use crate::*;
use serde_json::{json, Value};
use pgrx::{JsonB, pg_test};
// Helper macro for asserting success (no changes needed, but ensure it's present)
macro_rules! assert_success_with_json {
($result_jsonb:expr, $fmt:literal $(, $($args:tt)*)?) => {
let condition_result: Option<bool> = $result_jsonb.0.get("success").and_then(Value::as_bool);
if condition_result != Some(true) {
let base_msg = format!($fmt $(, $($args)*)?);
let pretty_json = serde_json::to_string_pretty(&$result_jsonb.0)
.unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", $result_jsonb.0));
let panic_msg = format!("Assertion Failed (expected success): {}\nResult JSON:\n{}", base_msg, pretty_json);
panic!("{}", panic_msg);
}
};
// Simpler version without message
($result_jsonb:expr) => {
let condition_result: Option<bool> = $result_jsonb.0.get("success").and_then(Value::as_bool);
if condition_result != Some(true) {
let pretty_json = serde_json::to_string_pretty(&$result_jsonb.0)
.unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", $result_jsonb.0));
let panic_msg = format!("Assertion Failed (expected success)\nResult JSON:\n{}", pretty_json);
panic!("{}", panic_msg);
}
};
}
// Updated helper macro for asserting failed JSON results with the new flat error structure
macro_rules! assert_failure_with_json {
// --- Arms with error count and message substring check ---
// With custom message:
($result:expr, $expected_error_count:expr, $expected_first_message_contains:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let success = json_result.get("success").and_then(Value::as_bool);
let error_val_opt = json_result.get("error"); // Changed key
let base_msg = format!($fmt $(, $($args)*)?);
if success != Some(false) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure, success was not false): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
match error_val_opt {
Some(error_val) => {
if error_val.is_array() {
let errors_array = error_val.as_array().unwrap();
if errors_array.len() != $expected_error_count {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json);
}
if $expected_error_count > 0 {
let first_error_message = errors_array[0].get("message").and_then(Value::as_str);
match first_error_message {
Some(msg) => {
if !msg.contains($expected_first_message_contains) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (first error message mismatch): Expected contains '{}', got: '{}'. {}\nResult JSON:\n{}", $expected_first_message_contains, msg, base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (first error in array has no 'message' string): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
}
} else if error_val.is_object() {
// Handle single error object case (like 'schema not found')
if $expected_error_count != 1 {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, but got a single error object. {}\nResult JSON:\n{}", $expected_error_count, base_msg, pretty_json);
}
let message = error_val.get("message").and_then(Value::as_str);
match message {
Some(msg) => {
if !msg.contains($expected_first_message_contains) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (error message mismatch): Expected object message contains '{}', got: '{}'. {}\nResult JSON:\n{}", $expected_first_message_contains, msg, base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (error object has no 'message' string): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
} else {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed ('error' value is not an array or object): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected 'error' key, but none found): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
};
// Without custom message (calls the one above with ""):
($result:expr, $expected_error_count:expr, $expected_first_message_contains:expr) => {
assert_failure_with_json!($result, $expected_error_count, $expected_first_message_contains, "");
};
// --- Arms with error count check only ---
// With custom message:
($result:expr, $expected_error_count:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let success = json_result.get("success").and_then(Value::as_bool);
let error_val_opt = json_result.get("error"); // Changed key
let base_msg = format!($fmt $(, $($args)*)?);
if success != Some(false) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure, success was not false): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
match error_val_opt {
Some(error_val) => {
if error_val.is_array() {
let errors_array = error_val.as_array().unwrap();
if errors_array.len() != $expected_error_count {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, got {}. {}\nResult JSON:\n{}", $expected_error_count, errors_array.len(), base_msg, pretty_json);
}
} else if error_val.is_object() {
if $expected_error_count != 1 {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (wrong error count): Expected {} errors, but got a single error object. {}\nResult JSON:\n{}", $expected_error_count, base_msg, pretty_json);
}
// Count check passes if expected is 1 and got object
} else {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed ('error' value is not an array or object): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected 'error' key, but none found): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
};
// Without custom message (calls the one above with ""):
($result:expr, $expected_error_count:expr) => {
assert_failure_with_json!($result, $expected_error_count, "");
};
// --- Arms checking failure only (expects at least one error) ---
// With custom message:
($result:expr, $fmt:literal $(, $($args:tt)*)?) => {
let json_result = &$result.0;
let success = json_result.get("success").and_then(Value::as_bool);
let error_val_opt = json_result.get("error"); // Changed key
let base_msg = format!($fmt $(, $($args)*)?);
if success != Some(false) {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected failure, success was not false): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
match error_val_opt {
Some(error_val) => {
if error_val.is_object() {
// OK: single error object is a failure
} else if error_val.is_array() {
if error_val.as_array().unwrap().is_empty() {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected errors, but 'error' array is empty): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
// OK: non-empty error array is a failure
} else {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed ('error' value is not an array or object): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
None => {
let pretty_json = serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| format!("(Failed to pretty-print JSON: {:?})", json_result));
panic!("Assertion Failed (expected 'error' key, but none found): {}\nResult JSON:\n{}", base_msg, pretty_json);
}
}
};
// Without custom message (calls the one above with ""):
($result:expr) => {
assert_failure_with_json!($result, "");
};
}
fn jsonb(val: Value) -> JsonB {
JsonB(val)
}
#[pg_test]
fn test_cache_and_validate_json_schema() {
clear_json_schemas(); // Call clear directly
let schema_id = "my_schema";
let schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name", "age"]
});
let valid_instance = json!({ "name": "Alice", "age": 30 });
let invalid_instance_type = json!({ "name": "Bob", "age": -5 });
let invalid_instance_missing = json!({ "name": "Charlie" });
let cache_result = cache_json_schema(schema_id, jsonb(schema.clone()));
assert_success_with_json!(cache_result, "Cache operation should succeed.");
let valid_result = validate_json_schema(schema_id, jsonb(valid_instance));
assert_success_with_json!(valid_result, "Validation of valid instance should succeed.");
// Invalid type
let invalid_result_type = validate_json_schema(schema_id, jsonb(invalid_instance_type));
assert_failure_with_json!(invalid_result_type, 1, "must be >=0", "Validation with invalid type should fail.");
let errors_type = invalid_result_type.0["error"].as_array().unwrap(); // Check 'error', expect array
assert_eq!(errors_type[0]["instance_path"], "/age");
assert_eq!(errors_type[0]["schema_path"], "urn:my_schema#/properties/age");
// Missing field
let invalid_result_missing = validate_json_schema(schema_id, jsonb(invalid_instance_missing));
assert_failure_with_json!(invalid_result_missing, 1, "missing properties 'age'", "Validation with missing field should fail.");
let errors_missing = invalid_result_missing.0["error"].as_array().unwrap(); // Check 'error', expect array
assert_eq!(errors_missing[0]["instance_path"], "");
assert_eq!(errors_missing[0]["schema_path"], "urn:my_schema#");
// Schema not found
let non_existent_id = "non_existent_schema";
let invalid_schema_result = validate_json_schema(non_existent_id, jsonb(json!({})));
assert_failure_with_json!(invalid_schema_result, 1, "Schema with id 'non_existent_schema' not found", "Validation with non-existent schema should fail.");
// Check 'error' is an object for 'schema not found'
let error_notfound_obj = invalid_schema_result.0["error"].as_object().expect("'error' should be an object for schema not found");
assert!(error_notfound_obj.contains_key("message")); // Check message exists
// Removed checks for schema_path/instance_path as they aren't added in lib.rs for this case
}
#[pg_test]
fn test_validate_json_schema_not_cached() {
clear_json_schemas(); // Call clear directly
let instance = json!({ "foo": "bar" });
let result = validate_json_schema("non_existent_schema", jsonb(instance));
// Use the updated macro, expecting count 1 and specific message (handles object case)
assert_failure_with_json!(result, 1, "Schema with id 'non_existent_schema' not found", "Validation with non-existent schema should fail.");
}
#[pg_test]
fn test_cache_invalid_json_schema() {
clear_json_schemas(); // Call clear directly
let schema_id = "invalid_schema";
// Schema with an invalid type *value*
let invalid_schema = json!({
"$id": "urn:invalid_schema",
"type": ["invalid_type_value"]
});
let cache_result = cache_json_schema(schema_id, jsonb(invalid_schema));
// Expect 2 leaf errors because the meta-schema validation fails at the type value
// and within the type array itself.
assert_failure_with_json!(
cache_result,
2, // Expect exactly two leaf errors
"value must be one of", // Check message substring (present in both)
"Caching invalid schema should fail with specific meta-schema validation errors."
);
// Ensure the error is an array and check specifics
let error_array = cache_result.0["error"].as_array().expect("Error field should be an array");
assert_eq!(error_array.len(), 2);
// Note: Order might vary depending on boon's internal processing, check both possibilities or sort.
// Assuming the order shown in the logs for now:
assert_eq!(error_array[0]["instance_path"], "/type");
assert!(error_array[0]["message"].as_str().unwrap().contains("value must be one of"));
assert_eq!(error_array[1]["instance_path"], "/type/0");
assert!(error_array[1]["message"].as_str().unwrap().contains("value must be one of"));
}
#[pg_test]
fn test_validate_json_schema_detailed_validation_errors() {
clear_json_schemas(); // Call clear directly
let schema_id = "detailed_errors";
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string", "maxLength": 10 }
},
"required": ["street", "city"]
}
},
"required": ["address"]
});
let _ = cache_json_schema(schema_id, jsonb(schema));
let invalid_instance = json!({
"address": {
"street": 123, // Wrong type
"city": "Supercalifragilisticexpialidocious" // Too long
}
});
let result = validate_json_schema(schema_id, jsonb(invalid_instance));
// Update: Expect 2 errors again, as boon reports both nested errors.
assert_failure_with_json!(result, 2);
}
#[pg_test]
fn test_validate_json_schema_oneof_validation_errors() {
clear_json_schemas(); // Call clear directly
let schema_id = "oneof_schema";
let schema = json!({
"oneOf": [
{ // Option 1: Object with string prop
"type": "object",
"properties": {
"string_prop": { "type": "string", "maxLength": 5 }
},
"required": ["string_prop"]
},
{ // Option 2: Object with number prop
"type": "object",
"properties": {
"number_prop": { "type": "number", "minimum": 10 }
},
"required": ["number_prop"]
}
]
});
let _ = cache_json_schema(schema_id, jsonb(schema));
// --- Test case 1: Fails string maxLength (in branch 0) AND missing number_prop (in branch 1) ---
let invalid_string_instance = json!({ "string_prop": "toolongstring" });
let result_invalid_string = validate_json_schema(schema_id, jsonb(invalid_string_instance));
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_string, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_string = result_invalid_string.0["error"].as_array().expect("Expected error array for invalid string");
assert!(errors_string.iter().any(|e| e["instance_path"] == "/string_prop" && e["message"].as_str().unwrap().contains("length must be <=5")), "Missing maxLength error");
assert!(errors_string.iter().any(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("missing properties 'number_prop'")), "Missing number_prop required error");
// --- Test case 2: Fails number minimum (in branch 1) AND missing string_prop (in branch 0) ---
let invalid_number_instance = json!({ "number_prop": 5 });
let result_invalid_number = validate_json_schema(schema_id, jsonb(invalid_number_instance));
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_number, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_number = result_invalid_number.0["error"].as_array().expect("Expected error array for invalid number");
assert!(errors_number.iter().any(|e| e["instance_path"] == "/number_prop" && e["message"].as_str().unwrap().contains("must be >=10")), "Missing minimum error");
assert!(errors_number.iter().any(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("missing properties 'string_prop'")), "Missing string_prop required error");
// --- Test case 3: Fails type check (not object) for both branches ---
// Input: boolean, expected object for both branches
let invalid_bool_instance = json!(true); // Not an object
let result_invalid_bool = validate_json_schema(schema_id, jsonb(invalid_bool_instance));
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_invalid_bool, 1);
// Explicitly check that the single remaining error is the type error for the root instance path
let errors_bool = result_invalid_bool.0["error"].as_array().expect("Expected error array for invalid bool");
assert_eq!(errors_bool.iter().filter(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("want object")).count(), 1, "Expected one 'want object' error at root after filtering");
// --- Test case 4: Fails missing required for both branches ---
// Input: empty object, expected string_prop (branch 0) OR number_prop (branch 1)
let invalid_empty_obj = json!({});
let result_empty_obj = validate_json_schema(schema_id, jsonb(invalid_empty_obj));
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_empty_obj, 1);
// Explicitly check that the single remaining error is one of the expected missing properties errors
let errors_empty = result_empty_obj.0["error"].as_array().expect("Expected error array for empty object");
assert_eq!(errors_empty.len(), 1, "Expected exactly one error after filtering empty object");
let the_error = &errors_empty[0];
assert_eq!(the_error["instance_path"], "", "Expected instance_path to be empty string");
let message = the_error["message"].as_str().unwrap();
assert!(message.contains("missing properties 'string_prop'") || message.contains("missing properties 'number_prop'"),
"Error message should indicate missing string_prop or number_prop, got: {}", message);
}
#[pg_test]
fn test_clear_json_schemas() {
clear_json_schemas(); // Call clear directly
let schema_id = "schema_to_clear";
let schema = json!({ "type": "string" });
cache_json_schema(schema_id, jsonb(schema.clone()));
let show_result1 = show_json_schemas();
assert!(show_result1.contains(&schema_id.to_string()));
clear_json_schemas();
let show_result2 = show_json_schemas();
assert!(show_result2.is_empty());
let instance = json!("test");
let validate_result = validate_json_schema(schema_id, jsonb(instance));
// Use the updated macro, expecting count 1 and specific message (handles object case)
assert_failure_with_json!(validate_result, 1, "Schema with id 'schema_to_clear' not found", "Validation should fail after clearing schemas.");
}
#[pg_test]
fn test_show_json_schemas() {
clear_json_schemas(); // Call clear directly
let schema_id1 = "schema1";
let schema_id2 = "schema2";
let schema = json!({ "type": "boolean" });
cache_json_schema(schema_id1, jsonb(schema.clone()));
cache_json_schema(schema_id2, jsonb(schema.clone()));
let mut result = show_json_schemas(); // Make result mutable
result.sort(); // Sort for deterministic testing
assert_eq!(result, vec!["schema1".to_string(), "schema2".to_string()]); // Check exact content
assert!(result.contains(&schema_id1.to_string())); // Keep specific checks too if desired
assert!(result.contains(&schema_id2.to_string()));
}

2088
src/tests/fixtures.rs Normal file

File diff suppressed because it is too large Load Diff

323
src/util.rs Normal file
View File

@ -0,0 +1,323 @@
use serde::Deserialize;
use std::fs;
#[derive(Debug, Deserialize)]
struct TestSuite {
#[allow(dead_code)]
description: String,
schema: Option<serde_json::Value>,
// Support JSPG-style test suites with explicit types/enums/puncs
types: Option<serde_json::Value>,
enums: Option<serde_json::Value>,
puncs: Option<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: Option<String>,
// Expected output for masking tests
#[allow(dead_code)]
expected: Option<serde_json::Value>,
}
// use crate::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();
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
// 3. Register root 'schemas' if present (generic test support)
// Some tests use a raw 'schema' or 'schemas' field at the group level
if let Some(schema_val) = &group.schema {
match serde_json::from_value::<crate::schema::Schema>(schema_val.clone()) {
Ok(mut schema) => {
let id_clone = schema.obj.id.clone();
if id_clone.is_some() {
validator.registry.add(schema);
} else {
// Fallback ID if none provided in schema
let id = format!("test:{}:{}", path, index);
schema.obj.id = Some(id);
validator.registry.add(schema);
}
}
Err(e) => {
eprintln!(
"DEBUG: FAILED to deserialize group schema for index {}: {}",
index, e
);
}
}
}
// 4. Run Tests
for (_test_index, test) in group.tests.iter().enumerate() {
let mut schema_id = test.schema_id.clone();
// If no explicit schema_id, try to infer from the single schema in the group
if schema_id.is_none() {
if let Some(s) = &group.schema {
// If 'schema' is a single object, use its ID or "root"
if let Some(obj) = s.as_object() {
if let Some(id_val) = obj.get("$id") {
schema_id = id_val.as_str().map(|s| s.to_string());
}
}
if schema_id.is_none() {
schema_id = Some(format!("test:{}:{}", path, index));
}
}
}
// Default to the first punc if present (for puncs.json style)
if schema_id.is_none() {
if let Some(Value::Array(puncs)) = &group.puncs {
if let Some(first_punc) = puncs.first() {
if let Some(Value::Array(schemas)) = first_punc.get("schemas") {
if let Some(first_schema) = schemas.first() {
if let Some(id) = first_schema.get("$id").and_then(|v| v.as_str()) {
schema_id = Some(id.to_string());
}
}
}
}
}
}
if let Some(sid) = schema_id {
let result = validator.validate(&sid, &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 let Some(expected) = &test.expected {
// Masking Test
let mut data_for_mask = test.data.clone();
match validator.mask(&sid, &mut data_for_mask) {
Ok(_) => {
if !equals(&data_for_mask, expected) {
let msg = format!(
"Masking Test '{}' failed.\nExpected: {:?}\nGot: {:?}",
test.description, expected, data_for_mask
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Masking Test '{}' failed with execution error: {:?}",
test.description, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
// Standard Validation Test
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
));
}
}
} else {
failures.push(format!(
"[{}] Test '{}' skipped: No schema ID found.",
group.description, test.description
));
}
}
if !failures.is_empty() {
return Err(failures.join("\n"));
}
Ok(())
}
pub fn run_test_file(path: &str) -> 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));
let mut failures = Vec::<String>::new();
for (group_index, group) in suite.into_iter().enumerate() {
// Create Validator Instance and parse enums, types, and puncs automatically
let mut validator = Validator::from_punc_definition(
group.enums.as_ref(),
group.types.as_ref(),
group.puncs.as_ref(),
);
let unique_id = format!("test:{}:{}", path, group_index);
// Register main 'schema' if present (Standard style)
if let Some(ref schema_val) = group.schema {
let mut schema: crate::schema::Schema =
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
// If schema has no ID, assign unique_id and use add() or manual insert?
// Compiler needs ID. Registry::add needs ID.
if schema.obj.id.is_none() {
schema.obj.id = Some(unique_id.clone());
}
validator.registry.add(schema);
}
for test in group.tests {
// Use explicit schema_id from test, or default to unique_id
let schema_id = test.schema_id.as_deref().unwrap_or(&unique_id).to_string();
let result = validator.validate(&schema_id, &test.data);
if test.valid {
match result {
Ok(res) => {
if !res.is_valid() {
let msg = format!(
"Test failed (expected valid): {}\nSchema: {:?}\nData: {:?}\nErrors: {:?}",
test.description,
group.schema, // We might need to find the actual schema used if schema_id is custom
test.data,
res.errors
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(e) => {
let msg = format!(
"Test failed (expected valid) but got execution error: {}\nSchema: {:?}\nData: {:?}\nError: {:?}",
test.description, group.schema, test.data, e
);
eprintln!("{}", msg);
failures.push(msg);
}
}
} else {
match result {
Ok(res) => {
if res.is_valid() {
let msg = format!(
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}",
test.description, group.schema, test.data
);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(_) => {
// Expected invalid, got error (which implies invalid/failure), so this is PASS.
}
}
}
}
}
if !failures.is_empty() {
return Err(format!(
"{} tests failed in file {}:\n\n{}",
failures.len(),
path,
failures.join("\n\n")
));
}
Ok(())
}
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 {
// eprintln!("Comparing {:?} with {:?}", v1, v2);
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,
}
}

258
src/validator.rs Normal file
View File

@ -0,0 +1,258 @@
pub use crate::context::ValidationContext;
pub use crate::error::ValidationError;
pub use crate::instance::{MutableInstance, ReadOnlyInstance};
pub use crate::result::ValidationResult;
use crate::registry::Registry;
use crate::schema::Schema;
use serde_json::Value;
use std::collections::HashSet;
use std::sync::Arc;
pub enum ResolvedRef<'a> {
Local(&'a Schema),
Global(&'a Schema, &'a Schema),
}
pub struct Validator {
pub registry: Registry,
pub families: std::collections::HashMap<String, Arc<Schema>>,
}
impl Validator {
pub fn from_punc_definition(
enums: Option<&Value>,
types: Option<&Value>,
puncs: Option<&Value>,
) -> Self {
let mut registry = Registry::new();
let mut families = std::collections::HashMap::new();
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
if let Some(Value::Array(arr)) = types {
for item in arr {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
if let Some(hierarchy) = item.get("hierarchy").and_then(|v| v.as_array()) {
for ancestor in hierarchy {
if let Some(anc_str) = ancestor.as_str() {
family_map
.entry(anc_str.to_string())
.or_default()
.insert(name.to_string());
}
}
}
}
}
}
for (family_name, members) in family_map {
let object_refs: Vec<Value> = members
.iter()
.map(|s| serde_json::json!({ "$ref": s }))
.collect();
let schema_json = serde_json::json!({
"oneOf": object_refs
});
if let Ok(schema) = serde_json::from_value::<Schema>(schema_json) {
let compiled = crate::compiler::Compiler::compile(schema, None);
families.insert(family_name, compiled);
}
}
let mut cache_items = |items_val: Option<&Value>| {
if let Some(Value::Array(arr)) = items_val {
for item in arr {
if let Some(Value::Array(schemas)) = item.get("schemas") {
for schema_val in schemas {
if let Ok(schema) = serde_json::from_value::<Schema>(schema_val.clone()) {
registry.add(schema);
}
}
}
}
}
};
cache_items(enums);
cache_items(types);
cache_items(puncs);
Self { registry, families }
}
pub fn get_schema_ids(&self) -> Vec<String> {
self.registry.schemas.keys().cloned().collect()
}
pub fn check_type(t: &str, val: &Value) -> bool {
if let Value::String(s) = val {
if s.is_empty() {
return true;
}
}
match t {
"null" => val.is_null(),
"boolean" => val.is_boolean(),
"string" => val.is_string(),
"number" => val.is_number(),
"integer" => crate::util::is_integer(val),
"object" => val.is_object(),
"array" => val.is_array(),
_ => true,
}
}
pub fn resolve_ref<'a>(
&'a self,
root: &'a Schema,
ref_string: &str,
scope: &str,
) -> Option<(ResolvedRef<'a>, String)> {
if ref_string.starts_with('#') {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(ref_string) {
return Some((ResolvedRef::Local(s.as_ref()), ref_string.to_string()));
}
}
}
if let Ok(base) = url::Url::parse(scope) {
if let Ok(joined) = base.join(ref_string) {
let joined_str = joined.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref()), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref()), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
} else {
if ref_string.starts_with('#') {
let joined_str = format!("{}{}", scope, ref_string);
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&joined_str) {
return Some((ResolvedRef::Local(s.as_ref()), joined_str));
}
}
if let Ok(decoded) = percent_encoding::percent_decode_str(&joined_str).decode_utf8() {
let decoded_str = decoded.to_string();
if decoded_str != joined_str {
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&decoded_str) {
return Some((ResolvedRef::Local(s.as_ref()), decoded_str));
}
}
}
}
if let Some(s) = self.registry.schemas.get(&joined_str) {
return Some((ResolvedRef::Global(s.as_ref(), s.as_ref()), joined_str));
}
}
}
if let Ok(parsed) = url::Url::parse(ref_string) {
let absolute = parsed.to_string();
if let Some(indexrs) = &root.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Local(s.as_ref()), absolute));
}
}
let resource_base = if let Some((base, _)) = absolute.split_once('#') {
base
} else {
&absolute
};
if let Some(compiled) = self.registry.schemas.get(resource_base) {
if let Some(indexrs) = &compiled.obj.compiled_registry {
if let Some(s) = indexrs.schemas.get(&absolute) {
return Some((ResolvedRef::Global(compiled.as_ref(), s.as_ref()), absolute));
}
}
}
}
if let Some(compiled) = self.registry.schemas.get(ref_string) {
return Some((
ResolvedRef::Global(compiled.as_ref(), compiled.as_ref()),
ref_string.to_string(),
));
}
None
}
pub fn validate(
&self,
schema_id: &str,
instance: &Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
ReadOnlyInstance(instance),
vec![],
HashSet::new(),
false,
false,
);
ctx.validate()
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
path: "".to_string(),
})
}
}
pub fn mask(
&self,
schema_id: &str,
instance: &mut Value,
) -> Result<ValidationResult, ValidationError> {
if let Some(schema) = self.registry.schemas.get(schema_id) {
let ctx = ValidationContext::new(
self,
schema,
schema,
MutableInstance::new(instance),
vec![],
HashSet::new(),
false,
false,
);
let res = ctx.validate()?;
Ok(res)
} else {
Err(ValidationError {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
path: "".to_string(),
})
}
}
}

2089
tests/fixtures.rs Normal file

File diff suppressed because it is too large Load Diff

132
tests/fixtures/additionalProperties.json vendored Normal file
View File

@ -0,0 +1,132 @@
[
{
"description": "additionalProperties validates properties not matched by properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "number"
}
},
"additionalProperties": {
"type": "boolean"
}
},
"tests": [
{
"description": "defined properties are valid",
"data": {
"foo": "value",
"bar": 123
},
"valid": true
},
{
"description": "additional property matching schema is valid",
"data": {
"foo": "value",
"is_active": true,
"hidden": false
},
"valid": true
},
{
"description": "additional property not matching schema is invalid",
"data": {
"foo": "value",
"is_active": 1
},
"valid": false
}
]
},
{
"description": "extensible: true with additionalProperties still validates structure",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "string"
}
},
"extensible": true,
"additionalProperties": {
"type": "integer"
}
},
"tests": [
{
"description": "additional property matching schema is valid",
"data": {
"foo": "hello",
"count": 5,
"age": 42
},
"valid": true
},
{
"description": "additional property not matching schema is invalid despite extensible: true",
"data": {
"foo": "hello",
"count": "five"
},
"valid": false
}
]
},
{
"description": "complex additionalProperties with object and array items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"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
},
{
"description": "invalid array of integers",
"data": {
"type": "my_type",
"group_a": [
1,
2
]
},
"valid": false
},
{
"description": "invalid non-array type",
"data": {
"type": "my_type",
"group_a": "field1"
},
"valid": false
}
]
}
]

563
tests/fixtures/allOf.json vendored Normal file
View File

@ -0,0 +1,563 @@
[
{
"description": "allOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "allOf",
"data": {
"foo": "baz",
"bar": 2
},
"valid": true
},
{
"description": "mismatch second",
"data": {
"foo": "baz"
},
"valid": false
},
{
"description": "mismatch first",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "wrong type",
"data": {
"foo": "baz",
"bar": "quux"
},
"valid": false
}
]
},
{
"description": "allOf with base schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"bar": {
"type": "integer"
},
"baz": {},
"foo": {
"type": "string"
}
},
"required": [
"bar"
],
"allOf": [
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
},
{
"properties": {
"baz": {
"type": "null"
}
},
"required": [
"baz"
]
}
]
},
"tests": [
{
"description": "valid",
"data": {
"foo": "quux",
"bar": 2,
"baz": null
},
"valid": true
},
{
"description": "mismatch base schema",
"data": {
"foo": "quux",
"baz": null
},
"valid": false
},
{
"description": "mismatch first allOf",
"data": {
"bar": 2,
"baz": null
},
"valid": false
},
{
"description": "mismatch second allOf",
"data": {
"foo": "quux",
"bar": 2
},
"valid": false
},
{
"description": "mismatch both",
"data": {
"bar": 2
},
"valid": false
}
]
},
{
"description": "allOf simple types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"maximum": 30
},
{
"minimum": 20
}
]
},
"tests": [
{
"description": "valid",
"data": 25,
"valid": true
},
{
"description": "mismatch one",
"data": 35,
"valid": false
}
]
},
{
"description": "allOf with boolean schemas, all true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
true,
true
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "allOf with boolean schemas, some false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
true,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "allOf with boolean schemas, all false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
false,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "allOf with one empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{}
]
},
"tests": [
{
"description": "any data is valid",
"data": 1,
"valid": true
}
]
},
{
"description": "allOf with two empty schemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{},
{}
]
},
"tests": [
{
"description": "any data is valid",
"data": 1,
"valid": true
}
]
},
{
"description": "allOf with the first empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{},
{
"type": "number"
}
]
},
"tests": [
{
"description": "number is valid",
"data": 1,
"valid": true
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "allOf with the last empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"type": "number"
},
{}
]
},
"tests": [
{
"description": "number is valid",
"data": 1,
"valid": true
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "nested allOf, to check validation semantics",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"allOf": [
{
"type": "null"
}
]
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "anything non-null is invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "allOf combined with anyOf, oneOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"multipleOf": 2
}
],
"anyOf": [
{
"multipleOf": 3
}
],
"oneOf": [
{
"multipleOf": 5
}
]
},
"tests": [
{
"description": "allOf: false, anyOf: false, oneOf: false",
"data": 1,
"valid": false
},
{
"description": "allOf: false, anyOf: false, oneOf: true",
"data": 5,
"valid": false
},
{
"description": "allOf: false, anyOf: true, oneOf: false",
"data": 3,
"valid": false
},
{
"description": "allOf: false, anyOf: true, oneOf: true",
"data": 15,
"valid": false
},
{
"description": "allOf: true, anyOf: false, oneOf: false",
"data": 2,
"valid": false
},
{
"description": "allOf: true, anyOf: false, oneOf: true",
"data": 10,
"valid": false
},
{
"description": "allOf: true, anyOf: true, oneOf: false",
"data": 6,
"valid": false
},
{
"description": "allOf: true, anyOf: true, oneOf: true",
"data": 30,
"valid": true
}
]
},
{
"description": "extensible: true allows extra properties in allOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
],
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": "baz",
"bar": 2,
"qux": 3
},
"valid": true
}
]
},
{
"description": "strict by default with allOf properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"properties": {
"foo": {
"const": 1
}
}
},
{
"properties": {
"bar": {
"const": 2
}
}
}
]
},
"tests": [
{
"description": "validates merged properties",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "fails on extra property z explicitly",
"data": {
"foo": 1,
"bar": 2,
"z": 3
},
"valid": false
}
]
},
{
"description": "allOf with nested extensible: true (partial looseness)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"properties": {
"foo": {
"const": 1
}
}
},
{
"extensible": true,
"properties": {
"bar": {
"const": 2
}
}
}
]
},
"tests": [
{
"description": "extensible subschema doesn't make root extensible if root is strict",
"data": {
"foo": 1,
"bar": 2,
"z": 3
},
"valid": true
}
]
},
{
"description": "strictness: allOf composition with strict refs",
"schema": {
"allOf": [
{
"$ref": "#/$defs/partA"
},
{
"$ref": "#/$defs/partB"
}
],
"$defs": {
"partA": {
"properties": {
"id": {
"type": "string"
}
}
},
"partB": {
"properties": {
"name": {
"type": "string"
}
}
}
}
},
"tests": [
{
"description": "merged instance is valid",
"data": {
"id": "1",
"name": "Me"
},
"valid": true
},
{
"description": "extra property is invalid (root is strict)",
"data": {
"id": "1",
"name": "Me",
"extra": 1
},
"valid": false
},
{
"description": "partA mismatch is invalid",
"data": {
"id": 1,
"name": "Me"
},
"valid": false
}
]
}
]

120
tests/fixtures/anchor.json vendored Normal file
View File

@ -0,0 +1,120 @@
[
{
"description": "Location-independent identifier",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "#foo",
"$defs": {
"A": {
"$anchor": "foo",
"type": "integer"
}
}
},
"tests": [
{
"data": 1,
"description": "match",
"valid": true
},
{
"data": "a",
"description": "mismatch",
"valid": false
}
]
},
{
"description": "Location-independent identifier with absolute URI",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "http://localhost:1234/draft2020-12/bar#foo",
"$defs": {
"A": {
"$id": "http://localhost:1234/draft2020-12/bar",
"$anchor": "foo",
"type": "integer"
}
}
},
"tests": [
{
"data": 1,
"description": "match",
"valid": true
},
{
"data": "a",
"description": "mismatch",
"valid": false
}
]
},
{
"description": "Location-independent identifier with base URI change in subschema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://localhost:1234/draft2020-12/root",
"$ref": "http://localhost:1234/draft2020-12/nested.json#foo",
"$defs": {
"A": {
"$id": "nested.json",
"$defs": {
"B": {
"$anchor": "foo",
"type": "integer"
}
}
}
}
},
"tests": [
{
"data": 1,
"description": "match",
"valid": true
},
{
"data": "a",
"description": "mismatch",
"valid": false
}
]
},
{
"description": "same $anchor with different base uri",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://localhost:1234/draft2020-12/foobar",
"$defs": {
"A": {
"$id": "child1",
"allOf": [
{
"$id": "child2",
"$anchor": "my_anchor",
"type": "number"
},
{
"$anchor": "my_anchor",
"type": "string"
}
]
}
},
"$ref": "child1#my_anchor"
},
"tests": [
{
"description": "$ref resolves to /$defs/A/allOf/1",
"data": "a",
"valid": true
},
{
"description": "$ref does not resolve to /$defs/A/allOf/0",
"data": 1,
"valid": false
}
]
}
]

295
tests/fixtures/anyOf.json vendored Normal file
View File

@ -0,0 +1,295 @@
[
{
"description": "anyOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "integer"
},
{
"minimum": 2
}
]
},
"tests": [
{
"description": "first anyOf valid",
"data": 1,
"valid": true
},
{
"description": "second anyOf valid",
"data": 2.5,
"valid": true
},
{
"description": "both anyOf valid",
"data": 3,
"valid": true
},
{
"description": "neither anyOf valid",
"data": 1.5,
"valid": false
}
]
},
{
"description": "anyOf with base schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"anyOf": [
{
"maxLength": 2
},
{
"minLength": 4
}
]
},
"tests": [
{
"description": "mismatch base schema",
"data": 3,
"valid": false
},
{
"description": "one anyOf valid",
"data": "foobar",
"valid": true
},
{
"description": "both anyOf invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "anyOf with boolean schemas, all true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
true,
true
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "anyOf with boolean schemas, some true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
true,
false
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "anyOf with boolean schemas, all false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
false,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "anyOf complex types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first anyOf valid (complex)",
"data": {
"bar": 2
},
"valid": true
},
{
"description": "second anyOf valid (complex)",
"data": {
"foo": "baz"
},
"valid": true
},
{
"description": "both anyOf valid (complex)",
"data": {
"foo": "baz",
"bar": 2
},
"valid": true
},
{
"description": "neither anyOf valid (complex)",
"data": {
"foo": 2,
"bar": "quux"
},
"valid": false
}
]
},
{
"description": "anyOf with one empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "number"
},
{}
]
},
"tests": [
{
"description": "string is valid",
"data": "foo",
"valid": true
},
{
"description": "number is valid",
"data": 123,
"valid": true
}
]
},
{
"description": "nested anyOf, to check validation semantics",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"anyOf": [
{
"type": "null"
}
]
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "anything non-null is invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in anyOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "integer"
},
{
"minimum": 2
}
],
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1
},
"valid": true
}
]
},
{
"description": "strict by default with anyOf properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"properties": {
"foo": {
"const": 1
}
}
},
{
"properties": {
"bar": {
"const": 2
}
}
}
]
},
"tests": [
{
"description": "valid match (foo)",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "fails on extra property z explicitly",
"data": {
"foo": 1,
"z": 3
},
"valid": false
}
]
}
]

112
tests/fixtures/boolean_schema.json vendored Normal file
View File

@ -0,0 +1,112 @@
[
{
"description": "boolean schema 'true'",
"schema": true,
"tests": [
{
"description": "number is valid",
"data": 1,
"valid": true
},
{
"description": "string is valid",
"data": "foo",
"valid": true
},
{
"description": "boolean true is valid",
"data": true,
"valid": true
},
{
"description": "boolean false is valid",
"data": false,
"valid": true
},
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "object is valid",
"data": {
"foo": "bar"
},
"valid": true
},
{
"description": "empty object is valid",
"data": {},
"valid": true
},
{
"description": "array is valid",
"data": [
"foo"
],
"valid": true
},
{
"description": "empty array is valid",
"data": [],
"valid": true
}
]
},
{
"description": "boolean schema 'false'",
"schema": false,
"tests": [
{
"description": "number is invalid",
"data": 1,
"valid": false
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
},
{
"description": "boolean true is invalid",
"data": true,
"valid": false
},
{
"description": "boolean false is invalid",
"data": false,
"valid": false
},
{
"description": "null is invalid",
"data": null,
"valid": false
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"valid": false
},
{
"description": "empty object is invalid",
"data": {},
"valid": false
},
{
"description": "array is invalid",
"data": [
"foo"
],
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
}
]
}
]

522
tests/fixtures/const.json vendored Normal file
View File

@ -0,0 +1,522 @@
[
{
"description": "const validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": 2
},
"tests": [
{
"description": "same value is valid",
"data": 2,
"valid": true
},
{
"description": "another value is invalid",
"data": 5,
"valid": false
},
{
"description": "another type is invalid",
"data": "a",
"valid": false
}
]
},
{
"description": "const with object",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": {
"foo": "bar",
"baz": "bax"
},
"properties": {
"foo": {},
"baz": {}
}
},
"tests": [
{
"description": "same object is valid",
"data": {
"foo": "bar",
"baz": "bax"
},
"valid": true
},
{
"description": "same object with different property order is valid",
"data": {
"baz": "bax",
"foo": "bar"
},
"valid": true
},
{
"description": "another object is invalid",
"data": {
"foo": "bar"
},
"valid": false
},
{
"description": "another type is invalid",
"data": [
1,
2
],
"valid": false
}
]
},
{
"description": "const with array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": [
{
"foo": "bar"
}
]
},
"tests": [
{
"description": "same array is valid",
"data": [
{
"foo": "bar"
}
],
"valid": true
},
{
"description": "another array item is invalid",
"data": [
2
],
"valid": false
},
{
"description": "array with additional items is invalid",
"data": [
1,
2,
3
],
"valid": false
}
]
},
{
"description": "const with null",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": null
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "not null is invalid",
"data": 0,
"valid": false
}
]
},
{
"description": "const with false does not match 0",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": false
},
"tests": [
{
"description": "false is valid",
"data": false,
"valid": true
},
{
"description": "integer zero is invalid",
"data": 0,
"valid": false
},
{
"description": "float zero is invalid",
"data": 0.0,
"valid": false
}
]
},
{
"description": "const with true does not match 1",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": true
},
"tests": [
{
"description": "true is valid",
"data": true,
"valid": true
},
{
"description": "integer one is invalid",
"data": 1,
"valid": false
},
{
"description": "float one is invalid",
"data": 1.0,
"valid": false
}
]
},
{
"description": "const with [false] does not match [0]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": [
false
]
},
"tests": [
{
"description": "[false] is valid",
"data": [
false
],
"valid": true
},
{
"description": "[0] is invalid",
"data": [
0
],
"valid": false
},
{
"description": "[0.0] is invalid",
"data": [
0.0
],
"valid": false
}
]
},
{
"description": "const with [true] does not match [1]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": [
true
]
},
"tests": [
{
"description": "[true] is valid",
"data": [
true
],
"valid": true
},
{
"description": "[1] is invalid",
"data": [
1
],
"valid": false
},
{
"description": "[1.0] is invalid",
"data": [
1.0
],
"valid": false
}
]
},
{
"description": "const with {\"a\": false} does not match {\"a\": 0}",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": {
"a": false
}
},
"tests": [
{
"description": "{\"a\": false} is valid",
"data": {
"a": false
},
"valid": true
},
{
"description": "{\"a\": 0} is invalid",
"data": {
"a": 0
},
"valid": false
},
{
"description": "{\"a\": 0.0} is invalid",
"data": {
"a": 0.0
},
"valid": false
}
]
},
{
"description": "const with {\"a\": true} does not match {\"a\": 1}",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": {
"a": true
}
},
"tests": [
{
"description": "{\"a\": true} is valid",
"data": {
"a": true
},
"valid": true
},
{
"description": "{\"a\": 1} is invalid",
"data": {
"a": 1
},
"valid": false
},
{
"description": "{\"a\": 1.0} is invalid",
"data": {
"a": 1.0
},
"valid": false
}
]
},
{
"description": "const with 0 does not match other zero-like types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": 0
},
"tests": [
{
"description": "false is invalid",
"data": false,
"valid": false
},
{
"description": "integer zero is valid",
"data": 0,
"valid": true
},
{
"description": "float zero is valid",
"data": 0.0,
"valid": true
},
{
"description": "empty object is invalid",
"data": {},
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
},
{
"description": "empty string is invalid",
"data": "",
"valid": false
}
]
},
{
"description": "const with 1 does not match true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": 1
},
"tests": [
{
"description": "true is invalid",
"data": true,
"valid": false
},
{
"description": "integer one is valid",
"data": 1,
"valid": true
},
{
"description": "float one is valid",
"data": 1.0,
"valid": true
}
]
},
{
"description": "const with -2.0 matches integer and float types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": -2.0
},
"tests": [
{
"description": "integer -2 is valid",
"data": -2,
"valid": true
},
{
"description": "integer 2 is invalid",
"data": 2,
"valid": false
},
{
"description": "float -2.0 is valid",
"data": -2.0,
"valid": true
},
{
"description": "float 2.0 is invalid",
"data": 2.0,
"valid": false
},
{
"description": "float -2.00001 is invalid",
"data": -2.00001,
"valid": false
}
]
},
{
"description": "float and integers are equal up to 64-bit representation limits",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": 9007199254740992
},
"tests": [
{
"description": "integer is valid",
"data": 9007199254740992,
"valid": true
},
{
"description": "integer minus one is invalid",
"data": 9007199254740991,
"valid": false
},
{
"description": "float is valid",
"data": 9007199254740992.0,
"valid": true
},
{
"description": "float minus one is invalid",
"data": 9007199254740991.0,
"valid": false
}
]
},
{
"description": "nul characters in strings",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": "hello\u0000there"
},
"tests": [
{
"description": "match string with nul",
"data": "hello\u0000there",
"valid": true
},
{
"description": "do not match string lacking nul",
"data": "hellothere",
"valid": false
}
]
},
{
"description": "characters with the same visual representation but different codepoint",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": "μ",
"$comment": "U+03BC"
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "μ",
"comment": "U+03BC",
"valid": true
},
{
"description": "character looks the same but uses a different codepoint",
"data": "µ",
"comment": "U+00B5",
"valid": false
}
]
},
{
"description": "characters with the same visual representation, but different number of codepoints",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": "ä",
"$comment": "U+00E4"
},
"tests": [
{
"description": "character uses the same codepoint",
"data": "ä",
"comment": "U+00E4",
"valid": true
},
{
"description": "character looks the same but uses combining marks",
"data": "ä",
"comment": "a, U+0308",
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in const object match",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"const": {
"a": 1
},
"extensible": true
},
"tests": [
{
"description": "extra property ignored during strict check, but const check still applies (mismatch)",
"data": {
"a": 1,
"b": 2
},
"valid": false
},
{
"description": "extra property match in const (this is effectively impossible if data has extra props not in const, it implicitly fails const check unless we assume const check ignored extra props? No, const check is strict. So this test is just to show strictness passes.)",
"data": {
"a": 1
},
"valid": true
}
]
}
]

286
tests/fixtures/contains.json vendored Normal file
View File

@ -0,0 +1,286 @@
[
{
"description": "contains keyword validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"minimum": 5
},
"items": true
},
"tests": [
{
"description": "array with item matching schema (5) is valid (items: true)",
"data": [
3,
4,
5
],
"valid": true
},
{
"description": "array with item matching schema (6) is valid (items: true)",
"data": [
3,
4,
6
],
"valid": true
},
{
"description": "array with two items matching schema (5, 6) is valid (items: true)",
"data": [
3,
4,
5,
6
],
"valid": true
},
{
"description": "array without items matching schema is invalid",
"data": [
2,
3,
4
],
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
},
{
"description": "not array is valid",
"data": {},
"valid": true
}
]
},
{
"description": "contains keyword with const keyword",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 5
},
"items": true
},
"tests": [
{
"description": "array with item 5 is valid (items: true)",
"data": [
3,
4,
5
],
"valid": true
},
{
"description": "array with two items 5 is valid (items: true)",
"data": [
3,
4,
5,
5
],
"valid": true
},
{
"description": "array without item 5 is invalid",
"data": [
1,
2,
3,
4
],
"valid": false
}
]
},
{
"description": "contains keyword with boolean schema true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": true
},
"tests": [
{
"description": "any non-empty array is valid",
"data": [
"foo"
],
"valid": true
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
}
]
},
{
"description": "contains keyword with boolean schema false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": false
},
"tests": [
{
"description": "any non-empty array is invalid",
"data": [
"foo"
],
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
},
{
"description": "non-arrays are valid",
"data": "contains does not apply to strings",
"valid": true
}
]
},
{
"description": "items + contains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"multipleOf": 2
},
"contains": {
"multipleOf": 3
}
},
"tests": [
{
"description": "matches items, does not match contains",
"data": [
2,
4,
8
],
"valid": false
},
{
"description": "does not match items, matches contains",
"data": [
3,
6,
9
],
"valid": false
},
{
"description": "matches both items and contains",
"data": [
6,
12
],
"valid": true
},
{
"description": "matches neither items nor contains",
"data": [
1,
5
],
"valid": false
}
]
},
{
"description": "contains with false if subschema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"if": false,
"else": true
}
},
"tests": [
{
"description": "any non-empty array is valid",
"data": [
"foo"
],
"valid": true
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
}
]
},
{
"description": "contains with null instance elements",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"type": "null"
}
},
"tests": [
{
"description": "allows null items",
"data": [
null
],
"valid": true
}
]
},
{
"description": "extensible: true allows non-matching items in contains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"extensible": true
},
"tests": [
{
"description": "extra items acceptable",
"data": [
1,
2
],
"valid": true
}
]
},
{
"description": "strict by default: non-matching items in contains are invalid",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
}
},
"tests": [
{
"description": "extra items cause failure",
"data": [
1,
2
],
"valid": false
},
{
"description": "only matching items is valid",
"data": [
1,
1
],
"valid": true
}
]
}
]

144
tests/fixtures/content.json vendored Normal file
View File

@ -0,0 +1,144 @@
[
{
"description": "validation of string-encoded content based on media type",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentMediaType": "application/json"
},
"tests": [
{
"description": "a valid JSON document",
"data": "{\"foo\": \"bar\"}",
"valid": true
},
{
"description": "an invalid JSON document; validates true",
"data": "{:}",
"valid": true
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true
}
]
},
{
"description": "validation of binary string-encoding",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentEncoding": "base64"
},
"tests": [
{
"description": "a valid base64 string",
"data": "eyJmb28iOiAiYmFyIn0K",
"valid": true
},
{
"description": "an invalid base64 string (% is not a valid character); validates true",
"data": "eyJmb28iOi%iYmFyIn0K",
"valid": true
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true
}
]
},
{
"description": "validation of binary-encoded media type documents",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentMediaType": "application/json",
"contentEncoding": "base64"
},
"tests": [
{
"description": "a valid base64-encoded JSON document",
"data": "eyJmb28iOiAiYmFyIn0K",
"valid": true
},
{
"description": "a validly-encoded invalid JSON document; validates true",
"data": "ezp9Cg==",
"valid": true
},
{
"description": "an invalid base64 string that is valid JSON; validates true",
"data": "{}",
"valid": true
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true
}
]
},
{
"description": "validation of binary-encoded media type documents with schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentMediaType": "application/json",
"contentEncoding": "base64",
"contentSchema": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string"
},
"boo": {
"type": "integer"
}
}
}
},
"tests": [
{
"description": "a valid base64-encoded JSON document",
"data": "eyJmb28iOiAiYmFyIn0K",
"valid": true
},
{
"description": "another valid base64-encoded JSON document",
"data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==",
"valid": true
},
{
"description": "an invalid base64-encoded JSON document; validates true",
"data": "eyJib28iOiAyMH0=",
"valid": true
},
{
"description": "an empty object as a base64-encoded JSON document; validates true",
"data": "e30=",
"valid": true
},
{
"description": "an empty array as a base64-encoded JSON document",
"data": "W10=",
"valid": true
},
{
"description": "a validly-encoded invalid JSON document; validates true",
"data": "ezp9Cg==",
"valid": true
},
{
"description": "an invalid base64 string that is valid JSON; validates true",
"data": "{}",
"valid": true
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true
}
]
}
]

220
tests/fixtures/dependentRequired.json vendored Normal file
View File

@ -0,0 +1,220 @@
[
{
"description": "single dependency",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"bar": [
"foo"
]
},
"extensible": true
},
"tests": [
{
"description": "neither",
"data": {},
"valid": true
},
{
"description": "nondependant",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "with dependency",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "missing dependency",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "ignores arrays",
"data": [
"bar"
],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "empty dependents",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"bar": []
},
"extensible": true
},
"tests": [
{
"description": "empty object",
"data": {},
"valid": true
},
{
"description": "object with one property",
"data": {
"bar": 2
},
"valid": true
},
{
"description": "non-object is valid",
"data": 1,
"valid": true
}
]
},
{
"description": "multiple dependents required",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"quux": [
"foo",
"bar"
]
},
"extensible": true
},
"tests": [
{
"description": "neither",
"data": {},
"valid": true
},
{
"description": "nondependants",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "with dependencies",
"data": {
"foo": 1,
"bar": 2,
"quux": 3
},
"valid": true
},
{
"description": "missing dependency",
"data": {
"foo": 1,
"quux": 2
},
"valid": false
},
{
"description": "missing other dependency",
"data": {
"bar": 1,
"quux": 2
},
"valid": false
},
{
"description": "missing both dependencies",
"data": {
"quux": 1
},
"valid": false
}
]
},
{
"description": "dependencies with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"foo\nbar": [
"foo\rbar"
],
"foo\"bar": [
"foo'bar"
]
},
"extensible": true
},
"tests": [
{
"description": "CRLF",
"data": {
"foo\nbar": 1,
"foo\rbar": 2
},
"valid": true
},
{
"description": "quoted quotes",
"data": {
"foo'bar": 1,
"foo\"bar": 2
},
"valid": true
},
{
"description": "CRLF missing dependent",
"data": {
"foo\nbar": 1,
"foo": 2
},
"valid": false
},
{
"description": "quoted quotes missing dependent",
"data": {
"foo\"bar": 2
},
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in dependentRequired",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"bar": [
"foo"
]
},
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": true
}
]
}
]

303
tests/fixtures/dependentSchemas.json vendored Normal file
View File

@ -0,0 +1,303 @@
[
{
"description": "single dependency (STRICT)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": true
},
"dependentSchemas": {
"bar": {
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
}
},
"tests": [
{
"description": "valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "no dependency",
"data": {
"foo": "quux"
},
"valid": true
},
{
"description": "wrong type",
"data": {
"foo": "quux",
"bar": 2
},
"valid": false
},
{
"description": "wrong type other",
"data": {
"foo": 2,
"bar": "quux"
},
"valid": false
},
{
"description": "wrong type both",
"data": {
"foo": "quux",
"bar": "quux"
},
"valid": false
},
{
"description": "ignores arrays (invalid in strict mode)",
"data": [
"bar"
],
"valid": false,
"expect_errors": [
{
"code": "STRICT_ITEM_VIOLATION"
}
]
},
{
"description": "ignores strings (invalid in strict mode - wait, strings are scalars, strict only checks obj/arr)",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "single dependency (EXTENSIBLE)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": true
},
"dependentSchemas": {
"bar": {
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "integer"
}
}
}
},
"extensible": true
},
"tests": [
{
"description": "ignores arrays (valid in extensible mode)",
"data": [
"bar"
],
"valid": true
}
]
},
{
"description": "boolean subschemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": true
},
"dependentSchemas": {
"foo": true,
"bar": false
}
},
"tests": [
{
"description": "object with property having schema true is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "object with property having schema false is invalid",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "object with both properties is invalid",
"data": {
"foo": 1,
"bar": 2
},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "dependencies with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo\tbar": true,
"foo'bar": true,
"a": true,
"b": true,
"c": true
},
"dependentSchemas": {
"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
},
{
"description": "quoted quote",
"data": {
"foo'bar": {
"foo\"bar": 1
}
},
"valid": false
},
{
"description": "quoted tab invalid under dependent schema",
"data": {
"foo\tbar": 1,
"a": 2
},
"valid": false
},
{
"description": "quoted quote invalid under dependent schema",
"data": {
"foo'bar": 1
},
"valid": false
}
]
},
{
"description": "dependent subschema incompatible with root (STRICT)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {},
"baz": true
},
"dependentSchemas": {
"foo": {
"properties": {
"bar": {}
}
}
}
},
"tests": [
{
"description": "matches root",
"data": {
"foo": 1
},
"valid": false
},
{
"description": "matches dependency (invalid in strict mode - bar not allowed if foo missing)",
"data": {
"bar": 1
},
"valid": false,
"expect_errors": [
{
"code": "STRICT_PROPERTY_VIOLATION"
}
]
},
{
"description": "matches both",
"data": {
"foo": 1,
"bar": 2
},
"valid": false
},
{
"description": "no dependency",
"data": {
"baz": 1
},
"valid": true
}
]
},
{
"description": "dependent subschema incompatible with root (EXTENSIBLE)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {},
"baz": true
},
"dependentSchemas": {
"foo": {
"properties": {
"bar": {}
},
"additionalProperties": false
}
},
"extensible": true
},
"tests": [
{
"description": "matches dependency (valid in extensible mode)",
"data": {
"bar": 1
},
"valid": true
}
]
}
]

1111
tests/fixtures/dynamicRef.json vendored Normal file

File diff suppressed because it is too large Load Diff

119
tests/fixtures/emptyString.json vendored Normal file
View File

@ -0,0 +1,119 @@
[
{
"description": "empty string is valid for all types (except const)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"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": ""
}
}
},
"tests": [
{
"description": "empty string valid for object",
"data": {
"obj": ""
},
"valid": true
},
{
"description": "empty string valid for array",
"data": {
"arr": ""
},
"valid": true
},
{
"description": "empty string valid for string",
"data": {
"str": ""
},
"valid": true
},
{
"description": "empty string valid for integer",
"data": {
"int": ""
},
"valid": true
},
{
"description": "empty string valid for number",
"data": {
"num": ""
},
"valid": true
},
{
"description": "empty string valid for boolean",
"data": {
"bool": ""
},
"valid": true
},
{
"description": "empty string valid for null",
"data": {
"nul": ""
},
"valid": true
},
{
"description": "empty string valid for format",
"data": {
"fmt": ""
},
"valid": true
},
{
"description": "empty string INVALID for const (unless const is empty string)",
"data": {
"con": ""
},
"valid": false,
"expect_errors": [
{
"code": "CONST_VIOLATED",
"path": "/con"
}
]
},
{
"description": "empty string VALID for const if const IS empty string",
"data": {
"con_empty": ""
},
"valid": true
}
]
}
]

488
tests/fixtures/enum.json vendored Normal file
View File

@ -0,0 +1,488 @@
[
{
"description": "simple enum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
1,
2,
3
]
},
"tests": [
{
"description": "one of the enum is valid",
"data": 1,
"valid": true
},
{
"description": "something else is invalid",
"data": 4,
"valid": false
}
]
},
{
"description": "heterogeneous enum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
6,
"foo",
[],
true,
{
"foo": 12
}
],
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "one of the enum is valid",
"data": [],
"valid": true
},
{
"description": "something else is invalid",
"data": null,
"valid": false
},
{
"description": "objects are deep compared",
"data": {
"foo": false
},
"valid": false
},
{
"description": "valid object matches",
"data": {
"foo": 12
},
"valid": true
},
{
"description": "extra properties in object is invalid",
"data": {
"foo": 12,
"boo": 42
},
"valid": false
}
]
},
{
"description": "heterogeneous enum-with-null validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
6,
null
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "number is valid",
"data": 6,
"valid": true
},
{
"description": "something else is invalid",
"data": "test",
"valid": false
}
]
},
{
"description": "enums in properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"foo": {
"enum": [
"foo"
]
},
"bar": {
"enum": [
"bar"
]
}
},
"required": [
"bar"
]
},
"tests": [
{
"description": "both properties are valid",
"data": {
"foo": "foo",
"bar": "bar"
},
"valid": true
},
{
"description": "wrong foo value",
"data": {
"foo": "foot",
"bar": "bar"
},
"valid": false
},
{
"description": "wrong bar value",
"data": {
"foo": "foo",
"bar": "bart"
},
"valid": false
},
{
"description": "missing optional property is valid",
"data": {
"bar": "bar"
},
"valid": true
},
{
"description": "missing required property is invalid",
"data": {
"foo": "foo"
},
"valid": false
},
{
"description": "missing all properties is invalid",
"data": {},
"valid": false
}
]
},
{
"description": "enum with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"foo\nbar",
"foo\rbar"
]
},
"tests": [
{
"description": "member 1 is valid",
"data": "foo\nbar",
"valid": true
},
{
"description": "member 2 is valid",
"data": "foo\rbar",
"valid": true
},
{
"description": "another string is invalid",
"data": "abc",
"valid": false
}
]
},
{
"description": "enum with false does not match 0",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
false
]
},
"tests": [
{
"description": "false is valid",
"data": false,
"valid": true
},
{
"description": "integer zero is invalid",
"data": 0,
"valid": false
},
{
"description": "float zero is invalid",
"data": 0.0,
"valid": false
}
]
},
{
"description": "enum with [false] does not match [0]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
[
false
]
]
},
"tests": [
{
"description": "[false] is valid",
"data": [
false
],
"valid": true
},
{
"description": "[0] is invalid",
"data": [
0
],
"valid": false
},
{
"description": "[0.0] is invalid",
"data": [
0.0
],
"valid": false
}
]
},
{
"description": "enum with true does not match 1",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
true
]
},
"tests": [
{
"description": "true is valid",
"data": true,
"valid": true
},
{
"description": "integer one is invalid",
"data": 1,
"valid": false
},
{
"description": "float one is invalid",
"data": 1.0,
"valid": false
}
]
},
{
"description": "enum with [true] does not match [1]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
[
true
]
]
},
"tests": [
{
"description": "[true] is valid",
"data": [
true
],
"valid": true
},
{
"description": "[1] is invalid",
"data": [
1
],
"valid": false
},
{
"description": "[1.0] is invalid",
"data": [
1.0
],
"valid": false
}
]
},
{
"description": "enum with 0 does not match false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
0
]
},
"tests": [
{
"description": "false is invalid",
"data": false,
"valid": false
},
{
"description": "integer zero is valid",
"data": 0,
"valid": true
},
{
"description": "float zero is valid",
"data": 0.0,
"valid": true
}
]
},
{
"description": "enum with [0] does not match [false]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
[
0
]
]
},
"tests": [
{
"description": "[false] is invalid",
"data": [
false
],
"valid": false
},
{
"description": "[0] is valid",
"data": [
0
],
"valid": true
},
{
"description": "[0.0] is valid",
"data": [
0.0
],
"valid": true
}
]
},
{
"description": "enum with 1 does not match true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
1
]
},
"tests": [
{
"description": "true is invalid",
"data": true,
"valid": false
},
{
"description": "integer one is valid",
"data": 1,
"valid": true
},
{
"description": "float one is valid",
"data": 1.0,
"valid": true
}
]
},
{
"description": "enum with [1] does not match [true]",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
[
1
]
]
},
"tests": [
{
"description": "[true] is invalid",
"data": [
true
],
"valid": false
},
{
"description": "[1] is valid",
"data": [
1
],
"valid": true
},
{
"description": "[1.0] is valid",
"data": [
1.0
],
"valid": true
}
]
},
{
"description": "nul characters in strings",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
"hello\u0000there"
]
},
"tests": [
{
"description": "match string with nul",
"data": "hello\u0000there",
"valid": true
},
{
"description": "do not match string lacking nul",
"data": "hellothere",
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in enum object match",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"enum": [
{
"foo": 1
}
],
"extensible": true
},
"tests": [
{
"description": "extra property ignored during strict check, but enum check still applies (mismatch here)",
"data": {
"foo": 1,
"bar": 2
},
"valid": false
},
{
"description": "extra property ignored during strict check, enum match succeeds",
"data": {
"foo": 1
},
"valid": true
}
]
}
]

31
tests/fixtures/exclusiveMaximum.json vendored Normal file
View File

@ -0,0 +1,31 @@
[
{
"description": "exclusiveMaximum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMaximum": 3.0
},
"tests": [
{
"description": "below the exclusiveMaximum is valid",
"data": 2.2,
"valid": true
},
{
"description": "boundary point is invalid",
"data": 3.0,
"valid": false
},
{
"description": "above the exclusiveMaximum is invalid",
"data": 3.5,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "x",
"valid": true
}
]
}
]

31
tests/fixtures/exclusiveMinimum.json vendored Normal file
View File

@ -0,0 +1,31 @@
[
{
"description": "exclusiveMinimum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"exclusiveMinimum": 1.1
},
"tests": [
{
"description": "above the exclusiveMinimum is valid",
"data": 1.2,
"valid": true
},
{
"description": "boundary point is invalid",
"data": 1.1,
"valid": false
},
{
"description": "below the exclusiveMinimum is invalid",
"data": 0.6,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "x",
"valid": true
}
]
}
]

3112
tests/fixtures/format.json vendored Normal file

File diff suppressed because it is too large Load Diff

404
tests/fixtures/if-then-else.json vendored Normal file
View File

@ -0,0 +1,404 @@
[
{
"description": "ignore if without then or else",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"const": 0
}
},
"tests": [
{
"description": "valid when valid against lone if",
"data": 0,
"valid": true
},
{
"description": "valid when invalid against lone if",
"data": "hello",
"valid": true
}
]
},
{
"description": "ignore then without if",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"then": {
"const": 0
}
},
"tests": [
{
"description": "valid when valid against lone then",
"data": 0,
"valid": true
},
{
"description": "valid when invalid against lone then",
"data": "hello",
"valid": true
}
]
},
{
"description": "ignore else without if",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"else": {
"const": 0
}
},
"tests": [
{
"description": "valid when valid against lone else",
"data": 0,
"valid": true
},
{
"description": "valid when invalid against lone else",
"data": "hello",
"valid": true
}
]
},
{
"description": "if and then without else",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"exclusiveMaximum": 0
},
"then": {
"minimum": -10
}
},
"tests": [
{
"description": "valid through then",
"data": -1,
"valid": true
},
{
"description": "invalid through then",
"data": -100,
"valid": false
},
{
"description": "valid when if test fails",
"data": 3,
"valid": true
}
]
},
{
"description": "if and else without then",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"exclusiveMaximum": 0
},
"else": {
"multipleOf": 2
}
},
"tests": [
{
"description": "valid when if test passes",
"data": -1,
"valid": true
},
{
"description": "valid through else",
"data": 4,
"valid": true
},
{
"description": "invalid through else",
"data": 3,
"valid": false
}
]
},
{
"description": "validate against correct branch, then vs else",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"exclusiveMaximum": 0
},
"then": {
"minimum": -10
},
"else": {
"multipleOf": 2
}
},
"tests": [
{
"description": "valid through then",
"data": -1,
"valid": true
},
{
"description": "invalid through then",
"data": -100,
"valid": false
},
{
"description": "valid through else",
"data": 4,
"valid": true
},
{
"description": "invalid through else",
"data": 3,
"valid": false
}
]
},
{
"description": "non-interference across combined schemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"if": {
"exclusiveMaximum": 0
}
},
{
"then": {
"minimum": -10
}
},
{
"else": {
"multipleOf": 2
}
}
]
},
"tests": [
{
"description": "valid, but would have been invalid through then",
"data": -100,
"valid": true
},
{
"description": "valid, but would have been invalid through else",
"data": 3,
"valid": true
}
]
},
{
"description": "if with boolean schema true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": true,
"then": {
"const": "then"
},
"else": {
"const": "else"
}
},
"tests": [
{
"description": "boolean schema true in if always chooses the then path (valid)",
"data": "then",
"valid": true
},
{
"description": "boolean schema true in if always chooses the then path (invalid)",
"data": "else",
"valid": false
}
]
},
{
"description": "if with boolean schema false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": false,
"then": {
"const": "then"
},
"else": {
"const": "else"
}
},
"tests": [
{
"description": "boolean schema false in if always chooses the else path (invalid)",
"data": "then",
"valid": false
},
{
"description": "boolean schema false in if always chooses the else path (valid)",
"data": "else",
"valid": true
}
]
},
{
"description": "if appears at the end when serialized (keyword processing sequence)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"then": {
"const": "yes"
},
"else": {
"const": "other"
},
"if": {
"maxLength": 4
}
},
"tests": [
{
"description": "yes redirects to then and passes",
"data": "yes",
"valid": true
},
{
"description": "other redirects to else and passes",
"data": "other",
"valid": true
},
{
"description": "no redirects to then and fails",
"data": "no",
"valid": false
},
{
"description": "invalid redirects to else and fails",
"data": "invalid",
"valid": false
}
]
},
{
"description": "then: false fails when condition matches",
"schema": {
"if": {
"const": 1
},
"then": false
},
"tests": [
{
"description": "matches if → then=false → invalid",
"data": 1,
"valid": false
},
{
"description": "does not match if → then ignored → valid",
"data": 2,
"valid": true
}
]
},
{
"description": "else: false fails when condition does not match",
"schema": {
"if": {
"const": 1
},
"else": false
},
"tests": [
{
"description": "matches if → else ignored → valid",
"data": 1,
"valid": true
},
{
"description": "does not match if → else executes → invalid",
"data": 2,
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in if-then-else",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"properties": {
"foo": {
"const": 1
}
},
"required": [
"foo"
]
},
"then": {
"properties": {
"bar": {
"const": 2
}
},
"required": [
"bar"
]
},
"extensible": true
},
"tests": [
{
"description": "extra property is valid (matches if and then)",
"data": {
"foo": 1,
"bar": 2,
"extra": "prop"
},
"valid": true
}
]
},
{
"description": "strict by default with if-then properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"properties": {
"foo": {
"const": 1
}
},
"required": [
"foo"
]
},
"then": {
"properties": {
"bar": {
"const": 2
}
}
}
},
"tests": [
{
"description": "valid match (foo + bar)",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "fails on extra property z explicitly",
"data": {
"foo": 1,
"bar": 2,
"z": 3
},
"valid": false
}
]
}
]

738
tests/fixtures/items.json vendored Normal file
View File

@ -0,0 +1,738 @@
[
{
"description": "a schema given for items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"type": "integer"
}
},
"tests": [
{
"description": "valid items",
"data": [
1,
2,
3
],
"valid": true
},
{
"description": "wrong type of items",
"data": [
1,
"x"
],
"valid": false
},
{
"description": "non-arrays are invalid",
"data": {
"foo": "bar"
},
"valid": false
},
{
"description": "JavaScript pseudo-arrays are invalid",
"data": {
"0": "invalid",
"length": 1
},
"valid": false
}
]
},
{
"description": "items with boolean schema (true)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": true
},
"tests": [
{
"description": "any array is valid",
"data": [
1,
"foo",
true
],
"valid": true
},
{
"description": "empty array is valid",
"data": [],
"valid": true
}
]
},
{
"description": "items with boolean schema (false)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": false
},
"tests": [
{
"description": "any non-empty array is invalid",
"data": [
1,
"foo",
true
],
"valid": false
},
{
"description": "empty array is valid",
"data": [],
"valid": true
}
]
},
{
"description": "items and subitems",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"item": {
"type": "array",
"items": false,
"prefixItems": [
{
"$ref": "#/$defs/sub-item"
},
{
"$ref": "#/$defs/sub-item"
}
]
},
"sub-item": {
"type": "object",
"required": [
"foo"
]
}
},
"type": "array",
"items": false,
"prefixItems": [
{
"$ref": "#/$defs/item"
},
{
"$ref": "#/$defs/item"
},
{
"$ref": "#/$defs/item"
}
]
},
"tests": [
{
"description": "valid items",
"data": [
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"valid": false
},
{
"description": "too many items",
"data": [
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"valid": false
},
{
"description": "too many sub-items",
"data": [
[
{
"foo": null
},
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"valid": false
},
{
"description": "wrong item",
"data": [
{
"foo": null
},
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"valid": false
},
{
"description": "wrong sub-item",
"data": [
[
{},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
],
[
{
"foo": null
},
{
"foo": null
}
]
],
"valid": false
},
{
"description": "fewer items is invalid",
"data": [
[
{
"foo": null
}
],
[
{
"foo": null
}
]
],
"valid": false
}
]
},
{
"description": "nested items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
},
"tests": [
{
"description": "valid nested array",
"data": [
[
[
[
1
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"valid": true
},
{
"description": "nested array with invalid type",
"data": [
[
[
[
"1"
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"valid": false
},
{
"description": "not deep enough",
"data": [
[
[
1
],
[
2
],
[
3
]
],
[
[
4
],
[
5
],
[
6
]
]
],
"valid": false
}
]
},
{
"description": "prefixItems with no additional items allowed",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{},
{},
{}
],
"items": false
},
"tests": [
{
"description": "empty array",
"data": [],
"valid": true
},
{
"description": "fewer number of items present (1)",
"data": [
1
],
"valid": true
},
{
"description": "fewer number of items present (2)",
"data": [
1,
2
],
"valid": true
},
{
"description": "equal number of items present",
"data": [
1,
2,
3
],
"valid": true
},
{
"description": "additional items are not permitted",
"data": [
1,
2,
3,
4
],
"valid": false
}
]
},
{
"description": "items does not look in applicators, valid case",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"prefixItems": [
{
"minimum": 3
}
]
}
],
"items": {
"minimum": 5
}
},
"tests": [
{
"description": "prefixItems in allOf does not constrain items, invalid case",
"data": [
3,
5
],
"valid": false
},
{
"description": "prefixItems in allOf does not constrain items, valid case",
"data": [
5,
5
],
"valid": true
}
]
},
{
"description": "prefixItems validation adjusts the starting index for items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "string"
}
],
"items": {
"type": "integer"
}
},
"tests": [
{
"description": "valid items",
"data": [
"x",
2,
3
],
"valid": true
},
{
"description": "wrong type of second item",
"data": [
"x",
"y"
],
"valid": false
}
]
},
{
"description": "items with heterogeneous array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{}
],
"items": false
},
"tests": [
{
"description": "heterogeneous invalid instance",
"data": [
"foo",
"bar",
37
],
"valid": false
},
{
"description": "valid instance",
"data": [
null
],
"valid": true
}
]
},
{
"description": "items with null instance elements",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"type": "null"
}
},
"tests": [
{
"description": "allows null elements",
"data": [
null
],
"valid": true
}
]
},
{
"description": "extensible: true allows extra items (when items is false)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": false,
"extensible": true
},
"tests": [
{
"description": "extra item is valid",
"data": [
1
],
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties for items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"minimum": 5
},
"extensible": true
},
"tests": [
{
"description": "valid item is valid",
"data": [
5,
6
],
"valid": true
},
{
"description": "invalid item (less than min) is invalid even with extensible: true",
"data": [
4
],
"valid": false
}
]
},
{
"description": "array: simple extensible array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"extensible": true
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"valid": true
},
{
"description": "array with items is valid (extensible)",
"data": [
1,
"foo"
],
"valid": true
}
]
},
{
"description": "array: strict array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"extensible": false
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"valid": true
},
{
"description": "array with items is invalid (strict)",
"data": [
1
],
"valid": false
}
]
},
{
"description": "array: items extensible",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"extensible": true
}
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"valid": true
},
{
"description": "array with items is valid (items explicitly allowed to be anything extensible)",
"data": [
1,
"foo",
{}
],
"valid": true
}
]
},
{
"description": "array: items strict",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"type": "object",
"extensible": false
}
},
"tests": [
{
"description": "empty array is valid (empty objects)",
"data": [
{}
],
"valid": true
},
{
"description": "array with strict object items is valid",
"data": [
{}
],
"valid": true
},
{
"description": "array with invalid strict object items (extra property)",
"data": [
{
"extra": 1
}
],
"valid": false
}
]
}
]

171
tests/fixtures/masking.json vendored Normal file
View File

@ -0,0 +1,171 @@
[
{
"description": "Masking Properties",
"schema": {
"$id": "mask_properties",
"type": "object",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "integer"
}
},
"required": [
"foo"
],
"extensible": false
},
"tests": [
{
"description": "Keep valid properties",
"data": {
"foo": "a",
"bar": 1
},
"valid": true,
"expected": {
"foo": "a",
"bar": 1
}
},
{
"description": "Remove unknown properties",
"data": {
"foo": "a",
"baz": true
},
"valid": true,
"expected": {
"foo": "a"
}
},
{
"description": "Keep valid properties with unknown",
"data": {
"foo": "a",
"bar": 1,
"baz": true
},
"valid": true,
"expected": {
"foo": "a",
"bar": 1
}
}
]
},
{
"description": "Masking Nested Objects",
"schema": {
"$id": "mask_nested",
"type": "object",
"properties": {
"meta": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
},
"extensible": false
}
},
"extensible": false
},
"tests": [
{
"description": "Mask nested object",
"data": {
"meta": {
"id": 1,
"extra": "x"
},
"top_extra": "y"
},
"valid": true,
"expected": {
"meta": {
"id": 1
}
}
}
]
},
{
"description": "Masking Arrays",
"schema": {
"$id": "mask_arrays",
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"extensible": false
},
"tests": [
{
"description": "Arrays are kept (items are valid)",
"data": {
"tags": [
"a",
"b"
]
},
"valid": true,
"expected": {
"tags": [
"a",
"b"
]
}
}
]
},
{
"description": "Masking Tuple Arrays (prefixItems)",
"schema": {
"$id": "mask_tuple",
"type": "object",
"properties": {
"coord": {
"type": "array",
"prefixItems": [
{
"type": "number"
},
{
"type": "number"
}
]
}
},
"extensible": false
},
"tests": [
{
"description": "Extra tuple items removed",
"data": {
"coord": [
1,
2,
3,
"extra"
]
},
"valid": true,
"expected": {
"coord": [
1,
2
]
}
}
]
}
]

163
tests/fixtures/maxContains.json vendored Normal file
View File

@ -0,0 +1,163 @@
[
{
"description": "maxContains without contains is ignored",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxContains": 1,
"extensible": true
},
"tests": [
{
"description": "one item valid against lone maxContains",
"data": [
1
],
"valid": true
},
{
"description": "two items still valid against lone maxContains",
"data": [
1,
2
],
"valid": true
}
]
},
{
"description": "maxContains with contains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": false
},
{
"description": "all elements match, valid maxContains",
"data": [
1
],
"valid": true
},
{
"description": "all elements match, invalid maxContains",
"data": [
1,
1
],
"valid": false
},
{
"description": "some elements match, valid maxContains",
"data": [
1,
2
],
"valid": true
},
{
"description": "some elements match, invalid maxContains",
"data": [
1,
2,
1
],
"valid": false
}
]
},
{
"description": "maxContains with contains, value with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"maxContains": 1.0,
"extensible": true
},
"tests": [
{
"description": "one element matches, valid maxContains",
"data": [
1
],
"valid": true
},
{
"description": "too many elements match, invalid maxContains",
"data": [
1,
1
],
"valid": false
}
]
},
{
"description": "minContains < maxContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 1,
"maxContains": 3,
"extensible": true
},
"tests": [
{
"description": "actual < minContains < maxContains",
"data": [],
"valid": false
},
{
"description": "minContains < actual < maxContains",
"data": [
1,
1
],
"valid": true
},
{
"description": "minContains < maxContains < actual",
"data": [
1,
1,
1,
1
],
"valid": false
}
]
},
{
"description": "extensible: true allows non-matching items in maxContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"maxContains": 1,
"extensible": true
},
"tests": [
{
"description": "extra items disregarded for maxContains",
"data": [
1,
2
],
"valid": true
}
]
}
]

86
tests/fixtures/maxItems.json vendored Normal file
View File

@ -0,0 +1,86 @@
[
{
"description": "maxItems validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxItems": 2,
"extensible": true
},
"tests": [
{
"description": "shorter is valid",
"data": [
1
],
"valid": true
},
{
"description": "exact length is valid",
"data": [
1,
2
],
"valid": true
},
{
"description": "too long is invalid",
"data": [
1,
2,
3
],
"valid": false
},
{
"description": "ignores non-arrays",
"data": "foobar",
"valid": true
}
]
},
{
"description": "maxItems validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxItems": 2.0,
"extensible": true
},
"tests": [
{
"description": "shorter is valid",
"data": [
1
],
"valid": true
},
{
"description": "too long is invalid",
"data": [
1,
2,
3
],
"valid": false
}
]
},
{
"description": "extensible: true allows extra items in maxItems (but counted)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxItems": 2,
"extensible": true
},
"tests": [
{
"description": "extra item counted towards maxItems",
"data": [
1,
2,
3
],
"valid": false
}
]
}
]

55
tests/fixtures/maxLength.json vendored Normal file
View File

@ -0,0 +1,55 @@
[
{
"description": "maxLength validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxLength": 2
},
"tests": [
{
"description": "shorter is valid",
"data": "f",
"valid": true
},
{
"description": "exact length is valid",
"data": "fo",
"valid": true
},
{
"description": "too long is invalid",
"data": "foo",
"valid": false
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true
},
{
"description": "two graphemes is long enough",
"data": "\uD83D\uDCA9\uD83D\uDCA9",
"valid": true
}
]
},
{
"description": "maxLength validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxLength": 2.0
},
"tests": [
{
"description": "shorter is valid",
"data": "f",
"valid": true
},
{
"description": "too long is invalid",
"data": "foo",
"valid": false
}
]
}
]

129
tests/fixtures/maxProperties.json vendored Normal file
View File

@ -0,0 +1,129 @@
[
{
"description": "maxProperties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 2,
"extensible": true
},
"tests": [
{
"description": "shorter is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "exact length is valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "too long is invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
},
{
"description": "ignores arrays",
"data": [
1,
2,
3
],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "maxProperties validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 2.0,
"extensible": true
},
"tests": [
{
"description": "shorter is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "too long is invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
}
]
},
{
"description": "maxProperties = 0 means the object is empty",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 0,
"extensible": true
},
"tests": [
{
"description": "no properties is valid",
"data": {},
"valid": true
},
{
"description": "one property is invalid",
"data": {
"foo": 1
},
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in maxProperties (though maxProperties still counts them!)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 2,
"extensible": true
},
"tests": [
{
"description": "extra property is counted towards maxProperties",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
},
{
"description": "extra property is valid if below maxProperties",
"data": {
"foo": 1
},
"valid": true
}
]
}
]

60
tests/fixtures/maximum.json vendored Normal file
View File

@ -0,0 +1,60 @@
[
{
"description": "maximum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 3.0
},
"tests": [
{
"description": "below the maximum is valid",
"data": 2.6,
"valid": true
},
{
"description": "boundary point is valid",
"data": 3.0,
"valid": true
},
{
"description": "above the maximum is invalid",
"data": 3.5,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "x",
"valid": true
}
]
},
{
"description": "maximum validation with unsigned integer",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maximum": 300
},
"tests": [
{
"description": "below the maximum is invalid",
"data": 299.97,
"valid": true
},
{
"description": "boundary point integer is valid",
"data": 300,
"valid": true
},
{
"description": "boundary point float is valid",
"data": 300.00,
"valid": true
},
{
"description": "above the maximum is invalid",
"data": 300.5,
"valid": false
}
]
}
]

226
tests/fixtures/merge.json vendored Normal file
View File

@ -0,0 +1,226 @@
[
{
"description": "merging: properties accumulate",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"base": {
"properties": {
"base_prop": {
"type": "string"
}
}
}
},
"$ref": "#/$defs/base",
"properties": {
"child_prop": {
"type": "string"
}
}
},
"tests": [
{
"description": "valid with both properties",
"data": {
"base_prop": "a",
"child_prop": "b"
},
"valid": true
},
{
"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"
}
]
}
]
},
{
"description": "merging: required fields accumulate",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"base": {
"properties": {
"a": {
"type": "string"
}
},
"required": [
"a"
]
}
},
"$ref": "#/$defs/base",
"properties": {
"b": {
"type": "string"
}
},
"required": [
"b"
]
},
"tests": [
{
"description": "valid when both present",
"data": {
"a": "ok",
"b": "ok"
},
"valid": true
},
{
"description": "invalid when base required missing",
"data": {
"b": "ok"
},
"valid": false,
"expect_errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"path": "/a"
}
]
},
{
"description": "invalid when child required missing",
"data": {
"a": "ok"
},
"valid": false,
"expect_errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"path": "/b"
}
]
}
]
},
{
"description": "merging: dependencies accumulate",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"base": {
"properties": {
"trigger": {
"type": "string"
},
"base_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"base_dep"
]
}
}
},
"$ref": "#/$defs/base",
"properties": {
"child_dep": {
"type": "string"
}
},
"dependencies": {
"trigger": [
"child_dep"
]
}
},
"tests": [
{
"description": "valid with all deps",
"data": {
"trigger": "go",
"base_dep": "ok",
"child_dep": "ok"
},
"valid": true
},
{
"description": "invalid missing base dep",
"data": {
"trigger": "go",
"child_dep": "ok"
},
"valid": false,
"expect_errors": [
{
"code": "DEPENDENCY_FAILED",
"path": "/base_dep"
}
]
},
{
"description": "invalid missing child dep",
"data": {
"trigger": "go",
"base_dep": "ok"
},
"valid": false,
"expect_errors": [
{
"code": "DEPENDENCY_FAILED",
"path": "/child_dep"
}
]
}
]
},
{
"description": "merging: form and display do NOT merge",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"base": {
"properties": {
"a": {
"type": "string"
},
"b": {
"type": "string"
}
},
"form": [
"a",
"b"
]
}
},
"$ref": "#/$defs/base",
"properties": {
"c": {
"type": "string"
}
},
"form": [
"c"
]
},
"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)"
}
]
}
]

325
tests/fixtures/minContains.json vendored Normal file
View File

@ -0,0 +1,325 @@
[
{
"description": "minContains without contains is ignored",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minContains": 1,
"extensible": true
},
"tests": [
{
"description": "one item valid against lone minContains",
"data": [
1
],
"valid": true
},
{
"description": "zero items still valid against lone minContains",
"data": [],
"valid": true
}
]
},
{
"description": "minContains=1 with contains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": false
},
{
"description": "no elements match",
"data": [
2
],
"valid": false
},
{
"description": "single element matches, valid minContains",
"data": [
1
],
"valid": true
},
{
"description": "some elements match, valid minContains",
"data": [
1,
2
],
"valid": true
},
{
"description": "all elements match, valid minContains",
"data": [
1,
1
],
"valid": true
}
]
},
{
"description": "minContains=2 with contains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 2,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": false
},
{
"description": "all elements match, invalid minContains",
"data": [
1
],
"valid": false
},
{
"description": "some elements match, invalid minContains",
"data": [
1,
2
],
"valid": false
},
{
"description": "all elements match, valid minContains (exactly as needed)",
"data": [
1,
1
],
"valid": true
},
{
"description": "all elements match, valid minContains (more than needed)",
"data": [
1,
1,
1
],
"valid": true
},
{
"description": "some elements match, valid minContains",
"data": [
1,
2,
1
],
"valid": true
}
]
},
{
"description": "minContains=2 with contains with a decimal value",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 2.0,
"extensible": true
},
"tests": [
{
"description": "one element matches, invalid minContains",
"data": [
1
],
"valid": false
},
{
"description": "both elements match, valid minContains",
"data": [
1,
1
],
"valid": true
}
]
},
{
"description": "maxContains = minContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"maxContains": 2,
"minContains": 2,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": false
},
{
"description": "all elements match, invalid minContains",
"data": [
1
],
"valid": false
},
{
"description": "all elements match, invalid maxContains",
"data": [
1,
1,
1
],
"valid": false
},
{
"description": "all elements match, valid maxContains and minContains",
"data": [
1,
1
],
"valid": true
}
]
},
{
"description": "maxContains < minContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"maxContains": 1,
"minContains": 3,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": false
},
{
"description": "invalid minContains",
"data": [
1
],
"valid": false
},
{
"description": "invalid maxContains",
"data": [
1,
1,
1
],
"valid": false
},
{
"description": "invalid maxContains and minContains",
"data": [
1,
1
],
"valid": false
}
]
},
{
"description": "minContains = 0",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 0,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": true
},
{
"description": "minContains = 0 makes contains always pass",
"data": [
2
],
"valid": true
}
]
},
{
"description": "minContains = 0 with maxContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 0,
"maxContains": 1,
"extensible": true
},
"tests": [
{
"description": "empty data",
"data": [],
"valid": true
},
{
"description": "not more than maxContains",
"data": [
1
],
"valid": true
},
{
"description": "too many",
"data": [
1,
1
],
"valid": false
}
]
},
{
"description": "extensible: true allows non-matching items in minContains",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contains": {
"const": 1
},
"minContains": 1,
"extensible": true
},
"tests": [
{
"description": "extra items disregarded for minContains",
"data": [
1,
2
],
"valid": true
}
]
}
]

77
tests/fixtures/minItems.json vendored Normal file
View File

@ -0,0 +1,77 @@
[
{
"description": "minItems validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minItems": 1,
"extensible": true
},
"tests": [
{
"description": "longer is valid",
"data": [
1,
2
],
"valid": true
},
{
"description": "exact length is valid",
"data": [
1
],
"valid": true
},
{
"description": "too short is invalid",
"data": [],
"valid": false
},
{
"description": "ignores non-arrays",
"data": "",
"valid": true
}
]
},
{
"description": "minItems validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minItems": 1.0,
"extensible": true
},
"tests": [
{
"description": "longer is valid",
"data": [
1,
2
],
"valid": true
},
{
"description": "too short is invalid",
"data": [],
"valid": false
}
]
},
{
"description": "extensible: true allows extra items in minItems",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minItems": 1,
"extensible": true
},
"tests": [
{
"description": "extra item counted towards minItems",
"data": [
1
],
"valid": true
}
]
}
]

55
tests/fixtures/minLength.json vendored Normal file
View File

@ -0,0 +1,55 @@
[
{
"description": "minLength validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minLength": 2
},
"tests": [
{
"description": "longer is valid",
"data": "foo",
"valid": true
},
{
"description": "exact length is valid",
"data": "fo",
"valid": true
},
{
"description": "too short is invalid",
"data": "f",
"valid": false
},
{
"description": "ignores non-strings",
"data": 1,
"valid": true
},
{
"description": "one grapheme is not long enough",
"data": "\uD83D\uDCA9",
"valid": false
}
]
},
{
"description": "minLength validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minLength": 2.0
},
"tests": [
{
"description": "longer is valid",
"data": "foo",
"valid": true
},
{
"description": "too short is invalid",
"data": "f",
"valid": false
}
]
}
]

87
tests/fixtures/minProperties.json vendored Normal file
View File

@ -0,0 +1,87 @@
[
{
"description": "minProperties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minProperties": 1,
"extensible": true
},
"tests": [
{
"description": "longer is valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "exact length is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "too short is invalid",
"data": {},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "minProperties validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minProperties": 1.0,
"extensible": true
},
"tests": [
{
"description": "longer is valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "too short is invalid",
"data": {},
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in minProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minProperties": 1,
"extensible": true
},
"tests": [
{
"description": "extra property counts towards minProperties",
"data": {
"foo": 1
},
"valid": true
}
]
}
]

75
tests/fixtures/minimum.json vendored Normal file
View File

@ -0,0 +1,75 @@
[
{
"description": "minimum validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minimum": 1.1
},
"tests": [
{
"description": "above the minimum is valid",
"data": 2.6,
"valid": true
},
{
"description": "boundary point is valid",
"data": 1.1,
"valid": true
},
{
"description": "below the minimum is invalid",
"data": 0.6,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "x",
"valid": true
}
]
},
{
"description": "minimum validation with signed integer",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minimum": -2
},
"tests": [
{
"description": "negative above the minimum is valid",
"data": -1,
"valid": true
},
{
"description": "positive above the minimum is valid",
"data": 0,
"valid": true
},
{
"description": "boundary point is valid",
"data": -2,
"valid": true
},
{
"description": "boundary point with float is valid",
"data": -2.0,
"valid": true
},
{
"description": "float below the minimum is invalid",
"data": -2.0001,
"valid": false
},
{
"description": "int below the minimum is invalid",
"data": -3,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "x",
"valid": true
}
]
}
]

84
tests/fixtures/multipleOf.json vendored Normal file
View File

@ -0,0 +1,84 @@
[
{
"description": "by int",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"multipleOf": 2
},
"tests": [
{
"description": "int by int",
"data": 10,
"valid": true
},
{
"description": "int by int fail",
"data": 7,
"valid": false
},
{
"description": "ignores non-numbers",
"data": "foo",
"valid": true
}
]
},
{
"description": "by number",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"multipleOf": 1.5
},
"tests": [
{
"description": "zero is multiple of anything",
"data": 0,
"valid": true
},
{
"description": "4.5 is multiple of 1.5",
"data": 4.5,
"valid": true
},
{
"description": "35 is not multiple of 1.5",
"data": 35,
"valid": false
}
]
},
{
"description": "by small number",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"multipleOf": 0.0001
},
"tests": [
{
"description": "0.0075 is multiple of 0.0001",
"data": 0.0075,
"valid": true
},
{
"description": "0.00751 is not multiple of 0.0001",
"data": 0.00751,
"valid": false
}
]
},
{
"description": "small multiple of large integer",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer",
"multipleOf": 1e-8
},
"tests": [
{
"description": "any integer is a multiple of 1e-8",
"data": 12391239123,
"valid": true
}
]
}
]

398
tests/fixtures/not.json vendored Normal file
View File

@ -0,0 +1,398 @@
[
{
"description": "not",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"type": "integer"
}
},
"tests": [
{
"description": "allowed",
"data": "foo",
"valid": true
},
{
"description": "disallowed",
"data": 1,
"valid": false
}
]
},
{
"description": "not multiple types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"type": [
"integer",
"boolean"
]
}
},
"tests": [
{
"description": "valid",
"data": "foo",
"valid": true
},
{
"description": "mismatch",
"data": 1,
"valid": false
},
{
"description": "other mismatch",
"data": true,
"valid": false
}
]
},
{
"description": "not more complex schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
},
"extensible": true
},
"tests": [
{
"description": "match",
"data": 1,
"valid": true
},
{
"description": "other match",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "mismatch",
"data": {
"foo": "bar"
},
"valid": false
}
]
},
{
"description": "forbidden property",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"not": {}
}
}
},
"tests": [
{
"description": "property present",
"data": {
"foo": 1,
"bar": 2
},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "forbid everything with empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {}
},
"tests": [
{
"description": "number is invalid",
"data": 1,
"valid": false
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
},
{
"description": "boolean true is invalid",
"data": true,
"valid": false
},
{
"description": "boolean false is invalid",
"data": false,
"valid": false
},
{
"description": "null is invalid",
"data": null,
"valid": false
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"valid": false
},
{
"description": "empty object is invalid",
"data": {},
"valid": false
},
{
"description": "array is invalid",
"data": [
"foo"
],
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
}
]
},
{
"description": "forbid everything with boolean schema true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": true
},
"tests": [
{
"description": "number is invalid",
"data": 1,
"valid": false
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
},
{
"description": "boolean true is invalid",
"data": true,
"valid": false
},
{
"description": "boolean false is invalid",
"data": false,
"valid": false
},
{
"description": "null is invalid",
"data": null,
"valid": false
},
{
"description": "object is invalid",
"data": {
"foo": "bar"
},
"valid": false
},
{
"description": "empty object is invalid",
"data": {},
"valid": false
},
{
"description": "array is invalid",
"data": [
"foo"
],
"valid": false
},
{
"description": "empty array is invalid",
"data": [],
"valid": false
}
]
},
{
"description": "allow everything with boolean schema false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": false,
"extensible": true
},
"tests": [
{
"description": "number is valid",
"data": 1,
"valid": true
},
{
"description": "string is valid",
"data": "foo",
"valid": true
},
{
"description": "boolean true is valid",
"data": true,
"valid": true
},
{
"description": "boolean false is valid",
"data": false,
"valid": true
},
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "object is valid",
"data": {
"foo": "bar"
},
"valid": true
},
{
"description": "empty object is valid",
"data": {},
"valid": true
},
{
"description": "array is valid",
"data": [
"foo"
],
"valid": true
},
{
"description": "empty array is valid",
"data": [],
"valid": true
}
]
},
{
"description": "double negation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"not": {}
}
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "extensible: true allows extra properties in not",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"type": "integer"
},
"extensible": true
},
"tests": [
{
"description": "extra property is valid (not integer matches)",
"data": {
"foo": 1
},
"valid": true
}
]
},
{
"description": "extensible: false (default) forbids extra properties in not",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"not": {
"type": "integer"
}
},
"tests": [
{
"description": "extra property is invalid due to strictness",
"data": {
"foo": 1
},
"valid": false
}
]
},
{
"description": "property next to not (extensible: true)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
},
"extensible": true
},
"tests": [
{
"description": "extra property allowed",
"data": {
"bar": "baz",
"foo": 1
},
"valid": true
}
]
},
{
"description": "property next to not (extensible: false)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"bar": {
"type": "string"
}
},
"not": {
"type": "integer"
}
},
"tests": [
{
"description": "extra property forbidden",
"data": {
"bar": "baz",
"foo": 1
},
"valid": false
},
{
"description": "defined property allowed",
"data": {
"bar": "baz"
},
"valid": true
}
]
}
]

482
tests/fixtures/oneOf.json vendored Normal file
View File

@ -0,0 +1,482 @@
[
{
"description": "oneOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"type": "integer"
},
{
"minimum": 2
}
]
},
"tests": [
{
"description": "first oneOf valid",
"data": 1,
"valid": true
},
{
"description": "second oneOf valid",
"data": 2.5,
"valid": true
},
{
"description": "both oneOf valid",
"data": 3,
"valid": false
},
{
"description": "neither oneOf valid",
"data": 1.5,
"valid": false
}
]
},
{
"description": "oneOf with base schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"oneOf": [
{
"minLength": 2
},
{
"maxLength": 4
}
]
},
"tests": [
{
"description": "mismatch base schema",
"data": 3,
"valid": false
},
{
"description": "one oneOf valid",
"data": "foobar",
"valid": true
},
{
"description": "both oneOf valid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, all true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
true,
true,
true
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, one true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
true,
false,
false
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "oneOf with boolean schemas, more than one true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
true,
true,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, all false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
false,
false,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf complex types",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first oneOf valid (complex)",
"data": {
"bar": 2
},
"valid": true
},
{
"description": "second oneOf valid (complex)",
"data": {
"foo": "baz"
},
"valid": true
},
{
"description": "both oneOf valid (complex)",
"data": {
"foo": "baz",
"bar": 2
},
"valid": false
},
{
"description": "neither oneOf valid (complex)",
"data": {
"foo": 2,
"bar": "quux"
},
"valid": false
}
]
},
{
"description": "oneOf with empty schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"type": "number"
},
{}
]
},
"tests": [
{
"description": "one valid - valid",
"data": "foo",
"valid": true
},
{
"description": "both valid - invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "oneOf with required",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"foo": true,
"bar": true,
"baz": true
},
"oneOf": [
{
"required": [
"foo",
"bar"
]
},
{
"required": [
"foo",
"baz"
]
}
]
},
"tests": [
{
"description": "both invalid - invalid",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "first valid - valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "second valid - valid",
"data": {
"foo": 1,
"baz": 3
},
"valid": true
},
{
"description": "both valid - invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
},
{
"description": "extra property invalid (strict)",
"data": {
"foo": 1,
"bar": 2,
"extra": 3
},
"valid": false
}
]
},
{
"description": "oneOf with required (extensible)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"extensible": true,
"oneOf": [
{
"required": [
"foo",
"bar"
]
},
{
"required": [
"foo",
"baz"
]
}
]
},
"tests": [
{
"description": "both invalid - invalid",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "first valid - valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "second valid - valid",
"data": {
"foo": 1,
"baz": 3
},
"valid": true
},
{
"description": "both valid - invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
},
{
"description": "extra properties are valid (extensible)",
"data": {
"foo": 1,
"bar": 2,
"extra": "value"
},
"valid": true
}
]
},
{
"description": "oneOf with missing optional property",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"properties": {
"bar": true,
"baz": true
},
"required": [
"bar"
]
},
{
"properties": {
"foo": true
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first oneOf valid",
"data": {
"bar": 8
},
"valid": true
},
{
"description": "second oneOf valid",
"data": {
"foo": "foo"
},
"valid": true
},
{
"description": "both oneOf valid",
"data": {
"foo": "foo",
"bar": 8
},
"valid": false
},
{
"description": "neither oneOf valid",
"data": {
"baz": "quux"
},
"valid": false
}
]
},
{
"description": "nested oneOf, to check validation semantics",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"oneOf": [
{
"type": "null"
}
]
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "anything non-null is invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties in oneOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
],
"extensible": true
},
"tests": [
{
"description": "extra property is valid (matches first option)",
"data": {
"bar": 2,
"extra": "prop"
},
"valid": true
}
]
}
]

65
tests/fixtures/pattern.json vendored Normal file
View File

@ -0,0 +1,65 @@
[
{
"description": "pattern validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "^a*$"
},
"tests": [
{
"description": "a matching pattern is valid",
"data": "aaa",
"valid": true
},
{
"description": "a non-matching pattern is invalid",
"data": "abc",
"valid": false
},
{
"description": "ignores booleans",
"data": true,
"valid": true
},
{
"description": "ignores integers",
"data": 123,
"valid": true
},
{
"description": "ignores floats",
"data": 1.0,
"valid": true
},
{
"description": "ignores objects",
"data": {},
"valid": true
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores null",
"data": null,
"valid": true
}
]
},
{
"description": "pattern is not anchored",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"pattern": "a+"
},
"tests": [
{
"description": "matches a substring",
"data": "xxaayy",
"valid": true
}
]
}
]

271
tests/fixtures/patternProperties.json vendored Normal file
View File

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

161
tests/fixtures/prefixItems.json vendored Normal file
View File

@ -0,0 +1,161 @@
[
{
"description": "a schema given for prefixItems",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"tests": [
{
"description": "correct types",
"data": [
1,
"foo"
],
"valid": true
},
{
"description": "wrong types",
"data": [
"foo",
1
],
"valid": false
},
{
"description": "incomplete array of items",
"data": [
1
],
"valid": true
},
{
"description": "array with additional items (invalid due to strictness)",
"data": [
1,
"foo",
true
],
"valid": false
},
{
"description": "empty array",
"data": [],
"valid": true
},
{
"description": "JavaScript pseudo-array is valid (invalid due to strict object validation)",
"data": {
"0": "invalid",
"1": "valid",
"length": 2
},
"valid": false
}
]
},
{
"description": "prefixItems with boolean schemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
true,
false
]
},
"tests": [
{
"description": "array with one item is valid",
"data": [
1
],
"valid": true
},
{
"description": "array with two items is invalid",
"data": [
1,
"foo"
],
"valid": false
},
{
"description": "empty array is valid",
"data": [],
"valid": true
}
]
},
{
"description": "additional items are allowed by default",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true
},
"tests": [
{
"description": "only the first item is validated",
"data": [
1,
"foo",
false
],
"valid": true
}
]
},
{
"description": "prefixItems with null instance elements",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "null"
}
]
},
"tests": [
{
"description": "allows null elements",
"data": [
null
],
"valid": true
}
]
},
{
"description": "extensible: true allows extra items with prefixItems",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "integer"
}
],
"extensible": true
},
"tests": [
{
"description": "extra item is valid",
"data": [
1,
"foo"
],
"valid": true
}
]
}
]

463
tests/fixtures/properties.json vendored Normal file
View File

@ -0,0 +1,463 @@
[
{
"description": "object properties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "integer"
},
"bar": {
"type": "string"
}
}
},
"tests": [
{
"description": "both properties present and valid is valid",
"data": {
"foo": 1,
"bar": "baz"
},
"valid": true
},
{
"description": "one property invalid is invalid",
"data": {
"foo": 1,
"bar": {}
},
"valid": false
},
{
"description": "both properties invalid is invalid",
"data": {
"foo": [],
"bar": {}
},
"valid": false
},
{
"description": "doesn't invalidate other properties",
"data": {},
"valid": true
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "properties with boolean schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": false
}
},
"tests": [
{
"description": "no property present is valid",
"data": {},
"valid": true
},
{
"description": "only 'true' property present is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "only 'false' property present is invalid",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "both properties present is invalid",
"data": {
"foo": 1,
"bar": 2
},
"valid": false
}
]
},
{
"description": "properties with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"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"
}
}
},
"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
},
{
"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
}
]
},
{
"description": "properties with null valued instance properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "null"
}
}
},
"tests": [
{
"description": "allows null values",
"data": {
"foo": null
},
"valid": true
}
]
},
{
"description": "properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"__proto__": {
"type": "number"
},
"toString": {
"properties": {
"length": {
"type": "string"
}
}
},
"constructor": {
"type": "number"
}
}
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "none of the properties mentioned",
"data": {},
"valid": true
},
{
"description": "__proto__ not valid",
"data": {
"__proto__": "foo"
},
"valid": false
},
{
"description": "toString not valid",
"data": {
"toString": {
"length": 37
}
},
"valid": false
},
{
"description": "constructor not valid",
"data": {
"constructor": {
"length": 37
}
},
"valid": false
},
{
"description": "all present and valid",
"data": {
"__proto__": 12,
"toString": {
"length": "foo"
},
"constructor": 37
},
"valid": true
}
]
},
{
"description": "extensible: true allows extra properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "integer"
}
},
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": "baz"
},
"valid": true
}
]
},
{
"description": "strict by default: extra properties invalid",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {
"type": "string"
}
}
},
"tests": [
{
"description": "extra property is invalid",
"data": {
"foo": "bar",
"extra": 1
},
"valid": false
}
]
},
{
"description": "inheritance: nested object inherits strictness from strict parent",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"tests": [
{
"description": "nested extra property is invalid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"valid": false
}
]
},
{
"description": "override: nested object allows extra properties if extensible: true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"nested": {
"extensible": true,
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"tests": [
{
"description": "nested extra property is valid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"valid": true
}
]
},
{
"description": "inheritance: nested object inherits looseness from loose parent",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"extensible": true,
"properties": {
"nested": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"tests": [
{
"description": "nested extra property is valid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"valid": true
}
]
},
{
"description": "override: nested object enforces strictness if extensible: false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"extensible": true,
"properties": {
"nested": {
"extensible": false,
"properties": {
"foo": {
"type": "string"
}
}
}
}
},
"tests": [
{
"description": "nested extra property is invalid",
"data": {
"nested": {
"foo": "bar",
"extra": 1
}
},
"valid": false
}
]
},
{
"description": "arrays: inline items inherit strictness from strict parent",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"tests": [
{
"description": "array item with extra property is invalid (strict parent)",
"data": {
"list": [
{
"foo": "bar",
"extra": 1
}
]
},
"valid": false
}
]
},
{
"description": "arrays: inline items inherit looseness from loose parent",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"extensible": true,
"properties": {
"list": {
"type": "array",
"items": {
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
},
"tests": [
{
"description": "array item with extra property is valid (loose parent)",
"data": {
"list": [
{
"foo": "bar",
"extra": 1
}
]
},
"valid": true
}
]
}
]

231
tests/fixtures/propertyNames.json vendored Normal file
View File

@ -0,0 +1,231 @@
[
{
"description": "propertyNames validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"maxLength": 3
},
"extensible": true
},
"tests": [
{
"description": "all property names valid",
"data": {
"f": {},
"foo": {}
},
"valid": true
},
{
"description": "some property names invalid",
"data": {
"foo": {},
"foobar": {}
},
"valid": false
},
{
"description": "object without properties is valid",
"data": {},
"valid": true
},
{
"description": "ignores arrays",
"data": [
1,
2,
3,
4
],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "propertyNames validation with pattern",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"pattern": "^a+$"
},
"extensible": true
},
"tests": [
{
"description": "matching property names valid",
"data": {
"a": {},
"aa": {},
"aaa": {}
},
"valid": true
},
{
"description": "non-matching property name is invalid",
"data": {
"aaA": {}
},
"valid": false
},
{
"description": "object without properties is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with boolean schema true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": true,
"extensible": true
},
"tests": [
{
"description": "object with any properties is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with boolean schema false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": false,
"extensible": true
},
"tests": [
{
"description": "object with any properties is invalid",
"data": {
"foo": 1
},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with const",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"const": "foo"
},
"extensible": true
},
"tests": [
{
"description": "object with property foo is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "object with any other property is invalid",
"data": {
"bar": 1
},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with enum",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"enum": [
"foo",
"bar"
]
},
"extensible": true
},
"tests": [
{
"description": "object with property foo is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "object with property foo and bar is valid",
"data": {
"foo": 1,
"bar": 1
},
"valid": true
},
{
"description": "object with any other property is invalid",
"data": {
"baz": 1
},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "extensible: true allows extra properties (checked by propertyNames)",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"maxLength": 3
},
"extensible": true
},
"tests": [
{
"description": "extra property with valid name is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "extra property with invalid name is invalid",
"data": {
"foobar": 1
},
"valid": false
}
]
}
]

1414
tests/fixtures/puncs.json vendored Normal file

File diff suppressed because it is too large Load Diff

1491
tests/fixtures/ref.json vendored Normal file

File diff suppressed because it is too large Load Diff

212
tests/fixtures/required.json vendored Normal file
View File

@ -0,0 +1,212 @@
[
{
"description": "required validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {},
"bar": {}
},
"required": [
"foo"
]
},
"tests": [
{
"description": "present required property is valid",
"data": {
"foo": 1
},
"valid": true
},
{
"description": "non-present required property is invalid",
"data": {
"bar": 1
},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "ignores null",
"data": null,
"valid": true
},
{
"description": "ignores boolean",
"data": true,
"valid": true
}
]
},
{
"description": "required default validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "not required by default",
"data": {},
"valid": true
}
]
},
{
"description": "required with empty array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
},
"required": []
},
"tests": [
{
"description": "property not required",
"data": {},
"valid": true
}
]
},
{
"description": "required with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"foo\nbar",
"foo\"bar",
"foo\\bar",
"foo\rbar",
"foo\tbar",
"foo\fbar"
],
"extensible": true
},
"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
},
{
"description": "object with some properties missing is invalid",
"data": {
"foo\nbar": "1",
"foo\"bar": "1"
},
"valid": false
}
]
},
{
"description": "required properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"__proto__",
"toString",
"constructor"
],
"extensible": true
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "none of the properties mentioned",
"data": {},
"valid": false
},
{
"description": "__proto__ present",
"data": {
"__proto__": "foo"
},
"valid": false
},
{
"description": "toString present",
"data": {
"toString": {
"length": 37
}
},
"valid": false
},
{
"description": "constructor present",
"data": {
"constructor": {
"length": 37
}
},
"valid": false
},
{
"description": "all present",
"data": {
"__proto__": 12,
"toString": {
"length": "foo"
},
"constructor": 37
},
"valid": true
}
]
},
{
"description": "extensible: true allows extra properties in required",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"foo"
],
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
}
]
}
]

540
tests/fixtures/type.json vendored Normal file
View File

@ -0,0 +1,540 @@
[
{
"description": "integer type matches integers",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
},
"tests": [
{
"description": "an integer is an integer",
"data": 1,
"valid": true
},
{
"description": "a float with zero fractional part is an integer",
"data": 1.0,
"valid": true
},
{
"description": "a float is not an integer",
"data": 1.1,
"valid": false
},
{
"description": "a string is not an integer",
"data": "foo",
"valid": false
},
{
"description": "a string is still not an integer, even if it looks like one",
"data": "1",
"valid": false
},
{
"description": "an object is not an integer",
"data": {},
"valid": false
},
{
"description": "an array is not an integer",
"data": [],
"valid": false
},
{
"description": "a boolean is not an integer",
"data": true,
"valid": false
},
{
"description": "null is not an integer",
"data": null,
"valid": false
}
]
},
{
"description": "number type matches numbers",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "number"
},
"tests": [
{
"description": "an integer is a number",
"data": 1,
"valid": true
},
{
"description": "a float with zero fractional part is a number (and an integer)",
"data": 1.0,
"valid": true
},
{
"description": "a float is a number",
"data": 1.1,
"valid": true
},
{
"description": "a string is not a number",
"data": "foo",
"valid": false
},
{
"description": "a string is still not a number, even if it looks like one",
"data": "1",
"valid": false
},
{
"description": "an object is not a number",
"data": {},
"valid": false
},
{
"description": "an array is not a number",
"data": [],
"valid": false
},
{
"description": "a boolean is not a number",
"data": true,
"valid": false
},
{
"description": "null is not a number",
"data": null,
"valid": false
}
]
},
{
"description": "string type matches strings",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string"
},
"tests": [
{
"description": "1 is not a string",
"data": 1,
"valid": false
},
{
"description": "a float is not a string",
"data": 1.1,
"valid": false
},
{
"description": "a string is a string",
"data": "foo",
"valid": true
},
{
"description": "a string is still a string, even if it looks like a number",
"data": "1",
"valid": true
},
{
"description": "an empty string is still a string",
"data": "",
"valid": true
},
{
"description": "an object is not a string",
"data": {},
"valid": false
},
{
"description": "an array is not a string",
"data": [],
"valid": false
},
{
"description": "a boolean is not a string",
"data": true,
"valid": false
},
{
"description": "null is not a string",
"data": null,
"valid": false
}
]
},
{
"description": "object type matches objects",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
},
"tests": [
{
"description": "an integer is not an object",
"data": 1,
"valid": false
},
{
"description": "a float is not an object",
"data": 1.1,
"valid": false
},
{
"description": "a string is not an object",
"data": "foo",
"valid": false
},
{
"description": "an object is an object",
"data": {},
"valid": true
},
{
"description": "an array is not an object",
"data": [],
"valid": false
},
{
"description": "a boolean is not an object",
"data": true,
"valid": false
},
{
"description": "null is not an object",
"data": null,
"valid": false
}
]
},
{
"description": "array type matches arrays",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array"
},
"tests": [
{
"description": "an integer is not an array",
"data": 1,
"valid": false
},
{
"description": "a float is not an array",
"data": 1.1,
"valid": false
},
{
"description": "a string is not an array",
"data": "foo",
"valid": false
},
{
"description": "an object is not an array",
"data": {},
"valid": false
},
{
"description": "an array is an array",
"data": [],
"valid": true
},
{
"description": "a boolean is not an array",
"data": true,
"valid": false
},
{
"description": "null is not an array",
"data": null,
"valid": false
}
]
},
{
"description": "boolean type matches booleans",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "boolean"
},
"tests": [
{
"description": "an integer is not a boolean",
"data": 1,
"valid": false
},
{
"description": "zero is not a boolean",
"data": 0,
"valid": false
},
{
"description": "a float is not a boolean",
"data": 1.1,
"valid": false
},
{
"description": "a string is not a boolean",
"data": "foo",
"valid": false
},
{
"description": "an empty string is a null",
"data": "",
"valid": true
},
{
"description": "an object is not a boolean",
"data": {},
"valid": false
},
{
"description": "an array is not a boolean",
"data": [],
"valid": false
},
{
"description": "true is a boolean",
"data": true,
"valid": true
},
{
"description": "false is a boolean",
"data": false,
"valid": true
},
{
"description": "null is not a boolean",
"data": null,
"valid": false
}
]
},
{
"description": "null type matches only the null object",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "null"
},
"tests": [
{
"description": "an integer is not null",
"data": 1,
"valid": false
},
{
"description": "a float is not null",
"data": 1.1,
"valid": false
},
{
"description": "zero is not null",
"data": 0,
"valid": false
},
{
"description": "a string is not null",
"data": "foo",
"valid": false
},
{
"description": "an empty string is null",
"data": "",
"valid": true
},
{
"description": "an object is not null",
"data": {},
"valid": false
},
{
"description": "an array is not null",
"data": [],
"valid": false
},
{
"description": "true is not null",
"data": true,
"valid": false
},
{
"description": "false is not null",
"data": false,
"valid": false
},
{
"description": "null is null",
"data": null,
"valid": true
}
]
},
{
"description": "multiple types can be specified in an array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [
"integer",
"string"
]
},
"tests": [
{
"description": "an integer is valid",
"data": 1,
"valid": true
},
{
"description": "a string is valid",
"data": "foo",
"valid": true
},
{
"description": "a float is invalid",
"data": 1.1,
"valid": false
},
{
"description": "an object is invalid",
"data": {},
"valid": false
},
{
"description": "an array is invalid",
"data": [],
"valid": false
},
{
"description": "a boolean is invalid",
"data": true,
"valid": false
},
{
"description": "null is invalid",
"data": null,
"valid": false
}
]
},
{
"description": "type as array with one item",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [
"string"
]
},
"tests": [
{
"description": "string is valid",
"data": "foo",
"valid": true
},
{
"description": "number is invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "type: array or object",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [
"array",
"object"
],
"items": {}
},
"tests": [
{
"description": "array is valid",
"data": [
1,
2,
3
],
"valid": true
},
{
"description": "object is valid",
"data": {},
"valid": true
},
{
"description": "number is invalid",
"data": 123,
"valid": false
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
},
{
"description": "null is invalid",
"data": null,
"valid": false
}
]
},
{
"description": "type: array, object or null",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": [
"array",
"object",
"null"
],
"items": {}
},
"tests": [
{
"description": "array is valid",
"data": [
1,
2,
3
],
"valid": true
},
{
"description": "object is valid",
"data": {},
"valid": true
},
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "number is invalid",
"data": 123,
"valid": false
},
{
"description": "string is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "extensible: true allows extra properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"extensible": true
},
"tests": [
{
"description": "extra property is valid",
"data": {
"foo": 1
},
"valid": true
}
]
}
]

859
tests/fixtures/uniqueItems.json vendored Normal file
View File

@ -0,0 +1,859 @@
[
{
"description": "uniqueItems validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"uniqueItems": true,
"extensible": true
},
"tests": [
{
"description": "unique array of integers is valid",
"data": [
1,
2
],
"valid": true
},
{
"description": "non-unique array of integers is invalid",
"data": [
1,
1
],
"valid": false
},
{
"description": "non-unique array of more than two integers is invalid",
"data": [
1,
2,
1
],
"valid": false
},
{
"description": "numbers are unique if mathematically unequal",
"data": [
1.0,
1.00,
1
],
"valid": false
},
{
"description": "false is not equal to zero",
"data": [
0,
false
],
"valid": true
},
{
"description": "true is not equal to one",
"data": [
1,
true
],
"valid": true
},
{
"description": "unique array of strings is valid",
"data": [
"foo",
"bar",
"baz"
],
"valid": true
},
{
"description": "non-unique array of strings is invalid",
"data": [
"foo",
"bar",
"foo"
],
"valid": false
},
{
"description": "unique array of objects is valid",
"data": [
{
"foo": "bar"
},
{
"foo": "baz"
}
],
"valid": true
},
{
"description": "non-unique array of objects is invalid",
"data": [
{
"foo": "bar"
},
{
"foo": "bar"
}
],
"valid": false
},
{
"description": "property order of array of objects is ignored",
"data": [
{
"foo": "bar",
"bar": "foo"
},
{
"bar": "foo",
"foo": "bar"
}
],
"valid": false
},
{
"description": "unique array of nested objects is valid",
"data": [
{
"foo": {
"bar": {
"baz": true
}
}
},
{
"foo": {
"bar": {
"baz": false
}
}
}
],
"valid": true
},
{
"description": "non-unique array of nested objects is invalid",
"data": [
{
"foo": {
"bar": {
"baz": true
}
}
},
{
"foo": {
"bar": {
"baz": true
}
}
}
],
"valid": false
},
{
"description": "unique array of arrays is valid",
"data": [
[
"foo"
],
[
"bar"
]
],
"valid": true
},
{
"description": "non-unique array of arrays is invalid",
"data": [
[
"foo"
],
[
"foo"
]
],
"valid": false
},
{
"description": "non-unique array of more than two arrays is invalid",
"data": [
[
"foo"
],
[
"bar"
],
[
"foo"
]
],
"valid": false
},
{
"description": "1 and true are unique",
"data": [
1,
true
],
"valid": true
},
{
"description": "0 and false are unique",
"data": [
0,
false
],
"valid": true
},
{
"description": "[1] and [true] are unique",
"data": [
[
1
],
[
true
]
],
"valid": true
},
{
"description": "[0] and [false] are unique",
"data": [
[
0
],
[
false
]
],
"valid": true
},
{
"description": "nested [1] and [true] are unique",
"data": [
[
[
1
],
"foo"
],
[
[
true
],
"foo"
]
],
"valid": true
},
{
"description": "nested [0] and [false] are unique",
"data": [
[
[
0
],
"foo"
],
[
[
false
],
"foo"
]
],
"valid": true
},
{
"description": "unique heterogeneous types are valid",
"data": [
{},
[
1
],
true,
null,
1,
"{}"
],
"valid": true
},
{
"description": "non-unique heterogeneous types are invalid",
"data": [
{},
[
1
],
true,
null,
{},
1
],
"valid": false
},
{
"description": "different objects are unique",
"data": [
{
"a": 1,
"b": 2
},
{
"a": 2,
"b": 1
}
],
"valid": true
},
{
"description": "objects are non-unique despite key order",
"data": [
{
"a": 1,
"b": 2
},
{
"b": 2,
"a": 1
}
],
"valid": false
},
{
"description": "{\"a\": false} and {\"a\": 0} are unique",
"data": [
{
"a": false
},
{
"a": 0
}
],
"valid": true
},
{
"description": "{\"a\": true} and {\"a\": 1} are unique",
"data": [
{
"a": true
},
{
"a": 1
}
],
"valid": true
}
]
},
{
"description": "uniqueItems with an array of items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"extensible": true
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"valid": true
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"valid": true
},
{
"description": "[false, false] from items array is not valid",
"data": [
false,
false
],
"valid": false
},
{
"description": "[true, true] from items array is not valid",
"data": [
true,
true
],
"valid": false
},
{
"description": "unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"bar"
],
"valid": true
},
{
"description": "unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"bar"
],
"valid": true
},
{
"description": "non-unique array extended from [false, true] is not valid",
"data": [
false,
true,
"foo",
"foo"
],
"valid": false
},
{
"description": "non-unique array extended from [true, false] is not valid",
"data": [
true,
false,
"foo",
"foo"
],
"valid": false
}
]
},
{
"description": "uniqueItems with an array of items and additionalItems=false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"items": false
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"valid": true
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"valid": true
},
{
"description": "[false, false] from items array is not valid",
"data": [
false,
false
],
"valid": false
},
{
"description": "[true, true] from items array is not valid",
"data": [
true,
true
],
"valid": false
},
{
"description": "extra items are invalid even if unique",
"data": [
false,
true,
null
],
"valid": false
}
]
},
{
"description": "uniqueItems=false validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"uniqueItems": false,
"extensible": true
},
"tests": [
{
"description": "unique array of integers is valid",
"data": [
1,
2
],
"valid": true
},
{
"description": "non-unique array of integers is valid",
"data": [
1,
1
],
"valid": true
},
{
"description": "numbers are unique if mathematically unequal",
"data": [
1.0,
1.00,
1
],
"valid": true
},
{
"description": "false is not equal to zero",
"data": [
0,
false
],
"valid": true
},
{
"description": "true is not equal to one",
"data": [
1,
true
],
"valid": true
},
{
"description": "unique array of objects is valid",
"data": [
{
"foo": "bar"
},
{
"foo": "baz"
}
],
"valid": true
},
{
"description": "non-unique array of objects is valid",
"data": [
{
"foo": "bar"
},
{
"foo": "bar"
}
],
"valid": true
},
{
"description": "unique array of nested objects is valid",
"data": [
{
"foo": {
"bar": {
"baz": true
}
}
},
{
"foo": {
"bar": {
"baz": false
}
}
}
],
"valid": true
},
{
"description": "non-unique array of nested objects is valid",
"data": [
{
"foo": {
"bar": {
"baz": true
}
}
},
{
"foo": {
"bar": {
"baz": true
}
}
}
],
"valid": true
},
{
"description": "unique array of arrays is valid",
"data": [
[
"foo"
],
[
"bar"
]
],
"valid": true
},
{
"description": "non-unique array of arrays is valid",
"data": [
[
"foo"
],
[
"foo"
]
],
"valid": true
},
{
"description": "1 and true are unique",
"data": [
1,
true
],
"valid": true
},
{
"description": "0 and false are unique",
"data": [
0,
false
],
"valid": true
},
{
"description": "unique heterogeneous types are valid",
"data": [
{},
[
1
],
true,
null,
1
],
"valid": true
},
{
"description": "non-unique heterogeneous types are valid",
"data": [
{},
[
1
],
true,
null,
{},
1
],
"valid": true
}
]
},
{
"description": "uniqueItems=false with an array of items",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"extensible": true
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"valid": true
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"valid": true
},
{
"description": "[false, false] from items array is valid",
"data": [
false,
false
],
"valid": true
},
{
"description": "[true, true] from items array is valid",
"data": [
true,
true
],
"valid": true
},
{
"description": "unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"bar"
],
"valid": true
},
{
"description": "unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"bar"
],
"valid": true
},
{
"description": "non-unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"foo"
],
"valid": true
},
{
"description": "non-unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"foo"
],
"valid": true
}
]
},
{
"description": "uniqueItems=false with an array of items and additionalItems=false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"items": false
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"valid": true
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"valid": true
},
{
"description": "[false, false] from items array is valid",
"data": [
false,
false
],
"valid": true
},
{
"description": "[true, true] from items array is valid",
"data": [
true,
true
],
"valid": true
},
{
"description": "extra items are invalid even if unique",
"data": [
false,
true,
null
],
"valid": false
}
]
},
{
"description": "extensible: true allows extra items in uniqueItems",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"uniqueItems": true,
"extensible": true
},
"tests": [
{
"description": "extra items must be unique",
"data": [
1,
1
],
"valid": false
},
{
"description": "extra unique items valid",
"data": [
1,
2
],
"valid": true
}
]
}
]

113
tests/lib.rs Normal file
View File

@ -0,0 +1,113 @@
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 puncs = json!([]);
let types = json!([{
"schemas": [{
"$id": "test_schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}]
}]);
let enums = json!([]);
let cache_drop = cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
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. Mask Happy Path
let mask_drop = mask_json_schema(
"test_schema",
JsonB(json!({"name": "Neo", "extra": "data"})),
);
assert_eq!(
mask_drop.0,
json!({
"type": "drop",
"response": {"name": "Neo"}
})
);
// 7. Clear Schemas
let clear_drop = clear_json_schemas();
assert_eq!(
clear_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
assert!(!json_schema_cached("test_schema"));
}

View File

@ -1 +1 @@
1.0.20
1.0.54