Compare commits

...

10 Commits

Author SHA1 Message Date
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
130 changed files with 25286 additions and 16410 deletions

3
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "flows"] [submodule "flows"]
path = flows path = flows
url = git@gitea-ssh.thoughtpatterns.ai:cellular/flows.git 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

1512
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

178
GEMINI.md
View File

@ -1,135 +1,129 @@
# Gemini Project Overview: `jspg` # JSPG: JSON Schema Postgres
This document outlines the purpose of the `jspg` project, its architecture, and the specific modifications made to the vendored `boon` JSON schema validator crate. **JSPG** is a high-performance PostgreSQL extension for in-memory JSON Schema validation, specifically targeting **Draft 2020-12**.
## What is `jspg`? It is designed to serve as the validation engine for the "Punc" architecture, where the database is the single source of truth for all data models and API contracts.
`jspg` is a PostgreSQL extension written in Rust using the `pgrx` framework. Its primary function is to provide fast, in-database JSON schema validation against the 2020-12 draft of the JSON Schema specification. ## 🎯 Goals
### How It Works 1. **Draft 2020-12 Compliance**: Attempt to adhere to the official JSON Schema Draft 2020-12 specification.
2. **Ultra-Fast Validation**: Compile schemas into an optimized in-memory representation for near-instant validation during high-throughput workloads.
3. **Connection-Bound Caching**: Leverage the PostgreSQL session lifecycle to maintain a per-connection schema cache, eliminating the need for repetitive parsing.
4. **Structural Inheritance**: Support object-oriented schema design via Implicit Keyword Shadowing and virtual `.family` schemas.
5. **Punc Integration**: validation is aware of the "Punc" context (request/response) and can validate `cue` objects efficiently.
The extension is designed for high-performance scenarios where schemas are defined once and used many times for validation. It achieves this through an in-memory cache. ## 🔌 API Reference
1. **Caching and Pre-processing:** A user first calls the `cache_json_schemas(enums, types, puncs)` SQL function. This function takes arrays of JSON objects representing different kinds of schemas: The extension exposes the following functions to PostgreSQL:
- `enums`: Standalone enum schemas (e.g., for a `task_priority` list).
- `types`: Schemas for core application data models (e.g., `person`, `organization`). These may contain a `hierarchy` array for inheritance information.
- `puncs`: Schemas for API/function-specific requests and responses.
Before compiling, `jspg` performs a crucial **pre-processing step** for type hierarchies. It inspects each definition in the `types` array. If a type includes a `hierarchy` array (e.g., a `person` type with `["entity", "organization", "user", "person"]`), `jspg` uses this to build a map of "type families." ### `cache_json_schemas(enums jsonb, types jsonb, puncs jsonb) -> jsonb`
From this map, it generates new, virtual schemas on the fly. For example, for the `organization` type, it will generate a schema with `$id: "organization.family"` that contains an `enum` of all its descendant types, such as `["organization", "user", 'person"]`. Loads and compiles the entire schema registry into the session's memory, atomically replacing the previous validator.
This allows developers to write more flexible schemas. Instead of strictly requiring a `const` type, you can validate against an entire inheritance chain: * **Inputs**:
* `enums`: Array of enum definitions.
* `types`: Array of type definitions (core entities).
* `puncs`: Array of punc (function) definitions with request/response schemas.
* **Behavior**:
* Parses all inputs into an internal schema graph.
* Resolves all internal references (`$ref`).
* Generates virtual `.family` schemas for type hierarchies.
* Compiles schemas into validators.
* **Returns**: `{"response": "success"}` or an error object.
```json ### `mask_json_schema(schema_id text, instance jsonb) -> jsonb`
// In an "organization" schema definition
"properties": {
"type": {
// Allows the 'type' field to be "organization", "user", or "person"
"$ref": "organization.family",
"override": true
}
}
```
Finally, all user-defined schemas and the newly generated `.family` schemas are passed to the vendored `boon` crate, compiled into an efficient internal format, and stored in a static, in-memory `SCHEMA_CACHE`. This cache is managed by a `RwLock` to allow for high-performance, concurrent reads during validation. Validates a JSON instance and returns a new JSON object with unknown properties removed (pruned) based on the schema.
2. **Validation:** The `validate_json_schema(schema_id, instance)` SQL function is then used to validate a JSONB `instance` against a specific, pre-cached schema identified by its `$id`. This function looks up the compiled schema in the cache and runs the validation, returning a success response or a detailed error report. * **Inputs**:
* `schema_id`: The `$id` of the schema to mask against.
* `instance`: The JSON data to mask.
* **Returns**:
* On success: A `Drop` containing the **masked data**.
* On failure: A `Drop` containing validation errors.
3. **Custom Logic:** `jspg` uses a locally modified (vendored) version of the `boon` crate. This allows for powerful, application-specific validation logic that goes beyond the standard JSON Schema specification, such as runtime-based strictness. ### `validate_json_schema(schema_id text, instance jsonb) -> jsonb`
### Error Handling Validates a JSON instance against a pre-compiled schema.
When validation fails, `jspg` provides a detailed error report in a consistent JSON format, which we refer to as a "DropError". This process involves two main helper functions in `src/lib.rs`: * **Inputs**:
* `schema_id`: The `$id` of the schema to validate against (e.g., `person`, `save_person.request`).
* `instance`: The JSON data to validate.
* **Returns**:
* On success: `{"response": "success"}`
* On failure: A JSON object containing structured errors (e.g., `{"errors": [...]}`).
1. **`collect_errors`**: `boon` returns a nested tree of `ValidationError` objects. This function recursively traverses that tree to find the most specific, underlying causes of the failure. It filters out structural errors (like `allOf` or `anyOf`) to create a flat list of concrete validation failures. ### `json_schema_cached(schema_id text) -> bool`
2. **`format_errors`**: This function takes the flat list of errors and transforms each one into the final DropError JSON format. It also de-duplicates errors that occur at the same JSON Pointer path, ensuring a cleaner output if a single value violates multiple constraints. Checks if a specific schema ID is currently present in the cache.
#### DropError Format ### `clear_json_schemas() -> jsonb`
A DropError object provides a clear, structured explanation of a validation failure: Clears the current session's schema cache, freeing memory.
```json ### `show_json_schemas() -> jsonb`
{
"code": "ADDITIONAL_PROPERTIES_NOT_ALLOWED",
"message": "Property 'extra' is not allowed",
"details": {
"path": "/extra",
"context": "not allowed",
"cause": {
"got": [
"extra"
]
},
"schema": "basic_strict_test.request"
}
}
```
- `code` (string): A machine-readable error code (e.g., `ADDITIONAL_PROPERTIES_NOT_ALLOWED`, `MIN_LENGTH_VIOLATED`). Returns a debug dump of the currently cached schemas (for development/debugging).
- `message` (string): A human-readable summary of the error.
- `details` (object):
- `path` (string): The JSON Pointer path to the invalid data within the instance.
- `context` (any): The actual value that failed validation.
- `cause` (any): The low-level reason from the validator, often including the expected value (`want`) and the actual value (`got`).
- `schema` (string): The `$id` of the schema that was being validated.
--- ## ✨ Custom Features & Deviations
## `boon` Crate Modifications JSPG implements specific extensions to the Draft 2020-12 standard to support the Punc architecture's object-oriented needs.
The version of `boon` located in the `validator/` directory has been significantly modified to support application-specific validation logic that goes beyond the standard JSON Schema specification. ### 1. 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.
### 1. Property-Level Overrides for 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`.
- **Problem:** A primary use case for this project is validating data models that use `$ref` to create inheritance chains (e.g., a `person` schema `$ref`s a `user` schema, which `$ref`s an `entity` schema). A common pattern is to use a `const` keyword on a `type` property to identify the specific model (e.g., `"type": {"const": "person"}`). However, standard JSON Schema composition with `allOf` (which is implicitly used by `$ref`) treats these as a logical AND. This creates an impossible condition where an instance's `type` property would need to be "person" AND "user" AND "entity" simultaneously. * **Composition (`allOf`)**: When using `allOf`, standard intersection rules apply. No shadowing occurs; all constraints from all branches must pass. This is used for mixins or interfaces.
- **Solution:** We've implemented a custom, explicit override mechanism. A new keyword, `"override": true`, can be added to any property definition within a schema. ### 2. Virtual Family Schemas (`.family`)
To support polymorphic fields (e.g., a field that accepts any "User" type), JSPG generates virtual schemas representing type hierarchies.
```json * **Mechanism**: When caching types, if a type defines a `hierarchy` (e.g., `["entity", "organization", "person"]`), JSPG generates a schema like `organization.family` which is a `oneOf` containing refs to all valid descendants.
// person.json
{
"$id": "person",
"$ref": "user",
"properties": {
"type": { "const": "person", "override": true }
}
}
```
This signals to the validator that this definition of the `type` property should be the *only* one applied, and any definitions for `type` found in base schemas (like `user` or `entity`) should be ignored for the duration of this validation. ### 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.
#### Key Changes * **Strictness**: By default, any property in the instance data that is not explicitly defined in the schema causes a validation error. This prevents clients from sending undeclared fields.
* **Extensibility (`extensible: true`)**: To allow additional, undefined properties, you must add `"extensible": true` to the schema. This is useful for types that are designed to be open for extension.
* **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`.
This was achieved by making the validator stateful, using a pattern already present in `boon` for handling `unevaluatedProperties`. ### 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".
1. **Meta-Schema Update**: The meta-schema for Draft 2020-12 was modified to recognize `"override": true` as a valid keyword within a schema object, preventing the compiler from rejecting our custom schemas. ### 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.
2. **Compiler Modification**: The schema compiler in `validator/src/compiler.rs` was updated. It now inspects sub-schemas within a `properties` keyword and, if it finds `"override": true`, it records the name of that property in a new `override_properties` `HashSet` on the compiled `Schema` struct. * **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.
3. **Stateful Validator with `Override` Context**: The core `Validator` in `validator/src/validator.rs` was modified to carry an `Override` context (a `HashSet` of property names) throughout the validation process. 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.
- **Initialization**: When validation begins, the `Override` context is created and populated with the names of any properties that the top-level schema has marked with `override`.
- **Propagation**: As the validator descends through a `$ref` or `allOf`, this `Override` context is cloned and passed down. The child schema adds its own override properties to the set, ensuring that higher-level overrides are always maintained.
- **Enforcement**: In `obj_validate`, before a property is validated, the validator first checks if the property's name exists in the `Override` context it has received. If it does, it means a parent schema has already claimed responsibility for validating this property, so the child validator **skips** it entirely. This effectively achieves the "top-level wins" inheritance model.
This approach cleanly integrates our desired inheritance behavior directly into the validator with minimal and explicit deviation from the standard, avoiding the need for a complex, post-processing validation function like the old `walk_and_validate_refs`. ## 🏗️ Architecture
### 2. Recursive Runtime Strictness Control The extension is written in Rust using `pgrx` and structures its schema parser to mirror the Punc Generator's design:
- **Problem:** The `jspg` project requires that certain schemas (specifically those for public `puncs` and global `type`s) enforce a strict "no extra properties" policy. This strictness needs to be decided at runtime and must cascade through the entire validation hierarchy, including all nested objects and `$ref` chains. A compile-time flag was unsuitable because it would incorrectly apply strictness to shared, reusable schemas. * **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`.
- **Solution:** A runtime validation option was implemented to enforce strictness recursively. This required several coordinated changes to the `boon` validator. ### Concurrency & Threading ("Atomic Swap")
#### Key Changes 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. **`ValidationOptions` Struct**: A new `ValidationOptions { be_strict: bool }` struct was added to `validator/src/lib.rs`. The `jspg` code in `src/lib.rs` determines if a validation run should be strict and passes this struct to the validator. 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.
2. **Strictness Check in `uneval_validate`**: The original `boon` only checked for unevaluated properties if the `unevaluatedProperties` keyword was present in the schema. We added an `else if be_strict` block to `uneval_validate` in `validator/src/validator.rs`. This block triggers a check for any leftover unevaluated properties at the end of a validation pass and reports them as errors, effectively enforcing our runtime strictness rule. ## 🧪 Testing
3. **Correct Context Propagation**: The most complex part of the fix was ensuring the set of unevaluated properties was correctly maintained across different validation contexts (especially `$ref` and nested property validations). Three critical changes were made: Testing is driven by standard Rust unit tests that load JSON fixtures.
- **Inheriting Context in `_validate_self`**: When validating keywords that apply to the same instance (like `$ref` or `allOf`), the sub-validator must know what properties the parent has already evaluated. We changed the creation of the `Validator` inside `_validate_self` to pass a clone of the parent's `uneval` state (`uneval: self.uneval.clone()`) instead of creating a new one from scratch. This allows the context to flow downwards.
- **Isolating Context in `validate_val`**: Conversely, when validating a property's value, that value is a *different* part of the JSON instance. The sub-validation should not affect the parent's list of unevaluated properties. We fixed this by commenting out the `self.uneval.merge(...)` call in the `validate_val` function.
- **Simplifying `Uneval::merge`**: The original logic for merging `uneval` state was different for `$ref` keywords. This was incorrect. We simplified the `merge` function to *always* perform an intersection (`retain`), which correctly combines the knowledge of evaluated properties from different schema parts that apply to the same instance.
4. **Removing Incompatible Assertions**: The changes to context propagation broke several `debug_assert!` macros in the `arr_validate` function, which were part of `boon`'s original design. Since our new validation flow is different but correct, these assertions were removed. * **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`.

107
build.rs Normal file
View File

@ -0,0 +1,107 @@
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=tests/fixtures");
println!("cargo:rerun-if-changed=Cargo.toml");
// File 1: src/tests.rs for #[pg_test]
let pg_dest_path = Path::new("src/tests.rs");
let mut pg_file = File::create(&pg_dest_path).unwrap();
// File 2: tests/tests.rs for standard #[test] integration
let std_dest_path = Path::new("tests/tests.rs");
let mut std_file = File::create(&std_dest_path).unwrap();
// Write headers
writeln!(std_file, "use jspg::util;").unwrap();
// Helper for snake_case conversion
// let _to_snake_case = |s: &str| -> String {
// s.chars().fold(String::new(), |mut acc, c| {
// if c.is_uppercase() {
// if !acc.is_empty() {
// acc.push('_');
// }
// acc.push(c.to_ascii_lowercase());
// } else if c == '-' || c == ' ' || c == '.' || c == '/' || c == ':' {
// acc.push('_');
// } else if c.is_alphanumeric() {
// acc.push(c);
// }
// acc
// })
// };
// 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}
// We sanitize the filename to be a valid identifier
// Use manual snake_case logic since we don't want to add a build-dependency just yet if not needed,
// but `dynamicRef` -> `dynamic_ref` requires parsing.
// Let's implement a simple camelToSnake helper.
let mut safe_filename = String::new();
for (i, c) in file_name.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
safe_filename.push('_');
}
safe_filename.push(c.to_ascii_lowercase());
} else if c == '-' || c == '.' {
safe_filename.push('_');
} else {
safe_filename.push(c);
}
}
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();
}
}
}
}
}
}

386
src/compiler.rs Normal file
View File

@ -0,0 +1,386 @@
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));
}
}
// ... 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(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)
}
}

68
src/drop.rs Normal file
View File

@ -0,0 +1,68 @@
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)]
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!({ "result": "success" })), // Or appropriate success response
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 {
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
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
}

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(())
}

View File

@ -1,88 +0,0 @@
use serde_json::Value;
use pgrx::JsonB;
// Simple test helpers for cleaner test code
pub fn assert_success(result: &JsonB) {
let json = &result.0;
if !json.get("response").is_some() || json.get("errors").is_some() {
let pretty = serde_json::to_string_pretty(json).unwrap_or_else(|_| format!("{:?}", json));
panic!("Expected success but got:\n{}", pretty);
}
}
pub fn assert_failure(result: &JsonB) {
let json = &result.0;
if json.get("response").is_some() || !json.get("errors").is_some() {
let pretty = serde_json::to_string_pretty(json).unwrap_or_else(|_| format!("{:?}", json));
panic!("Expected failure but got:\n{}", pretty);
}
}
pub fn assert_error_count(result: &JsonB, expected_count: usize) {
assert_failure(result);
let errors = get_errors(result);
if errors.len() != expected_count {
let pretty = serde_json::to_string_pretty(&result.0).unwrap_or_else(|_| format!("{:?}", result.0));
panic!("Expected {} errors, got {}:\n{}", expected_count, errors.len(), pretty);
}
}
pub fn get_errors(result: &JsonB) -> &Vec<Value> {
result.0["errors"].as_array().expect("errors should be an array")
}
pub fn has_error_with_code(result: &JsonB, code: &str) -> bool {
get_errors(result).iter().any(|e| e["code"] == code)
}
pub fn has_error_with_code_and_path(result: &JsonB, code: &str, path: &str) -> bool {
get_errors(result).iter().any(|e| e["code"] == code && e["details"]["path"] == path)
}
pub fn assert_has_error(result: &JsonB, code: &str, path: &str) {
if !has_error_with_code_and_path(result, code, path) {
let pretty = serde_json::to_string_pretty(&result.0).unwrap_or_else(|_| format!("{:?}", result.0));
panic!("Expected error with code='{}' and path='{}' but not found:\n{}", code, path, pretty);
}
}
pub fn find_error_with_code<'a>(result: &'a JsonB, code: &str) -> &'a Value {
get_errors(result).iter().find(|e| e["code"] == code)
.unwrap_or_else(|| panic!("No error found with code '{}'", code))
}
pub fn find_error_with_code_and_path<'a>(result: &'a JsonB, code: &str, path: &str) -> &'a Value {
get_errors(result).iter().find(|e| e["code"] == code && e["details"]["path"] == path)
.unwrap_or_else(|| panic!("No error found with code '{}' and path '{}'", code, path))
}
pub fn assert_error_detail(error: &Value, detail_key: &str, expected_value: &str) {
let actual = error["details"][detail_key].as_str()
.unwrap_or_else(|| panic!("Error detail '{}' is not a string", detail_key));
assert_eq!(actual, expected_value, "Error detail '{}' mismatch", detail_key);
}
// Additional convenience helpers for common patterns
pub fn assert_error_message_contains(error: &Value, substring: &str) {
let message = error["message"].as_str().expect("error should have message");
assert!(message.contains(substring), "Expected message to contain '{}', got '{}'", substring, message);
}
pub fn assert_error_cause_json(error: &Value, expected_cause: &Value) {
let cause = &error["details"]["cause"];
assert!(cause.is_object(), "cause should be JSON object");
assert_eq!(cause, expected_cause, "cause mismatch");
}
pub fn assert_error_context(error: &Value, expected_context: &Value) {
assert_eq!(&error["details"]["context"], expected_context, "context mismatch");
}
pub fn jsonb(val: Value) -> JsonB {
JsonB(val)
}

File diff suppressed because it is too large Load Diff

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()
}
}

217
src/schema.rs Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

457
src/util.rs Normal file
View File

@ -0,0 +1,457 @@
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 Local Registry for this test group
let mut registry = crate::registry::Registry::new();
// Helper to register items with 'schemas'
let register_schemas = |registry: &mut crate::registry::Registry, items_val: Option<&Value>| {
if let Some(val) = items_val {
if let Value::Array(arr) = val {
for item in arr {
if let Some(schemas_val) = item.get("schemas") {
if let Value::Array(schemas) = schemas_val {
for schema_val in schemas {
if let Ok(schema) =
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
{
registry.add(schema);
}
}
}
}
}
}
}
};
// 1. Register Family Schemas if 'types' is present
if let Some(types_val) = &group.types {
if let Value::Array(arr) = types_val {
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
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 id = format!("{}.family", family_name);
let object_refs: Vec<Value> = members
.iter()
.map(|s| serde_json::json!({ "$ref": s }))
.collect();
let schema_json = serde_json::json!({
"$id": id,
"oneOf": object_refs
});
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
registry.add(schema);
}
}
}
}
// 2. Register items directly
register_schemas(&mut registry, group.enums.as_ref());
register_schemas(&mut registry, group.types.as_ref());
register_schemas(&mut registry, 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() {
registry.add(schema);
} else {
// Fallback ID if none provided in schema
let id = format!("test:{}:{}", path, index);
schema.obj.id = Some(id);
registry.add(schema);
}
}
Err(e) => {
eprintln!(
"DEBUG: FAILED to deserialize group schema for index {}: {}",
index, e
);
}
}
}
// Create Validator Instance (Takes ownership of registry)
let validator = Validator::new(registry);
// 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 Isolated Registry for this test group
let mut registry = crate::registry::Registry::new();
// Helper to register items with 'schemas'
let register_schemas = |registry: &mut crate::registry::Registry, items_val: Option<Value>| {
if let Some(val) = items_val {
if let Value::Array(arr) = val {
for item in arr {
if let Some(schemas_val) = item.get("schemas") {
if let Value::Array(schemas) = schemas_val {
for schema_val in schemas {
if let Ok(schema) =
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
{
registry.add(schema);
}
}
}
}
}
}
}
};
// 1. Register Family Schemas if 'types' is present
if let Some(types_val) = &group.types {
if let Value::Array(arr) = types_val {
let mut family_map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::new();
for item in arr {
if let Some(name) = item.get("name").and_then(|v| v.as_str()) {
// Default hierarchy contains self if not specified?
// Usually hierarchy is explicit in these tests.
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 id = format!("{}.family", family_name);
let object_refs: Vec<Value> = members
.into_iter()
.map(|s| serde_json::json!({ "$ref": s }))
.collect();
let schema_json = serde_json::json!({
"$id": id,
"oneOf": object_refs
});
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
registry.add(schema);
}
}
}
}
// Register 'types', 'enums', and 'puncs' if present (JSPG style)
register_schemas(&mut registry, group.types);
register_schemas(&mut registry, group.enums);
register_schemas(&mut registry, group.puncs);
// Register main 'schema' if present (Standard style)
// Ensure ID is a valid URI to avoid Url::parse errors in Compiler
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());
}
registry.add(schema);
}
// Create Instance (Takes Ownership)
let validator = Validator::new(registry);
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,
}
}

1433
src/validator.rs Normal file

File diff suppressed because it is too large Load Diff

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

View File

@ -1,48 +1,21 @@
[ [
{ {
"description": "validation of binary-encoded media type documents with schema", "description": "validation of string-encoded content based on media type",
"schema": { "schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"contentMediaType": "application/json", "contentMediaType": "application/json"
"contentEncoding": "base64",
"contentSchema": { "required": ["foo"], "properties": { "foo": { "type": "string" } } }
}, },
"tests": [ "tests": [
{ {
"description": "a valid base64-encoded JSON document", "description": "a valid JSON document",
"data": "eyJmb28iOiAiYmFyIn0K", "data": "{\"foo\": \"bar\"}",
"valid": true "valid": true
}, },
{ {
"description": "another valid base64-encoded JSON document", "description": "an invalid JSON document; validates true",
"data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==", "data": "{:}",
"valid": true "valid": true
}, },
{
"description": "an invalid base64-encoded JSON document; validates false",
"data": "eyJib28iOiAyMH0=",
"valid": false
},
{
"description": "an empty object as a base64-encoded JSON document; validates false",
"data": "e30=",
"valid": false
},
{
"description": "an empty array as a base64-encoded JSON document",
"data": "W10=",
"valid": true
},
{
"description": "a validly-encoded invalid JSON document; validates false",
"data": "ezp9Cg==",
"valid": false
},
{
"description": "an invalid base64 string that is valid JSON; validates false",
"data": "{}",
"valid": false
},
{ {
"description": "ignores non-strings", "description": "ignores non-strings",
"data": 100, "data": 100,
@ -51,11 +24,79 @@
] ]
}, },
{ {
"description": "contentSchema without contentMediaType", "description": "validation of binary string-encoding",
"schema": { "schema": {
"$schema": "https://json-schema.org/draft/2020-12/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", "contentEncoding": "base64",
"contentSchema": { "required": ["foo"], "properties": { "foo": { "type": "string" } } } "contentSchema": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "string"
},
"boo": {
"type": "integer"
}
}
}
}, },
"tests": [ "tests": [
{ {
@ -89,49 +130,9 @@
"valid": true "valid": true
}, },
{ {
"description": "an invalid base64 string that is valid JSON; validates false", "description": "an invalid base64 string that is valid JSON; validates true",
"data": "{}", "data": "{}",
"valid": false
},
{
"description": "ignores non-strings",
"data": 100,
"valid": true "valid": true
}
]
},
{
"description": "contentSchema without contentEncoding",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"contentMediaType": "application/json",
"contentSchema": { "required": ["foo"], "properties": { "foo": { "type": "string" } } }
},
"tests": [
{
"description": "a valid JSON document",
"data": "{\"foo\": \"bar\"}",
"valid": true
},
{
"description": "another valid base64-encoded JSON document",
"data": "{\"boo\": 20, \"foo\": \"baz\"}",
"valid": true
},
{
"description": "an empty object; validates false",
"data": "{}",
"valid": false
},
{
"description": "an empty array; validates false",
"data": "[]",
"valid": true
},
{
"description": "invalid JSON document; validates false",
"data": "[}",
"valid": false
}, },
{ {
"description": "ignores non-strings", "description": "ignores non-strings",
@ -140,4 +141,4 @@
} }
] ]
} }
] ]

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

1382
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
}
]
}
]

2071
tests/tests.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,81 +0,0 @@
# Changelog
## [Unreleased]
### Bug Fixes
- validator: ensure `uneval` state is propagated when `$ref` validation fails
## [0.6.1] - 2025-01-07
### Bug Fixes
- fix: FileLoader should not be used in wasm
## [0.6.0] - 2024-05-30
### Braking Changes
- loader: Allow to replace entirely
### Bug Fixes
- seperate doc loading from root creation
- validator: if contentEncoding fails, skip contentMediaType
- loader: should load latest from metaschemas dir
- fix: hash for json numbers with zero fractions
- fix: resources/anchors in non-std schema loc not supported
### Changes
- boon binary artificats under github release
- boon binary `--cacert` option
- boon binary `--insecure` flag
## [0.5.3] - 2024-01-27
### Changes
- updated dependencies
## [0.5.2] - 2024-01-27
### Bug Fixes
- Error message for failed const validation is wrong
## [0.5.1] - 2023-07-13
### Changes
- WASM compatibility
- minor performance improvements
## [0.5.0] - 2023-03-29
### Breaking Changes
- chages to error api
### Performance
- minor improvements in validation
## [0.4.0] - 2023-03-24
### Breaking Changes
- chages to error api
### Fixed
- Compler.add_resource should not check file exists
### Added
- implement `contentSchema` keyword
- ECMA-262 regex compatibility
- add example_custom_content_encoding
- add example_custom_content_media_type
### Performance
- significant improvement in validation
## [0.3.1] - 2023-03-07
### Added
- add example_from_yaml_files
- cli: support yaml files
### Fixed
- ensure fragment decoded before use
- $dynamicRef w/o anchor is same as $ref

1441
validator/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
[package]
name = "boon"
version = "0.6.1"
edition = "2024"
description = "JSONSchema (draft 2020-12, draft 2019-09, draft-7, draft-6, draft-4) Validation"
readme = "README.md"
repository = "https://github.com/santhosh-tekuri/boon"
authors = ["santhosh kumar tekuri <santhosh.tekuri@gmail.com>"]
keywords = ["jsonschema", "validation"]
license = "MIT OR Apache-2.0"
categories = ["web-programming"]
exclude = [ "tests", ".github", ".gitmodules" ]
[dependencies]
pgrx = "0.16.1"
serde = "1"
serde_json = "1"
regex = "1.12.2"
regex-syntax = "0.8.8"
url = "2"
fluent-uri = "0.4.1"
idna = "1.1"
percent-encoding = "2"
once_cell = "1"
base64 = "0.22"
ahash = "0.8.12"
appendlist = "1.4"
[dev-dependencies]
pgrx-tests = "0.16.1"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
ureq = "3.1"
rustls = "0.23"
criterion = "0.7"
[[bench]]
name = "bench"
harness = false

View File

@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1,18 +0,0 @@
Copyright 2023 Santhosh Kumar Tekuri
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,88 +0,0 @@
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Crates.io](https://img.shields.io/crates/v/boon.svg)](https://crates.io/crates/boon)
[![docs.rs](https://docs.rs/boon/badge.svg)](https://docs.rs/boon/)
[![Build Status](https://github.com/santhosh-tekuri/boon/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/santhosh-tekuri/boon/actions/workflows/rust.yml)
[![codecov](https://codecov.io/gh/santhosh-tekuri/boon/branch/main/graph/badge.svg?token=A2YC4A0BLG)](https://codecov.io/gh/santhosh-tekuri/boon)
[![dependency status](https://deps.rs/repo/github/Santhosh-tekuri/boon/status.svg?refresh)](https://deps.rs/repo/github/Santhosh-tekuri/boon)
[Examples](https://github.com/santhosh-tekuri/boon/blob/main/tests/examples.rs)
[Changelog](https://github.com/santhosh-tekuri/boon/blob/main/CHANGELOG.md)
## Library Features
- [x] pass [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) excluding optional(compare with other impls at [bowtie](https://bowtie-json-schema.github.io/bowtie/#))
- [x] [![draft-04](https://img.shields.io/endpoint?url=https://bowtie.report/badges/rust-boon/compliance/draft4.json)](https://bowtie.report/#/dialects/draft4)
- [x] [![draft-06](https://img.shields.io/endpoint?url=https://bowtie.report/badges/rust-boon/compliance/draft6.json)](https://bowtie.report/#/dialects/draft6)
- [x] [![draft-07](https://img.shields.io/endpoint?url=https://bowtie.report/badges/rust-boon/compliance/draft7.json)](https://bowtie.report/#/dialects/draft7)
- [x] [![draft/2019-09](https://img.shields.io/endpoint?url=https://bowtie.report/badges/rust-boon/compliance/draft2019-09.json)](https://bowtie.report/#/dialects/draft2019-09)
- [x] [![draft/2020-12](https://img.shields.io/endpoint?url=https://bowtie.report/badges/rust-boon/compliance/draft2020-12.json)](https://bowtie.report/#/dialects/draft2020-12)
- [x] detect infinite loop traps
- [x] `$schema` cycle
- [x] validation cycle
- [x] custom `$schema` url
- [x] vocabulary based validation
- [x] ECMA-262 regex compatibility (pass tests from `optional/ecmascript-regex.json`)
- [x] format assertions
- [x] flag to enable in draft >= 2019-09
- [x] custom format registration
- [x] built-in formats
- [x] regex, uuid
- [x] ipv4, ipv6
- [x] hostname, email
- [x] idn-hostname, idn-email
- [x] date, time, date-time, duration
- [x] json-pointer, relative-json-pointer
- [x] uri, uri-reference, uri-template
- [x] iri, iri-reference
- [x] period
- [x] content assertions
- [x] flag to enable in draft >= 7
- [x] contentEncoding
- [x] base64
- [x] custom
- [x] contentMediaType
- [x] application/json
- [x] custom
- [x] contentSchema
- [x] errors
- [x] introspectable
- [x] hierarchy
- [x] alternative display with `#`
- [x] output
- [x] flag
- [x] basic
- [x] detailed
- [ ] custom vocabulary
## CLI
to install: `cargo install boon-cli --locked`
or download it from [releases](https://github.com/santhosh-tekuri/boon/releases)
```
Usage: boon [OPTIONS] SCHEMA [INSTANCE...]
Options:
-h, --help Print help information
-q, --quiet Do not print errors
-d, --draft <VER> Draft used when '$schema' is missing. Valid values 4,
6, 7, 2019, 2020 (default 2020)
-o, --output <FMT> Output format. Valid values simple, alt, flag, basic,
detailed (default simple)
-f, --assert-format
Enable format assertions with draft >= 2019
-c, --assert-content
Enable content assertions with draft >= 7
--cacert <FILE> Use the specified PEM certificate file to verify the
peer. The file may contain multiple CA certificates
-k, --insecure Use insecure TLS connection
```
This cli can validate both schema and multiple instances.
It support both json and yaml files
exit code is:
- `1` if command line arguments are invalid.
- `2` if there are errors

View File

@ -1,26 +0,0 @@
use std::{env, fs::File};
use boon::{Compiler, Schemas};
use criterion::{criterion_group, criterion_main, Criterion};
use serde_json::Value;
pub fn validate(c: &mut Criterion) {
let (Ok(schema), Ok(instance)) = (env::var("SCHEMA"), env::var("INSTANCE")) else {
panic!("SCHEMA, INSTANCE environment variables not set");
};
let mut schemas = Schemas::new();
let mut compiler = Compiler::new();
compiler.enable_format_assertions();
let sch = compiler.compile(&schema, &mut schemas).unwrap();
let rdr = File::open(&instance).unwrap();
let inst: Value = if instance.ends_with(".yaml") || instance.ends_with(".yml") {
serde_yaml::from_reader(rdr).unwrap()
} else {
serde_json::from_reader(rdr).unwrap()
};
c.bench_function("boon", |b| b.iter(|| schemas.validate(&inst, sch).unwrap()));
}
criterion_group!(benches, validate);
criterion_main!(benches);

1156
validator/cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
[package]
name = "boon-cli"
version = "0.6.2"
edition = "2021"
description = "cli for JSONSchema (draft 2020-12, draft 2019-09, draft-7, draft-6, draft-4) Validation"
repository = "https://github.com/santhosh-tekuri/boon/cli"
authors = ["santhosh kumar tekuri <santhosh.tekuri@gmail.com>"]
keywords = ["jsonschema", "validation"]
categories = ["web-programming"]
license = "MIT OR Apache-2.0"
[dependencies]
boon = { version = "0.6.1", path = ".."}
url = "2"
getopts = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
ureq = "2.12"
rustls = { version = "0.23", features = ["ring"] }
rustls-pemfile = "2.1"
[[bin]]
name = "boon"
path = "src/main.rs"

View File

@ -1,316 +0,0 @@
use core::panic;
use std::{env, error::Error, fs::File, io::BufReader, process, str::FromStr, sync::Arc};
use boon::{Compiler, Draft, Schemas, SchemeUrlLoader, UrlLoader};
use getopts::Options;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use serde_json::Value;
use ureq::Agent;
use url::Url;
fn main() {
let opts = options();
let matches = match opts.parse(env::args().skip(1)) {
Ok(m) => m,
Err(f) => {
eprintln!("{f}");
eprintln!();
eprintln!("{}", opts.usage(BRIEF));
process::exit(1)
}
};
if matches.opt_present("version") {
println!("{}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
if matches.opt_present("help") {
println!("{}", opts.usage(BRIEF));
process::exit(0);
}
// draft --
let mut draft = Draft::default();
if let Some(v) = matches.opt_str("draft") {
let Ok(v) = usize::from_str(&v) else {
eprintln!("invalid draft: {v}");
eprintln!();
eprintln!("{}", opts.usage(BRIEF));
process::exit(1);
};
draft = match v {
4 => Draft::V4,
6 => Draft::V6,
7 => Draft::V7,
2019 => Draft::V2019_09,
2020 => Draft::V2020_12,
_ => {
eprintln!("invalid draft: {v}");
eprintln!();
eprintln!("{}", opts.usage(BRIEF));
process::exit(1);
}
};
}
// output --
let output = matches.opt_str("output");
if let Some(o) = &output {
if !matches!(o.as_str(), "simple" | "alt" | "flag" | "basic" | "detailed") {
eprintln!("invalid output: {o}");
eprintln!();
eprintln!("{}", opts.usage(BRIEF));
process::exit(1);
}
}
// flags --
let quiet = matches.opt_present("quiet");
let assert_format = matches.opt_present("assert-format");
let assert_content = matches.opt_present("assert-content");
let insecure = matches.opt_present("insecure");
// schema --
let Some(schema) = matches.free.first() else {
eprintln!("missing SCHEMA");
eprintln!();
eprintln!("{}", opts.usage(BRIEF));
process::exit(1);
};
// compile --
let mut schemas = Schemas::new();
let mut compiler = Compiler::new();
let mut loader = SchemeUrlLoader::new();
loader.register("file", Box::new(FileUrlLoader));
let cacert = matches.opt_str("cacert");
let cacert = cacert.as_deref();
loader.register("http", Box::new(HttpUrlLoader::new(cacert, insecure)));
loader.register("https", Box::new(HttpUrlLoader::new(cacert, insecure)));
compiler.use_loader(Box::new(loader));
compiler.set_default_draft(draft);
if assert_format {
compiler.enable_format_assertions();
}
if assert_content {
compiler.enable_content_assertions();
}
let sch = match compiler.compile(schema, &mut schemas) {
Ok(sch) => {
println!("schema {schema}: ok");
sch
}
Err(e) => {
println!("schema {schema}: failed");
if !quiet {
println!("{e:#}");
}
process::exit(2);
}
};
// validate --
let mut all_valid = true;
for instance in &matches.free[1..] {
if !quiet {
println!();
}
let rdr = match File::open(instance) {
Ok(rdr) => BufReader::new(rdr),
Err(e) => {
println!("instance {instance}: failed");
if !quiet {
println!("error reading file {instance}: {e}");
}
all_valid = false;
continue;
}
};
let value: Result<Value, String> =
if instance.ends_with(".yaml") || instance.ends_with(".yml") {
serde_yaml::from_reader(rdr).map_err(|e| e.to_string())
} else {
serde_json::from_reader(rdr).map_err(|e| e.to_string())
};
let value = match value {
Ok(v) => v,
Err(e) => {
println!("instance {instance}: failed");
if !quiet {
println!("error parsing file {instance}: {e}");
}
all_valid = false;
continue;
}
};
match schemas.validate(&value, sch) {
Ok(_) => println!("instance {instance}: ok"),
Err(e) => {
println!("instance {instance}: failed");
if !quiet {
match &output {
Some(out) => match out.as_str() {
"simple" => println!("{e}"),
"alt" => println!("{e:#}"),
"flag" => println!("{:#}", e.flag_output()),
"basic" => println!("{:#}", e.basic_output()),
"detailed" => println!("{:#}", e.detailed_output()),
_ => (),
},
None => println!("{e}"),
}
}
all_valid = false;
continue;
}
};
}
if !all_valid {
process::exit(2);
}
}
const BRIEF: &str = "Usage: boon [OPTIONS] SCHEMA [INSTANCE...]";
fn options() -> Options {
let mut opts = Options::new();
opts.optflag("v", "version", "Print version and exit");
opts.optflag("h", "help", "Print help information");
opts.optflag("q", "quiet", "Do not print errors");
opts.optopt(
"d",
"draft",
"Draft used when '$schema' is missing. Valid values 4, 6, 7, 2019, 2020 (default 2020)",
"<VER>",
);
opts.optopt(
"o",
"output",
"Output format. Valid values simple, alt, flag, basic, detailed (default simple)",
"<FMT>",
);
opts.optflag(
"f",
"assert-format",
"Enable format assertions with draft >= 2019",
);
opts.optflag(
"c",
"assert-content",
"Enable content assertions with draft >= 7",
);
opts.optopt(
"",
"cacert",
"Use the specified PEM certificate file to verify the peer. The file may contain multiple CA certificates",
"<FILE>",
);
opts.optflag("k", "insecure", "Use insecure TLS connection");
opts
}
struct FileUrlLoader;
impl UrlLoader for FileUrlLoader {
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
let url = Url::parse(url)?;
let path = url.to_file_path().map_err(|_| "invalid file path")?;
let file = File::open(&path)?;
if path
.extension()
.filter(|&ext| ext == "yaml" || ext == "yml")
.is_some()
{
Ok(serde_yaml::from_reader(file)?)
} else {
Ok(serde_json::from_reader(file)?)
}
}
}
struct HttpUrlLoader(Agent);
impl HttpUrlLoader {
fn new(cacert: Option<&str>, insecure: bool) -> Self {
let mut builder = ureq::builder();
if let Some(cacert) = cacert {
let file = File::open(cacert).unwrap_or_else(|e| panic!("error opening {cacert}: {e}"));
let certs: Result<Vec<_>, _> =
rustls_pemfile::certs(&mut BufReader::new(file)).collect();
let certs = certs.unwrap_or_else(|e| panic!("error reading cacert: {e}"));
assert!(!certs.is_empty(), "no certs in cacert");
let mut store = rustls::RootCertStore::empty();
for cert in certs {
store
.add(cert)
.unwrap_or_else(|e| panic!("error adding cert: {e}"))
}
let tls_config = rustls::ClientConfig::builder()
.with_root_certificates(store)
.with_no_client_auth();
builder = builder.tls_config(tls_config.into());
} else if insecure {
let tls_config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
.with_no_client_auth();
builder = builder.tls_config(tls_config.into());
}
Self(builder.build())
}
}
impl UrlLoader for HttpUrlLoader {
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
let response = self.0.get(url).call()?;
let is_yaml = url.ends_with(".yaml") || url.ends_with(".yml") || {
let ctype = response.content_type();
ctype.ends_with("/yaml") || ctype.ends_with("-yaml")
};
if is_yaml {
Ok(serde_yaml::from_reader(response.into_reader())?)
} else {
Ok(serde_json::from_reader(response.into_reader())?)
}
}
}
#[derive(Debug)]
struct InsecureVerifier;
impl ServerCertVerifier for InsecureVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}

View File

@ -1,999 +0,0 @@
use std::{cmp::Ordering, collections::HashMap, error::Error, fmt::Display};
use regex::Regex;
use serde_json::{Map, Value};
use url::Url;
use crate::{content::*, draft::*, ecma, formats::*, root::*, roots::*, util::*, *};
/// Supported draft versions
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Draft {
/// Draft for `http://json-schema.org/draft-04/schema`
V4,
/// Draft for `http://json-schema.org/draft-06/schema`
V6,
/// Draft for `http://json-schema.org/draft-07/schema`
V7,
/// Draft for `https://json-schema.org/draft/2019-09/schema`
V2019_09,
/// Draft for `https://json-schema.org/draft/2020-12/schema`
V2020_12,
}
impl Draft {
/**
Get [`Draft`] for given `url`
# Arguments
* `url` - accepts both `http` and `https` and ignores any fragments in url
# Examples
```
# use boon::*;
assert_eq!(Draft::from_url("https://json-schema.org/draft/2020-12/schema"), Some(Draft::V2020_12));
assert_eq!(Draft::from_url("http://json-schema.org/draft-07/schema#"), Some(Draft::V7));
```
*/
pub fn from_url(url: &str) -> Option<Draft> {
match crate::draft::Draft::from_url(url) {
Some(draft) => match draft.version {
4 => Some(Draft::V4),
6 => Some(Draft::V6),
7 => Some(Draft::V7),
2019 => Some(Draft::V2019_09),
2020 => Some(Draft::V2020_12),
_ => None,
},
None => None,
}
}
pub(crate) fn internal(&self) -> &'static crate::draft::Draft {
match self {
Draft::V4 => &DRAFT4,
Draft::V6 => &DRAFT6,
Draft::V7 => &DRAFT7,
Draft::V2019_09 => &DRAFT2019,
Draft::V2020_12 => &DRAFT2020,
}
}
}
/// Returns latest draft supported
impl Default for Draft {
fn default() -> Self {
Draft::V2020_12
}
}
/// JsonSchema compiler.
#[derive(Default)]
pub struct Compiler {
roots: Roots,
assert_format: bool,
assert_content: bool,
formats: HashMap<&'static str, Format>,
decoders: HashMap<&'static str, Decoder>,
media_types: HashMap<&'static str, MediaType>,
}
impl Compiler {
pub fn new() -> Self {
Self::default()
}
/**
Overrides the draft used to compile schemas without
explicit `$schema` field.
By default this library uses latest draft supported.
The use of this option is HIGHLY encouraged to ensure
continued correct operation of your schema. The current
default value will not stay the same over time.
*/
pub fn set_default_draft(&mut self, d: Draft) {
self.roots.default_draft = d.internal()
}
/**
Always enable format assertions.
# Default Behavior
- for draft-07 and earlier: enabled
- for draft/2019-09: disabled, unless
metaschema says `format` vocabulary is required
- for draft/2020-12: disabled, unless
metaschema says `format-assertion` vocabulary is required
*/
pub fn enable_format_assertions(&mut self) {
self.assert_format = true;
}
/**
Always enable content assertions.
content assertions include keywords:
- contentEncoding
- contentMediaType
- contentSchema
Default Behavior is always disabled.
*/
pub fn enable_content_assertions(&mut self) {
self.assert_content = true;
}
/// Overrides default [`UrlLoader`] used to load schema resources
pub fn use_loader(&mut self, url_loader: Box<dyn UrlLoader>) {
self.roots.loader.use_loader(url_loader);
}
/**
Registers custom `format`
# Note
- `regex` format cannot be overridden
- format assertions are disabled for draft >= 2019-09.
see [`Compiler::enable_format_assertions`]
*/
pub fn register_format(&mut self, format: Format) {
if format.name != "regex" {
self.formats.insert(format.name, format);
}
}
/**
Registers custom `contentEncoding`
Note that content assertions are disabled by default.
see [`Compiler::enable_content_assertions`]
*/
pub fn register_content_encoding(&mut self, decoder: Decoder) {
self.decoders.insert(decoder.name, decoder);
}
/**
Registers custom `contentMediaType`
Note that content assertions are disabled by default.
see [`Compiler::enable_content_assertions`]
*/
pub fn register_content_media_type(&mut self, media_type: MediaType) {
self.media_types.insert(media_type.name, media_type);
}
/**
Adds schema resource which used later in reference resoltion
If you do not know which schema resources required, then use [`UrlLoader`].
The argument `loc` can be file path or url. any fragment in `loc` is ignored.
# Errors
returns [`CompileError`] if url parsing failed.
*/
pub fn add_resource(&mut self, loc: &str, json: Value) -> Result<(), CompileError> {
let uf = UrlFrag::absolute(loc)?;
self.roots.loader.add_doc(uf.url, json);
Ok(())
}
/**
Compile given `loc` into `target` and return an identifier to the compiled
schema.
the argument `loc` can be file path or url with optional fragment.
examples: `http://example.com/schema.json#/defs/address`,
`samples/schema_file.json#defs/address`
if `loc` is already compiled, it simply returns the same [`SchemaIndex`]
*/
pub fn compile(
&mut self,
loc: &str,
target: &mut Schemas,
) -> Result<SchemaIndex, CompileError> {
let uf = UrlFrag::absolute(loc)?;
// resolve anchor
let up = self.roots.resolve_fragment(uf)?;
let result = self.do_compile(up, target);
if let Err(bug @ CompileError::Bug(_)) = &result {
debug_assert!(false, "{bug}");
}
result
}
fn do_compile(
&mut self,
up: UrlPtr,
target: &mut Schemas,
) -> Result<SchemaIndex, CompileError> {
let mut queue = Queue::new();
let mut compiled = Vec::new();
let index = queue.enqueue_schema(target, up);
if queue.schemas.is_empty() {
// already got compiled
return Ok(index);
}
while queue.schemas.len() > compiled.len() {
let up = &queue.schemas[compiled.len()];
self.roots.ensure_subschema(up)?;
let Some(root) = self.roots.get(&up.url) else {
return Err(CompileError::Bug("or_load didn't add".into()));
};
let doc = self.roots.loader.load(&root.url)?;
let v = up.lookup(doc)?;
let sch = self.compile_value(target, v, &up.clone(), root, &mut queue)?;
compiled.push(sch);
self.roots.insert(&mut queue.roots);
}
target.insert(queue.schemas, compiled);
Ok(index)
}
fn compile_value(
&self,
schemas: &Schemas,
v: &Value,
up: &UrlPtr,
root: &Root,
queue: &mut Queue,
) -> Result<Schema, CompileError> {
let mut s = Schema::new(up.to_string());
s.draft_version = root.draft.version;
// we know it is already in queue, we just want to get its index
let len = queue.schemas.len();
s.idx = queue.enqueue_schema(schemas, up.to_owned());
debug_assert_eq!(queue.schemas.len(), len, "{up} should already be in queue");
s.resource = {
let base = UrlPtr {
url: up.url.clone(),
ptr: root.resource(&up.ptr).ptr.clone(),
};
queue.enqueue_schema(schemas, base)
};
// if resource, enqueue dynamicAnchors for compilation
if s.idx == s.resource && root.draft.version >= 2020 {
let res = root.resource(&up.ptr);
for (anchor, anchor_ptr) in &res.anchors {
if res.dynamic_anchors.contains(anchor) {
let up = UrlPtr {
url: up.url.clone(),
ptr: anchor_ptr.clone(),
};
let danchor_sch = queue.enqueue_schema(schemas, up);
s.dynamic_anchors.insert(anchor.to_string(), danchor_sch);
}
}
}
match v {
Value::Object(obj) => {
if obj.is_empty() {
s.boolean = Some(true);
} else {
ObjCompiler {
c: self,
obj,
up,
schemas,
root,
queue,
}
.compile_obj(&mut s)?;
}
}
Value::Bool(b) => s.boolean = Some(*b),
_ => {}
}
s.all_props_evaluated = s.additional_properties.is_some();
s.all_items_evaluated = if s.draft_version < 2020 {
s.additional_items.is_some() || matches!(s.items, Some(Items::SchemaRef(_)))
} else {
s.items2020.is_some()
};
s.num_items_evaluated = if let Some(Items::SchemaRefs(list)) = &s.items {
list.len()
} else {
s.prefix_items.len()
};
Ok(s)
}
}
struct ObjCompiler<'c, 'v, 'l, 's, 'r, 'q> {
c: &'c Compiler,
obj: &'v Map<String, Value>,
up: &'l UrlPtr,
schemas: &'s Schemas,
root: &'r Root,
queue: &'q mut Queue,
}
// compile supported drafts
impl ObjCompiler<'_, '_, '_, '_, '_, '_> {
fn compile_obj(&mut self, s: &mut Schema) -> Result<(), CompileError> {
self.compile_draft4(s)?;
if self.draft_version() >= 6 {
self.compile_draft6(s)?;
}
if self.draft_version() >= 7 {
self.compile_draft7(s)?;
}
if self.draft_version() >= 2019 {
self.compile_draft2019(s)?;
}
if self.draft_version() >= 2020 {
self.compile_draft2020(s)?;
}
Ok(())
}
fn compile_draft4(&mut self, s: &mut Schema) -> Result<(), CompileError> {
if self.has_vocab("core") {
s.ref_ = self.enqueue_ref("$ref")?;
if s.ref_.is_some() && self.draft_version() < 2019 {
// All other properties in a "$ref" object MUST be ignored
return Ok(());
}
}
if self.has_vocab("applicator") {
s.all_of = self.enqueue_arr("allOf");
s.any_of = self.enqueue_arr("anyOf");
s.one_of = self.enqueue_arr("oneOf");
s.not = self.enqueue_prop("not");
if self.draft_version() < 2020 {
match self.value("items") {
Some(Value::Array(_)) => {
s.items = Some(Items::SchemaRefs(self.enqueue_arr("items")));
s.additional_items = self.enquue_additional("additionalItems");
}
_ => s.items = self.enqueue_prop("items").map(Items::SchemaRef),
}
}
if let Some(Value::Object(props_obj)) = self.value("properties") {
let mut properties = AHashMap::with_capacity(props_obj.len());
for (pname, pvalue) in props_obj {
let ptr = self.up.ptr.append2("properties", pname);
let sch_idx = self.enqueue_schema(ptr);
properties.insert(pname.clone(), sch_idx);
if let Some(prop_schema_obj) = pvalue.as_object() {
if let Some(Value::Bool(true)) = prop_schema_obj.get("override") {
s.override_properties.insert(pname.clone());
}
}
}
s.properties = properties;
}
s.pattern_properties = {
let mut v = vec![];
if let Some(Value::Object(obj)) = self.value("patternProperties") {
for pname in obj.keys() {
let ecma =
ecma::convert(pname).map_err(|src| CompileError::InvalidRegex {
url: self.up.format("patternProperties"),
regex: pname.to_owned(),
src,
})?;
let regex =
Regex::new(ecma.as_ref()).map_err(|e| CompileError::InvalidRegex {
url: self.up.format("patternProperties"),
regex: ecma.into_owned(),
src: e.into(),
})?;
let ptr = self.up.ptr.append2("patternProperties", pname);
let sch = self.enqueue_schema(ptr);
v.push((regex, sch));
}
}
v
};
s.additional_properties = self.enquue_additional("additionalProperties");
if let Some(Value::Object(deps)) = self.value("dependencies") {
s.dependencies = deps
.iter()
.filter_map(|(k, v)| {
let v = match v {
Value::Array(_) => Some(Dependency::Props(to_strings(v))),
_ => {
let ptr = self.up.ptr.append2("dependencies", k);
Some(Dependency::SchemaRef(self.enqueue_schema(ptr)))
}
};
v.map(|v| (k.clone(), v))
})
.collect();
}
}
if self.has_vocab("validation") {
match self.value("type") {
Some(Value::String(t)) => {
if let Some(t) = Type::from_str(t) {
s.types.add(t)
}
}
Some(Value::Array(arr)) => {
for t in arr {
if let Value::String(t) = t {
if let Some(t) = Type::from_str(t) {
s.types.add(t)
}
}
}
}
_ => {}
}
if let Some(Value::Array(e)) = self.value("enum") {
let mut types = Types::default();
for item in e {
types.add(Type::of(item));
}
s.enum_ = Some(Enum {
types,
values: e.clone(),
});
}
s.multiple_of = self.num("multipleOf");
s.maximum = self.num("maximum");
if let Some(Value::Bool(exclusive)) = self.value("exclusiveMaximum") {
if *exclusive {
s.exclusive_maximum = s.maximum.take();
}
} else {
s.exclusive_maximum = self.num("exclusiveMaximum");
}
s.minimum = self.num("minimum");
if let Some(Value::Bool(exclusive)) = self.value("exclusiveMinimum") {
if *exclusive {
s.exclusive_minimum = s.minimum.take();
}
} else {
s.exclusive_minimum = self.num("exclusiveMinimum");
}
s.max_length = self.usize("maxLength");
s.min_length = self.usize("minLength");
if let Some(Value::String(p)) = self.value("pattern") {
let p = ecma::convert(p).map_err(CompileError::Bug)?;
s.pattern = Some(Regex::new(p.as_ref()).map_err(|e| CompileError::Bug(e.into()))?);
}
s.max_items = self.usize("maxItems");
s.min_items = self.usize("minItems");
s.unique_items = self.bool("uniqueItems");
s.max_properties = self.usize("maxProperties");
s.min_properties = self.usize("minProperties");
if let Some(req) = self.value("required") {
s.required = to_strings(req);
}
}
// format --
if self.c.assert_format
|| self.has_vocab(match self.draft_version().cmp(&2019) {
Ordering::Less => "core",
Ordering::Equal => "format",
Ordering::Greater => "format-assertion",
})
{
if let Some(Value::String(format)) = self.value("format") {
s.format = self
.c
.formats
.get(format.as_str())
.or_else(|| FORMATS.get(format.as_str()))
.cloned();
}
}
Ok(())
}
fn compile_draft6(&mut self, s: &mut Schema) -> Result<(), CompileError> {
if self.has_vocab("applicator") {
s.contains = self.enqueue_prop("contains");
s.property_names = self.enqueue_prop("propertyNames");
}
if self.has_vocab("validation") {
s.constant = self.value("const").cloned();
}
Ok(())
}
fn compile_draft7(&mut self, s: &mut Schema) -> Result<(), CompileError> {
if self.has_vocab("applicator") {
s.if_ = self.enqueue_prop("if");
if s.if_.is_some() {
if !self.bool_schema("if", false) {
s.then = self.enqueue_prop("then");
}
if !self.bool_schema("if", true) {
s.else_ = self.enqueue_prop("else");
}
}
}
if self.c.assert_content {
if let Some(Value::String(encoding)) = self.value("contentEncoding") {
s.content_encoding = self
.c
.decoders
.get(encoding.as_str())
.or_else(|| DECODERS.get(encoding.as_str()))
.cloned();
}
if let Some(Value::String(media_type)) = self.value("contentMediaType") {
s.content_media_type = self
.c
.media_types
.get(media_type.as_str())
.or_else(|| MEDIA_TYPES.get(media_type.as_str()))
.cloned();
}
}
Ok(())
}
fn compile_draft2019(&mut self, s: &mut Schema) -> Result<(), CompileError> {
if self.has_vocab("core") {
s.recursive_ref = self.enqueue_ref("$recursiveRef")?;
s.recursive_anchor = self.bool("$recursiveAnchor");
}
if self.has_vocab("validation") {
if s.contains.is_some() {
s.max_contains = self.usize("maxContains");
s.min_contains = self.usize("minContains");
}
if let Some(Value::Object(dep_req)) = self.value("dependentRequired") {
for (pname, pvalue) in dep_req {
s.dependent_required
.push((pname.clone(), to_strings(pvalue)));
}
}
}
if self.has_vocab("applicator") {
s.dependent_schemas = self.enqueue_map("dependentSchemas");
}
if self.has_vocab(match self.draft_version() {
2019 => "applicator",
_ => "unevaluated",
}) {
s.unevaluated_items = self.enqueue_prop("unevaluatedItems");
s.unevaluated_properties = self.enqueue_prop("unevaluatedProperties");
}
if self.c.assert_content
&& s.content_media_type
.map(|mt| mt.json_compatible)
.unwrap_or(false)
{
s.content_schema = self.enqueue_prop("contentSchema");
}
Ok(())
}
fn compile_draft2020(&mut self, s: &mut Schema) -> Result<(), CompileError> {
if self.has_vocab("core") {
if let Some(sch) = self.enqueue_ref("$dynamicRef")? {
if let Some(Value::String(dref)) = self.value("$dynamicRef") {
let Ok((_, frag)) = Fragment::split(dref) else {
let loc = self.up.format("$dynamicRef");
return Err(CompileError::ParseAnchorError { loc });
};
let anchor = match frag {
Fragment::Anchor(Anchor(s)) => Some(s),
Fragment::JsonPointer(_) => None,
};
s.dynamic_ref = Some(DynamicRef { sch, anchor });
}
};
if let Some(Value::String(anchor)) = self.value("$dynamicAnchor") {
s.dynamic_anchor = Some(anchor.to_owned());
}
}
if self.has_vocab("applicator") {
s.prefix_items = self.enqueue_arr("prefixItems");
s.items2020 = self.enqueue_prop("items");
}
Ok(())
}
}
// enqueue helpers
impl ObjCompiler<'_, '_, '_, '_, '_, '_> {
fn enqueue_schema(&mut self, ptr: JsonPointer) -> SchemaIndex {
let up = UrlPtr {
url: self.up.url.clone(),
ptr,
};
self.queue.enqueue_schema(self.schemas, up)
}
fn enqueue_prop(&mut self, pname: &'static str) -> Option<SchemaIndex> {
if self.obj.contains_key(pname) {
let ptr = self.up.ptr.append(pname);
Some(self.enqueue_schema(ptr))
} else {
None
}
}
fn enqueue_arr(&mut self, pname: &'static str) -> Vec<SchemaIndex> {
if let Some(Value::Array(arr)) = self.obj.get(pname) {
(0..arr.len())
.map(|i| {
let ptr = self.up.ptr.append2(pname, &i.to_string());
self.enqueue_schema(ptr)
})
.collect()
} else {
Vec::new()
}
}
fn enqueue_map<T>(&mut self, pname: &'static str) -> T
where
T: Default,
T: FromIterator<(String, SchemaIndex)>,
{
if let Some(Value::Object(obj)) = self.obj.get(pname) {
obj.keys()
.map(|k| {
let ptr = self.up.ptr.append2(pname, k);
(k.clone(), self.enqueue_schema(ptr))
})
.collect()
} else {
T::default()
}
}
fn enqueue_ref(&mut self, pname: &str) -> Result<Option<SchemaIndex>, CompileError> {
let Some(Value::String(ref_)) = self.obj.get(pname) else {
return Ok(None);
};
let base_url = self.root.base_url(&self.up.ptr);
let abs_ref = UrlFrag::join(base_url, ref_)?;
if let Some(resolved_ref) = self.root.resolve(&abs_ref)? {
// local ref
return Ok(Some(self.enqueue_schema(resolved_ref.ptr)));
}
// remote ref
let up = self.queue.resolve_anchor(abs_ref, &self.c.roots)?;
Ok(Some(self.queue.enqueue_schema(self.schemas, up)))
}
fn enquue_additional(&mut self, pname: &'static str) -> Option<Additional> {
if let Some(Value::Bool(b)) = self.obj.get(pname) {
Some(Additional::Bool(*b))
} else {
self.enqueue_prop(pname).map(Additional::SchemaRef)
}
}
}
// query helpers
impl<'v> ObjCompiler<'_, 'v, '_, '_, '_, '_> {
fn draft_version(&self) -> usize {
self.root.draft.version
}
fn has_vocab(&self, name: &str) -> bool {
self.root.has_vocab(name)
}
fn value(&self, pname: &str) -> Option<&'v Value> {
self.obj.get(pname)
}
fn bool(&self, pname: &str) -> bool {
matches!(self.obj.get(pname), Some(Value::Bool(true)))
}
fn usize(&self, pname: &str) -> Option<usize> {
let Some(Value::Number(n)) = self.obj.get(pname) else {
return None;
};
if n.is_u64() {
n.as_u64().map(|n| n as usize)
} else {
n.as_f64()
.filter(|n| n.is_sign_positive() && n.fract() == 0.0)
.map(|n| n as usize)
}
}
fn num(&self, pname: &str) -> Option<Number> {
if let Some(Value::Number(n)) = self.obj.get(pname) {
Some(n.clone())
} else {
None
}
}
fn bool_schema(&self, pname: &str, b: bool) -> bool {
if let Some(Value::Bool(v)) = self.obj.get(pname) {
return *v == b;
}
false
}
}
/// Error type for compilation failures.
#[derive(Debug)]
pub enum CompileError {
/// Error in parsing `url`.
ParseUrlError { url: String, src: Box<dyn Error> },
/// Failed loading `url`.
LoadUrlError { url: String, src: Box<dyn Error> },
/// no [`UrlLoader`] registered for the `url`
UnsupportedUrlScheme { url: String },
/// Error in parsing `$schema` url.
InvalidMetaSchemaUrl { url: String, src: Box<dyn Error> },
/// draft `url` is not supported
UnsupportedDraft { url: String },
/// Cycle in resolving `$schema` in `url`.
MetaSchemaCycle { url: String },
/// `url` is not valid against metaschema.
ValidationError {
url: String,
src: ValidationError<'static, 'static>,
},
/// Error in parsing id at `loc`
ParseIdError { loc: String },
/// Error in parsing anchor at `loc`
ParseAnchorError { loc: String },
/// Duplicate id `id` in `url` at `ptr1` and `ptr2`.
DuplicateId {
url: String,
id: String,
ptr1: String,
ptr2: String,
},
/// Duplicate anchor `anchor` in `url` at `ptr1` and `ptr2`.
DuplicateAnchor {
anchor: String,
url: String,
ptr1: String,
ptr2: String,
},
/// Not a valid json pointer.
InvalidJsonPointer(String),
/// JsonPointer evaluated to nothing.
JsonPointerNotFound(String),
/// anchor in `reference` not found in `url`.
AnchorNotFound { url: String, reference: String },
/// Unsupported vocabulary `vocabulary` in `url`.
UnsupportedVocabulary { url: String, vocabulary: String },
/// Invalid Regex `regex` at `url`.
InvalidRegex {
url: String,
regex: String,
src: Box<dyn Error>,
},
/// Encountered bug in compiler implementation. Please report
/// this as an issue for this crate.
Bug(Box<dyn Error>),
}
impl Error for CompileError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::ParseUrlError { src, .. } => Some(src.as_ref()),
Self::LoadUrlError { src, .. } => Some(src.as_ref()),
Self::InvalidMetaSchemaUrl { src, .. } => Some(src.as_ref()),
Self::ValidationError { src, .. } => Some(src),
Self::Bug(src) => Some(src.as_ref()),
_ => None,
}
}
}
impl Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseUrlError { url, src } => {
if f.alternate() {
write!(f, "error parsing url {url}: {src}")
} else {
write!(f, "error parsing {url}")
}
}
Self::LoadUrlError { url, src } => {
if f.alternate() {
write!(f, "error loading {url}: {src}")
} else {
write!(f, "error loading {url}")
}
}
Self::UnsupportedUrlScheme { url } => write!(f, "unsupported scheme in {url}"),
Self::InvalidMetaSchemaUrl { url, src } => {
if f.alternate() {
write!(f, "invalid $schema in {url}: {src}")
} else {
write!(f, "invalid $schema in {url}")
}
}
Self::UnsupportedDraft { url } => write!(f, "draft {url} is not supported"),
Self::MetaSchemaCycle { url } => {
write!(f, "cycle in resolving $schema in {url}")
}
Self::ValidationError { url, src } => {
if f.alternate() {
write!(f, "{url} is not valid against metaschema: {src}")
} else {
write!(f, "{url} is not valid against metaschema")
}
}
Self::ParseIdError { loc } => write!(f, "error in parsing id at {loc}"),
Self::ParseAnchorError { loc } => write!(f, "error in parsing anchor at {loc}"),
Self::DuplicateId {
url,
id,
ptr1,
ptr2,
} => write!(f, "duplicate $id {id} in {url} at {ptr1:?} and {ptr2:?}"),
Self::DuplicateAnchor {
anchor,
url,
ptr1,
ptr2,
} => {
write!(
f,
"duplicate anchor {anchor:?} in {url} at {ptr1:?} and {ptr2:?}"
)
}
Self::InvalidJsonPointer(loc) => write!(f, "invalid json-pointer {loc}"),
Self::JsonPointerNotFound(loc) => write!(f, "json-pointer in {loc} not found"),
Self::AnchorNotFound { url, reference } => {
write!(
f,
"anchor in reference {reference} is not found in schema {url}"
)
}
Self::UnsupportedVocabulary { url, vocabulary } => {
write!(f, "unsupported vocabulary {vocabulary} in {url}")
}
Self::InvalidRegex { url, regex, src } => {
if f.alternate() {
write!(f, "invalid regex {} at {url}: {src}", quote(regex))
} else {
write!(f, "invalid regex {} at {url}", quote(regex))
}
}
Self::Bug(src) => {
write!(
f,
"encountered bug in jsonschema compiler. please report: {src}"
)
}
}
}
}
// helpers --
fn to_strings(v: &Value) -> Vec<String> {
if let Value::Array(a) = v {
a.iter()
.filter_map(|t| {
if let Value::String(t) = t {
Some(t.clone())
} else {
None
}
})
.collect()
} else {
vec![]
}
}
pub(crate) struct Queue {
pub(crate) schemas: Vec<UrlPtr>,
pub(crate) roots: HashMap<Url, Root>,
}
impl Queue {
fn new() -> Self {
Self {
schemas: vec![],
roots: HashMap::new(),
}
}
pub(crate) fn resolve_anchor(
&mut self,
uf: UrlFrag,
roots: &Roots,
) -> Result<UrlPtr, CompileError> {
match uf.frag {
Fragment::JsonPointer(ptr) => Ok(UrlPtr { url: uf.url, ptr }),
Fragment::Anchor(_) => {
let root = match roots.get(&uf.url).or_else(|| self.roots.get(&uf.url)) {
Some(root) => root,
None => {
let doc = roots.loader.load(&uf.url)?;
let r = roots.create_root(uf.url.clone(), doc)?;
self.roots.entry(uf.url).or_insert(r)
}
};
root.resolve_fragment(&uf.frag)
}
}
}
pub(crate) fn enqueue_schema(&mut self, schemas: &Schemas, up: UrlPtr) -> SchemaIndex {
if let Some(sch) = schemas.get_by_loc(&up) {
// already got compiled
return sch.idx;
}
if let Some(qindex) = self.schemas.iter().position(|e| *e == up) {
// already queued for compilation
return SchemaIndex(schemas.size() + qindex);
}
// new compilation request
self.schemas.push(up);
SchemaIndex(schemas.size() + self.schemas.len() - 1)
}
}

View File

@ -1,82 +0,0 @@
use std::{collections::HashMap, error::Error};
use base64::Engine;
use once_cell::sync::Lazy;
use serde::de::IgnoredAny;
use serde_json::Value;
// decoders --
/// Defines Decoder for `contentEncoding`.
#[derive(Clone, Copy)]
pub struct Decoder {
/// Name of the encoding
pub name: &'static str,
/// Decodes given string to bytes
#[allow(clippy::type_complexity)]
pub func: fn(s: &str) -> Result<Vec<u8>, Box<dyn Error>>,
}
pub(crate) static DECODERS: Lazy<HashMap<&'static str, Decoder>> = Lazy::new(|| {
let mut m = HashMap::<&'static str, Decoder>::new();
m.insert(
"base64",
Decoder {
name: "base64",
func: decode_base64,
},
);
m
});
fn decode_base64(s: &str) -> Result<Vec<u8>, Box<dyn Error>> {
Ok(base64::engine::general_purpose::STANDARD.decode(s)?)
}
// mediatypes --
/// Defines Mediatype for `contentMediaType`.
#[derive(Clone, Copy)]
pub struct MediaType {
/// Name of this media-type as defined in RFC 2046.
/// Example: `application/json`
pub name: &'static str,
/// whether this media type can be deserialized to json. If so it can
/// be validated by `contentSchema` keyword.
pub json_compatible: bool,
/**
Check whether `bytes` conforms to this media-type.
Should return `Ok(Some(Value))` if `deserialize` is `true`, otherwise it can return `Ok(None)`.
Ideally you could deserialize to `serde::de::IgnoredAny` if `deserialize` is `false` to gain
some performance.
`deserialize` is always `false` if `json_compatible` is `false`.
*/
#[allow(clippy::type_complexity)]
pub func: fn(bytes: &[u8], deserialize: bool) -> Result<Option<Value>, Box<dyn Error>>,
}
pub(crate) static MEDIA_TYPES: Lazy<HashMap<&'static str, MediaType>> = Lazy::new(|| {
let mut m = HashMap::<&'static str, MediaType>::new();
m.insert(
"application/json",
MediaType {
name: "application/json",
json_compatible: true,
func: check_json,
},
);
m
});
fn check_json(bytes: &[u8], deserialize: bool) -> Result<Option<Value>, Box<dyn Error>> {
if deserialize {
return Ok(Some(serde_json::from_slice(bytes)?));
}
serde_json::from_slice::<IgnoredAny>(bytes)?;
Ok(None)
}

View File

@ -1,576 +0,0 @@
use std::{
collections::{hash_map::Entry, HashMap},
str::FromStr,
};
use once_cell::sync::Lazy;
use serde_json::{Map, Value};
use url::Url;
use crate::{compiler::*, root::Resource, util::*, SchemaIndex, Schemas};
const POS_SELF: u8 = 1 << 0;
const POS_PROP: u8 = 1 << 1;
const POS_ITEM: u8 = 1 << 2;
pub(crate) static DRAFT4: Lazy<Draft> = Lazy::new(|| Draft {
version: 4,
id: "id",
url: "http://json-schema.org/draft-04/schema",
subschemas: HashMap::from([
// type agnostic
("definitions", POS_PROP),
("not", POS_SELF),
("allOf", POS_ITEM),
("anyOf", POS_ITEM),
("oneOf", POS_ITEM),
// object
("properties", POS_PROP),
("additionalProperties", POS_SELF),
("patternProperties", POS_PROP),
// array
("items", POS_SELF | POS_ITEM),
("additionalItems", POS_SELF),
("dependencies", POS_PROP),
]),
vocab_prefix: "",
all_vocabs: vec![],
default_vocabs: vec![],
});
pub(crate) static DRAFT6: Lazy<Draft> = Lazy::new(|| {
let mut subschemas = DRAFT4.subschemas.clone();
subschemas.extend([("propertyNames", POS_SELF), ("contains", POS_SELF)]);
Draft {
version: 6,
id: "$id",
url: "http://json-schema.org/draft-06/schema",
subschemas,
vocab_prefix: "",
all_vocabs: vec![],
default_vocabs: vec![],
}
});
pub(crate) static DRAFT7: Lazy<Draft> = Lazy::new(|| {
let mut subschemas = DRAFT6.subschemas.clone();
subschemas.extend([("if", POS_SELF), ("then", POS_SELF), ("else", POS_SELF)]);
Draft {
version: 7,
id: "$id",
url: "http://json-schema.org/draft-07/schema",
subschemas,
vocab_prefix: "",
all_vocabs: vec![],
default_vocabs: vec![],
}
});
pub(crate) static DRAFT2019: Lazy<Draft> = Lazy::new(|| {
let mut subschemas = DRAFT7.subschemas.clone();
subschemas.extend([
("$defs", POS_PROP),
("dependentSchemas", POS_PROP),
("unevaluatedProperties", POS_SELF),
("unevaluatedItems", POS_SELF),
("contentSchema", POS_SELF),
]);
Draft {
version: 2019,
id: "$id",
url: "https://json-schema.org/draft/2019-09/schema",
subschemas,
vocab_prefix: "https://json-schema.org/draft/2019-09/vocab/",
all_vocabs: vec![
"core",
"applicator",
"validation",
"meta-data",
"format",
"content",
],
default_vocabs: vec!["core", "applicator", "validation"],
}
});
pub(crate) static DRAFT2020: Lazy<Draft> = Lazy::new(|| {
let mut subschemas = DRAFT2019.subschemas.clone();
subschemas.extend([("prefixItems", POS_ITEM)]);
Draft {
version: 2020,
id: "$id",
url: "https://json-schema.org/draft/2020-12/schema",
subschemas,
vocab_prefix: "https://json-schema.org/draft/2020-12/vocab/",
all_vocabs: vec![
"core",
"applicator",
"unevaluated",
"validation",
"meta-data",
"format-annotation",
"format-assertion",
"content",
],
default_vocabs: vec!["core", "applicator", "unevaluated", "validation"],
}
});
pub(crate) static STD_METASCHEMAS: Lazy<Schemas> =
Lazy::new(|| load_std_metaschemas().expect("std metaschemas must be compilable"));
pub(crate) fn latest() -> &'static Draft {
crate::Draft::default().internal()
}
// --
pub(crate) struct Draft {
pub(crate) version: usize,
pub(crate) url: &'static str,
id: &'static str, // property name used to represent id
subschemas: HashMap<&'static str, u8>, // location of subschemas
pub(crate) vocab_prefix: &'static str, // prefix used for vocabulary
pub(crate) all_vocabs: Vec<&'static str>, // names of supported vocabs
pub(crate) default_vocabs: Vec<&'static str>, // names of default vocabs
}
impl Draft {
pub(crate) fn from_url(url: &str) -> Option<&'static Draft> {
let (mut url, frag) = split(url);
if !frag.is_empty() {
return None;
}
if let Some(s) = url.strip_prefix("http://") {
url = s;
}
if let Some(s) = url.strip_prefix("https://") {
url = s;
}
match url {
"json-schema.org/schema" => Some(latest()),
"json-schema.org/draft/2020-12/schema" => Some(&DRAFT2020),
"json-schema.org/draft/2019-09/schema" => Some(&DRAFT2019),
"json-schema.org/draft-07/schema" => Some(&DRAFT7),
"json-schema.org/draft-06/schema" => Some(&DRAFT6),
"json-schema.org/draft-04/schema" => Some(&DRAFT4),
_ => None,
}
}
fn get_schema(&self) -> Option<SchemaIndex> {
let url = match self.version {
2020 => "https://json-schema.org/draft/2020-12/schema",
2019 => "https://json-schema.org/draft/2019-09/schema",
7 => "http://json-schema.org/draft-07/schema",
6 => "http://json-schema.org/draft-06/schema",
4 => "http://json-schema.org/draft-04/schema",
_ => return None,
};
let up = UrlPtr {
url: Url::parse(url).unwrap_or_else(|_| panic!("{url} should be valid url")),
ptr: "".into(),
};
STD_METASCHEMAS.get_by_loc(&up).map(|s| s.idx)
}
pub(crate) fn validate(&self, up: &UrlPtr, v: &Value) -> Result<(), CompileError> {
let Some(sch) = self.get_schema() else {
return Err(CompileError::Bug(
format!("no metaschema preloaded for draft {}", self.version).into(),
));
};
STD_METASCHEMAS
.validate(v, sch, None)
.map_err(|src| CompileError::ValidationError {
url: up.to_string(),
src: src.clone_static(),
})
}
fn get_id<'a>(&self, obj: &'a Map<String, Value>) -> Option<&'a str> {
if self.version < 2019 && obj.contains_key("$ref") {
return None; // All other properties in a "$ref" object MUST be ignored
}
let Some(Value::String(id)) = obj.get(self.id) else {
return None;
};
let (id, _) = split(id); // ignore fragment
Some(id).filter(|id| !id.is_empty())
}
pub(crate) fn get_vocabs(
&self,
url: &Url,
doc: &Value,
) -> Result<Option<Vec<String>>, CompileError> {
if self.version < 2019 {
return Ok(None);
}
let Value::Object(obj) = doc else {
return Ok(None);
};
let Some(Value::Object(obj)) = obj.get("$vocabulary") else {
return Ok(None);
};
let mut vocabs = vec![];
for (vocab, reqd) in obj {
if let Value::Bool(true) = reqd {
let name = vocab
.strip_prefix(self.vocab_prefix)
.filter(|name| self.all_vocabs.contains(name));
if let Some(name) = name {
vocabs.push(name.to_owned()); // todo: avoid alloc
} else {
return Err(CompileError::UnsupportedVocabulary {
url: url.as_str().to_owned(),
vocabulary: vocab.to_owned(),
});
}
}
}
Ok(Some(vocabs))
}
// collects anchors/dynamic_achors from `sch` into `res`.
// note this does not collect from subschemas in sch.
pub(crate) fn collect_anchors(
&self,
sch: &Value,
sch_ptr: &JsonPointer,
res: &mut Resource,
url: &Url,
) -> Result<(), CompileError> {
let Value::Object(obj) = sch else {
return Ok(());
};
let mut add_anchor = |anchor: Anchor| match res.anchors.entry(anchor) {
Entry::Occupied(entry) => {
if entry.get() == sch_ptr {
// anchor with same root_ptr already exists
return Ok(());
}
Err(CompileError::DuplicateAnchor {
url: url.as_str().to_owned(),
anchor: entry.key().to_string(),
ptr1: entry.get().to_string(),
ptr2: sch_ptr.to_string(),
})
}
entry => {
entry.or_insert(sch_ptr.to_owned());
Ok(())
}
};
if self.version < 2019 {
if obj.contains_key("$ref") {
return Ok(()); // All other properties in a "$ref" object MUST be ignored
}
// anchor is specified in id
if let Some(Value::String(id)) = obj.get(self.id) {
let Ok((_, frag)) = Fragment::split(id) else {
let loc = UrlFrag::format(url, sch_ptr.as_str());
return Err(CompileError::ParseAnchorError { loc });
};
if let Fragment::Anchor(anchor) = frag {
add_anchor(anchor)?;
};
return Ok(());
}
}
if self.version >= 2019 {
if let Some(Value::String(anchor)) = obj.get("$anchor") {
add_anchor(anchor.as_str().into())?;
}
}
if self.version >= 2020 {
if let Some(Value::String(anchor)) = obj.get("$dynamicAnchor") {
add_anchor(anchor.as_str().into())?;
res.dynamic_anchors.insert(anchor.as_str().into());
}
}
Ok(())
}
// error is json-ptr to invalid id
pub(crate) fn collect_resources(
&self,
sch: &Value,
base: &Url, // base of json
sch_ptr: JsonPointer, // ptr of json
url: &Url,
resources: &mut HashMap<JsonPointer, Resource>,
) -> Result<(), CompileError> {
if resources.contains_key(&sch_ptr) {
// resources are already collected
return Ok(());
}
if let Value::Bool(_) = sch {
if sch_ptr.is_empty() {
// root resource
resources.insert(sch_ptr.clone(), Resource::new(sch_ptr, base.clone()));
}
return Ok(());
}
let Value::Object(obj) = sch else {
return Ok(());
};
let mut base = base;
let tmp;
let res = if let Some(id) = self.get_id(obj) {
let Ok(id) = UrlFrag::join(base, id) else {
let loc = UrlFrag::format(url, sch_ptr.as_str());
return Err(CompileError::ParseIdError { loc });
};
tmp = id.url;
base = &tmp;
Some(Resource::new(sch_ptr.clone(), base.clone()))
} else if sch_ptr.is_empty() {
// root resource
Some(Resource::new(sch_ptr.clone(), base.clone()))
} else {
None
};
if let Some(res) = res {
if let Some(dup) = resources.values_mut().find(|res| res.id == *base) {
return Err(CompileError::DuplicateId {
url: url.to_string(),
id: base.to_string(),
ptr1: res.ptr.to_string(),
ptr2: dup.ptr.to_string(),
});
}
resources.insert(sch_ptr.clone(), res);
}
// collect anchors into base resource
if let Some(res) = resources.values_mut().find(|res| res.id == *base) {
self.collect_anchors(sch, &sch_ptr, res, url)?;
} else {
debug_assert!(false, "base resource must exist");
}
for (&kw, &pos) in &self.subschemas {
let Some(v) = obj.get(kw) else {
continue;
};
if pos & POS_SELF != 0 {
let ptr = sch_ptr.append(kw);
self.collect_resources(v, base, ptr, url, resources)?;
}
if pos & POS_ITEM != 0 {
if let Value::Array(arr) = v {
for (i, item) in arr.iter().enumerate() {
let ptr = sch_ptr.append2(kw, &i.to_string());
self.collect_resources(item, base, ptr, url, resources)?;
}
}
}
if pos & POS_PROP != 0 {
if let Value::Object(obj) = v {
for (pname, pvalue) in obj {
let ptr = sch_ptr.append2(kw, pname);
self.collect_resources(pvalue, base, ptr, url, resources)?;
}
}
}
}
Ok(())
}
pub(crate) fn is_subschema(&self, ptr: &str) -> bool {
if ptr.is_empty() {
return true;
}
fn split(mut ptr: &str) -> (&str, &str) {
ptr = &ptr[1..]; // rm `/` prefix
if let Some(i) = ptr.find('/') {
(&ptr[..i], &ptr[i..])
} else {
(ptr, "")
}
}
let (tok, ptr) = split(ptr);
if let Some(&pos) = self.subschemas.get(tok) {
if pos & POS_SELF != 0 && self.is_subschema(ptr) {
return true;
}
if !ptr.is_empty() {
if pos & POS_PROP != 0 {
let (_, ptr) = split(ptr);
if self.is_subschema(ptr) {
return true;
}
}
if pos & POS_ITEM != 0 {
let (tok, ptr) = split(ptr);
if usize::from_str(tok).is_ok() && self.is_subschema(ptr) {
return true;
}
}
}
}
false
}
}
fn load_std_metaschemas() -> Result<Schemas, CompileError> {
let mut schemas = Schemas::new();
let mut compiler = Compiler::new();
compiler.enable_format_assertions();
compiler.compile("https://json-schema.org/draft/2020-12/schema", &mut schemas)?;
compiler.compile("https://json-schema.org/draft/2019-09/schema", &mut schemas)?;
compiler.compile("http://json-schema.org/draft-07/schema", &mut schemas)?;
compiler.compile("http://json-schema.org/draft-06/schema", &mut schemas)?;
compiler.compile("http://json-schema.org/draft-04/schema", &mut schemas)?;
Ok(schemas)
}
#[cfg(test)]
mod tests {
use crate::{Compiler, Schemas};
use super::*;
#[test]
fn test_meta() {
let mut schemas = Schemas::default();
let mut compiler = Compiler::default();
let v: Value = serde_json::from_str(include_str!("metaschemas/draft-04/schema")).unwrap();
let url = "https://json-schema.org/draft-04/schema";
compiler.add_resource(url, v).unwrap();
compiler.compile(url, &mut schemas).unwrap();
}
#[test]
fn test_from_url() {
let tests = [
("http://json-schema.org/draft/2020-12/schema", Some(2020)), // http url
("https://json-schema.org/draft/2020-12/schema", Some(2020)), // https url
("https://json-schema.org/schema", Some(latest().version)), // latest
("https://json-schema.org/draft-04/schema", Some(4)),
];
for (url, version) in tests {
let got = Draft::from_url(url).map(|d| d.version);
assert_eq!(got, version, "for {url}");
}
}
#[test]
fn test_collect_ids() {
let url = Url::parse("http://a.com/schema.json").unwrap();
let json: Value = serde_json::from_str(
r#"{
"id": "http://a.com/schemas/schema.json",
"definitions": {
"s1": { "id": "http://a.com/definitions/s1" },
"s2": {
"id": "../s2",
"items": [
{ "id": "http://c.com/item" },
{ "id": "http://d.com/item" }
]
},
"s3": {
"definitions": {
"s1": {
"id": "s3",
"items": {
"id": "http://b.com/item"
}
}
}
},
"s4": { "id": "http://e.com/def#abcd" }
}
}"#,
)
.unwrap();
let want = {
let mut m = HashMap::new();
m.insert("", "http://a.com/schemas/schema.json"); // root with id
m.insert("/definitions/s1", "http://a.com/definitions/s1");
m.insert("/definitions/s2", "http://a.com/s2"); // relative id
m.insert("/definitions/s3/definitions/s1", "http://a.com/schemas/s3");
m.insert("/definitions/s3/definitions/s1/items", "http://b.com/item");
m.insert("/definitions/s2/items/0", "http://c.com/item");
m.insert("/definitions/s2/items/1", "http://d.com/item");
m.insert("/definitions/s4", "http://e.com/def"); // id with fragments
m
};
let mut got = HashMap::new();
DRAFT4
.collect_resources(&json, &url, "".into(), &url, &mut got)
.unwrap();
let got = got
.iter()
.map(|(k, v)| (k.as_str(), v.id.as_str()))
.collect::<HashMap<&str, &str>>();
assert_eq!(got, want);
}
#[test]
fn test_collect_anchors() {
let url = Url::parse("http://a.com/schema.json").unwrap();
let json: Value = serde_json::from_str(
r#"{
"$defs": {
"s2": {
"$id": "http://b.com",
"$anchor": "b1",
"items": [
{ "$anchor": "b2" },
{
"$id": "http//c.com",
"items": [
{"$anchor": "c1"},
{"$dynamicAnchor": "c2"}
]
},
{ "$dynamicAnchor": "b3" }
]
}
}
}"#,
)
.unwrap();
let mut resources = HashMap::new();
DRAFT2020
.collect_resources(&json, &url, "".into(), &url, &mut resources)
.unwrap();
assert!(resources.get("").unwrap().anchors.is_empty());
assert_eq!(resources.get("/$defs/s2").unwrap().anchors, {
let mut want = HashMap::new();
want.insert("b1".into(), "/$defs/s2".into());
want.insert("b2".into(), "/$defs/s2/items/0".into());
want.insert("b3".into(), "/$defs/s2/items/2".into());
want
});
assert_eq!(resources.get("/$defs/s2/items/1").unwrap().anchors, {
let mut want = HashMap::new();
want.insert("c1".into(), "/$defs/s2/items/1/items/0".into());
want.insert("c2".into(), "/$defs/s2/items/1/items/1".into());
want
});
}
#[test]
fn test_is_subschema() {
let tests = vec![("/allOf/0", true), ("/allOf/$defs", false)];
for test in tests {
let got = DRAFT2020.is_subschema(test.0);
assert_eq!(got, test.1, "{}", test.0);
}
}
}

View File

@ -1,197 +0,0 @@
use std::borrow::Cow;
use regex_syntax::ast::parse::Parser;
use regex_syntax::ast::{self, *};
// covert ecma regex to rust regex if possible
// see https://262.ecma-international.org/11.0/#sec-regexp-regular-expression-objects
pub(crate) fn convert(pattern: &str) -> Result<Cow<'_, str>, Box<dyn std::error::Error>> {
let mut pattern = Cow::Borrowed(pattern);
let mut ast = loop {
match Parser::new().parse(pattern.as_ref()) {
Ok(ast) => break ast,
Err(e) => {
if let Some(s) = fix_error(&e) {
pattern = Cow::Owned(s);
} else {
Err(e)?;
}
}
}
};
loop {
let translator = Translator {
pat: pattern.as_ref(),
out: None,
};
if let Some(updated_pattern) = ast::visit(&ast, translator)? {
match Parser::new().parse(&updated_pattern) {
Ok(updated_ast) => {
pattern = Cow::Owned(updated_pattern);
ast = updated_ast;
}
Err(e) => {
debug_assert!(
false,
"ecma::translate changed {:?} to {:?}: {e}",
pattern, updated_pattern
);
break;
}
}
} else {
break;
}
}
Ok(pattern)
}
fn fix_error(e: &Error) -> Option<String> {
if let ErrorKind::EscapeUnrecognized = e.kind() {
let (start, end) = (e.span().start.offset, e.span().end.offset);
let s = &e.pattern()[start..end];
if let r"\c" = s {
// handle \c{control_letter}
if let Some(control_letter) = e.pattern()[end..].chars().next() {
if control_letter.is_ascii_alphabetic() {
return Some(format!(
"{}{}{}",
&e.pattern()[..start],
((control_letter as u8) % 32) as char,
&e.pattern()[end + 1..],
));
}
}
}
}
None
}
/**
handles following translations:
- \d should ascii digits only. so replace with [0-9]
- \D should match everything but ascii digits. so replace with [^0-9]
- \w should match ascii letters only. so replace with [a-zA-Z0-9_]
- \W should match everything but ascii letters. so replace with [^a-zA-Z0-9_]
- \s and \S differences
- \a is not an ECMA 262 control escape
*/
struct Translator<'a> {
pat: &'a str,
out: Option<String>,
}
impl Translator<'_> {
fn replace(&mut self, span: &Span, with: &str) {
let (start, end) = (span.start.offset, span.end.offset);
self.out = Some(format!("{}{with}{}", &self.pat[..start], &self.pat[end..]));
}
fn replace_class_class(&mut self, perl: &ClassPerl) {
match perl.kind {
ClassPerlKind::Digit => {
self.replace(&perl.span, if perl.negated { "[^0-9]" } else { "[0-9]" });
}
ClassPerlKind::Word => {
let with = &if perl.negated {
"[^A-Za-z0-9_]"
} else {
"[A-Za-z0-9_]"
};
self.replace(&perl.span, with);
}
ClassPerlKind::Space => {
let with = &if perl.negated {
"[^ \t\n\r\u{000b}\u{000c}\u{00a0}\u{feff}\u{2003}\u{2029}]"
} else {
"[ \t\n\r\u{000b}\u{000c}\u{00a0}\u{feff}\u{2003}\u{2029}]"
};
self.replace(&perl.span, with);
}
}
}
}
impl Visitor for Translator<'_> {
type Output = Option<String>;
type Err = &'static str;
fn finish(self) -> Result<Self::Output, Self::Err> {
Ok(self.out)
}
fn visit_class_set_item_pre(&mut self, ast: &ast::ClassSetItem) -> Result<(), Self::Err> {
if let ClassSetItem::Perl(perl) = ast {
self.replace_class_class(perl);
}
Ok(())
}
fn visit_post(&mut self, ast: &Ast) -> Result<(), Self::Err> {
if self.out.is_some() {
return Ok(());
}
match ast {
Ast::ClassPerl(perl) => {
self.replace_class_class(perl);
}
Ast::Literal(literal) => {
if let Literal {
kind: LiteralKind::Special(SpecialLiteralKind::Bell),
..
} = literal.as_ref()
{
return Err("\\a is not an ECMA 262 control escape");
}
}
_ => (),
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecma_compat_valid() {
// println!("{:#?}", Parser::new().parse(r#"a\a"#));
let tests = [
(r"ab\cAcde\cBfg", "ab\u{1}cde\u{2}fg"), // \c{control_letter}
(r"\\comment", r"\\comment"), // there is no \c
(r"ab\def", r#"ab[0-9]ef"#), // \d
(r"ab[a-z\d]ef", r#"ab[a-z[0-9]]ef"#), // \d inside classSet
(r"ab\Def", r#"ab[^0-9]ef"#), // \d
(r"ab[a-z\D]ef", r#"ab[a-z[^0-9]]ef"#), // \D inside classSet
];
for (input, want) in tests {
match convert(input) {
Ok(got) => {
if got.as_ref() != want {
panic!("convert({input:?}): got: {got:?}, want: {want:?}");
}
}
Err(e) => {
panic!("convert({input:?}) failed: {e}");
}
}
}
}
#[test]
fn test_ecma_compat_invalid() {
// println!("{:#?}", Parser::new().parse(r#"a\a"#));
let tests = [
r"\c\n", // \c{invalid_char}
r"abc\adef", // \a is not valid
];
for input in tests {
if convert(input).is_ok() {
panic!("convert({input:?}) mut fail");
}
}
}
}

View File

@ -1,838 +0,0 @@
use std::{
collections::HashMap,
error::Error,
net::{Ipv4Addr, Ipv6Addr},
};
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use serde_json::Value;
use url::Url;
use crate::ecma;
/// 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>>,
}
pub(crate) static FORMATS: Lazy<HashMap<&'static str, Format>> = Lazy::new(|| {
let mut m = HashMap::<&'static str, Format>::new();
let mut register = |name, func| m.insert(name, Format { name, func });
register("regex", validate_regex);
register("ipv4", validate_ipv4);
register("ipv6", validate_ipv6);
register("hostname", validate_hostname);
register("idn-hostname", validate_idn_hostname);
register("email", validate_email);
register("idn-email", validate_idn_email);
register("date", validate_date);
register("time", validate_time);
register("date-time", validate_date_time);
register("duration", validate_duration);
register("period", validate_period);
register("json-pointer", validate_json_pointer);
register("relative-json-pointer", validate_relative_json_pointer);
register("uuid", validate_uuid);
register("uri", validate_uri);
register("iri", validate_iri);
register("uri-reference", validate_uri_reference);
register("iri-reference", validate_iri_reference);
register("uri-template", validate_uri_template);
m
});
fn validate_regex(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
ecma::convert(s).map(|_| ())
}
fn validate_ipv4(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv4Addr>()?;
Ok(())
}
fn validate_ipv6(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
s.parse::<Ipv6Addr>()?;
Ok(())
}
fn validate_date(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
check_date(s)
}
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>> {
// 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>> {
let Value::String(s) = v else {
return Ok(());
};
check_time(s)
}
fn check_time(mut str: &str) -> Result<(), Box<dyn Error>> {
// 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>> {
let Value::String(s) = v else {
return Ok(());
};
check_date_time(s)
}
fn check_date_time(s: &str) -> Result<(), Box<dyn Error>> {
// 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>> {
let Value::String(s) = v else {
return Ok(());
};
check_duration(s)
}
// see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
fn check_duration(s: &str) -> Result<(), Box<dyn Error>> {
// 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>> {
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>> {
let Value::String(s) = v else {
return Ok(());
};
check_hostname(s)
}
// see https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
fn check_hostname(mut s: &str) -> Result<(), Box<dyn Error>> {
// entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters
s = s.strip_suffix('.').unwrap_or(s);
if s.len() > 253 {
Err("more than 253 characters long")?
}
// 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:?}"))?;
}
}
Ok(())
}
fn validate_idn_hostname(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
check_idn_hostname(s)
}
fn check_idn_hostname(s: &str) -> Result<(), Box<dyn Error>> {
let s = idna::domain_to_ascii_strict(s)?;
let unicode = idna::domain_to_unicode(&s).0;
// see https://www.rfc-editor.org/rfc/rfc5892#section-2.6
{
static DISALLOWED: [char; 10] = [
'\u{0640}', // ARABIC TATWEEL
'\u{07FA}', // NKO LAJANYALAN
'\u{302E}', // HANGUL SINGLE DOT TONE MARK
'\u{302F}', // HANGUL DOUBLE DOT TONE MARK
'\u{3031}', // VERTICAL KANA REPEAT MARK
'\u{3032}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK
'\u{3033}', // VERTICAL KANA REPEAT MARK UPPER HALF
'\u{3034}', // VERTICAL KANA REPEAT WITH VOICED SOUND MARK UPPER HA
'\u{3035}', // VERTICAL KANA REPEAT MARK LOWER HALF
'\u{303B}', // VERTICAL IDEOGRAPHIC ITERATION MARK
];
if unicode.contains(DISALLOWED) {
Err("contains disallowed character")?;
}
}
// 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.as_str();
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.as_str();
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.as_str();
while let Some(i) = s.find(ch) {
let prefix = &s[..i];
let suffix = &s[i + ch.len_utf8()..];
if !prefix.ends_with(|c| hebrew.contains(&c)) {
if i == 0 {
Err("Hebrew GERESH must be preceded by Hebrew character")?;
} else {
Err("Hebrew GERESHYIM must be preceded by Hebrew character")?;
}
}
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}';
let hiragana = '\u{3040}'..='\u{309F}';
let katakana = '\u{30A0}'..='\u{30FF}';
let han = '\u{4E00}'..='\u{9FFF}'; // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block): is this range correct??
if unicode.contains(katakana_middle_dot) {
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.as_str();
while let Some(i) = s.find(zero_width_jointer) {
let prefix = &s[..i];
let suffix = &s[i + zero_width_jointer.len_utf8()..];
if !prefix.ends_with(VIRAMA) {
Err("ZERO WIDTH JOINER must be preceded by Virama")?;
}
s = suffix;
}
}
check_hostname(&s)
}
fn validate_email(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
check_email(s)
}
// see https://en.wikipedia.org/wiki/Email_address
fn check_email(s: &str) -> Result<(), Box<dyn Error>> {
// 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>> {
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)?;
let domain = idna::domain_to_ascii_strict(domain)?;
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>> {
let Value::String(s) = v else {
return Ok(());
};
check_json_pointer(s)
}
// see https://www.rfc-editor.org/rfc/rfc6901#section-3
fn check_json_pointer(s: &str) -> Result<(), Box<dyn Error>> {
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>> {
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>> {
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>> {
let Value::String(s) = v else {
return Ok(());
};
if fluent_uri::UriRef::parse(s.as_str())?.scheme().is_none() {
Err("relative url")?;
};
Ok(())
}
fn validate_iri(v: &Value) -> Result<(), Box<dyn Error>> {
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)?,
}
}
static TEMP_URL: Lazy<Url> = Lazy::new(|| Url::parse("http://temp.com").unwrap());
fn parse_uri_reference(s: &str) -> Result<Url, Box<dyn Error>> {
if s.contains('\\') {
Err("contains \\\\")?;
}
Ok(TEMP_URL.join(s)?)
}
fn validate_uri_reference(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
fluent_uri::UriRef::parse(s.as_str())?;
Ok(())
}
fn validate_iri_reference(v: &Value) -> Result<(), Box<dyn Error>> {
let Value::String(s) = v else {
return Ok(());
};
parse_uri_reference(s)?;
Ok(())
}
fn validate_uri_template(v: &Value) -> Result<(), Box<dyn Error>> {
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(())
}

View File

@ -1,725 +0,0 @@
/*! This crate supports JsonSchema validation for drafts `2020-12`, `2019-09`, `7`, `6` and `4`.
```rust,no_run
# use std::fs::File;
# use std::error::Error;
# use boon::*;
# use serde_json::Value;
# fn main() -> Result<(), Box<dyn Error>>{
let mut schemas = Schemas::new(); // container for compiled schemas
let mut compiler = Compiler::new();
let sch_index = compiler.compile("schema.json", &mut schemas)?;
let instance: Value = serde_json::from_reader(File::open("instance.json")?)?;
let valid = schemas.validate(&instance, sch_index, None).is_ok();
# Ok(())
# }
```
If schema file has no `$schema`, it assumes latest draft.
You can override this:
```rust,no_run
# use boon::*;
# let mut compiler = Compiler::new();
compiler.set_default_draft(Draft::V7);
```
The use of this option is HIGHLY encouraged to ensure continued
correct operation of your schema. The current default value will
not stay the same over time.
# Examples
- [example_from_strings]: loading schemas from Strings
- [example_from_https]: loading schemas from `http(s)`
- [example_custom_format]: registering custom format
- [example_custom_content_encoding]: registering custom contentEncoding
- [example_custom_content_media_type]: registering custom contentMediaType
# Compile Errors
```no_compile
println!("{compile_error}");
println!("{compile_error:#}"); // prints cause if any
```
Using alterate form in display will print cause if any.
This will be useful in cases like [`CompileError::LoadUrlError`],
as it would be useful to know whether the url does not exist or
the resource at url is not a valid json document.
# Validation Errors
[`ValidationError`] may have multiple `causes` resulting
in tree of errors.
`println!("{validation_error}")` prints:
```no_compile
jsonschema validation failed with file:///tmp/customer.json#
at '': missing properties 'age'
at '/billing_address': missing properties 'street_address', 'city', 'state'
```
The alternate form `println!("{validation_error:#}")` prints:
```no_compile
jsonschema validation failed with file:///tmp/customer.json#
[I#] [S#/required] missing properties 'age'
[I#/billing_address] [S#/properties/billing_address/$ref] validation failed with file:///tmp/address.json#
[I#/billing_address] [S#/required] missing properties 'street_address', 'city', 'state'
```
here `I` refers to the instance document and `S` refers to last schema document.
for example:
- after line 1: `S` refers to `file:///tmp/customer.json`
- after line 3: `S` refers to `file://tmp/address.json`
# Output Formats
[`ValidationError`] can be converted into following output formats:
- [flag] `validation_error.flag_output()`
- [basic] `validation_error.basic_output()`
- [detailed] `validation_error.detailed_output()`
The output object implements `serde::Serialize`.
It also implement `Display` to print json:
```no_compile
println!("{output}"); // prints unformatted json
println!("{output:#}"); // prints indented json
```
[example_from_strings]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L22
[example_from_https]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L62
[example_from_yaml_files]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L86
[example_custom_format]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L119
[example_custom_content_encoding]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L153
[example_custom_content_media_type]: https://github.com/santhosh-tekuri/boon/blob/d466730e5e5c7c663bd6739e74e39d1e2f7baae4/tests/examples.rs#L198
[flag]: https://json-schema.org/draft/2020-12/json-schema-core.html#name-flag
[basic]: https://json-schema.org/draft/2020-12/json-schema-core.html#name-basic
[detailed]: https://json-schema.org/draft/2020-12/json-schema-core.html#name-detailed
*/
mod compiler;
mod content;
mod draft;
mod ecma;
mod formats;
mod loader;
mod output;
mod root;
mod roots;
mod util;
mod validator;
#[cfg(not(target_arch = "wasm32"))]
pub use loader::FileLoader;
pub use {
compiler::{CompileError, Compiler, Draft},
content::{Decoder, MediaType},
formats::Format,
loader::{SchemeUrlLoader, UrlLoader},
output::{
AbsoluteKeywordLocation, FlagOutput, KeywordPath, OutputError, OutputUnit, SchemaToken,
},
validator::{InstanceLocation, InstanceToken},
};
use std::{borrow::Cow, collections::HashMap, error::Error, fmt::Display};
use ahash::{AHashMap, AHashSet};
use regex::Regex;
use serde_json::{Number, Value};
use util::*;
/// Options for validation process
#[derive(Default, Debug, Clone, Copy)]
pub struct ValidationOptions {
/// treat unevaluated properties as an error
pub be_strict: bool,
}
/// Identifier to compiled schema.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SchemaIndex(usize);
/// Collection of compiled schemas.
#[derive(Default)]
pub struct Schemas {
list: Vec<Schema>,
map: HashMap<UrlPtr, usize>, // loc => schema-index
}
impl Schemas {
pub fn new() -> Self {
Self::default()
}
fn insert(&mut self, locs: Vec<UrlPtr>, compiled: Vec<Schema>) {
for (up, sch) in locs.into_iter().zip(compiled.into_iter()) {
let i = self.list.len();
self.list.push(sch);
self.map.insert(up, i);
}
}
fn get(&self, idx: SchemaIndex) -> &Schema {
&self.list[idx.0] // todo: return bug
}
fn get_by_loc(&self, up: &UrlPtr) -> Option<&Schema> {
self.map.get(up).and_then(|&i| self.list.get(i))
}
/// Returns true if `sch_index` is generated for this instance.
pub fn contains(&self, sch_index: SchemaIndex) -> bool {
self.list.get(sch_index.0).is_some()
}
pub fn size(&self) -> usize {
self.list.len()
}
/**
Validates `v` with schema identified by `sch_index`
# Panics
Panics if `sch_index` is not generated for this instance.
[`Schemas::contains`] can be used too ensure that it does not panic.
*/
pub fn validate<'s, 'v>(
&'s self,
v: &'v Value,
sch_index: SchemaIndex,
options: Option<ValidationOptions>,
) -> Result<(), ValidationError<'s, 'v>> {
let Some(sch) = self.list.get(sch_index.0) else {
panic!("Schemas::validate: schema index out of bounds");
};
validator::validate(v, sch, self, options)
}
}
#[derive(Default)]
struct Schema {
draft_version: usize,
idx: SchemaIndex,
loc: String,
resource: SchemaIndex,
dynamic_anchors: HashMap<String, SchemaIndex>,
all_props_evaluated: bool,
all_items_evaluated: bool,
num_items_evaluated: usize,
// type agnostic --
boolean: Option<bool>, // boolean schema
ref_: Option<SchemaIndex>,
recursive_ref: Option<SchemaIndex>,
recursive_anchor: bool,
dynamic_ref: Option<DynamicRef>,
dynamic_anchor: Option<String>,
types: Types,
enum_: Option<Enum>,
constant: Option<Value>,
not: Option<SchemaIndex>,
all_of: Vec<SchemaIndex>,
any_of: Vec<SchemaIndex>,
one_of: Vec<SchemaIndex>,
if_: Option<SchemaIndex>,
then: Option<SchemaIndex>,
else_: Option<SchemaIndex>,
format: Option<Format>,
// object --
min_properties: Option<usize>,
max_properties: Option<usize>,
required: Vec<String>,
properties: AHashMap<String, SchemaIndex>,
override_properties: AHashSet<String>,
pattern_properties: Vec<(Regex, SchemaIndex)>,
property_names: Option<SchemaIndex>,
additional_properties: Option<Additional>,
dependent_required: Vec<(String, Vec<String>)>,
dependent_schemas: Vec<(String, SchemaIndex)>,
dependencies: Vec<(String, Dependency)>,
unevaluated_properties: Option<SchemaIndex>,
// array --
min_items: Option<usize>,
max_items: Option<usize>,
unique_items: bool,
min_contains: Option<usize>,
max_contains: Option<usize>,
contains: Option<SchemaIndex>,
items: Option<Items>,
additional_items: Option<Additional>,
prefix_items: Vec<SchemaIndex>,
items2020: Option<SchemaIndex>,
unevaluated_items: Option<SchemaIndex>,
// string --
min_length: Option<usize>,
max_length: Option<usize>,
pattern: Option<Regex>,
content_encoding: Option<Decoder>,
content_media_type: Option<MediaType>,
content_schema: Option<SchemaIndex>,
// number --
minimum: Option<Number>,
maximum: Option<Number>,
exclusive_minimum: Option<Number>,
exclusive_maximum: Option<Number>,
multiple_of: Option<Number>,
}
#[derive(Debug)]
struct Enum {
/// types that occur in enum
types: Types,
/// values in enum
values: Vec<Value>,
}
#[derive(Debug)]
enum Items {
SchemaRef(SchemaIndex),
SchemaRefs(Vec<SchemaIndex>),
}
#[derive(Debug)]
enum Additional {
Bool(bool),
SchemaRef(SchemaIndex),
}
#[derive(Debug)]
enum Dependency {
Props(Vec<String>),
SchemaRef(SchemaIndex),
}
struct DynamicRef {
sch: SchemaIndex,
anchor: Option<String>,
}
impl Schema {
fn new(loc: String) -> Self {
Self {
loc,
..Default::default()
}
}
}
/// JSON data types for JSONSchema
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Type {
Null = 1,
Boolean = 2,
Number = 4,
Integer = 8,
String = 16,
Array = 32,
Object = 64,
}
impl Type {
fn of(v: &Value) -> Self {
match v {
Value::Null => Type::Null,
Value::Bool(_) => Type::Boolean,
Value::Number(_) => Type::Number,
Value::String(_) => Type::String,
Value::Array(_) => Type::Array,
Value::Object(_) => Type::Object,
}
}
fn from_str(value: &str) -> Option<Self> {
match value {
"null" => Some(Self::Null),
"boolean" => Some(Self::Boolean),
"number" => Some(Self::Number),
"integer" => Some(Self::Integer),
"string" => Some(Self::String),
"array" => Some(Self::Array),
"object" => Some(Self::Object),
_ => None,
}
}
fn primitive(v: &Value) -> bool {
!matches!(Self::of(v), Self::Array | Self::Object)
}
}
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Null => write!(f, "null"),
Type::Boolean => write!(f, "boolean"),
Type::Number => write!(f, "number"),
Type::Integer => write!(f, "integer"),
Type::String => write!(f, "string"),
Type::Array => write!(f, "array"),
Type::Object => write!(f, "object"),
}
}
}
/// Set of [`Type`]s
#[derive(Debug, Default, Clone, Copy)]
pub struct Types(u8);
impl Types {
fn is_empty(self) -> bool {
self.0 == 0
}
fn add(&mut self, t: Type) {
self.0 |= t as u8;
}
/// Returns `true` if this set contains given type.
pub fn contains(&self, t: Type) -> bool {
self.0 & t as u8 != 0
}
/// Returns an iterator over types.
pub fn iter(&self) -> impl Iterator<Item = Type> + '_ {
static TYPES: [Type; 7] = [
Type::Null,
Type::Boolean,
Type::Number,
Type::Integer,
Type::String,
Type::Array,
Type::Object,
];
TYPES.iter().cloned().filter(|t| self.contains(*t))
}
}
impl FromIterator<Type> for Types {
fn from_iter<T: IntoIterator<Item = Type>>(iter: T) -> Self {
let mut types = Types::default();
for t in iter {
types.add(t);
}
types
}
}
/// Error type for validation failures.
#[derive(Debug)]
pub struct ValidationError<'s, 'v> {
/// The absolute, dereferenced schema location.
pub schema_url: &'s str,
/// The location of the JSON value within the instance being validated
pub instance_location: InstanceLocation<'v>,
/// kind of error
pub kind: ErrorKind<'s, 'v>,
/// Holds nested errors
pub causes: Vec<ValidationError<'s, 'v>>,
}
impl Error for ValidationError<'_, '_> {}
/// A list specifying general categories of validation errors.
#[derive(Debug)]
pub enum ErrorKind<'s, 'v> {
Group,
Schema {
url: &'s str,
},
ContentSchema,
PropertyName {
prop: String,
},
Reference {
kw: &'static str,
url: &'s str,
},
RefCycle {
url: &'s str,
kw_loc1: String,
kw_loc2: String,
},
FalseSchema,
Type {
got: Type,
want: Types,
},
Enum {
want: &'s Vec<Value>,
},
Const {
want: &'s Value,
},
Format {
got: Cow<'v, Value>,
want: &'static str,
err: Box<dyn Error>,
},
MinProperties {
got: usize,
want: usize,
},
MaxProperties {
got: usize,
want: usize,
},
AdditionalProperties {
got: Vec<Cow<'v, str>>,
},
Required {
want: Vec<&'s str>,
},
Dependency {
/// dependency of prop that failed.
prop: &'s str,
/// missing props.
missing: Vec<&'s str>,
},
DependentRequired {
/// dependency of prop that failed.
prop: &'s str,
/// missing props.
missing: Vec<&'s str>,
},
MinItems {
got: usize,
want: usize,
},
MaxItems {
got: usize,
want: usize,
},
Contains,
MinContains {
got: Vec<usize>,
want: usize,
},
MaxContains {
got: Vec<usize>,
want: usize,
},
UniqueItems {
got: [usize; 2],
},
AdditionalItems {
got: usize,
},
MinLength {
got: usize,
want: usize,
},
MaxLength {
got: usize,
want: usize,
},
Pattern {
got: Cow<'v, str>,
want: &'s str,
},
ContentEncoding {
want: &'static str,
err: Box<dyn Error>,
},
ContentMediaType {
got: Vec<u8>,
want: &'static str,
err: Box<dyn Error>,
},
Minimum {
got: Cow<'v, Number>,
want: &'s Number,
},
Maximum {
got: Cow<'v, Number>,
want: &'s Number,
},
ExclusiveMinimum {
got: Cow<'v, Number>,
want: &'s Number,
},
ExclusiveMaximum {
got: Cow<'v, Number>,
want: &'s Number,
},
MultipleOf {
got: Cow<'v, Number>,
want: &'s Number,
},
Not,
/// none of the subschemas matched
AllOf,
/// none of the subschemas matched.
AnyOf,
/// - `None`: none of the schemas matched.
/// - Some(i, j): subschemas at i, j matched
OneOf(Option<(usize, usize)>),
}
impl Display for ErrorKind<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Group => write!(f, "validation failed"),
Self::Schema { url } => write!(f, "validation failed with {url}"),
Self::ContentSchema => write!(f, "contentSchema failed"),
Self::PropertyName { prop } => write!(f, "invalid property {}", quote(prop)),
Self::Reference { .. } => {
write!(f, "validation failed")
}
Self::RefCycle {
url,
kw_loc1,
kw_loc2,
} => write!(
f,
"both {} and {} resolve to {url} causing reference cycle",
quote(&kw_loc1.to_string()),
quote(&kw_loc2.to_string())
),
Self::FalseSchema => write!(f, "false schema"),
Self::Type { got, want } => {
// todo: why join not working for Type struct ??
let want = join_iter(want.iter(), " or ");
write!(f, "want {want}, but got {got}",)
}
Self::Enum { want } => {
if want.iter().all(Type::primitive) {
if want.len() == 1 {
write!(f, "value must be ")?;
display(f, &want[0])
} else {
let want = join_iter(want.iter().map(string), ", ");
write!(f, "value must be one of {want}")
}
} else {
write!(f, "enum failed")
}
}
Self::Const { want } => {
if Type::primitive(want) {
write!(f, "value must be ")?;
display(f, want)
} else {
write!(f, "const failed")
}
}
Self::Format { got, want, err } => {
display(f, got)?;
write!(f, " is not valid {want}: {err}")
}
Self::MinProperties { got, want } => write!(
f,
"minimum {want} properties required, but got {got} properties"
),
Self::MaxProperties { got, want } => write!(
f,
"maximum {want} properties required, but got {got} properties"
),
Self::AdditionalProperties { got } => {
write!(
f,
"additionalProperties {} not allowed",
join_iter(got.iter().map(quote), ", ")
)
}
Self::Required { want } => write!(
f,
"missing properties {}",
join_iter(want.iter().map(quote), ", ")
),
Self::Dependency { prop, missing } => {
write!(
f,
"properties {} required, if {} property exists",
join_iter(missing.iter().map(quote), ", "),
quote(prop)
)
}
Self::DependentRequired { prop, missing } => write!(
f,
"properties {} required, if {} property exists",
join_iter(missing.iter().map(quote), ", "),
quote(prop)
),
Self::MinItems { got, want } => {
write!(f, "minimum {want} items required, but got {got} items")
}
Self::MaxItems { got, want } => {
write!(f, "maximum {want} items required, but got {got} items")
}
Self::MinContains { got, want } => {
if got.is_empty() {
write!(
f,
"minimum {want} items required to match contains schema, but found none",
)
} else {
write!(
f,
"minimum {want} items required to match contains schema, but found {} items at {}",
got.len(),
join_iter(got, ", ")
)
}
}
Self::Contains => write!(f, "no items match contains schema"),
Self::MaxContains { got, want } => {
write!(
f,
"maximum {want} items required to match contains schema, but found {} items at {}",
got.len(),
join_iter(got, ", ")
)
}
Self::UniqueItems { got: [i, j] } => write!(f, "items at {i} and {j} are equal"),
Self::AdditionalItems { got } => write!(f, "last {got} additionalItems not allowed"),
Self::MinLength { got, want } => write!(f, "length must be >={want}, but got {got}"),
Self::MaxLength { got, want } => write!(f, "length must be <={want}, but got {got}"),
Self::Pattern { got, want } => {
write!(f, "{} does not match pattern {}", quote(got), quote(want))
}
Self::ContentEncoding { want, err } => {
write!(f, "value is not {} encoded: {err}", quote(want))
}
Self::ContentMediaType { want, err, .. } => {
write!(f, "value is not of mediatype {}: {err}", quote(want))
}
Self::Minimum { got, want } => write!(f, "must be >={want}, but got {got}"),
Self::Maximum { got, want } => write!(f, "must be <={want}, but got {got}"),
Self::ExclusiveMinimum { got, want } => write!(f, "must be > {want} but got {got}"),
Self::ExclusiveMaximum { got, want } => write!(f, "must be < {want} but got {got}"),
Self::MultipleOf { got, want } => write!(f, "{got} is not multipleOf {want}"),
Self::Not => write!(f, "not failed"),
Self::AllOf => write!(f, "allOf failed",),
Self::AnyOf => write!(f, "anyOf failed"),
Self::OneOf(None) => write!(f, "oneOf failed, none matched"),
Self::OneOf(Some((i, j))) => write!(f, "oneOf failed, subschemas {i}, {j} matched"),
}
}
}
fn display(f: &mut std::fmt::Formatter, v: &Value) -> std::fmt::Result {
match v {
Value::String(s) => write!(f, "{}", quote(s)),
Value::Array(_) | Value::Object(_) => write!(f, "value"),
_ => write!(f, "{v}"),
}
}
fn string(primitive: &Value) -> String {
if let Value::String(s) = primitive {
quote(s)
} else {
format!("{primitive}")
}
}

View File

@ -1,243 +0,0 @@
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
error::Error,
};
#[cfg(not(target_arch = "wasm32"))]
use std::fs::File;
use appendlist::AppendList;
use once_cell::sync::Lazy;
use serde_json::Value;
use url::Url;
use crate::{
compiler::CompileError,
draft::{latest, Draft},
util::split,
UrlPtr,
};
/// A trait for loading json from given `url`
pub trait UrlLoader {
/// Loads json from given absolute `url`.
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>>;
}
// --
#[cfg(not(target_arch = "wasm32"))]
pub struct FileLoader;
#[cfg(not(target_arch = "wasm32"))]
impl UrlLoader for FileLoader {
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
let url = Url::parse(url)?;
let path = url.to_file_path().map_err(|_| "invalid file path")?;
let file = File::open(path)?;
Ok(serde_json::from_reader(file)?)
}
}
// --
#[derive(Default)]
pub struct SchemeUrlLoader {
loaders: HashMap<&'static str, Box<dyn UrlLoader>>,
}
impl SchemeUrlLoader {
pub fn new() -> Self {
Self::default()
}
/// Registers [`UrlLoader`] for given url `scheme`
pub fn register(&mut self, scheme: &'static str, url_loader: Box<dyn UrlLoader>) {
self.loaders.insert(scheme, url_loader);
}
}
impl UrlLoader for SchemeUrlLoader {
fn load(&self, url: &str) -> Result<Value, Box<dyn Error>> {
let url = Url::parse(url)?;
let Some(loader) = self.loaders.get(url.scheme()) else {
return Err(CompileError::UnsupportedUrlScheme {
url: url.as_str().to_owned(),
}
.into());
};
loader.load(url.as_str())
}
}
// --
pub(crate) struct DefaultUrlLoader {
doc_map: RefCell<HashMap<Url, usize>>,
doc_list: AppendList<Value>,
loader: Box<dyn UrlLoader>,
}
impl DefaultUrlLoader {
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
pub fn new() -> Self {
let mut loader = SchemeUrlLoader::new();
#[cfg(not(target_arch = "wasm32"))]
loader.register("file", Box::new(FileLoader));
Self {
doc_map: Default::default(),
doc_list: AppendList::new(),
loader: Box::new(loader),
}
}
pub fn get_doc(&self, url: &Url) -> Option<&Value> {
self.doc_map
.borrow()
.get(url)
.and_then(|i| self.doc_list.get(*i))
}
pub fn add_doc(&self, url: Url, json: Value) {
if self.get_doc(&url).is_some() {
return;
}
self.doc_list.push(json);
self.doc_map
.borrow_mut()
.insert(url, self.doc_list.len() - 1);
}
pub fn use_loader(&mut self, loader: Box<dyn UrlLoader>) {
self.loader = loader;
}
pub(crate) fn load(&self, url: &Url) -> Result<&Value, CompileError> {
if let Some(doc) = self.get_doc(url) {
return Ok(doc);
}
// check in STD_METAFILES
let doc = if let Some(content) = load_std_meta(url.as_str()) {
serde_json::from_str::<Value>(content).map_err(|e| CompileError::LoadUrlError {
url: url.to_string(),
src: e.into(),
})?
} else {
self.loader
.load(url.as_str())
.map_err(|src| CompileError::LoadUrlError {
url: url.as_str().to_owned(),
src,
})?
};
self.add_doc(url.clone(), doc);
self.get_doc(url)
.ok_or(CompileError::Bug("doc must exist".into()))
}
pub(crate) fn get_draft(
&self,
up: &UrlPtr,
doc: &Value,
default_draft: &'static Draft,
mut cycle: HashSet<Url>,
) -> Result<&'static Draft, CompileError> {
let Value::Object(obj) = &doc else {
return Ok(default_draft);
};
let Some(Value::String(sch)) = obj.get("$schema") else {
return Ok(default_draft);
};
if let Some(draft) = Draft::from_url(sch) {
return Ok(draft);
}
let (sch, _) = split(sch);
let sch = Url::parse(sch).map_err(|e| CompileError::InvalidMetaSchemaUrl {
url: up.to_string(),
src: e.into(),
})?;
if up.ptr.is_empty() && sch == up.url {
return Err(CompileError::UnsupportedDraft { url: sch.into() });
}
if !cycle.insert(sch.clone()) {
return Err(CompileError::MetaSchemaCycle { url: sch.into() });
}
let doc = self.load(&sch)?;
let up = UrlPtr {
url: sch,
ptr: "".into(),
};
self.get_draft(&up, doc, default_draft, cycle)
}
pub(crate) fn get_meta_vocabs(
&self,
doc: &Value,
draft: &'static Draft,
) -> Result<Option<Vec<String>>, CompileError> {
let Value::Object(obj) = &doc else {
return Ok(None);
};
let Some(Value::String(sch)) = obj.get("$schema") else {
return Ok(None);
};
if Draft::from_url(sch).is_some() {
return Ok(None);
}
let (sch, _) = split(sch);
let sch = Url::parse(sch).map_err(|e| CompileError::ParseUrlError {
url: sch.to_string(),
src: e.into(),
})?;
let doc = self.load(&sch)?;
draft.get_vocabs(&sch, doc)
}
}
pub(crate) static STD_METAFILES: Lazy<HashMap<String, &str>> = Lazy::new(|| {
let mut files = HashMap::new();
macro_rules! add {
($path:expr) => {
files.insert(
$path["metaschemas/".len()..].to_owned(),
include_str!($path),
);
};
}
add!("metaschemas/draft-04/schema");
add!("metaschemas/draft-06/schema");
add!("metaschemas/draft-07/schema");
add!("metaschemas/draft/2019-09/schema");
add!("metaschemas/draft/2019-09/meta/core");
add!("metaschemas/draft/2019-09/meta/applicator");
add!("metaschemas/draft/2019-09/meta/validation");
add!("metaschemas/draft/2019-09/meta/meta-data");
add!("metaschemas/draft/2019-09/meta/format");
add!("metaschemas/draft/2019-09/meta/content");
add!("metaschemas/draft/2020-12/schema");
add!("metaschemas/draft/2020-12/meta/core");
add!("metaschemas/draft/2020-12/meta/applicator");
add!("metaschemas/draft/2020-12/meta/unevaluated");
add!("metaschemas/draft/2020-12/meta/validation");
add!("metaschemas/draft/2020-12/meta/meta-data");
add!("metaschemas/draft/2020-12/meta/content");
add!("metaschemas/draft/2020-12/meta/format-annotation");
add!("metaschemas/draft/2020-12/meta/format-assertion");
files
});
fn load_std_meta(url: &str) -> Option<&'static str> {
let meta = url
.strip_prefix("http://json-schema.org/")
.or_else(|| url.strip_prefix("https://json-schema.org/"));
if let Some(meta) = meta {
if meta == "schema" {
return load_std_meta(latest().url);
}
return STD_METAFILES.get(meta).cloned();
}
None
}

View File

@ -1,151 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uriref"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" },
"format": { "type": "string" },
"$ref": { "type": "string" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}

View File

@ -1,151 +0,0 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$id": "http://json-schema.org/draft-06/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"regexProperties": true,
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": {},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": {}
}

View File

@ -1,172 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://json-schema.org/draft-07/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$comment": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": true
},
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "$ref": "#" },
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": { "$ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": true,
"enum": {
"type": "array",
"items": true,
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"if": { "$ref": "#" },
"then": { "$ref": "#" },
"else": { "$ref": "#" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"default": true
}

View File

@ -1,55 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/applicator": true
},
"$recursiveAnchor": true,
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"additionalItems": { "$recursiveRef": "#" },
"unevaluatedItems": { "$recursiveRef": "#" },
"items": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "#/$defs/schemaArray" }
]
},
"contains": { "$recursiveRef": "#" },
"additionalProperties": { "$recursiveRef": "#" },
"unevaluatedProperties": { "$recursiveRef": "#" },
"properties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": {
"$recursiveRef": "#"
}
},
"propertyNames": { "$recursiveRef": "#" },
"if": { "$recursiveRef": "#" },
"then": { "$recursiveRef": "#" },
"else": { "$recursiveRef": "#" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$recursiveRef": "#" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$recursiveRef": "#" }
}
}
}

View File

@ -1,15 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentMediaType": { "type": "string" },
"contentEncoding": { "type": "string" },
"contentSchema": { "$recursiveRef": "#" }
}
}

View File

@ -1,56 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true
},
"$recursiveAnchor": true,
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"type": "string",
"format": "uri-reference",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": {
"type": "string",
"format": "uri"
},
"$anchor": {
"type": "string",
"pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$"
},
"$ref": {
"type": "string",
"format": "uri-reference"
},
"$recursiveRef": {
"type": "string",
"format": "uri-reference"
},
"$recursiveAnchor": {
"type": "boolean",
"default": false
},
"$vocabulary": {
"type": "object",
"propertyNames": {
"type": "string",
"format": "uri"
},
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
}
}
}

View File

@ -1,13 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/format",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/format": true
},
"$recursiveAnchor": true,
"title": "Format vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}

View File

@ -1,35 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/meta-data": true
},
"$recursiveAnchor": true,
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}

View File

@ -1,97 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/validation": true
},
"$recursiveAnchor": true,
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}

View File

@ -1,41 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/schema",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/applicator": true,
"https://json-schema.org/draft/2019-09/vocab/validation": true,
"https://json-schema.org/draft/2019-09/vocab/meta-data": true,
"https://json-schema.org/draft/2019-09/vocab/format": false,
"https://json-schema.org/draft/2019-09/vocab/content": true
},
"$recursiveAnchor": true,
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"properties": {
"definitions": {
"$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.",
"type": "object",
"additionalProperties": { "$recursiveRef": "#" },
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$recursiveRef": "#" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
}
}
}
}

View File

@ -1,47 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/applicator",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/applicator": true
},
"$dynamicAnchor": "meta",
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"prefixItems": { "$ref": "#/$defs/schemaArray" },
"items": { "$dynamicRef": "#meta" },
"contains": { "$dynamicRef": "#meta" },
"additionalProperties": { "$dynamicRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"propertyNames": { "$dynamicRef": "#meta" },
"if": { "$dynamicRef": "#meta" },
"then": { "$dynamicRef": "#meta" },
"else": { "$dynamicRef": "#meta" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$dynamicRef": "#meta" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$dynamicRef": "#meta" }
}
}
}

View File

@ -1,15 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/content",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$dynamicRef": "#meta" }
}
}

View File

@ -1,50 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/core",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true
},
"$dynamicAnchor": "meta",
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"$ref": "#/$defs/uriReferenceString",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": { "$ref": "#/$defs/uriString" },
"$ref": { "$ref": "#/$defs/uriReferenceString" },
"$anchor": { "$ref": "#/$defs/anchorString" },
"$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
"$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
"$vocabulary": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/uriString" },
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" }
}
},
"$defs": {
"anchorString": {
"type": "string",
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
},
"uriString": {
"type": "string",
"format": "uri"
},
"uriReferenceString": {
"type": "string",
"format": "uri-reference"
}
}
}

View File

@ -1,13 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for annotation results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}

View File

@ -1,13 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-assertion",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/format-assertion": true
},
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for assertion results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}

View File

@ -1,35 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/meta-data": true
},
"$dynamicAnchor": "meta",
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}

View File

@ -1,14 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true
},
"$dynamicAnchor": "meta",
"title": "Unevaluated applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"unevaluatedItems": { "$dynamicRef": "#meta" },
"unevaluatedProperties": { "$dynamicRef": "#meta" }
}
}

View File

@ -1,97 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/validation",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/validation": true
},
"$dynamicAnchor": "meta",
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}

View File

@ -1,60 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/unevaluated"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format-annotation"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
"properties": {
"override": {
"type": "boolean"
},
"definitions": {
"$comment": "\"definitions\" has been replaced by \"$defs\".",
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"deprecated": true,
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$dynamicRef": "#meta" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
},
"deprecated": true,
"default": {}
},
"$recursiveAnchor": {
"$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
"$ref": "meta/core#/$defs/anchorString",
"deprecated": true
},
"$recursiveRef": {
"$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
"$ref": "meta/core#/$defs/uriReferenceString",
"deprecated": true
}
}
}

View File

@ -1,622 +0,0 @@
use std::{
borrow::Cow,
fmt::{Display, Formatter, Write},
};
use serde::{
ser::{SerializeMap, SerializeSeq},
Serialize,
};
use crate::{util::*, ErrorKind, InstanceLocation, ValidationError};
impl<'s> ValidationError<'s, '_> {
fn absolute_keyword_location(&self) -> AbsoluteKeywordLocation<'s> {
if let ErrorKind::Reference { url, .. } = &self.kind {
AbsoluteKeywordLocation {
schema_url: url,
keyword_path: None,
}
} else {
AbsoluteKeywordLocation {
schema_url: self.schema_url,
keyword_path: self.kind.keyword_path(),
}
}
}
fn skip(&self) -> bool {
self.causes.len() == 1 && matches!(self.kind, ErrorKind::Reference { .. })
}
/// The `Flag` output format, merely the boolean result.
pub fn flag_output(&self) -> FlagOutput {
FlagOutput { valid: false }
}
/// The `Basic` structure, a flat list of output units.
pub fn basic_output(&self) -> OutputUnit<'_, '_, '_> {
let mut outputs = vec![];
let mut in_ref = InRef::default();
let mut kw_loc = KeywordLocation::default();
for node in DfsIterator::new(self) {
match node {
DfsItem::Pre(e) => {
in_ref.pre(e);
kw_loc.pre(e);
if e.skip() || matches!(e.kind, ErrorKind::Schema { .. }) {
continue;
}
let absolute_keyword_location = if in_ref.get() {
Some(e.absolute_keyword_location())
} else {
None
};
outputs.push(OutputUnit {
valid: false,
keyword_location: kw_loc.get(e),
absolute_keyword_location,
instance_location: &e.instance_location,
error: OutputError::Leaf(&e.kind),
});
}
DfsItem::Post(e) => {
in_ref.post();
kw_loc.post();
if e.skip() || matches!(e.kind, ErrorKind::Schema { .. }) {
continue;
}
}
}
}
let error = if outputs.is_empty() {
OutputError::Leaf(&self.kind)
} else {
OutputError::Branch(outputs)
};
OutputUnit {
valid: false,
keyword_location: String::new(),
absolute_keyword_location: None,
instance_location: &self.instance_location,
error,
}
}
/// The `Detailed` structure, based on the schema.
pub fn detailed_output(&self) -> OutputUnit<'_, '_, '_> {
let mut root = None;
let mut stack: Vec<OutputUnit> = vec![];
let mut in_ref = InRef::default();
let mut kw_loc = KeywordLocation::default();
for node in DfsIterator::new(self) {
match node {
DfsItem::Pre(e) => {
in_ref.pre(e);
kw_loc.pre(e);
if e.skip() {
continue;
}
let absolute_keyword_location = if in_ref.get() {
Some(e.absolute_keyword_location())
} else {
None
};
stack.push(OutputUnit {
valid: false,
keyword_location: kw_loc.get(e),
absolute_keyword_location,
instance_location: &e.instance_location,
error: OutputError::Leaf(&e.kind),
});
}
DfsItem::Post(e) => {
in_ref.post();
kw_loc.post();
if e.skip() {
continue;
}
let output = stack.pop().unwrap();
if let Some(parent) = stack.last_mut() {
match &mut parent.error {
OutputError::Leaf(_) => {
parent.error = OutputError::Branch(vec![output]);
}
OutputError::Branch(v) => v.push(output),
}
} else {
root.replace(output);
}
}
}
}
root.unwrap()
}
}
// DfsIterator --
impl Display for ValidationError<'_, '_> {
/// Formats error hierarchy. Use `#` to show the schema location.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut indent = Indent::default();
let mut sloc = SchemaLocation::default();
// let mut kw_loc = KeywordLocation::default();
for node in DfsIterator::new(self) {
match node {
DfsItem::Pre(e) => {
// kw_loc.pre(e);
if e.skip() {
continue;
}
indent.pre(f)?;
if f.alternate() {
sloc.pre(e);
}
if let ErrorKind::Schema { .. } = &e.kind {
write!(f, "jsonschema {}", e.kind)?;
} else {
write!(f, "at {}", quote(&e.instance_location.to_string()))?;
if f.alternate() {
write!(f, " [{}]", sloc)?;
// write!(f, " [{}]", kw_loc.get(e))?;
// write!(f, " [{}]", e.absolute_keyword_location())?;
}
write!(f, ": {}", e.kind)?;
}
}
DfsItem::Post(e) => {
// kw_loc.post();
if e.skip() {
continue;
}
indent.post();
sloc.post();
}
}
}
Ok(())
}
}
struct DfsIterator<'a, 'v, 's> {
root: Option<&'a ValidationError<'v, 's>>,
stack: Vec<Frame<'a, 'v, 's>>,
}
impl<'a, 'v, 's> DfsIterator<'a, 'v, 's> {
fn new(err: &'a ValidationError<'v, 's>) -> Self {
DfsIterator {
root: Some(err),
stack: vec![],
}
}
}
impl<'a, 'v, 's> Iterator for DfsIterator<'a, 'v, 's> {
type Item = DfsItem<&'a ValidationError<'v, 's>>;
fn next(&mut self) -> Option<Self::Item> {
let Some(mut frame) = self.stack.pop() else {
if let Some(err) = self.root.take() {
self.stack.push(Frame::from(err));
return Some(DfsItem::Pre(err));
} else {
return None;
}
};
if frame.causes.is_empty() {
return Some(DfsItem::Post(frame.err));
}
let err = &frame.causes[0];
frame.causes = &frame.causes[1..];
self.stack.push(frame);
self.stack.push(Frame::from(err));
Some(DfsItem::Pre(err))
}
}
struct Frame<'a, 'v, 's> {
err: &'a ValidationError<'v, 's>,
causes: &'a [ValidationError<'v, 's>],
}
impl<'a, 'v, 's> Frame<'a, 'v, 's> {
fn from(err: &'a ValidationError<'v, 's>) -> Self {
Self {
err,
causes: &err.causes,
}
}
}
enum DfsItem<T> {
Pre(T),
Post(T),
}
// Indent --
#[derive(Default)]
struct Indent {
n: usize,
}
impl Indent {
fn pre(&mut self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.n > 0 {
writeln!(f)?;
for _ in 0..self.n - 1 {
write!(f, " ")?;
}
write!(f, "- ")?;
}
self.n += 1;
Ok(())
}
fn post(&mut self) {
self.n -= 1;
}
}
// SchemaLocation
#[derive(Default)]
struct SchemaLocation<'a, 's, 'v> {
stack: Vec<&'a ValidationError<'s, 'v>>,
}
impl<'a, 's, 'v> SchemaLocation<'a, 's, 'v> {
fn pre(&mut self, e: &'a ValidationError<'s, 'v>) {
self.stack.push(e);
}
fn post(&mut self) {
self.stack.pop();
}
}
impl Display for SchemaLocation<'_, '_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut iter = self.stack.iter().cloned();
let cur = iter.next_back().unwrap();
let cur: Cow<str> = match &cur.kind {
ErrorKind::Schema { url } => Cow::Borrowed(url),
ErrorKind::Reference { url, .. } => Cow::Borrowed(url),
_ => Cow::Owned(cur.absolute_keyword_location().to_string()),
};
let Some(prev) = iter.next_back() else {
return write!(f, "{cur}");
};
let p = match &prev.kind {
ErrorKind::Schema { url } => {
let (p, _) = split(url);
p
}
ErrorKind::Reference { url, .. } => {
let (p, _) = split(url);
p
}
_ => {
let (p, _) = split(prev.schema_url);
p
}
};
let (c, frag) = split(cur.as_ref());
if c == p {
write!(f, "S#{frag}")
} else {
write!(f, "{cur}")
}
}
}
// KeywordLocation --
#[derive(Default)]
struct KeywordLocation<'a> {
loc: String,
stack: Vec<(&'a str, usize)>, // (schema_url, len)
}
impl<'a> KeywordLocation<'a> {
fn pre(&mut self, e: &'a ValidationError) {
let cur = match &e.kind {
ErrorKind::Schema { url } => url,
ErrorKind::Reference { url, .. } => url,
_ => e.schema_url,
};
if let Some((prev, _)) = self.stack.last() {
self.loc.push_str(&e.schema_url[prev.len()..]); // todo: url-decode
if let ErrorKind::Reference { kw, .. } = &e.kind {
self.loc.push('/');
self.loc.push_str(kw);
}
}
self.stack.push((cur, self.loc.len()));
}
fn post(&mut self) {
self.stack.pop();
if let Some((_, len)) = self.stack.last() {
self.loc.truncate(*len);
}
}
fn get(&mut self, cur: &'a ValidationError) -> String {
if let ErrorKind::Reference { .. } = &cur.kind {
self.loc.clone()
} else if let Some(kw_path) = &cur.kind.keyword_path() {
let len = self.loc.len();
self.loc.push('/');
write!(self.loc, "{}", kw_path).expect("write kw_path to String should not fail");
let loc = self.loc.clone();
self.loc.truncate(len);
loc
} else {
self.loc.clone()
}
}
}
#[derive(Default)]
struct InRef {
stack: Vec<bool>,
}
impl InRef {
fn pre(&mut self, e: &ValidationError) {
let in_ref: bool = self.get() || matches!(e.kind, ErrorKind::Reference { .. });
self.stack.push(in_ref);
}
fn post(&mut self) {
self.stack.pop();
}
fn get(&self) -> bool {
self.stack.last().cloned().unwrap_or_default()
}
}
// output formats --
/// Simplest output format, merely the boolean result.
pub struct FlagOutput {
pub valid: bool,
}
impl Serialize for FlagOutput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("valid", &self.valid)?;
map.end()
}
}
impl Display for FlagOutput {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_json_to_fmt(f, self)
}
}
/// Single OutputUnit used in Basic/Detailed output formats.
pub struct OutputUnit<'e, 's, 'v> {
pub valid: bool,
pub keyword_location: String,
/// The absolute, dereferenced location of the validating keyword
pub absolute_keyword_location: Option<AbsoluteKeywordLocation<'s>>,
/// The location of the JSON value within the instance being validated
pub instance_location: &'e InstanceLocation<'v>,
pub error: OutputError<'e, 's, 'v>,
}
impl Serialize for OutputUnit<'_, '_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let n = 4 + self.absolute_keyword_location.as_ref().map_or(0, |_| 1);
let mut map = serializer.serialize_map(Some(n))?;
map.serialize_entry("valid", &self.valid)?;
map.serialize_entry("keywordLocation", &self.keyword_location.to_string())?;
if let Some(s) = &self.absolute_keyword_location {
map.serialize_entry("absoluteKeywordLocation", &s.to_string())?;
}
map.serialize_entry("instanceLocation", &self.instance_location.to_string())?;
let pname = match self.error {
OutputError::Leaf(_) => "error",
OutputError::Branch(_) => "errors",
};
map.serialize_entry(pname, &self.error)?;
map.end()
}
}
impl Display for OutputUnit<'_, '_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_json_to_fmt(f, self)
}
}
/// Error of [`OutputUnit`].
pub enum OutputError<'e, 's, 'v> {
/// Single.
Leaf(&'e ErrorKind<'s, 'v>),
/// Nested.
Branch(Vec<OutputUnit<'e, 's, 'v>>),
}
impl Serialize for OutputError<'_, '_, '_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
OutputError::Leaf(kind) => serializer.serialize_str(&kind.to_string()),
OutputError::Branch(units) => {
let mut seq = serializer.serialize_seq(Some(units.len()))?;
for unit in units {
seq.serialize_element(unit)?;
}
seq.end()
}
}
}
}
// AbsoluteKeywordLocation --
impl<'s> ErrorKind<'s, '_> {
pub fn keyword_path(&self) -> Option<KeywordPath<'s>> {
#[inline(always)]
fn kw(kw: &'static str) -> Option<KeywordPath<'static>> {
Some(KeywordPath {
keyword: kw,
token: None,
})
}
#[inline(always)]
fn kw_prop<'s>(kw: &'static str, prop: &'s str) -> Option<KeywordPath<'s>> {
Some(KeywordPath {
keyword: kw,
token: Some(SchemaToken::Prop(prop)),
})
}
use ErrorKind::*;
match self {
Group => None,
Schema { .. } => None,
ContentSchema => kw("contentSchema"),
PropertyName { .. } => kw("propertyNames"),
Reference { kw: kword, .. } => kw(kword),
RefCycle { .. } => None,
FalseSchema => None,
Type { .. } => kw("type"),
Enum { .. } => kw("enum"),
Const { .. } => kw("const"),
Format { .. } => kw("format"),
MinProperties { .. } => kw("minProperties"),
MaxProperties { .. } => kw("maxProperties"),
AdditionalProperties { .. } => kw("additionalProperty"),
Required { .. } => kw("required"),
Dependency { prop, .. } => kw_prop("dependencies", prop),
DependentRequired { prop, .. } => kw_prop("dependentRequired", prop),
MinItems { .. } => kw("minItems"),
MaxItems { .. } => kw("maxItems"),
Contains => kw("contains"),
MinContains { .. } => kw("minContains"),
MaxContains { .. } => kw("maxContains"),
UniqueItems { .. } => kw("uniqueItems"),
AdditionalItems { .. } => kw("additionalItems"),
MinLength { .. } => kw("minLength"),
MaxLength { .. } => kw("maxLength"),
Pattern { .. } => kw("pattern"),
ContentEncoding { .. } => kw("contentEncoding"),
ContentMediaType { .. } => kw("contentMediaType"),
Minimum { .. } => kw("minimum"),
Maximum { .. } => kw("maximum"),
ExclusiveMinimum { .. } => kw("exclusiveMinimum"),
ExclusiveMaximum { .. } => kw("exclusiveMaximum"),
MultipleOf { .. } => kw("multipleOf"),
Not => kw("not"),
AllOf => kw("allOf"),
AnyOf => kw("anyOf"),
OneOf(_) => kw("oneOf"),
}
}
}
/// The absolute, dereferenced location of the validating keyword
#[derive(Debug, Clone)]
pub struct AbsoluteKeywordLocation<'s> {
/// The absolute, dereferenced schema location.
pub schema_url: &'s str,
/// Location within the `schema_url`.
pub keyword_path: Option<KeywordPath<'s>>,
}
impl Display for AbsoluteKeywordLocation<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.schema_url.fmt(f)?;
if let Some(path) = &self.keyword_path {
f.write_str("/")?;
path.keyword.fmt(f)?;
if let Some(token) = &path.token {
f.write_str("/")?;
match token {
SchemaToken::Prop(p) => write!(f, "{}", escape(p))?, // todo: url-encode
SchemaToken::Item(i) => write!(f, "{i}")?,
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
/// JsonPointer in schema.
pub struct KeywordPath<'s> {
/// The first token.
pub keyword: &'static str,
/// Optinal token within keyword.
pub token: Option<SchemaToken<'s>>,
}
impl Display for KeywordPath<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.keyword.fmt(f)?;
if let Some(token) = &self.token {
f.write_str("/")?;
token.fmt(f)?;
}
Ok(())
}
}
/// Token for schema.
#[derive(Debug, Clone)]
pub enum SchemaToken<'s> {
/// Token for property.
Prop(&'s str),
/// Token for array item.
Item(usize),
}
impl Display for SchemaToken<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaToken::Prop(p) => write!(f, "{}", escape(p)),
SchemaToken::Item(i) => write!(f, "{i}"),
}
}
}
// helpers --
fn write_json_to_fmt<T>(f: &mut std::fmt::Formatter, value: &T) -> Result<(), std::fmt::Error>
where
T: ?Sized + Serialize,
{
let s = if f.alternate() {
serde_json::to_string_pretty(value)
} else {
serde_json::to_string(value)
};
let s = s.map_err(|_| std::fmt::Error)?;
f.write_str(&s)
}

View File

@ -1,128 +0,0 @@
use std::collections::{HashMap, HashSet};
use crate::{compiler::CompileError, draft::*, util::*};
use serde_json::Value;
use url::Url;
pub(crate) struct Root {
pub(crate) draft: &'static Draft,
pub(crate) resources: HashMap<JsonPointer, Resource>, // ptr => _
pub(crate) url: Url,
pub(crate) meta_vocabs: Option<Vec<String>>,
}
impl Root {
pub(crate) fn has_vocab(&self, name: &str) -> bool {
if self.draft.version < 2019 || name == "core" {
return true;
}
if let Some(vocabs) = &self.meta_vocabs {
return vocabs.iter().any(|s| s == name);
}
self.draft.default_vocabs.contains(&name)
}
fn resolve_fragment_in(&self, frag: &Fragment, res: &Resource) -> Result<UrlPtr, CompileError> {
let ptr = match frag {
Fragment::Anchor(anchor) => {
let Some(ptr) = res.anchors.get(anchor) else {
return Err(CompileError::AnchorNotFound {
url: self.url.to_string(),
reference: UrlFrag::format(&res.id, frag.as_str()),
});
};
ptr.clone()
}
Fragment::JsonPointer(ptr) => res.ptr.concat(ptr),
};
Ok(UrlPtr {
url: self.url.clone(),
ptr,
})
}
pub(crate) fn resolve_fragment(&self, frag: &Fragment) -> Result<UrlPtr, CompileError> {
let res = self.resources.get("").ok_or(CompileError::Bug(
format!("no root resource found for {}", self.url).into(),
))?;
self.resolve_fragment_in(frag, res)
}
// resolves `UrlFrag` to `UrlPtr` from root.
// returns `None` if it is external.
pub(crate) fn resolve(&self, uf: &UrlFrag) -> Result<Option<UrlPtr>, CompileError> {
let res = {
if uf.url == self.url {
self.resources.get("").ok_or(CompileError::Bug(
format!("no root resource found for {}", self.url).into(),
))?
} else {
// look for resource with id==uf.url
let Some(res) = self.resources.values().find(|res| res.id == uf.url) else {
return Ok(None); // external url
};
res
}
};
self.resolve_fragment_in(&uf.frag, res).map(Some)
}
pub(crate) fn resource(&self, ptr: &JsonPointer) -> &Resource {
let mut ptr = ptr.as_str();
loop {
if let Some(res) = self.resources.get(ptr) {
return res;
}
let Some((prefix, _)) = ptr.rsplit_once('/') else {
break;
};
ptr = prefix;
}
self.resources.get("").expect("root resource should exist")
}
pub(crate) fn base_url(&self, ptr: &JsonPointer) -> &Url {
&self.resource(ptr).id
}
pub(crate) fn add_subschema(
&mut self,
doc: &Value,
ptr: &JsonPointer,
) -> Result<(), CompileError> {
let v = ptr.lookup(doc, &self.url)?;
let base_url = self.base_url(ptr).clone();
self.draft
.collect_resources(v, &base_url, ptr.clone(), &self.url, &mut self.resources)?;
// collect anchors
if !self.resources.contains_key(ptr) {
let res = self.resource(ptr);
if let Some(res) = self.resources.get_mut(&res.ptr.clone()) {
self.draft.collect_anchors(v, ptr, res, &self.url)?;
}
}
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct Resource {
pub(crate) ptr: JsonPointer, // from root
pub(crate) id: Url,
pub(crate) anchors: HashMap<Anchor, JsonPointer>, // anchor => ptr
pub(crate) dynamic_anchors: HashSet<Anchor>,
}
impl Resource {
pub(crate) fn new(ptr: JsonPointer, id: Url) -> Self {
Self {
ptr,
id,
anchors: HashMap::new(),
dynamic_anchors: HashSet::new(),
}
}
}

View File

@ -1,107 +0,0 @@
use std::collections::{HashMap, HashSet};
use crate::{compiler::CompileError, draft::*, loader::DefaultUrlLoader, root::Root, util::*};
use serde_json::Value;
use url::Url;
// --
pub(crate) struct Roots {
pub(crate) default_draft: &'static Draft,
map: HashMap<Url, Root>,
pub(crate) loader: DefaultUrlLoader,
}
impl Roots {
fn new() -> Self {
Self {
default_draft: latest(),
map: Default::default(),
loader: DefaultUrlLoader::new(),
}
}
}
impl Default for Roots {
fn default() -> Self {
Self::new()
}
}
impl Roots {
pub(crate) fn get(&self, url: &Url) -> Option<&Root> {
self.map.get(url)
}
pub(crate) fn resolve_fragment(&mut self, uf: UrlFrag) -> Result<UrlPtr, CompileError> {
self.or_load(uf.url.clone())?;
let Some(root) = self.map.get(&uf.url) else {
return Err(CompileError::Bug("or_load didn't add".into()));
};
root.resolve_fragment(&uf.frag)
}
pub(crate) fn ensure_subschema(&mut self, up: &UrlPtr) -> Result<(), CompileError> {
self.or_load(up.url.clone())?;
let Some(root) = self.map.get_mut(&up.url) else {
return Err(CompileError::Bug("or_load didn't add".into()));
};
if !root.draft.is_subschema(up.ptr.as_str()) {
let doc = self.loader.load(&root.url)?;
let v = up.ptr.lookup(doc, &up.url)?;
root.draft.validate(up, v)?;
root.add_subschema(doc, &up.ptr)?;
}
Ok(())
}
pub(crate) fn or_load(&mut self, url: Url) -> Result<(), CompileError> {
debug_assert!(url.fragment().is_none(), "trying to add root with fragment");
if self.map.contains_key(&url) {
return Ok(());
}
let doc = self.loader.load(&url)?;
let r = self.create_root(url.clone(), doc)?;
self.map.insert(url, r);
Ok(())
}
pub(crate) fn create_root(&self, url: Url, doc: &Value) -> Result<Root, CompileError> {
let draft = {
let up = UrlPtr {
url: url.clone(),
ptr: "".into(),
};
self.loader
.get_draft(&up, doc, self.default_draft, HashSet::new())?
};
let vocabs = self.loader.get_meta_vocabs(doc, draft)?;
let resources = {
let mut m = HashMap::default();
draft.collect_resources(doc, &url, "".into(), &url, &mut m)?;
m
};
if !matches!(url.host_str(), Some("json-schema.org")) {
draft.validate(
&UrlPtr {
url: url.clone(),
ptr: "".into(),
},
doc,
)?;
}
Ok(Root {
draft,
resources,
url: url.clone(),
meta_vocabs: vocabs,
})
}
pub(crate) fn insert(&mut self, roots: &mut HashMap<Url, Root>) {
self.map.extend(roots.drain());
}
}

View File

@ -1,545 +0,0 @@
use std::{
borrow::{Borrow, Cow},
fmt::Display,
hash::{Hash, Hasher},
str::FromStr,
};
use ahash::{AHashMap, AHasher};
use percent_encoding::{percent_decode_str, AsciiSet, CONTROLS};
use serde_json::Value;
use url::Url;
use crate::CompileError;
// --
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub(crate) struct JsonPointer(pub(crate) String);
impl JsonPointer {
pub(crate) fn escape(token: &str) -> Cow<'_, str> {
const SPECIAL: [char; 2] = ['~', '/'];
if token.contains(SPECIAL) {
token.replace('~', "~0").replace('/', "~1").into()
} else {
token.into()
}
}
pub(crate) fn unescape(mut tok: &str) -> Result<Cow<'_, str>, ()> {
let Some(mut tilde) = tok.find('~') else {
return Ok(Cow::Borrowed(tok));
};
let mut s = String::with_capacity(tok.len());
loop {
s.push_str(&tok[..tilde]);
tok = &tok[tilde + 1..];
match tok.chars().next() {
Some('1') => s.push('/'),
Some('0') => s.push('~'),
_ => return Err(()),
}
tok = &tok[1..];
let Some(i) = tok.find('~') else {
s.push_str(tok);
break;
};
tilde = i;
}
Ok(Cow::Owned(s))
}
pub(crate) fn lookup<'a>(
&self,
mut v: &'a Value,
v_url: &Url,
) -> Result<&'a Value, CompileError> {
for tok in self.0.split('/').skip(1) {
let Ok(tok) = Self::unescape(tok) else {
let loc = UrlFrag::format(v_url, self.as_str());
return Err(CompileError::InvalidJsonPointer(loc));
};
match v {
Value::Object(obj) => {
if let Some(pvalue) = obj.get(tok.as_ref()) {
v = pvalue;
continue;
}
}
Value::Array(arr) => {
if let Ok(i) = usize::from_str(tok.as_ref()) {
if let Some(item) = arr.get(i) {
v = item;
continue;
}
};
}
_ => {}
}
let loc = UrlFrag::format(v_url, self.as_str());
return Err(CompileError::JsonPointerNotFound(loc));
}
Ok(v)
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn concat(&self, next: &Self) -> Self {
JsonPointer(format!("{}{}", self.0, next.0))
}
pub(crate) fn append(&self, tok: &str) -> Self {
Self(format!("{}/{}", self, Self::escape(tok)))
}
pub(crate) fn append2(&self, tok1: &str, tok2: &str) -> Self {
Self(format!(
"{}/{}/{}",
self,
Self::escape(tok1),
Self::escape(tok2)
))
}
}
impl Display for JsonPointer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Borrow<str> for JsonPointer {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<&str> for JsonPointer {
fn from(value: &str) -> Self {
Self(value.into())
}
}
// --
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub(crate) struct Anchor(pub(crate) String);
impl Display for Anchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Borrow<str> for Anchor {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<&str> for Anchor {
fn from(value: &str) -> Self {
Self(value.into())
}
}
// --
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum Fragment {
Anchor(Anchor),
JsonPointer(JsonPointer),
}
impl Fragment {
pub(crate) fn split(s: &str) -> Result<(&str, Fragment), CompileError> {
let (u, frag) = split(s);
let frag = percent_decode_str(frag)
.decode_utf8()
.map_err(|src| CompileError::ParseUrlError {
url: s.to_string(),
src: src.into(),
})?
.to_string();
let frag = if frag.is_empty() || frag.starts_with('/') {
Fragment::JsonPointer(JsonPointer(frag))
} else {
Fragment::Anchor(Anchor(frag))
};
Ok((u, frag))
}
pub(crate) fn encode(frag: &str) -> String {
// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS
.add(b'%')
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
percent_encoding::utf8_percent_encode(frag, FRAGMENT).to_string()
}
pub(crate) fn as_str(&self) -> &str {
match self {
Fragment::Anchor(s) => &s.0,
Fragment::JsonPointer(s) => &s.0,
}
}
}
// --
#[derive(Clone)]
pub(crate) struct UrlFrag {
pub(crate) url: Url,
pub(crate) frag: Fragment,
}
impl UrlFrag {
pub(crate) fn absolute(input: &str) -> Result<UrlFrag, CompileError> {
let (u, frag) = Fragment::split(input)?;
// note: windows drive letter is treated as url scheme by url parser
#[cfg(not(target_arch = "wasm32"))]
if std::env::consts::OS == "windows" && starts_with_windows_drive(u) {
let url = Url::from_file_path(u)
.map_err(|_| CompileError::Bug(format!("failed to convert {u} into url").into()))?;
return Ok(UrlFrag { url, frag });
}
match Url::parse(u) {
Ok(url) => Ok(UrlFrag { url, frag }),
#[cfg(not(target_arch = "wasm32"))]
Err(url::ParseError::RelativeUrlWithoutBase) => {
let p = std::path::absolute(u).map_err(|e| CompileError::ParseUrlError {
url: u.to_owned(),
src: e.into(),
})?;
let url = Url::from_file_path(p).map_err(|_| {
CompileError::Bug(format!("failed to convert {u} into url").into())
})?;
Ok(UrlFrag { url, frag })
}
Err(e) => Err(CompileError::ParseUrlError {
url: u.to_owned(),
src: e.into(),
}),
}
}
pub(crate) fn join(url: &Url, input: &str) -> Result<UrlFrag, CompileError> {
let (input, frag) = Fragment::split(input)?;
if input.is_empty() {
return Ok(UrlFrag {
url: url.clone(),
frag,
});
}
let url = url.join(input).map_err(|e| CompileError::ParseUrlError {
url: input.to_string(),
src: e.into(),
})?;
Ok(UrlFrag { url, frag })
}
pub(crate) fn format(url: &Url, frag: &str) -> String {
if frag.is_empty() {
url.to_string()
} else {
format!("{}#{}", url, Fragment::encode(frag))
}
}
}
impl Display for UrlFrag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}#{}", self.url, Fragment::encode(self.frag.as_str()))
}
}
// --
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub(crate) struct UrlPtr {
pub(crate) url: Url,
pub(crate) ptr: JsonPointer,
}
impl UrlPtr {
pub(crate) fn lookup<'a>(&self, doc: &'a Value) -> Result<&'a Value, CompileError> {
self.ptr.lookup(doc, &self.url)
}
pub(crate) fn format(&self, tok: &str) -> String {
format!(
"{}#{}/{}",
self.url,
Fragment::encode(self.ptr.as_str()),
Fragment::encode(JsonPointer::escape(tok).as_ref()),
)
}
}
impl Display for UrlPtr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}#{}", self.url, Fragment::encode(self.ptr.as_str()))
}
}
// --
pub(crate) 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,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn starts_with_windows_drive(p: &str) -> bool {
p.chars().next().filter(char::is_ascii_uppercase).is_some() && p[1..].starts_with(":\\")
}
/// returns single-quoted string
pub(crate) fn quote<T>(s: &T) -> String
where
T: AsRef<str> + std::fmt::Debug + ?Sized,
{
let s = format!("{s:?}").replace(r#"\""#, "\"").replace('\'', r"\'");
format!("'{}'", &s[1..s.len() - 1])
}
pub(crate) fn join_iter<T>(iterable: T, sep: &str) -> String
where
T: IntoIterator,
T::Item: Display,
{
iterable
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(sep)
}
pub(crate) fn escape(token: &str) -> Cow<'_, str> {
JsonPointer::escape(token)
}
pub(crate) fn split(url: &str) -> (&str, &str) {
if let Some(i) = url.find('#') {
(&url[..i], &url[i + 1..])
} else {
(url, "")
}
}
/// serde_json treats 0 and 0.0 not equal. so we cannot simply use v1==v2
pub(crate) fn equals(v1: &Value, v2: &Value) -> bool {
match (v1, v2) {
(Value::Null, Value::Null) => true,
(Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
(Value::Number(n1), Value::Number(n2)) => {
if let (Some(n1), Some(n2)) = (n1.as_u64(), n2.as_u64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_i64(), n2.as_i64()) {
return n1 == n2;
}
if let (Some(n1), Some(n2)) = (n1.as_f64(), n2.as_f64()) {
return n1 == n2;
}
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,
}
}
pub(crate) fn duplicates(arr: &Vec<Value>) -> Option<(usize, usize)> {
match arr.as_slice() {
[e0, e1] => {
if equals(e0, e1) {
return Some((0, 1));
}
}
[e0, e1, e2] => {
if equals(e0, e1) {
return Some((0, 1));
} else if equals(e0, e2) {
return Some((0, 2));
} else if equals(e1, e2) {
return Some((1, 2));
}
}
_ => {
let len = arr.len();
if len <= 20 {
for i in 0..len - 1 {
for j in i + 1..len {
if equals(&arr[i], &arr[j]) {
return Some((i, j));
}
}
}
} else {
let mut seen = AHashMap::with_capacity(len);
for (i, item) in arr.iter().enumerate() {
if let Some(j) = seen.insert(HashedValue(item), i) {
return Some((j, i));
}
}
}
}
}
None
}
// HashedValue --
// Based on implementation proposed by Sven Marnach:
// https://stackoverflow.com/questions/60882381/what-is-the-fastest-correct-way-to-detect-that-there-are-no-duplicates-in-a-json
pub(crate) struct HashedValue<'a>(pub(crate) &'a Value);
impl PartialEq for HashedValue<'_> {
fn eq(&self, other: &Self) -> bool {
equals(self.0, other.0)
}
}
impl Eq for HashedValue<'_> {}
impl Hash for HashedValue<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self.0 {
Value::Null => state.write_u32(3_221_225_473), // chosen randomly
Value::Bool(b) => b.hash(state),
Value::Number(num) => {
if let Some(num) = num.as_f64() {
num.to_bits().hash(state);
} else if let Some(num) = num.as_u64() {
num.hash(state);
} else if let Some(num) = num.as_i64() {
num.hash(state);
}
}
Value::String(str) => str.hash(state),
Value::Array(arr) => {
for item in arr {
HashedValue(item).hash(state);
}
}
Value::Object(obj) => {
let mut hash = 0;
for (pname, pvalue) in obj {
// We have no way of building a new hasher of type `H`, so we
// hardcode using the default hasher of a hash map.
let mut hasher = AHasher::default();
pname.hash(&mut hasher);
HashedValue(pvalue).hash(&mut hasher);
hash ^= hasher.finish();
}
state.write_u64(hash);
}
}
}
}
#[cfg(test)]
mod tests {
use ahash::AHashMap;
use serde_json::json;
use super::*;
#[test]
fn test_quote() {
assert_eq!(quote(r#"abc"def'ghi"#), r#"'abc"def\'ghi'"#);
}
#[test]
fn test_fragment_split() {
let tests = [
("#", Fragment::JsonPointer("".into())),
("#/a/b", Fragment::JsonPointer("/a/b".into())),
("#abcd", Fragment::Anchor("abcd".into())),
("#%61%62%63%64", Fragment::Anchor("abcd".into())),
(
"#%2F%61%62%63%64%2fef",
Fragment::JsonPointer("/abcd/ef".into()),
), // '/' is encoded
("#abcd+ef", Fragment::Anchor("abcd+ef".into())), // '+' should not traslate to space
];
for test in tests {
let (_, got) = Fragment::split(test.0).unwrap();
assert_eq!(got, test.1, "Fragment::split({:?})", test.0);
}
}
#[test]
fn test_unescape() {
let tests = [
("bar~0", Some("bar~")),
("bar~1", Some("bar/")),
("bar~01", Some("bar~1")),
("bar~", None),
("bar~~", None),
];
for (tok, want) in tests {
let res = JsonPointer::unescape(tok).ok();
let got = res.as_ref().map(|c| c.as_ref());
assert_eq!(got, want, "unescape({:?})", tok)
}
}
#[test]
fn test_equals() {
let tests = [["1.0", "1"], ["-1.0", "-1"]];
for [a, b] in tests {
let a = serde_json::from_str(a).unwrap();
let b = serde_json::from_str(b).unwrap();
assert!(equals(&a, &b));
}
}
#[test]
fn test_hashed_value() {
let mut seen = AHashMap::with_capacity(10);
let (v1, v2) = (json!(2), json!(2.0));
assert!(equals(&v1, &v2));
assert!(seen.insert(HashedValue(&v1), 1).is_none());
assert!(seen.insert(HashedValue(&v2), 1).is_some());
}
}

Some files were not shown because too many files have changed in this diff Show More