Compare commits

..

12 Commits

35 changed files with 1397 additions and 6788 deletions

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

@ -0,0 +1,49 @@
---
description: jspg work preparation
---
This workflow will get you up-to-speed on the JSPG custom json-schema-based cargo pgrx postgres validation extension. Everything you read will be in the jspg directory/project.
Read over this entire workflow and commit to every section of work in a task list, so that you don't stop half way through before reviewing all of the directories and files mentioned. Do not ask for confirmation after generating this task list and proceed through all sections in your list.
Please analyze the files and directories and do not use cat, find, or the terminal to discover or read in any of these files. Analyze every file mentioned. If a directory is mentioned or a /*, please analyze the directory, every single file at its root, and recursively analyze every subdirectory and every single file in every subdirectory to capture not just critical files, but the entirety of what is requested. I state again, DO NOT just review a cherry picking of files in any folder or wildcard specified. Review 100% of all files discovered recursively!
Section 1: Documentation
- GEMINI.md at the root
Section 2: Flow file for cmd interface
- flow at the root
Section 3: Source
- src/*
Section 4: Test Fixtures
- Just review some of the *.json files in tests/fixtures/*
Section 5: Build
- build.rs
Section 6: Cargo TOML
- Cargo.toml
Section 7: Some PUNC Syntax
Now, review some punc type and enum source in the api project with api/ these files:
- punc/sql/tables.sql
- punc/sql/domains.sql
- punc/sql/indexes.sql
- punc/sql/functions/entity.sql
- punc/sql/functions/puncs.sql
- punc/sql/puncs/entity.sql
- punc/sql/puncs/persons.sql
- punc/sql/puncs/puncs.sql
- punc/sql/puncs/job.sql
Now you are ready to help me work on this extension.

View File

@ -18,7 +18,7 @@ The extension exposes the following functions to PostgreSQL:
### `cache_json_schemas(enums jsonb, types jsonb, puncs jsonb) -> jsonb`
Loads and compiles the entire schema registry into the session's memory.
Loads and compiles the entire schema registry into the session's memory, atomically replacing the previous validator.
* **Inputs**:
* `enums`: Array of enum definitions.
@ -31,6 +31,17 @@ Loads and compiles the entire schema registry into the session's memory.
* Compiles schemas into validators.
* **Returns**: `{"response": "success"}` or an error object.
### `mask_json_schema(schema_id text, instance jsonb) -> jsonb`
Validates a JSON instance and returns a new JSON object with unknown properties removed (pruned) based on the schema.
* **Inputs**:
* `schema_id`: The `$id` of the schema to mask against.
* `instance`: The JSON data to mask.
* **Returns**:
* On success: A `Drop` containing the **masked data**.
* On failure: A `Drop` containing validation errors.
### `validate_json_schema(schema_id text, instance jsonb) -> jsonb`
Validates a JSON instance against a pre-compiled schema.
@ -75,15 +86,25 @@ To support polymorphic fields (e.g., a field that accepts any "User" type), JSPG
### 3. Strict by Default & Extensibility
JSPG enforces a "Secure by Default" philosophy. All schemas are treated as if `unevaluatedProperties: false` (and `unevaluatedItems: false`) is set, unless explicitly overridden.
* **Strictness**: By default, any property 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.
* **Strictness**: By default, any property or array item in the instance data that is not explicitly defined in the schema causes a validation error. This prevents clients from sending undeclared fields or extra array elements.
* **Extensibility (`extensible: true`)**: To allow a free-for-all of additional, undefined properties or extra array items, you must add `"extensible": true` to the schema. This globally disables the strictness check for that object or array, useful for types designed to be completely open.
* **Structured Additional Properties (`additionalProperties: {...}`)**: Instead of a boolean free-for-all, you can define `additionalProperties` as a schema object (e.g., `{"type": "string"}`). This maintains strictness (no arbitrary keys) but allows any extra keys as long as their values match the defined structure.
* **Ref Boundaries**: Strictness is reset when crossing `$ref` boundaries. The referenced schema's strictness is determined by its own definition (strict by default unless `extensible: true`), ignoring the caller's state.
* **Inheritance**: Strictness is inherited. A schema extending a strict parent will also be strict unless it declares itself `extensible: true`. Conversely, a schema extending a loose parent will also be loose unless it declares itself `extensible: false`.
### 4. Format Leniency for Empty Strings
To simplify frontend form logic, the format validators for `uuid`, `date-time`, and `email` explicitly allow empty strings (`""`). This treats an empty string as "present but unset" rather than "invalid format".
### 5. Masking (Constructive Validation)
JSPG supports a "Constructive Validation" mode via `mask_json_schema`. This is designed for high-performance API responses where the schema dictates the exact shape of the returned data.
* **Mechanism**: The validator traverses the instance against the schema.
* **Valid Fields**: Kept in the output.
* **Unknown/Extra Fields**: Silently removed (pruned) if `extensible: false` (default).
* **Invalid Fields**: Still trigger standard validation errors.
This allows the database to return "raw" joined rows (e.g. `SELECT * FROM person JOIN organization ...`) and have JSPG automatically shape the result into the expected API response, removing any internal or unrelated columns not defined in the schema.
## 🏗️ Architecture
The extension is written in Rust using `pgrx` and structures its schema parser to mirror the Punc Generator's design:
@ -92,8 +113,18 @@ The extension is written in Rust using `pgrx` and structures its schema parser t
* **Compiler Phase**: schema JSONs are parsed into this struct, linked (references resolved), and then compiled into an efficient validation tree.
* **Validation Phase**: The compiled validators traverse the JSON instance using `serde_json::Value`.
### Concurrency & Threading ("Atomic Swap")
To support high-throughput validation while allowing for runtime schema updates (e.g., during development or hot-reloading), JSPG uses an **Atomic Swap** pattern.
1. **Immutable Validator**: The `Validator` struct immutably owns the `Registry`. Once created, a validator instance (and its registry) never changes.
2. **Global Pointer**: A global `RwLock<Option<Arc<Validator>>>` holds the current active validator.
3. **Lock-Free Reads**: Validation requests acquire a read lock just long enough to clone the `Arc` (incrementing a reference count), then release the lock immediately. Validation proceeds on the snapshot, ensuring no blocking during schema updates.
4. **Atomic Updates**: When schemas are reloaded (`cache_json_schemas`), a new `Registry` and `Validator` are built entirely on the stack. The global pointer is then atomically swapped to the new instance under a write lock.
## 🧪 Testing
Testing is driven by standard Rust unit tests that load JSON fixtures.
The tests are located in `tests/fixtures/*.json` and are executed via `cargo test`.
* **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`.

View File

@ -3,38 +3,38 @@ use std::fs::File;
use std::io::Write;
use std::path::Path;
fn to_safe_identifier(name: &str) -> String {
let mut safe = String::new();
for (i, c) in name.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
safe.push('_');
}
safe.push(c.to_ascii_lowercase());
} else if c == '-' || c == '.' {
safe.push('_');
} else {
safe.push(c);
}
}
safe
}
fn main() {
println!("cargo:rerun-if-changed=tests/fixtures");
println!("cargo:rerun-if-changed=Cargo.toml");
// File 1: src/tests.rs for #[pg_test]
let pg_dest_path = Path::new("src/tests.rs");
// File 1: src/tests/fixtures.rs for #[pg_test]
let pg_dest_path = Path::new("src/tests/fixtures.rs");
let mut pg_file = File::create(&pg_dest_path).unwrap();
// File 2: tests/tests.rs for standard #[test] integration
let std_dest_path = Path::new("tests/tests.rs");
// File 2: tests/fixtures.rs for standard #[test] integration
let std_dest_path = Path::new("tests/fixtures.rs");
let mut std_file = File::create(&std_dest_path).unwrap();
// Write headers
writeln!(std_file, "use jspg::util;").unwrap();
// 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() {
@ -51,24 +51,7 @@ fn main() {
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 safe_filename = to_safe_identifier(file_name);
let fn_name = format!("test_{}_{}", safe_filename, i);
// Write to src/tests.rs (PG Test)

813
debug.log
View File

@ -1,813 +0,0 @@
warning: function `test_uniqueItems_0` should have a snake case name
--> tests/tests.rs:52:4
|
52 | fn test_uniqueItems_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_0`
|
= note: `#[warn(non_snake_case)]` (part of `#[warn(nonstandard_style)]`) on by default
warning: function `test_uniqueItems_1` should have a snake case name
--> tests/tests.rs:58:4
|
58 | fn test_uniqueItems_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_1`
warning: function `test_uniqueItems_2` should have a snake case name
--> tests/tests.rs:64:4
|
64 | fn test_uniqueItems_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_2`
warning: function `test_uniqueItems_3` should have a snake case name
--> tests/tests.rs:70:4
|
70 | fn test_uniqueItems_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_3`
warning: function `test_uniqueItems_4` should have a snake case name
--> tests/tests.rs:76:4
|
76 | fn test_uniqueItems_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_4`
warning: function `test_uniqueItems_5` should have a snake case name
--> tests/tests.rs:82:4
|
82 | fn test_uniqueItems_5() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_5`
warning: function `test_uniqueItems_6` should have a snake case name
--> tests/tests.rs:88:4
|
88 | fn test_uniqueItems_6() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_6`
warning: function `test_minItems_0` should have a snake case name
--> tests/tests.rs:94:4
|
94 | fn test_minItems_0() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_0`
warning: function `test_minItems_1` should have a snake case name
--> tests/tests.rs:100:4
|
100 | fn test_minItems_1() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_1`
warning: function `test_minItems_2` should have a snake case name
--> tests/tests.rs:106:4
|
106 | fn test_minItems_2() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_2`
warning: function `test_exclusiveMinimum_0` should have a snake case name
--> tests/tests.rs:160:4
|
160 | fn test_exclusiveMinimum_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_exclusive_minimum_0`
warning: function `test_anyOf_0` should have a snake case name
--> tests/tests.rs:274:4
|
274 | fn test_anyOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_0`
warning: function `test_anyOf_1` should have a snake case name
--> tests/tests.rs:280:4
|
280 | fn test_anyOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_1`
warning: function `test_anyOf_2` should have a snake case name
--> tests/tests.rs:286:4
|
286 | fn test_anyOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_2`
warning: function `test_anyOf_3` should have a snake case name
--> tests/tests.rs:292:4
|
292 | fn test_anyOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_3`
warning: function `test_anyOf_4` should have a snake case name
--> tests/tests.rs:298:4
|
298 | fn test_anyOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_4`
warning: function `test_anyOf_5` should have a snake case name
--> tests/tests.rs:304:4
|
304 | fn test_anyOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_5`
warning: function `test_anyOf_6` should have a snake case name
--> tests/tests.rs:310:4
|
310 | fn test_anyOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_6`
warning: function `test_anyOf_7` should have a snake case name
--> tests/tests.rs:316:4
|
316 | fn test_anyOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_7`
warning: function `test_anyOf_8` should have a snake case name
--> tests/tests.rs:322:4
|
322 | fn test_anyOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_8`
warning: function `test_anyOf_9` should have a snake case name
--> tests/tests.rs:328:4
|
328 | fn test_anyOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_9`
warning: function `test_propertyNames_0` should have a snake case name
--> tests/tests.rs:334:4
|
334 | fn test_propertyNames_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_0`
warning: function `test_propertyNames_1` should have a snake case name
--> tests/tests.rs:340:4
|
340 | fn test_propertyNames_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_1`
warning: function `test_propertyNames_2` should have a snake case name
--> tests/tests.rs:346:4
|
346 | fn test_propertyNames_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_2`
warning: function `test_propertyNames_3` should have a snake case name
--> tests/tests.rs:352:4
|
352 | fn test_propertyNames_3() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_3`
warning: function `test_propertyNames_4` should have a snake case name
--> tests/tests.rs:358:4
|
358 | fn test_propertyNames_4() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_4`
warning: function `test_propertyNames_5` should have a snake case name
--> tests/tests.rs:364:4
|
364 | fn test_propertyNames_5() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_5`
warning: function `test_propertyNames_6` should have a snake case name
--> tests/tests.rs:370:4
|
370 | fn test_propertyNames_6() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_6`
warning: function `test_minProperties_0` should have a snake case name
--> tests/tests.rs:646:4
|
646 | fn test_minProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_0`
warning: function `test_minProperties_1` should have a snake case name
--> tests/tests.rs:652:4
|
652 | fn test_minProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_1`
warning: function `test_minProperties_2` should have a snake case name
--> tests/tests.rs:658:4
|
658 | fn test_minProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_2`
warning: function `test_minContains_0` should have a snake case name
--> tests/tests.rs:664:4
|
664 | fn test_minContains_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_0`
warning: function `test_minContains_1` should have a snake case name
--> tests/tests.rs:670:4
|
670 | fn test_minContains_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_1`
warning: function `test_minContains_2` should have a snake case name
--> tests/tests.rs:676:4
|
676 | fn test_minContains_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_2`
warning: function `test_minContains_3` should have a snake case name
--> tests/tests.rs:682:4
|
682 | fn test_minContains_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_3`
warning: function `test_minContains_4` should have a snake case name
--> tests/tests.rs:688:4
|
688 | fn test_minContains_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_4`
warning: function `test_minContains_5` should have a snake case name
--> tests/tests.rs:694:4
|
694 | fn test_minContains_5() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_5`
warning: function `test_minContains_6` should have a snake case name
--> tests/tests.rs:700:4
|
700 | fn test_minContains_6() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_6`
warning: function `test_minContains_7` should have a snake case name
--> tests/tests.rs:706:4
|
706 | fn test_minContains_7() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_7`
warning: function `test_minContains_8` should have a snake case name
--> tests/tests.rs:712:4
|
712 | fn test_minContains_8() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_8`
warning: function `test_maxContains_0` should have a snake case name
--> tests/tests.rs:796:4
|
796 | fn test_maxContains_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_0`
warning: function `test_maxContains_1` should have a snake case name
--> tests/tests.rs:802:4
|
802 | fn test_maxContains_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_1`
warning: function `test_maxContains_2` should have a snake case name
--> tests/tests.rs:808:4
|
808 | fn test_maxContains_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_2`
warning: function `test_maxContains_3` should have a snake case name
--> tests/tests.rs:814:4
|
814 | fn test_maxContains_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_3`
warning: function `test_maxContains_4` should have a snake case name
--> tests/tests.rs:820:4
|
820 | fn test_maxContains_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_4`
warning: function `test_maxLength_0` should have a snake case name
--> tests/tests.rs:826:4
|
826 | fn test_maxLength_0() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_length_0`
warning: function `test_maxLength_1` should have a snake case name
--> tests/tests.rs:832:4
|
832 | fn test_maxLength_1() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_length_1`
warning: function `test_dependentSchemas_0` should have a snake case name
--> tests/tests.rs:838:4
|
838 | fn test_dependentSchemas_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_0`
warning: function `test_dependentSchemas_1` should have a snake case name
--> tests/tests.rs:844:4
|
844 | fn test_dependentSchemas_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_1`
warning: function `test_dependentSchemas_2` should have a snake case name
--> tests/tests.rs:850:4
|
850 | fn test_dependentSchemas_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_2`
warning: function `test_dependentSchemas_3` should have a snake case name
--> tests/tests.rs:856:4
|
856 | fn test_dependentSchemas_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_3`
warning: function `test_exclusiveMaximum_0` should have a snake case name
--> tests/tests.rs:862:4
|
862 | fn test_exclusiveMaximum_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_exclusive_maximum_0`
warning: function `test_prefixItems_0` should have a snake case name
--> tests/tests.rs:868:4
|
868 | fn test_prefixItems_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_0`
warning: function `test_prefixItems_1` should have a snake case name
--> tests/tests.rs:874:4
|
874 | fn test_prefixItems_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_1`
warning: function `test_prefixItems_2` should have a snake case name
--> tests/tests.rs:880:4
|
880 | fn test_prefixItems_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_2`
warning: function `test_prefixItems_3` should have a snake case name
--> tests/tests.rs:886:4
|
886 | fn test_prefixItems_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_3`
warning: function `test_prefixItems_4` should have a snake case name
--> tests/tests.rs:892:4
|
892 | fn test_prefixItems_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_4`
warning: function `test_oneOf_0` should have a snake case name
--> tests/tests.rs:910:4
|
910 | fn test_oneOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_0`
warning: function `test_oneOf_1` should have a snake case name
--> tests/tests.rs:916:4
|
916 | fn test_oneOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_1`
warning: function `test_oneOf_2` should have a snake case name
--> tests/tests.rs:922:4
|
922 | fn test_oneOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_2`
warning: function `test_oneOf_3` should have a snake case name
--> tests/tests.rs:928:4
|
928 | fn test_oneOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_3`
warning: function `test_oneOf_4` should have a snake case name
--> tests/tests.rs:934:4
|
934 | fn test_oneOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_4`
warning: function `test_oneOf_5` should have a snake case name
--> tests/tests.rs:940:4
|
940 | fn test_oneOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_5`
warning: function `test_oneOf_6` should have a snake case name
--> tests/tests.rs:946:4
|
946 | fn test_oneOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_6`
warning: function `test_oneOf_7` should have a snake case name
--> tests/tests.rs:952:4
|
952 | fn test_oneOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_7`
warning: function `test_oneOf_8` should have a snake case name
--> tests/tests.rs:958:4
|
958 | fn test_oneOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_8`
warning: function `test_oneOf_9` should have a snake case name
--> tests/tests.rs:964:4
|
964 | fn test_oneOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_9`
warning: function `test_oneOf_10` should have a snake case name
--> tests/tests.rs:970:4
|
970 | fn test_oneOf_10() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_10`
warning: function `test_oneOf_11` should have a snake case name
--> tests/tests.rs:976:4
|
976 | fn test_oneOf_11() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_11`
warning: function `test_oneOf_12` should have a snake case name
--> tests/tests.rs:982:4
|
982 | fn test_oneOf_12() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_12`
warning: function `test_emptyString_0` should have a snake case name
--> tests/tests.rs:1072:4
|
1072 | fn test_emptyString_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_empty_string_0`
warning: function `test_maxProperties_0` should have a snake case name
--> tests/tests.rs:1090:4
|
1090 | fn test_maxProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_0`
warning: function `test_maxProperties_1` should have a snake case name
--> tests/tests.rs:1096:4
|
1096 | fn test_maxProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_1`
warning: function `test_maxProperties_2` should have a snake case name
--> tests/tests.rs:1102:4
|
1102 | fn test_maxProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_2`
warning: function `test_maxProperties_3` should have a snake case name
--> tests/tests.rs:1108:4
|
1108 | fn test_maxProperties_3() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_3`
warning: function `test_dependentRequired_0` should have a snake case name
--> tests/tests.rs:1114:4
|
1114 | fn test_dependentRequired_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_0`
warning: function `test_dependentRequired_1` should have a snake case name
--> tests/tests.rs:1120:4
|
1120 | fn test_dependentRequired_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_1`
warning: function `test_dependentRequired_2` should have a snake case name
--> tests/tests.rs:1126:4
|
1126 | fn test_dependentRequired_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_2`
warning: function `test_dependentRequired_3` should have a snake case name
--> tests/tests.rs:1132:4
|
1132 | fn test_dependentRequired_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_3`
warning: function `test_dependentRequired_4` should have a snake case name
--> tests/tests.rs:1138:4
|
1138 | fn test_dependentRequired_4() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_4`
warning: function `test_multipleOf_0` should have a snake case name
--> tests/tests.rs:1252:4
|
1252 | fn test_multipleOf_0() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_0`
warning: function `test_multipleOf_1` should have a snake case name
--> tests/tests.rs:1258:4
|
1258 | fn test_multipleOf_1() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_1`
warning: function `test_multipleOf_2` should have a snake case name
--> tests/tests.rs:1264:4
|
1264 | fn test_multipleOf_2() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_2`
warning: function `test_multipleOf_3` should have a snake case name
--> tests/tests.rs:1270:4
|
1270 | fn test_multipleOf_3() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_3`
warning: function `test_patternProperties_0` should have a snake case name
--> tests/tests.rs:1276:4
|
1276 | fn test_patternProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_0`
warning: function `test_patternProperties_1` should have a snake case name
--> tests/tests.rs:1282:4
|
1282 | fn test_patternProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_1`
warning: function `test_patternProperties_2` should have a snake case name
--> tests/tests.rs:1288:4
|
1288 | fn test_patternProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_2`
warning: function `test_patternProperties_3` should have a snake case name
--> tests/tests.rs:1294:4
|
1294 | fn test_patternProperties_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_3`
warning: function `test_patternProperties_4` should have a snake case name
--> tests/tests.rs:1300:4
|
1300 | fn test_patternProperties_4() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_4`
warning: function `test_patternProperties_5` should have a snake case name
--> tests/tests.rs:1306:4
|
1306 | fn test_patternProperties_5() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_5`
warning: function `test_allOf_0` should have a snake case name
--> tests/tests.rs:1336:4
|
1336 | fn test_allOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_0`
warning: function `test_allOf_1` should have a snake case name
--> tests/tests.rs:1342:4
|
1342 | fn test_allOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_1`
warning: function `test_allOf_2` should have a snake case name
--> tests/tests.rs:1348:4
|
1348 | fn test_allOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_2`
warning: function `test_allOf_3` should have a snake case name
--> tests/tests.rs:1354:4
|
1354 | fn test_allOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_3`
warning: function `test_allOf_4` should have a snake case name
--> tests/tests.rs:1360:4
|
1360 | fn test_allOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_4`
warning: function `test_allOf_5` should have a snake case name
--> tests/tests.rs:1366:4
|
1366 | fn test_allOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_5`
warning: function `test_allOf_6` should have a snake case name
--> tests/tests.rs:1372:4
|
1372 | fn test_allOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_6`
warning: function `test_allOf_7` should have a snake case name
--> tests/tests.rs:1378:4
|
1378 | fn test_allOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_7`
warning: function `test_allOf_8` should have a snake case name
--> tests/tests.rs:1384:4
|
1384 | fn test_allOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_8`
warning: function `test_allOf_9` should have a snake case name
--> tests/tests.rs:1390:4
|
1390 | fn test_allOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_9`
warning: function `test_allOf_10` should have a snake case name
--> tests/tests.rs:1396:4
|
1396 | fn test_allOf_10() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_10`
warning: function `test_allOf_11` should have a snake case name
--> tests/tests.rs:1402:4
|
1402 | fn test_allOf_11() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_11`
warning: function `test_allOf_12` should have a snake case name
--> tests/tests.rs:1408:4
|
1408 | fn test_allOf_12() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_12`
warning: function `test_allOf_13` should have a snake case name
--> tests/tests.rs:1414:4
|
1414 | fn test_allOf_13() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_13`
warning: function `test_allOf_14` should have a snake case name
--> tests/tests.rs:1420:4
|
1420 | fn test_allOf_14() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_14`
warning: function `test_allOf_15` should have a snake case name
--> tests/tests.rs:1426:4
|
1426 | fn test_allOf_15() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_15`
warning: function `test_minLength_0` should have a snake case name
--> tests/tests.rs:1828:4
|
1828 | fn test_minLength_0() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_length_0`
warning: function `test_minLength_1` should have a snake case name
--> tests/tests.rs:1834:4
|
1834 | fn test_minLength_1() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_length_1`
warning: function `test_maxItems_0` should have a snake case name
--> tests/tests.rs:1840:4
|
1840 | fn test_maxItems_0() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_0`
warning: function `test_maxItems_1` should have a snake case name
--> tests/tests.rs:1846:4
|
1846 | fn test_maxItems_1() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_1`
warning: function `test_maxItems_2` should have a snake case name
--> tests/tests.rs:1852:4
|
1852 | fn test_maxItems_2() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_2`
warning: function `test_dynamicRef_0` should have a snake case name
--> tests/tests.rs:1912:4
|
1912 | fn test_dynamicRef_0() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_0`
warning: function `test_dynamicRef_1` should have a snake case name
--> tests/tests.rs:1918:4
|
1918 | fn test_dynamicRef_1() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_1`
warning: function `test_dynamicRef_2` should have a snake case name
--> tests/tests.rs:1924:4
|
1924 | fn test_dynamicRef_2() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_2`
warning: function `test_dynamicRef_3` should have a snake case name
--> tests/tests.rs:1930:4
|
1930 | fn test_dynamicRef_3() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_3`
warning: function `test_dynamicRef_4` should have a snake case name
--> tests/tests.rs:1936:4
|
1936 | fn test_dynamicRef_4() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_4`
warning: function `test_dynamicRef_5` should have a snake case name
--> tests/tests.rs:1942:4
|
1942 | fn test_dynamicRef_5() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_5`
warning: function `test_dynamicRef_6` should have a snake case name
--> tests/tests.rs:1948:4
|
1948 | fn test_dynamicRef_6() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_6`
warning: function `test_dynamicRef_7` should have a snake case name
--> tests/tests.rs:1954:4
|
1954 | fn test_dynamicRef_7() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_7`
warning: function `test_dynamicRef_8` should have a snake case name
--> tests/tests.rs:1960:4
|
1960 | fn test_dynamicRef_8() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_8`
warning: function `test_dynamicRef_9` should have a snake case name
--> tests/tests.rs:1966:4
|
1966 | fn test_dynamicRef_9() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_9`
warning: function `test_dynamicRef_10` should have a snake case name
--> tests/tests.rs:1972:4
|
1972 | fn test_dynamicRef_10() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_10`
warning: function `test_dynamicRef_11` should have a snake case name
--> tests/tests.rs:1978:4
|
1978 | fn test_dynamicRef_11() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_11`
warning: function `test_dynamicRef_12` should have a snake case name
--> tests/tests.rs:1984:4
|
1984 | fn test_dynamicRef_12() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_12`
warning: function `test_dynamicRef_13` should have a snake case name
--> tests/tests.rs:1990:4
|
1990 | fn test_dynamicRef_13() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_13`
warning: function `test_dynamicRef_14` should have a snake case name
--> tests/tests.rs:1996:4
|
1996 | fn test_dynamicRef_14() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_14`
warning: function `test_dynamicRef_15` should have a snake case name
--> tests/tests.rs:2002:4
|
2002 | fn test_dynamicRef_15() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_15`
warning: function `test_dynamicRef_16` should have a snake case name
--> tests/tests.rs:2008:4
|
2008 | fn test_dynamicRef_16() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_16`
warning: function `test_dynamicRef_17` should have a snake case name
--> tests/tests.rs:2014:4
|
2014 | fn test_dynamicRef_17() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_17`
warning: function `test_dynamicRef_18` should have a snake case name
--> tests/tests.rs:2020:4
|
2020 | fn test_dynamicRef_18() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_18`
warning: function `test_dynamicRef_19` should have a snake case name
--> tests/tests.rs:2026:4
|
2026 | fn test_dynamicRef_19() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_19`
warning: function `test_dynamicRef_20` should have a snake case name
--> tests/tests.rs:2032:4
|
2032 | fn test_dynamicRef_20() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_20`
warning: `jspg` (test "tests") generated 132 warnings
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.42s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
thread 'test_ref_39' (14864151) panicked at tests/tests.rs:1812:45:
called `Result::unwrap()` on an `Err` value: "[implicit keyword shadowing] Test 'child type overrides parent type' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'age'\", details: ErrorDetails { path: \"/age\" } }]\n[implicit keyword shadowing] Test 'parent max age (20) is shadowed (replaced) by child definition' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'age'\", details: ErrorDetails { path: \"/age\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_ref_39 ... FAILED
failures:
failures:
test_ref_39
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.01s
error: test failed, to rerun pass `--test tests`

View File

@ -1,44 +0,0 @@
Blocking waiting for file lock on artifact directory
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
error[E0424]: expected value, found module `self`
--> src/util.rs:162:33
|
40 | pub fn run_test_file_at_index(path: &str, index: usi...
| ---------------------- this function can't have a `self` parameter
...
162 | let mut new_overrides = self.overrides.clone();
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
error[E0424]: expected value, found module `self`
--> src/util.rs:164:31
|
40 | pub fn run_test_file_at_index(path: &str, index: usi...
| ---------------------- this function can't have a `self` parameter
...
164 | if let Some(props) = &self.schema.properties {
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
error[E0282]: type annotations needed
--> src/util.rs:166:32
|
166 | new_overrides.extend(props.keys().cloned());
| ^^^^^ cannot infer type
error[E0599]: no method named `is_valid` found for struct `drop::Drop` in the current scope
--> src/util.rs:204:18
|
204 | ...ult.is_valid(), // Use is_valid() for clear "Got"...
| ^^^^^^^^ method not found in `drop::Drop`
|
::: src/drop.rs:5:1
|
5 | pub struct Drop {
| --------------- method `is_valid` not found for this struct
|
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `is_valid`, perhaps you need to implement it:
candidate #1: `NullLayout`
Some errors have detailed explanations: E0282, E0424, E0599.
For more information about an error, try `rustc --explain E0282`.
error: could not compile `jspg` (lib) due to 4 previous errors

View File

@ -1,815 +0,0 @@
Blocking waiting for file lock on artifact directory
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
warning: function `test_uniqueItems_0` should have a snake case name
--> tests/tests.rs:52:4
|
52 | fn test_uniqueItems_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_0`
|
= note: `#[warn(non_snake_case)]` (part of `#[warn(nonstandard_style)]`) on by default
warning: function `test_uniqueItems_1` should have a snake case name
--> tests/tests.rs:58:4
|
58 | fn test_uniqueItems_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_1`
warning: function `test_uniqueItems_2` should have a snake case name
--> tests/tests.rs:64:4
|
64 | fn test_uniqueItems_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_2`
warning: function `test_uniqueItems_3` should have a snake case name
--> tests/tests.rs:70:4
|
70 | fn test_uniqueItems_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_3`
warning: function `test_uniqueItems_4` should have a snake case name
--> tests/tests.rs:76:4
|
76 | fn test_uniqueItems_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_4`
warning: function `test_uniqueItems_5` should have a snake case name
--> tests/tests.rs:82:4
|
82 | fn test_uniqueItems_5() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_5`
warning: function `test_uniqueItems_6` should have a snake case name
--> tests/tests.rs:88:4
|
88 | fn test_uniqueItems_6() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_unique_items_6`
warning: function `test_minItems_0` should have a snake case name
--> tests/tests.rs:94:4
|
94 | fn test_minItems_0() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_0`
warning: function `test_minItems_1` should have a snake case name
--> tests/tests.rs:100:4
|
100 | fn test_minItems_1() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_1`
warning: function `test_minItems_2` should have a snake case name
--> tests/tests.rs:106:4
|
106 | fn test_minItems_2() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_items_2`
warning: function `test_exclusiveMinimum_0` should have a snake case name
--> tests/tests.rs:160:4
|
160 | fn test_exclusiveMinimum_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_exclusive_minimum_0`
warning: function `test_anyOf_0` should have a snake case name
--> tests/tests.rs:274:4
|
274 | fn test_anyOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_0`
warning: function `test_anyOf_1` should have a snake case name
--> tests/tests.rs:280:4
|
280 | fn test_anyOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_1`
warning: function `test_anyOf_2` should have a snake case name
--> tests/tests.rs:286:4
|
286 | fn test_anyOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_2`
warning: function `test_anyOf_3` should have a snake case name
--> tests/tests.rs:292:4
|
292 | fn test_anyOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_3`
warning: function `test_anyOf_4` should have a snake case name
--> tests/tests.rs:298:4
|
298 | fn test_anyOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_4`
warning: function `test_anyOf_5` should have a snake case name
--> tests/tests.rs:304:4
|
304 | fn test_anyOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_5`
warning: function `test_anyOf_6` should have a snake case name
--> tests/tests.rs:310:4
|
310 | fn test_anyOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_6`
warning: function `test_anyOf_7` should have a snake case name
--> tests/tests.rs:316:4
|
316 | fn test_anyOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_7`
warning: function `test_anyOf_8` should have a snake case name
--> tests/tests.rs:322:4
|
322 | fn test_anyOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_8`
warning: function `test_anyOf_9` should have a snake case name
--> tests/tests.rs:328:4
|
328 | fn test_anyOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_any_of_9`
warning: function `test_propertyNames_0` should have a snake case name
--> tests/tests.rs:334:4
|
334 | fn test_propertyNames_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_0`
warning: function `test_propertyNames_1` should have a snake case name
--> tests/tests.rs:340:4
|
340 | fn test_propertyNames_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_1`
warning: function `test_propertyNames_2` should have a snake case name
--> tests/tests.rs:346:4
|
346 | fn test_propertyNames_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_2`
warning: function `test_propertyNames_3` should have a snake case name
--> tests/tests.rs:352:4
|
352 | fn test_propertyNames_3() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_3`
warning: function `test_propertyNames_4` should have a snake case name
--> tests/tests.rs:358:4
|
358 | fn test_propertyNames_4() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_4`
warning: function `test_propertyNames_5` should have a snake case name
--> tests/tests.rs:364:4
|
364 | fn test_propertyNames_5() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_5`
warning: function `test_propertyNames_6` should have a snake case name
--> tests/tests.rs:370:4
|
370 | fn test_propertyNames_6() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_property_names_6`
warning: function `test_minProperties_0` should have a snake case name
--> tests/tests.rs:646:4
|
646 | fn test_minProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_0`
warning: function `test_minProperties_1` should have a snake case name
--> tests/tests.rs:652:4
|
652 | fn test_minProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_1`
warning: function `test_minProperties_2` should have a snake case name
--> tests/tests.rs:658:4
|
658 | fn test_minProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_properties_2`
warning: function `test_minContains_0` should have a snake case name
--> tests/tests.rs:664:4
|
664 | fn test_minContains_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_0`
warning: function `test_minContains_1` should have a snake case name
--> tests/tests.rs:670:4
|
670 | fn test_minContains_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_1`
warning: function `test_minContains_2` should have a snake case name
--> tests/tests.rs:676:4
|
676 | fn test_minContains_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_2`
warning: function `test_minContains_3` should have a snake case name
--> tests/tests.rs:682:4
|
682 | fn test_minContains_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_3`
warning: function `test_minContains_4` should have a snake case name
--> tests/tests.rs:688:4
|
688 | fn test_minContains_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_4`
warning: function `test_minContains_5` should have a snake case name
--> tests/tests.rs:694:4
|
694 | fn test_minContains_5() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_5`
warning: function `test_minContains_6` should have a snake case name
--> tests/tests.rs:700:4
|
700 | fn test_minContains_6() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_6`
warning: function `test_minContains_7` should have a snake case name
--> tests/tests.rs:706:4
|
706 | fn test_minContains_7() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_7`
warning: function `test_minContains_8` should have a snake case name
--> tests/tests.rs:712:4
|
712 | fn test_minContains_8() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_contains_8`
warning: function `test_maxContains_0` should have a snake case name
--> tests/tests.rs:796:4
|
796 | fn test_maxContains_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_0`
warning: function `test_maxContains_1` should have a snake case name
--> tests/tests.rs:802:4
|
802 | fn test_maxContains_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_1`
warning: function `test_maxContains_2` should have a snake case name
--> tests/tests.rs:808:4
|
808 | fn test_maxContains_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_2`
warning: function `test_maxContains_3` should have a snake case name
--> tests/tests.rs:814:4
|
814 | fn test_maxContains_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_3`
warning: function `test_maxContains_4` should have a snake case name
--> tests/tests.rs:820:4
|
820 | fn test_maxContains_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_contains_4`
warning: function `test_maxLength_0` should have a snake case name
--> tests/tests.rs:826:4
|
826 | fn test_maxLength_0() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_length_0`
warning: function `test_maxLength_1` should have a snake case name
--> tests/tests.rs:832:4
|
832 | fn test_maxLength_1() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_length_1`
warning: function `test_dependentSchemas_0` should have a snake case name
--> tests/tests.rs:838:4
|
838 | fn test_dependentSchemas_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_0`
warning: function `test_dependentSchemas_1` should have a snake case name
--> tests/tests.rs:844:4
|
844 | fn test_dependentSchemas_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_1`
warning: function `test_dependentSchemas_2` should have a snake case name
--> tests/tests.rs:850:4
|
850 | fn test_dependentSchemas_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_2`
warning: function `test_dependentSchemas_3` should have a snake case name
--> tests/tests.rs:856:4
|
856 | fn test_dependentSchemas_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_schemas_3`
warning: function `test_exclusiveMaximum_0` should have a snake case name
--> tests/tests.rs:862:4
|
862 | fn test_exclusiveMaximum_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_exclusive_maximum_0`
warning: function `test_prefixItems_0` should have a snake case name
--> tests/tests.rs:868:4
|
868 | fn test_prefixItems_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_0`
warning: function `test_prefixItems_1` should have a snake case name
--> tests/tests.rs:874:4
|
874 | fn test_prefixItems_1() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_1`
warning: function `test_prefixItems_2` should have a snake case name
--> tests/tests.rs:880:4
|
880 | fn test_prefixItems_2() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_2`
warning: function `test_prefixItems_3` should have a snake case name
--> tests/tests.rs:886:4
|
886 | fn test_prefixItems_3() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_3`
warning: function `test_prefixItems_4` should have a snake case name
--> tests/tests.rs:892:4
|
892 | fn test_prefixItems_4() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_prefix_items_4`
warning: function `test_oneOf_0` should have a snake case name
--> tests/tests.rs:910:4
|
910 | fn test_oneOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_0`
warning: function `test_oneOf_1` should have a snake case name
--> tests/tests.rs:916:4
|
916 | fn test_oneOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_1`
warning: function `test_oneOf_2` should have a snake case name
--> tests/tests.rs:922:4
|
922 | fn test_oneOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_2`
warning: function `test_oneOf_3` should have a snake case name
--> tests/tests.rs:928:4
|
928 | fn test_oneOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_3`
warning: function `test_oneOf_4` should have a snake case name
--> tests/tests.rs:934:4
|
934 | fn test_oneOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_4`
warning: function `test_oneOf_5` should have a snake case name
--> tests/tests.rs:940:4
|
940 | fn test_oneOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_5`
warning: function `test_oneOf_6` should have a snake case name
--> tests/tests.rs:946:4
|
946 | fn test_oneOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_6`
warning: function `test_oneOf_7` should have a snake case name
--> tests/tests.rs:952:4
|
952 | fn test_oneOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_7`
warning: function `test_oneOf_8` should have a snake case name
--> tests/tests.rs:958:4
|
958 | fn test_oneOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_8`
warning: function `test_oneOf_9` should have a snake case name
--> tests/tests.rs:964:4
|
964 | fn test_oneOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_9`
warning: function `test_oneOf_10` should have a snake case name
--> tests/tests.rs:970:4
|
970 | fn test_oneOf_10() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_10`
warning: function `test_oneOf_11` should have a snake case name
--> tests/tests.rs:976:4
|
976 | fn test_oneOf_11() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_11`
warning: function `test_oneOf_12` should have a snake case name
--> tests/tests.rs:982:4
|
982 | fn test_oneOf_12() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_one_of_12`
warning: function `test_emptyString_0` should have a snake case name
--> tests/tests.rs:1072:4
|
1072 | fn test_emptyString_0() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_empty_string_0`
warning: function `test_maxProperties_0` should have a snake case name
--> tests/tests.rs:1090:4
|
1090 | fn test_maxProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_0`
warning: function `test_maxProperties_1` should have a snake case name
--> tests/tests.rs:1096:4
|
1096 | fn test_maxProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_1`
warning: function `test_maxProperties_2` should have a snake case name
--> tests/tests.rs:1102:4
|
1102 | fn test_maxProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_2`
warning: function `test_maxProperties_3` should have a snake case name
--> tests/tests.rs:1108:4
|
1108 | fn test_maxProperties_3() {
| ^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_properties_3`
warning: function `test_dependentRequired_0` should have a snake case name
--> tests/tests.rs:1114:4
|
1114 | fn test_dependentRequired_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_0`
warning: function `test_dependentRequired_1` should have a snake case name
--> tests/tests.rs:1120:4
|
1120 | fn test_dependentRequired_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_1`
warning: function `test_dependentRequired_2` should have a snake case name
--> tests/tests.rs:1126:4
|
1126 | fn test_dependentRequired_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_2`
warning: function `test_dependentRequired_3` should have a snake case name
--> tests/tests.rs:1132:4
|
1132 | fn test_dependentRequired_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_3`
warning: function `test_dependentRequired_4` should have a snake case name
--> tests/tests.rs:1138:4
|
1138 | fn test_dependentRequired_4() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dependent_required_4`
warning: function `test_multipleOf_0` should have a snake case name
--> tests/tests.rs:1252:4
|
1252 | fn test_multipleOf_0() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_0`
warning: function `test_multipleOf_1` should have a snake case name
--> tests/tests.rs:1258:4
|
1258 | fn test_multipleOf_1() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_1`
warning: function `test_multipleOf_2` should have a snake case name
--> tests/tests.rs:1264:4
|
1264 | fn test_multipleOf_2() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_2`
warning: function `test_multipleOf_3` should have a snake case name
--> tests/tests.rs:1270:4
|
1270 | fn test_multipleOf_3() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_multiple_of_3`
warning: function `test_patternProperties_0` should have a snake case name
--> tests/tests.rs:1276:4
|
1276 | fn test_patternProperties_0() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_0`
warning: function `test_patternProperties_1` should have a snake case name
--> tests/tests.rs:1282:4
|
1282 | fn test_patternProperties_1() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_1`
warning: function `test_patternProperties_2` should have a snake case name
--> tests/tests.rs:1288:4
|
1288 | fn test_patternProperties_2() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_2`
warning: function `test_patternProperties_3` should have a snake case name
--> tests/tests.rs:1294:4
|
1294 | fn test_patternProperties_3() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_3`
warning: function `test_patternProperties_4` should have a snake case name
--> tests/tests.rs:1300:4
|
1300 | fn test_patternProperties_4() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_4`
warning: function `test_patternProperties_5` should have a snake case name
--> tests/tests.rs:1306:4
|
1306 | fn test_patternProperties_5() {
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_pattern_properties_5`
warning: function `test_allOf_0` should have a snake case name
--> tests/tests.rs:1336:4
|
1336 | fn test_allOf_0() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_0`
warning: function `test_allOf_1` should have a snake case name
--> tests/tests.rs:1342:4
|
1342 | fn test_allOf_1() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_1`
warning: function `test_allOf_2` should have a snake case name
--> tests/tests.rs:1348:4
|
1348 | fn test_allOf_2() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_2`
warning: function `test_allOf_3` should have a snake case name
--> tests/tests.rs:1354:4
|
1354 | fn test_allOf_3() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_3`
warning: function `test_allOf_4` should have a snake case name
--> tests/tests.rs:1360:4
|
1360 | fn test_allOf_4() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_4`
warning: function `test_allOf_5` should have a snake case name
--> tests/tests.rs:1366:4
|
1366 | fn test_allOf_5() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_5`
warning: function `test_allOf_6` should have a snake case name
--> tests/tests.rs:1372:4
|
1372 | fn test_allOf_6() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_6`
warning: function `test_allOf_7` should have a snake case name
--> tests/tests.rs:1378:4
|
1378 | fn test_allOf_7() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_7`
warning: function `test_allOf_8` should have a snake case name
--> tests/tests.rs:1384:4
|
1384 | fn test_allOf_8() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_8`
warning: function `test_allOf_9` should have a snake case name
--> tests/tests.rs:1390:4
|
1390 | fn test_allOf_9() {
| ^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_9`
warning: function `test_allOf_10` should have a snake case name
--> tests/tests.rs:1396:4
|
1396 | fn test_allOf_10() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_10`
warning: function `test_allOf_11` should have a snake case name
--> tests/tests.rs:1402:4
|
1402 | fn test_allOf_11() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_11`
warning: function `test_allOf_12` should have a snake case name
--> tests/tests.rs:1408:4
|
1408 | fn test_allOf_12() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_12`
warning: function `test_allOf_13` should have a snake case name
--> tests/tests.rs:1414:4
|
1414 | fn test_allOf_13() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_13`
warning: function `test_allOf_14` should have a snake case name
--> tests/tests.rs:1420:4
|
1420 | fn test_allOf_14() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_14`
warning: function `test_allOf_15` should have a snake case name
--> tests/tests.rs:1426:4
|
1426 | fn test_allOf_15() {
| ^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_all_of_15`
warning: function `test_minLength_0` should have a snake case name
--> tests/tests.rs:1828:4
|
1828 | fn test_minLength_0() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_length_0`
warning: function `test_minLength_1` should have a snake case name
--> tests/tests.rs:1834:4
|
1834 | fn test_minLength_1() {
| ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_min_length_1`
warning: function `test_maxItems_0` should have a snake case name
--> tests/tests.rs:1840:4
|
1840 | fn test_maxItems_0() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_0`
warning: function `test_maxItems_1` should have a snake case name
--> tests/tests.rs:1846:4
|
1846 | fn test_maxItems_1() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_1`
warning: function `test_maxItems_2` should have a snake case name
--> tests/tests.rs:1852:4
|
1852 | fn test_maxItems_2() {
| ^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_max_items_2`
warning: function `test_dynamicRef_0` should have a snake case name
--> tests/tests.rs:1912:4
|
1912 | fn test_dynamicRef_0() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_0`
warning: function `test_dynamicRef_1` should have a snake case name
--> tests/tests.rs:1918:4
|
1918 | fn test_dynamicRef_1() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_1`
warning: function `test_dynamicRef_2` should have a snake case name
--> tests/tests.rs:1924:4
|
1924 | fn test_dynamicRef_2() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_2`
warning: function `test_dynamicRef_3` should have a snake case name
--> tests/tests.rs:1930:4
|
1930 | fn test_dynamicRef_3() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_3`
warning: function `test_dynamicRef_4` should have a snake case name
--> tests/tests.rs:1936:4
|
1936 | fn test_dynamicRef_4() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_4`
warning: function `test_dynamicRef_5` should have a snake case name
--> tests/tests.rs:1942:4
|
1942 | fn test_dynamicRef_5() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_5`
warning: function `test_dynamicRef_6` should have a snake case name
--> tests/tests.rs:1948:4
|
1948 | fn test_dynamicRef_6() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_6`
warning: function `test_dynamicRef_7` should have a snake case name
--> tests/tests.rs:1954:4
|
1954 | fn test_dynamicRef_7() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_7`
warning: function `test_dynamicRef_8` should have a snake case name
--> tests/tests.rs:1960:4
|
1960 | fn test_dynamicRef_8() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_8`
warning: function `test_dynamicRef_9` should have a snake case name
--> tests/tests.rs:1966:4
|
1966 | fn test_dynamicRef_9() {
| ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_9`
warning: function `test_dynamicRef_10` should have a snake case name
--> tests/tests.rs:1972:4
|
1972 | fn test_dynamicRef_10() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_10`
warning: function `test_dynamicRef_11` should have a snake case name
--> tests/tests.rs:1978:4
|
1978 | fn test_dynamicRef_11() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_11`
warning: function `test_dynamicRef_12` should have a snake case name
--> tests/tests.rs:1984:4
|
1984 | fn test_dynamicRef_12() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_12`
warning: function `test_dynamicRef_13` should have a snake case name
--> tests/tests.rs:1990:4
|
1990 | fn test_dynamicRef_13() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_13`
warning: function `test_dynamicRef_14` should have a snake case name
--> tests/tests.rs:1996:4
|
1996 | fn test_dynamicRef_14() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_14`
warning: function `test_dynamicRef_15` should have a snake case name
--> tests/tests.rs:2002:4
|
2002 | fn test_dynamicRef_15() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_15`
warning: function `test_dynamicRef_16` should have a snake case name
--> tests/tests.rs:2008:4
|
2008 | fn test_dynamicRef_16() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_16`
warning: function `test_dynamicRef_17` should have a snake case name
--> tests/tests.rs:2014:4
|
2014 | fn test_dynamicRef_17() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_17`
warning: function `test_dynamicRef_18` should have a snake case name
--> tests/tests.rs:2020:4
|
2020 | fn test_dynamicRef_18() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_18`
warning: function `test_dynamicRef_19` should have a snake case name
--> tests/tests.rs:2026:4
|
2026 | fn test_dynamicRef_19() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_19`
warning: function `test_dynamicRef_20` should have a snake case name
--> tests/tests.rs:2032:4
|
2032 | fn test_dynamicRef_20() {
| ^^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `test_dynamic_ref_20`
warning: `jspg` (test "tests") generated 132 warnings
Finished `test` profile [unoptimized + debuginfo] target(s) in 6.12s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
thread 'test_ref_39' (14867888) panicked at tests/tests.rs:1812:45:
called `Result::unwrap()` on an `Err` value: "[implicit keyword shadowing] Test 'child type overrides parent type' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'age'\", details: ErrorDetails { path: \"/age\" } }]\n[implicit keyword shadowing] Test 'parent max age (20) is shadowed (replaced) by child definition' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'age'\", details: ErrorDetails { path: \"/age\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_ref_39 ... FAILED
failures:
failures:
test_ref_39
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.00s
error: test failed, to rerun pass `--test tests`

12
flow
View File

@ -98,14 +98,9 @@ install() {
fi
}
test-jspg() {
test() {
info "Running jspg tests..."
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
}
test-validator() {
info "Running validator tests..."
cargo test -p boon --features "pgrx/pg${POSTGRES_VERSION}" "$@" || return $?
cargo test --tests "$@" || return $?
}
clean() {
@ -128,8 +123,7 @@ jspg-flow() {
build) build; return $?;;
install) install; return $?;;
reinstall) clean && install; return $?;;
test-jspg) test-jspg "${@:2}"; return $?;;
test-validator) test-validator "${@:2}"; return $?;;
test) test "${@:2}"; return $?;;
clean) clean; return $?;;
*) return 1 ;;
esac

2
flows

Submodule flows updated: e154758056...404da626c7

View File

@ -1,106 +0,0 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.34s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"job_id", "manager_id", "name"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"manager_id", "type", "job_id", "name"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"name", "job_id", "manager_id", "type"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"job_id", "name"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "name", "type"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name", "type"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "type", "name"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"name", "job_id", "manager_id", "nested_or_super_job", "type"}
DEBUG: check_strictness at . Extensible: false. Keys: {"root_job", "name", "job_id", "manager_id", "nested_or_super_job", "type"}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"name", "manager_id", "job_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"manager_id", "name", "type", "job_id"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"manager_id", "type", "job_id", "name"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"manager_id", "type", "name", "job_id"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "job_id", "type"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id", "type"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "type", "name"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"manager_id", "type", "nested_or_super_job", "name", "job_id"}
DEBUG: check_strictness at . Extensible: false. Keys: {"manager_id", "root_job", "type", "nested_or_super_job", "name", "job_id"}
DEBUG: validate_refs merging res from entity. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/my_job/name. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/my_job/type. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"type", "name"}
DEBUG: validate_refs merging res from job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: validate_object inserted 'my_job' at /nested_or_super_job/my_job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"job_id", "manager_id", "name"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "type", "manager_id", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
thread 'test_puncs_6' (15117383) panicked at tests/tests.rs:150:44:
called `Result::unwrap()` on an `Err` value: "[complex punc type matching with oneOf and nested refs] Test 'valid person against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", details: ErrorDetails { path: \"/first_name\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against strict punc' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_puncs_6 ... FAILED
failures:
failures:
test_puncs_6
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.00s
error: test failed, to rerun pass `--test tests`

View File

@ -1,243 +0,0 @@
use pgrx::*;
pg_module_magic!();
// mod schema;
mod registry;
mod validator;
mod util;
use crate::registry::REGISTRY;
// use crate::schema::Schema;
use crate::validator::{Validator, ValidationOptions};
use lazy_static::lazy_static;
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Copy, Debug, PartialEq)]
enum SchemaType {
Enum,
Type,
Family,
PublicPunc,
PrivatePunc,
}
struct CachedSchema {
t: SchemaType,
}
lazy_static! {
static ref SCHEMA_META: std::sync::RwLock<HashMap<String, CachedSchema>> = std::sync::RwLock::new(HashMap::new());
}
#[pg_extern(strict)]
fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
let mut meta = SCHEMA_META.write().unwrap();
let enums_value: Value = enums.0;
let types_value: Value = types.0;
let puncs_value: Value = puncs.0;
let mut schemas_to_register = Vec::new();
// Phase 1: Enums
if let Some(enums_array) = enums_value.as_array() {
for enum_row in enums_array {
if let Some(schemas_raw) = enum_row.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), SchemaType::Enum));
}
}
}
}
}
}
// Phase 2: Types & Hierarchy
let mut hierarchy_map: HashMap<String, HashSet<String>> = HashMap::new();
if let Some(types_array) = types_value.as_array() {
for type_row in types_array {
if let Some(schemas_raw) = type_row.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), SchemaType::Type));
}
}
}
}
if let Some(type_name) = type_row.get("name").and_then(|v| v.as_str()) {
if let Some(hierarchy_raw) = type_row.get("hierarchy") {
if let Some(hierarchy_array) = hierarchy_raw.as_array() {
for ancestor_val in hierarchy_array {
if let Some(ancestor_name) = ancestor_val.as_str() {
hierarchy_map.entry(ancestor_name.to_string()).or_default().insert(type_name.to_string());
}
}
}
}
}
}
}
for (base_type, descendant_types) in hierarchy_map {
let family_id = format!("{}.family", base_type);
let values: Vec<String> = descendant_types.into_iter().collect();
let family_schema = json!({ "$id": family_id, "type": "string", "enum": values });
schemas_to_register.push((family_id, family_schema, SchemaType::Family));
}
// Phase 3: Puncs
if let Some(puncs_array) = puncs_value.as_array() {
for punc_row in puncs_array {
if let Some(punc_obj) = punc_row.as_object() {
if let Some(punc_name) = punc_obj.get("name").and_then(|v| v.as_str()) {
let is_public = punc_obj.get("public").and_then(|v| v.as_bool()).unwrap_or(false);
let punc_type = if is_public { SchemaType::PublicPunc } else { SchemaType::PrivatePunc };
if let Some(schemas_raw) = punc_obj.get("schemas") {
if let Some(schemas_array) = schemas_raw.as_array() {
for schema_def in schemas_array {
if let Some(schema_id) = schema_def.get("$id").and_then(|v| v.as_str()) {
let req_id = format!("{}.request", punc_name);
let resp_id = format!("{}.response", punc_name);
let st = if schema_id == req_id || schema_id == resp_id { punc_type } else { SchemaType::Type };
schemas_to_register.push((schema_id.to_string(), schema_def.clone(), st));
}
}
}
}
}
}
}
}
let mut all_errors = Vec::new();
for (id, value, st) in schemas_to_register {
// Meta-validation: Check 'type' enum if present
if let Some(type_val) = value.get("type") {
let types = match type_val {
Value::String(s) => vec![s.as_str()],
Value::Array(a) => a.iter().filter_map(|v| v.as_str()).collect(),
_ => vec![],
};
let valid_primitives = ["string", "number", "integer", "boolean", "array", "object", "null"];
for t in types {
if !valid_primitives.contains(&t) {
all_errors.push(json!({ "code": "ENUM_VIOLATED", "message": format!("Invalid type: {}", t) }));
}
}
}
// Clone value for insertion since it might be consumed/moved if we were doing other things
let value_for_registry = value.clone();
// Validation: just ensure it is an object or boolean
if value.is_object() || value.is_boolean() {
REGISTRY.insert(id.clone(), value_for_registry);
meta.insert(id, CachedSchema { t: st });
} else {
all_errors.push(json!({ "code": "INVALID_SCHEMA_TYPE", "message": format!("Schema {} must be an object or boolean", id) }));
}
}
if !all_errors.is_empty() {
return JsonB(json!({ "errors": all_errors }));
}
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
let schema = match REGISTRY.get(schema_id) {
Some(s) => s,
None => return JsonB(json!({
"errors": [{
"code": "SCHEMA_NOT_FOUND",
"message": format!("Schema '{}' not found", schema_id),
"details": { "schema": schema_id }
}]
})),
};
let meta = SCHEMA_META.read().unwrap();
let st = meta.get(schema_id).map(|m| m.t).unwrap_or(SchemaType::Type);
let be_strict = match st {
SchemaType::PublicPunc => true,
_ => false,
};
let options = ValidationOptions {
be_strict,
};
let mut validator = Validator::new(options, schema_id);
match validator.validate(&schema, &instance.0) {
Ok(_) => JsonB(json!({ "response": "success" })),
Err(errors) => {
let drop_errors: Vec<Value> = errors.into_iter().map(|e| json!({
"code": e.code,
"message": e.message,
"details": {
"path": e.path,
"context": e.context,
"cause": e.cause,
"schema": e.schema_id
}
})).collect();
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/debug_jspg_errors.log") {
use std::io::Write;
let _ = writeln!(f, "VALIDATION FAILED for {}: {:?}", schema_id, drop_errors);
}
JsonB(json!({ "errors": drop_errors }))
}
}
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
REGISTRY.get(schema_id).is_some()
}
#[pg_extern(strict)]
fn clear_json_schemas() -> JsonB {
REGISTRY.reset();
let mut meta = SCHEMA_META.write().unwrap();
meta.clear();
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> JsonB {
let meta = SCHEMA_META.read().unwrap();
let ids: Vec<String> = meta.keys().cloned().collect();
JsonB(json!({ "response": ids }))
}
/// This module is required by `cargo pgrx test` invocations.
/// It must be visible at the root of your extension crate.
#[cfg(test)]
pub mod pg_test {
pub fn setup(_options: Vec<&str>) {
// perform one-off initialization when the pg_test framework starts
}
#[must_use]
pub fn postgresql_conf_options() -> Vec<&'static str> {
// return any postgresql.conf settings that are required for your tests
vec![]
}
}
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
use pgrx::pg_test;
include!("suite.rs");
}

View File

@ -1,217 +0,0 @@
use serde_json::Value;
use std::collections::HashMap;
use std::sync::RwLock;
use lazy_static::lazy_static;
lazy_static! {
pub static ref REGISTRY: Registry = Registry::new();
}
pub struct Registry {
schemas: RwLock<HashMap<String, Value>>,
}
impl Registry {
pub fn new() -> Self {
Self {
schemas: RwLock::new(HashMap::new()),
}
}
pub fn reset(&self) {
let mut schemas = self.schemas.write().unwrap();
schemas.clear();
}
pub fn insert(&self, id: String, schema: Value) {
let mut schemas = self.schemas.write().unwrap();
// Index the schema and its sub-resources (IDs and anchors)
self.index_schema(&schema, &mut schemas, Some(&id));
// Ensure the root ID is inserted (index_schema handles it, but let's be explicit)
schemas.insert(id, schema);
}
fn index_schema(&self, schema: &Value, registry: &mut HashMap<String, Value>, current_scope: Option<&str>) {
if let Value::Object(map) = schema {
// Only strictly index $id for scope resolution
let mut my_scope = current_scope.map(|s| s.to_string());
if let Some(Value::String(id)) = map.get("$id") {
if id.contains("://") {
my_scope = Some(id.clone());
} else if let Some(scope) = current_scope {
if let Some(pos) = scope.rfind('/') {
my_scope = Some(format!("{}{}", &scope[..pos + 1], id));
} else {
my_scope = Some(id.clone());
}
} else {
my_scope = Some(id.clone());
}
if let Some(final_id) = &my_scope {
registry.insert(final_id.clone(), schema.clone());
}
}
// Minimal recursion only for definitions where sub-IDs often live
// This is a tradeoff: we don't index EVERYWHERE, but we catch the 90% common case of
// bundled definitions without full tree traversal.
if let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions")) {
for (_, def_schema) in defs {
self.index_schema(def_schema, registry, my_scope.as_deref());
}
}
}
}
pub fn get(&self, id: &str) -> Option<Value> {
let schemas = self.schemas.read().unwrap();
schemas.get(id).cloned()
}
pub fn resolve(&self, ref_str: &str, current_id: Option<&str>) -> Option<(Value, String)> {
// 1. Try full lookup (Absolute or explicit ID)
if let Some(s) = self.get(ref_str) {
return Some((s, ref_str.to_string()));
}
// 2. Try Relative lookup against current scope
if let Some(curr) = current_id {
if let Some(pos) = curr.rfind('/') {
let joined = format!("{}{}", &curr[..pos + 1], ref_str);
if let Some(s) = self.get(&joined) {
return Some((s, joined));
}
}
}
// 3. Pointer Resolution
// Split into Base URI + Fragment
let (base, fragment) = match ref_str.split_once('#') {
Some((b, f)) => (b, Some(f)),
None => (ref_str, None),
};
// If base is empty, we stay in current schema.
// If base is present, we resolve it first.
let (root_schema, scope) = if base.is_empty() {
if let Some(curr) = current_id {
// If we are looking up internally, we rely on the caller having passed the correct current ID
// But typically internal refs are just fragments.
if let Some(s) = self.get(curr) {
(s, curr.to_string())
} else {
return None;
}
} else {
return None;
}
} else {
// Resolve external base
if let Some(s) = self.get(base) {
(s, base.to_string())
} else if let Some(curr) = current_id {
// Try relative base
if let Some(pos) = curr.rfind('/') {
let joined = format!("{}{}", &curr[..pos + 1], base);
if let Some(s) = self.get(&joined) {
(s, joined)
} else {
return None;
}
} else {
return None;
}
} else {
return None;
}
};
if let Some(frag_raw) = fragment {
if frag_raw.is_empty() {
return Some((root_schema, scope));
}
// Decode fragment (it is URI encoded)
let frag_cow = percent_encoding::percent_decode_str(frag_raw).decode_utf8().unwrap_or(std::borrow::Cow::Borrowed(frag_raw));
let frag = frag_cow.as_ref();
if frag.starts_with('/') {
if let Some(sub) = root_schema.pointer(frag) {
return Some((sub.clone(), scope));
}
} else {
// It is an anchor. We scan for it at runtime to avoid complex indexing at insertion.
if let Some(sub) = self.find_anchor(&root_schema, frag) {
return Some((sub, scope));
}
}
None
} else {
Some((root_schema, scope))
}
}
fn find_anchor(&self, schema: &Value, anchor: &str) -> Option<Value> {
match schema {
Value::Object(map) => {
// Check if this schema itself has the anchor
if let Some(Value::String(a)) = map.get("$anchor") {
if a == anchor {
return Some(schema.clone());
}
}
// Recurse into $defs / definitions (Map of Schemas)
if let Some(Value::Object(defs)) = map.get("$defs").or_else(|| map.get("definitions")) {
for val in defs.values() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
// Recurse into properties / patternProperties / dependentSchemas (Map of Schemas)
for key in ["properties", "patternProperties", "dependentSchemas"] {
if let Some(Value::Object(props)) = map.get(key) {
for val in props.values() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
}
// Recurse into arrays of schemas
for key in ["allOf", "anyOf", "oneOf", "prefixItems"] {
if let Some(Value::Array(arr)) = map.get(key) {
for item in arr {
if let Some(found) = self.find_anchor(item, anchor) { return Some(found); }
}
}
}
// Recurse into single sub-schemas
for key in ["items", "contains", "additionalProperties", "unevaluatedProperties", "not", "if", "then", "else"] {
if let Some(val) = map.get(key) {
if val.is_object() || val.is_boolean() {
if let Some(found) = self.find_anchor(val, anchor) { return Some(found); }
}
}
}
None
}
Value::Array(arr) => {
// Should not happen for a schema object, but if we are passed an array of schemas?
// Standard schema is object or bool.
// But let's be safe.
for item in arr {
if let Some(found) = self.find_anchor(item, anchor) {
return Some(found);
}
}
None
}
_ => None,
}
}
}

View File

@ -1,236 +0,0 @@
// use crate::schema::Schema;
use crate::registry::REGISTRY;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use pgrx::JsonB;
use std::{fs, path::Path};
#[derive(Debug, Serialize, Deserialize)]
struct ExpectedError {
code: String,
path: String,
message_contains: Option<String>,
cause: Option<Value>,
context: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Group {
description: String,
schema: Option<Value>,
enums: Option<Value>,
types: Option<Value>,
puncs: Option<Value>,
tests: Vec<TestCase>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TestCase {
description: String,
data: Value,
valid: bool,
action: Option<String>,
schema_id: Option<String>,
expect_errors: Option<Vec<ExpectedError>>,
}
include!("tests.rs");
fn load_remotes(dir: &Path, base_url: &str) {
if !dir.exists() { return; }
for entry in fs::read_dir(dir).expect("Failed to read remotes directory") {
let entry = entry.unwrap();
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
if path.is_file() && file_name.ends_with(".json") {
let content = fs::read_to_string(&path).expect("Failed to read remote file");
if let Ok(schema_value) = serde_json::from_str::<Value>(&content) {
// Just check if it's a valid JSON value for a schema (object or bool)
if schema_value.is_object() || schema_value.is_boolean() {
let schema_id = format!("{}{}", base_url, file_name);
REGISTRY.insert(schema_id, schema_value);
}
}
} else if path.is_dir() {
load_remotes(&path, &format!("{}{}/", base_url, file_name));
}
}
// Mock the meta-schema for testing recursive refs
let meta_id = "https://json-schema.org/draft/2020-12/schema";
if REGISTRY.get(meta_id).is_none() {
// Just mock it as a permissive schema for now so refs resolve
REGISTRY.insert(meta_id.to_string(), json!({ "$id": meta_id }));
}
}
#[allow(dead_code)]
fn run_dir(dir: &Path, base_url: Option<&str>) -> (usize, usize) {
let mut file_count = 0;
let mut test_count = 0;
for entry in fs::read_dir(dir).expect("Failed to read directory") {
let entry = entry.unwrap();
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
if path.is_file() && file_name.ends_with(".json") {
let count = run_file(&path, base_url);
test_count += count;
file_count += 1;
} else if path.is_dir() {
if !file_name.starts_with('.') && file_name != "optional" {
let (f, t) = run_dir(&path, base_url);
file_count += f;
test_count += t;
}
}
}
(file_count, test_count)
}
fn run_file(path: &Path, base_url: Option<&str>) -> usize {
let content = fs::read_to_string(path).expect("Failed to read file");
let groups: Vec<Group> = serde_json::from_str(&content).expect("Failed to parse JSON");
let filename = path.file_name().unwrap().to_str().unwrap();
let mut test_count = 0;
for group in groups {
// Handle JSPG setup if any JSPG fields are present
if group.enums.is_some() || group.types.is_some() || group.puncs.is_some() {
let enums = group.enums.clone().unwrap_or(json!([]));
let types = group.types.clone().unwrap_or(json!([]));
let puncs = group.puncs.clone().unwrap_or(json!([]));
// Use internal helper to register without clearing
let result = crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
if let Some(errors) = result.0.get("errors") {
// If the group has a test specifically for caching failures, don't panic here
let has_cache_test = group.tests.iter().any(|t| t.action.as_deref() == Some("cache"));
if !has_cache_test {
panic!("FAILED: File: {}, Group: {}\nCache failed: {:?}", filename, group.description, errors);
}
}
}
let mut temp_id = "test_root".to_string();
if let Some(schema_value) = &group.schema {
temp_id = base_url.map(|b| format!("{}schema.json", b)).unwrap_or_else(|| "test_root".to_string());
if schema_value.is_object() || schema_value.is_boolean() {
REGISTRY.insert(temp_id.clone(), schema_value.clone());
}
} else {
// Fallback for JSPG style tests where the schema is in the puncs/types
let get_first_id = |items: &Option<Value>| {
items.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("schemas"))
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|sch| sch.get("$id"))
.and_then(|id| id.as_str())
.map(|s| s.to_string())
};
if let Some(id) = get_first_id(&group.puncs).or_else(|| get_first_id(&group.types)) {
temp_id = id;
}
}
for test in &group.tests {
test_count += 1;
let sid = test.schema_id.clone().unwrap_or_else(|| temp_id.clone());
let action = test.action.as_deref().unwrap_or("validate");
pgrx::notice!("Starting Test: {}", test.description);
let result = if action == "cache" {
let enums = group.enums.clone().unwrap_or(json!([]));
let types = group.types.clone().unwrap_or(json!([]));
let puncs = group.puncs.clone().unwrap_or(json!([]));
crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs))
} else {
crate::validate_json_schema(&sid, JsonB(test.data.clone()))
};
let is_success = result.0.get("response").is_some();
pgrx::notice!("TEST: file={}, group={}, test={}, valid={}, outcome={}",
filename,
&group.description,
&test.description,
test.valid,
if is_success { "SUCCESS" } else { "ERRORS" }
);
if is_success != test.valid {
if let Some(errs) = result.0.get("errors") {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nExpected valid: {}, got ERRORS: {:?}",
filename,
group.description,
test.description,
test.valid,
errs
);
} else {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nExpected invalid, got SUCCESS",
filename,
group.description,
test.description
);
}
}
// Perform detailed assertions if present
if let Some(expectations) = &test.expect_errors {
let actual_errors = result.0.get("errors").and_then(|e| e.as_array()).expect("Expected errors array in failure response");
for expected in expectations {
let found = actual_errors.iter().any(|e| {
let code = e["code"].as_str().unwrap_or("");
let path = e["details"]["path"].as_str().unwrap_or("");
let message = e["message"].as_str().unwrap_or("");
let code_match = code == expected.code;
let path_match = path == expected.path;
let msg_match = if let Some(sub) = &expected.message_contains {
message.contains(sub)
} else {
true
};
let matches_cause = if let Some(expected_cause) = &expected.cause {
e["details"]["cause"] == *expected_cause
} else {
true
};
let matches_context = if let Some(expected_context) = &expected.context {
e["details"]["context"] == *expected_context
} else {
true
};
code_match && path_match && msg_match && matches_cause && matches_context
});
if !found {
panic!(
"FAILED: File: {}, Group: {}, Test: {}\nMissing expected error: code='{}', path='{}'\nActual errors: {:?}",
filename,
group.description,
test.description,
expected.code,
expected.path,
actual_errors
);
}
}
}
} // end of test loop
} // end of group loop
test_count
}

View File

@ -1,482 +0,0 @@
#[pg_test]
fn test_jspg_additional_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/additionalProperties.json"), None);
}
#[pg_test]
fn test_jspg_cache() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/cache.json"), None);
}
#[pg_test]
fn test_jspg_const() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/const.json"), None);
}
#[pg_test]
fn test_jspg_dependencies() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/dependencies.json"), None);
}
#[pg_test]
fn test_jspg_enum() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/enum.json"), None);
}
#[pg_test]
fn test_jspg_errors() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/errors.json"), None);
}
#[pg_test]
fn test_jspg_format() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/format.json"), None);
}
#[pg_test]
fn test_jspg_infinite_loop_detection() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/infinite-loop-detection.json"), None);
}
#[pg_test]
fn test_jspg_one_of() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/oneOf.json"), None);
}
#[pg_test]
fn test_jspg_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/properties.json"), None);
}
#[pg_test]
fn test_jspg_punc() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/punc.json"), None);
}
#[pg_test]
fn test_jspg_ref() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/ref.json"), None);
}
#[pg_test]
fn test_jspg_required() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/required.json"), None);
}
#[pg_test]
fn test_jspg_simple() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/simple.json"), None);
}
#[pg_test]
fn test_jspg_strict() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/strict.json"), None);
}
#[pg_test]
fn test_jspg_title() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/title.json"), None);
}
#[pg_test]
fn test_jspg_type() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/type.json"), None);
}
#[pg_test]
fn test_jspg_unevaluated_properties() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/unevaluatedProperties.json"), None);
}
#[pg_test]
fn test_jspg_unique_items() {
REGISTRY.reset();
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSPG-Test-Suite/uniqueItems.json"), None);
}
#[pg_test]
fn test_json_schema_additional_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/additionalProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_all_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/allOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_anchor() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/anchor.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_any_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/anyOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_boolean_schema() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/boolean_schema.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_const() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/const.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/contains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_content() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/content.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_default() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/default.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_defs() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/defs.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dependent_required() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dependentRequired.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dependent_schemas() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dependentSchemas.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_dynamic_ref() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/dynamicRef.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_enum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/enum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_exclusive_maximum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/exclusiveMaximum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_exclusive_minimum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/exclusiveMinimum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_format() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/format.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_if_then_else() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/if-then-else.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_infinite_loop_detection() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/infinite-loop-detection.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/items.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxContains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_length() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxLength.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_max_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maxProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_maximum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/maximum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_contains() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minContains.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_length() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minLength.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_min_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_minimum() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/minimum.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_multiple_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/multipleOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_not() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/not.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_one_of() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/oneOf.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_pattern() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/pattern.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_pattern_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/patternProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_prefix_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/prefixItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/properties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_property_names() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/propertyNames.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_ref() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/ref.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_ref_remote() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/refRemote.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_required() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/required.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_type() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/type.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unevaluated_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unevaluated_properties() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/unevaluatedProperties.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_unique_items() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/uniqueItems.json"), Some("http://localhost:1234/"));
}
#[pg_test]
fn test_json_schema_vocabulary() {
REGISTRY.reset();
let remotes_dir = Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/remotes");
load_remotes(remotes_dir, "http://localhost:1234/");
run_file(Path::new("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/tests/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12/vocabulary.json"), Some("http://localhost:1234/"));
}

View File

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

View File

@ -1,621 +0,0 @@
use crate::registry::REGISTRY;
use crate::util::{equals, is_integer};
use serde_json::{Value, json, Map};
use std::collections::HashSet;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub path: String,
pub context: Value,
pub cause: Value,
pub schema_id: String,
}
#[derive(Default, Clone, Copy)]
pub struct ValidationOptions {
pub be_strict: bool,
}
pub struct Validator<'a> {
options: ValidationOptions,
// The top-level root schema ID we started with
root_schema_id: String,
// Accumulated errors
errors: Vec<ValidationError>,
// Max depth to prevent stack overflow
max_depth: usize,
_phantom: std::marker::PhantomData<&'a ()>,
}
/// Context passed down through the recursion
#[derive(Clone)]
struct ValidationContext {
// Current JSON pointer path in the instance (e.g. "/users/0/name")
current_path: String,
// The properties overridden by parent schemas (for JSPG inheritance)
overrides: HashSet<String>,
// Current resolution scope for $ref (changes when following refs)
resolution_scope: String,
// Current recursion depth
depth: usize,
}
impl ValidationContext {
fn append_path(&self, extra: &str) -> ValidationContext {
let mut new_ctx = self.clone();
if new_ctx.current_path.ends_with('/') {
new_ctx.current_path.push_str(extra);
} else if new_ctx.current_path.is_empty() {
new_ctx.current_path.push('/');
new_ctx.current_path.push_str(extra);
} else {
new_ctx.current_path.push('/');
new_ctx.current_path.push_str(extra);
}
new_ctx
}
fn append_path_new_scope(&self, extra: &str) -> ValidationContext {
let mut new_ctx = self.append_path(extra);
// Structural recursion clears overrides
new_ctx.overrides.clear();
new_ctx
}
}
impl<'a> Validator<'a> {
pub fn new(options: ValidationOptions, root_schema_id: &str) -> Self {
Self {
options,
root_schema_id: root_schema_id.to_string(),
errors: Vec::new(),
max_depth: 100,
_phantom: std::marker::PhantomData,
}
}
pub fn validate(&mut self, schema: &Value, instance: &Value) -> Result<(), Vec<ValidationError>> {
let ctx = ValidationContext {
current_path: String::new(),
overrides: HashSet::new(),
resolution_scope: self.root_schema_id.clone(),
depth: 0,
};
// We treat the top-level validate as "not lax" by default, unless specific schema logic says otherwise.
let is_lax = !self.options.be_strict;
self.validate_node(schema, instance, ctx, is_lax, false, false);
if self.errors.is_empty() {
Ok(())
} else {
Err(self.errors.clone())
}
}
fn validate_node(
&mut self,
schema: &Value,
instance: &Value,
mut ctx: ValidationContext,
is_lax: bool,
skip_strict: bool,
skip_id: bool,
) -> HashSet<String> {
let mut evaluated = HashSet::new();
// Recursion limit
if ctx.depth > self.max_depth {
self.add_error("MAX_DEPTH_REACHED", "Maximum recursion depth exceeded".to_string(), instance, json!({ "depth": ctx.depth }), &ctx);
return evaluated;
}
ctx.depth += 1;
// Handle Boolean Schemas
if let Value::Bool(b) = schema {
if !b {
self.add_error("FALSE_SCHEMA", "Schema is always false".to_string(), instance, Value::Null, &ctx);
}
return evaluated;
}
let schema_obj = match schema.as_object() {
Some(o) => o,
None => return evaluated, // Should be object or bool
};
// 1. Update Resolution Scope ($id)
if !skip_id {
if let Some(Value::String(id)) = schema_obj.get("$id") {
if id.contains("://") {
ctx.resolution_scope = id.clone();
} else {
if let Some(pos) = ctx.resolution_scope.rfind('/') {
let base = &ctx.resolution_scope[..pos + 1];
ctx.resolution_scope = format!("{}{}", base, id);
} else {
ctx.resolution_scope = id.clone();
}
}
}
}
// 2. Identify Overrides (JSPG Custom Logic)
let mut inheritance_ctx = ctx.clone();
if let Some(Value::Object(props)) = schema_obj.get("properties") {
for (pname, pval) in props {
if let Some(Value::Bool(true)) = pval.get("override") {
inheritance_ctx.overrides.insert(pname.clone());
}
}
}
// 3. Determine Laxness
let mut current_lax = is_lax;
if let Some(Value::Bool(true)) = schema_obj.get("unevaluatedProperties") { current_lax = true; }
if let Some(Value::Bool(true)) = schema_obj.get("additionalProperties") { current_lax = true; }
// ======== VALIDATION KEYWORDS ========
// Type
if let Some(type_val) = schema_obj.get("type") {
if !self.check_type(type_val, instance) {
let got = value_type_name(instance);
let want_json = serde_json::to_value(type_val).unwrap_or(json!("unknown"));
self.add_error("TYPE_MISMATCH", format!("Expected type {:?} but got {}", type_val, got), instance, json!({ "want": type_val, "got": got }), &ctx);
}
}
// Enum
if let Some(Value::Array(vals)) = schema_obj.get("enum") {
if !vals.iter().any(|v| equals(v, instance)) {
self.add_error("ENUM_VIOLATED", "Value not in enum".to_string(), instance, json!({ "want": vals }), &ctx);
}
}
// Const
if let Some(c) = schema_obj.get("const") {
if !equals(c, instance) {
self.add_error("CONST_VIOLATED", "Value does not match constant".to_string(), instance, json!({ "want": c }), &ctx);
}
}
// Object Validation
if let Value::Object(obj) = instance {
let obj_eval = self.validate_object(schema_obj, obj, instance, &ctx, current_lax);
evaluated.extend(obj_eval);
}
// Array Validation
if let Value::Array(arr) = instance {
self.validate_array(schema_obj, arr, &ctx, current_lax);
}
// Primitive Validation
self.validate_primitives(schema_obj, instance, &ctx);
// Combinators
evaluated.extend(self.validate_combinators(schema_obj, instance, &inheritance_ctx, current_lax));
// Conditionals
evaluated.extend(self.validate_conditionals(schema_obj, instance, &inheritance_ctx, current_lax));
// $ref
if let Some(Value::String(ref_str)) = schema_obj.get("$ref") {
if let Some((ref_schema, scope_uri)) = REGISTRY.resolve(ref_str, Some(&inheritance_ctx.resolution_scope)) {
let mut new_ctx = inheritance_ctx.clone();
new_ctx.resolution_scope = scope_uri;
let ref_evaluated = self.validate_node(&ref_schema, instance, new_ctx, is_lax, true, true);
evaluated.extend(ref_evaluated);
} else {
self.add_error("SCHEMA_NOT_FOUND", format!("Ref '{}' not found", ref_str), instance, json!({ "ref": ref_str }), &ctx);
}
}
// Unevaluated / Strictness Check
self.check_unevaluated(schema_obj, instance, &evaluated, &ctx, current_lax, skip_strict);
evaluated
}
fn validate_object(
&mut self,
schema: &Map<String, Value>,
obj: &Map<String, Value>,
instance: &Value,
ctx: &ValidationContext,
is_lax: bool,
) -> HashSet<String> {
let mut evaluated = HashSet::new();
// required
if let Some(Value::Array(req)) = schema.get("required") {
for field_val in req {
if let Some(field) = field_val.as_str() {
if !obj.contains_key(field) {
self.add_error("REQUIRED_FIELD_MISSING", format!("Required field '{}' is missing", field), &Value::Null, json!({ "want": [field] }), &ctx.append_path(field));
}
}
}
}
// properties
if let Some(Value::Object(props)) = schema.get("properties") {
for (pname, psch) in props {
if obj.contains_key(pname) {
if ctx.overrides.contains(pname) {
evaluated.insert(pname.clone());
continue;
}
evaluated.insert(pname.clone());
let sub_ctx = ctx.append_path_new_scope(pname);
self.validate_node(psch, &obj[pname], sub_ctx, is_lax, false, false);
}
}
}
// patternProperties
if let Some(Value::Object(pprops)) = schema.get("patternProperties") {
for (pattern, psch) in pprops {
if let Ok(re) = regex::Regex::new(pattern) {
for (pname, pval) in obj {
if re.is_match(pname) {
if ctx.overrides.contains(pname) {
evaluated.insert(pname.clone());
continue;
}
evaluated.insert(pname.clone());
let sub_ctx = ctx.append_path_new_scope(pname);
self.validate_node(psch, pval, sub_ctx, is_lax, false, false);
}
}
}
}
}
// additionalProperties
if let Some(apsch) = schema.get("additionalProperties") {
if apsch.is_object() || apsch.is_boolean() {
for (key, val) in obj {
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
let in_patterns = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| {
pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false))
});
if !in_props && !in_patterns {
evaluated.insert(key.clone());
let sub_ctx = ctx.append_path_new_scope(key);
self.validate_node(apsch, val, sub_ctx, is_lax, false, false);
}
}
}
}
// dependentRequired
if let Some(Value::Object(dep_req)) = schema.get("dependentRequired") {
for (prop, required_fields_val) in dep_req {
if obj.contains_key(prop) {
if let Value::Array(required_fields) = required_fields_val {
for req_field_val in required_fields {
if let Some(req_field) = req_field_val.as_str() {
if !obj.contains_key(req_field) {
self.add_error("DEPENDENCY_FAILED", format!("Field '{}' is required when '{}' is present", req_field, prop), &Value::Null, json!({ "prop": prop, "missing": [req_field] }), &ctx.append_path(req_field));
}
}
}
}
}
}
}
// dependentSchemas
if let Some(Value::Object(dep_sch)) = schema.get("dependentSchemas") {
for (prop, psch) in dep_sch {
if obj.contains_key(prop) {
let sub_evaluated = self.validate_node(psch, instance, ctx.clone(), is_lax, false, false);
evaluated.extend(sub_evaluated);
}
}
}
// legacy dependencies (Draft 4-7 compat)
if let Some(Value::Object(deps)) = schema.get("dependencies") {
for (prop, dep_val) in deps {
if obj.contains_key(prop) {
match dep_val {
Value::Array(arr) => {
for req_val in arr {
if let Some(req_field) = req_val.as_str() {
if !obj.contains_key(req_field) {
self.add_error(
"DEPENDENCY_FAILED",
format!("Field '{}' is required when '{}' is present", req_field, prop),
&Value::Null,
json!({ "prop": prop, "missing": [req_field] }),
&ctx.append_path(req_field),
);
}
}
}
}
Value::Object(_) => {
// Schema dependency
let sub_evaluated = self.validate_node(dep_val, instance, ctx.clone(), is_lax, false, false);
evaluated.extend(sub_evaluated);
}
_ => {}
}
}
}
}
// minProperties / maxProperties
if let Some(min) = schema.get("minProperties").and_then(|v| v.as_u64()) {
if (obj.len() as u64) < min {
self.add_error("MIN_PROPERTIES_VIOLATED", format!("Object must have at least {} properties", min), &json!(obj.len()), json!({ "want": min, "got": obj.len() }), ctx);
}
}
if let Some(max) = schema.get("maxProperties").and_then(|v| v.as_u64()) {
if (obj.len() as u64) > max {
self.add_error("MAX_PROPERTIES_VIOLATED", format!("Object must have at most {} properties", max), &json!(obj.len()), json!({ "want": max, "got": obj.len() }), ctx);
}
}
evaluated
}
fn validate_array(
&mut self,
schema: &Map<String, Value>,
arr: &Vec<Value>,
ctx: &ValidationContext,
is_lax: bool,
) {
if let Some(min) = schema.get("minItems").and_then(|v| v.as_u64()) {
if (arr.len() as u64) < min {
self.add_error("MIN_ITEMS_VIOLATED", format!("Array must have at least {} items", min), &json!(arr.len()), json!({ "want": min, "got": arr.len() }), ctx);
}
}
if let Some(max) = schema.get("maxItems").and_then(|v| v.as_u64()) {
if (arr.len() as u64) > max {
self.add_error("MAX_ITEMS_VIOLATED", format!("Array must have at most {} items", max), &json!(arr.len()), json!({ "want": max, "got": arr.len() }), ctx);
}
}
let mut evaluated_index = 0;
if let Some(Value::Array(prefix)) = schema.get("prefixItems") {
for (i, psch) in prefix.iter().enumerate() {
if let Some(item) = arr.get(i) {
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
self.validate_node(psch, item, sub_ctx, is_lax, false, false);
evaluated_index = i + 1;
}
}
}
if let Some(items_val) = schema.get("items") {
if let Value::Bool(false) = items_val {
if arr.len() > evaluated_index {
self.add_error("ADDITIONAL_ITEMS_NOT_ALLOWED", "Extra items not allowed".to_string(), &json!(arr.len()), json!({ "got": arr.len() - evaluated_index }), ctx);
}
} else {
// Schema or true
for i in evaluated_index..arr.len() {
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
self.validate_node(items_val, &arr[i], sub_ctx, is_lax, false, false);
}
}
}
if let Some(contains_sch) = schema.get("contains") {
let mut matches = 0;
for (i, item) in arr.iter().enumerate() {
let mut sub = self.branch();
let sub_ctx = ctx.append_path_new_scope(&i.to_string());
sub.validate_node(contains_sch, item, sub_ctx, is_lax, false, false);
if sub.errors.is_empty() {
matches += 1;
}
}
if matches == 0 {
self.add_error("CONTAINS_FAILED", "No items match 'contains' schema".to_string(), &json!(arr), json!({}), ctx);
}
if let Some(min) = schema.get("minContains").and_then(|v| v.as_u64()) {
if (matches as u64) < min {
self.add_error("MIN_CONTAINS_VIOLATED", format!("Expected at least {} items to match 'contains'", min), &json!(arr), json!({ "want": min, "got": matches }), ctx);
}
}
if let Some(max) = schema.get("maxContains").and_then(|v| v.as_u64()) {
if (matches as u64) > max {
self.add_error("MAX_CONTAINS_VIOLATED", format!("Expected at most {} items to match 'contains'", max), &json!(arr), json!({ "want": max, "got": matches }), ctx);
}
}
}
// uniqueItems
if let Some(Value::Bool(true)) = schema.get("uniqueItems") {
for i in 0..arr.len() {
for j in (i + 1)..arr.len() {
if equals(&arr[i], &arr[j]) {
self.add_error("UNIQUE_ITEMS_VIOLATED", format!("Array items at indices {} and {} are equal", i, j), &json!(arr), json!({ "got": [i, j] }), ctx);
return;
}
}
}
}
}
fn validate_primitives(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext) {
if let Some(s) = instance.as_str() {
if let Some(min) = schema.get("minLength").and_then(|v| v.as_u64()) {
if (s.chars().count() as u64) < min { self.add_error("MIN_LENGTH_VIOLATED", format!("String too short (min {})", min), instance, json!({ "want": min, "got": s.len() }), ctx); }
}
if let Some(max) = schema.get("maxLength").and_then(|v| v.as_u64()) {
if (s.chars().count() as u64) > max { self.add_error("MAX_LENGTH_VIOLATED", format!("String too long (max {})", max), instance, json!({ "want": max, "got": s.len() }), ctx); }
}
if let Some(Value::String(pat)) = schema.get("pattern") {
if let Ok(re) = regex::Regex::new(pat) {
if !re.is_match(s) { self.add_error("PATTERN_VIOLATED", format!("String does not match pattern '{}'", pat), instance, json!({ "want": pat, "got": s }), ctx); }
}
}
if let Some(Value::String(fmt)) = schema.get("format") {
if !s.is_empty() {
match fmt.as_str() {
"uuid" => { if uuid::Uuid::parse_str(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid UUID", s), instance, json!({ "format": "uuid" }), ctx); } }
"date-time" => { if chrono::DateTime::parse_from_rfc3339(s).is_err() { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid date-time", s), instance, json!({ "format": "date-time" }), ctx); } }
"email" => { if !s.contains('@') { self.add_error("FORMAT_INVALID", format!("Value '{}' is not a valid email", s), instance, json!({ "format": "email" }), ctx); } }
_ => {}
}
}
}
}
if let Some(n) = instance.as_f64() {
if let Some(min) = schema.get("minimum").and_then(|v| v.as_f64()) {
if n < min { self.add_error("MINIMUM_VIOLATED", format!("Value {} < minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
}
if let Some(max) = schema.get("maximum").and_then(|v| v.as_f64()) {
if n > max { self.add_error("MAXIMUM_VIOLATED", format!("Value {} > maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
}
if let Some(min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
if n <= min { self.add_error("EXCLUSIVE_MINIMUM_VIOLATED", format!("Value {} <= exclusive minimum {}", n, min), instance, json!({ "want": min, "got": n }), ctx); }
}
if let Some(max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
if n >= max { self.add_error("EXCLUSIVE_MAXIMUM_VIOLATED", format!("Value {} >= exclusive maximum {}", n, max), instance, json!({ "want": max, "got": n }), ctx); }
}
if let Some(mult) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
let rem = (n / mult).fract();
if rem.abs() > f64::EPSILON && (1.0 - rem).abs() > f64::EPSILON {
self.add_error("MULTIPLE_OF_VIOLATED", format!("Value {} not multiple of {}", n, mult), instance, json!({ "want": mult, "got": n }), ctx);
}
}
}
}
fn validate_combinators(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
let mut evaluated = HashSet::new();
if let Some(Value::Array(all_of)) = schema.get("allOf") {
for sch in all_of { evaluated.extend(self.validate_node(sch, instance, ctx.clone(), is_lax, true, false)); }
}
if let Some(Value::Array(any_of)) = schema.get("anyOf") {
let mut matched = false;
let mut errors_acc = Vec::new();
for sch in any_of {
let mut sub = self.branch();
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { matched = true; evaluated.extend(sub_eval); } else { errors_acc.extend(sub.errors); }
}
if !matched { self.add_error("ANY_OF_VIOLATED", "Value did not match any allowed schema".to_string(), instance, json!({ "causes": errors_acc }), ctx); }
}
if let Some(Value::Array(one_of)) = schema.get("oneOf") {
let mut match_count = 0;
let mut last_eval = HashSet::new();
let mut error_causes = Vec::new();
for sch in one_of {
let mut sub = self.branch();
let sub_eval = sub.validate_node(sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { match_count += 1; last_eval = sub_eval; } else { error_causes.extend(sub.errors); }
}
if match_count == 1 { evaluated.extend(last_eval); }
else { self.add_error("ONE_OF_VIOLATED", format!("Value matched {} schemas, expected 1", match_count), instance, json!({ "matched": match_count, "causes": error_causes }), ctx); }
}
if let Some(not_sch) = schema.get("not") {
let mut sub = self.branch();
sub.validate_node(not_sch, instance, ctx.clone(), is_lax, false, false);
if sub.errors.is_empty() { self.add_error("NOT_VIOLATED", "Value matched 'not' schema".to_string(), instance, Value::Null, ctx); }
}
evaluated
}
fn validate_conditionals(&mut self, schema: &Map<String, Value>, instance: &Value, ctx: &ValidationContext, is_lax: bool) -> HashSet<String> {
let mut evaluated = HashSet::new();
if let Some(if_sch) = schema.get("if") {
let mut sub = self.branch();
let sub_eval = sub.validate_node(if_sch, instance, ctx.clone(), is_lax, true, false);
if sub.errors.is_empty() {
evaluated.extend(sub_eval);
if let Some(then_sch) = schema.get("then") { evaluated.extend(self.validate_node(then_sch, instance, ctx.clone(), is_lax, false, false)); }
} else if let Some(else_sch) = schema.get("else") {
evaluated.extend(self.validate_node(else_sch, instance, ctx.clone(), is_lax, false, false));
}
}
evaluated
}
fn check_unevaluated(&mut self, schema: &Map<String, Value>, instance: &Value, evaluated: &HashSet<String>, ctx: &ValidationContext, is_lax: bool, skip_strict: bool) {
if let Value::Object(obj) = instance {
if let Some(Value::Bool(false)) = schema.get("additionalProperties") {
for key in obj.keys() {
let in_props = schema.get("properties").and_then(|p| p.as_object()).map_or(false, |p| p.contains_key(key));
let in_pattern = schema.get("patternProperties").and_then(|p| p.as_object()).map_or(false, |pp| pp.keys().any(|k| regex::Regex::new(k).map(|re| re.is_match(key)).unwrap_or(false)));
if !in_props && !in_pattern {
if ctx.overrides.contains(key) { continue; }
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
}
}
}
let explicit_opts = schema.contains_key("unevaluatedProperties") || schema.contains_key("additionalProperties");
let should_check_strict = self.options.be_strict && !is_lax && !explicit_opts && !skip_strict;
let check_unevaluated = matches!(schema.get("unevaluatedProperties"), Some(Value::Bool(false)));
if should_check_strict || check_unevaluated {
for key in obj.keys() {
if !evaluated.contains(key) {
if ctx.overrides.contains(key) { continue; }
self.add_error("ADDITIONAL_PROPERTIES_NOT_ALLOWED", format!("Property '{}' is not allowed (strict/unevaluated)", key), &Value::Null, json!({ "got": [key] }), &ctx.append_path(key));
}
}
}
}
}
fn check_type(&self, expected: &Value, instance: &Value) -> bool {
match expected {
Value::String(s) => self.is_primitive_type(s, instance),
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).any(|pt| self.is_primitive_type(pt, instance)),
_ => false
}
}
fn is_primitive_type(&self, pt: &str, instance: &Value) -> bool {
match pt {
"string" => instance.is_string(),
"number" => instance.is_number(),
"integer" => is_integer(instance),
"boolean" => instance.is_boolean(),
"array" => instance.is_array(),
"object" => instance.is_object(),
"null" => instance.is_null(),
_ => false
}
}
fn branch(&self) -> Self {
Self { options: self.options, root_schema_id: self.root_schema_id.clone(), errors: Vec::new(), max_depth: self.max_depth, _phantom: std::marker::PhantomData }
}
fn add_error(&mut self, code: &str, message: String, context: &Value, cause: Value, ctx: &ValidationContext) {
let path = ctx.current_path.clone();
if self.errors.iter().any(|e| e.code == code && e.path == path) { return; }
self.errors.push(ValidationError { code: code.to_string(), message, path, context: context.clone(), cause, schema_id: self.root_schema_id.clone() });
}
fn extend_unique(&mut self, errors: Vec<ValidationError>) {
for e in errors { if !self.errors.iter().any(|existing| existing.code == e.code && existing.path == e.path) { self.errors.push(e); } }
}
}
fn value_type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) => if n.is_i64() { "integer" } else { "number" },
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}

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

File diff suppressed because it is too large Load Diff

View File

@ -1,106 +0,0 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.39s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"name", "job_id", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "name", "manager_id", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"name", "manager_id", "job_id", "type"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"name", "manager_id", "job_id", "type"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"job_id", "name"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "type", "name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "type", "name"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "job_id", "type"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"name", "manager_id", "job_id", "type", "nested_or_super_job"}
DEBUG: check_strictness at . Extensible: false. Keys: {"name", "manager_id", "job_id", "type", "nested_or_super_job", "root_job"}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"job_id", "name", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "name", "type", "manager_id"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"type", "job_id", "manager_id", "name"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"job_id", "name", "manager_id", "type"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "type", "job_id"}
DEBUG: validate_refs merging res from job. Keys: {"name", "type", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"type", "name", "job_id"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"job_id", "name", "manager_id", "type", "nested_or_super_job"}
DEBUG: check_strictness at . Extensible: false. Keys: {"root_job", "job_id", "name", "manager_id", "type", "nested_or_super_job"}
DEBUG: validate_refs merging res from entity. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/my_job/name. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/my_job/type. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: validate_refs merging res from job. Keys: {"name", "type"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: validate_object inserted 'my_job' at /nested_or_super_job/my_job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"job_id", "name", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
thread 'test_puncs_6' (15118678) panicked at tests/tests.rs:150:44:
called `Result::unwrap()` on an `Err` value: "[complex punc type matching with oneOf and nested refs] Test 'valid person against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", details: ErrorDetails { path: \"/first_name\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against strict punc' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_puncs_6 ... FAILED
failures:
failures:
test_puncs_6
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.01s
error: test failed, to rerun pass `--test tests`

View File

@ -1,103 +0,0 @@
Blocking waiting for file lock on artifact directory
Compiling jspg v0.1.0 (/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg)
Finished `test` profile [unoptimized + debuginfo] target(s) in 7.63s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"name", "job_id", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"manager_id", "type", "name", "job_id"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"name", "job_id", "manager_id", "type"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "job_id", "type"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id", "type"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "type", "name"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"name", "job_id", "nested_or_super_job", "manager_id", "type"}
DEBUG: check_strictness at . Extensible: false. Keys: {"root_job", "name", "job_id", "nested_or_super_job", "manager_id", "type"}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"job_id", "manager_id", "name"}
DEBUG: validate_refs merging res from super_job. Keys: {"type", "job_id", "manager_id", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"name", "manager_id", "job_id", "type"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "type", "job_id"}
DEBUG: validate_refs merging res from job. Keys: {"name", "type", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"type", "job_id", "name"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"name", "manager_id", "job_id", "type", "nested_or_super_job"}
DEBUG: check_strictness at . Extensible: false. Keys: {"name", "root_job", "manager_id", "job_id", "type", "nested_or_super_job"}
DEBUG: validate_refs merging res from entity. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/my_job/name. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/my_job/type. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: validate_refs merging res from job. Keys: {"name", "type"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"type", "name"}
DEBUG: validate_object inserted 'my_job' at /nested_or_super_job/my_job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"name", "job_id", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "manager_id", "name", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: check_strictness at . Extensible: false. Keys: {}
thread 'test_puncs_6' (15113120) panicked at tests/tests.rs:150:44:
called `Result::unwrap()` on an `Err` value: "[complex punc type matching with oneOf and nested refs] Test 'valid person against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", details: ErrorDetails { path: \"/first_name\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against strict punc' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_puncs_6 ... FAILED
failures:
failures:
test_puncs_6
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.01s
error: test failed, to rerun pass `--test tests`

View File

@ -1,57 +0,0 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.47s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "type", "manager_id", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"type", "name", "manager_id", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {"manager_id", "type", "job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"manager_id", "job_id", "type", "name"}
DEBUG: validate_refs merging res from entity. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/my_job/name
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job/type. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"type", "name"}
DEBUG: validate_refs merging res from job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {"manager_id", "type", "name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
thread 'test_puncs_6' (15109801) panicked at tests/tests.rs:150:44:
called `Result::unwrap()` on an `Err` value: "[complex punc type matching with oneOf and nested refs] Test 'valid person against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", details: ErrorDetails { path: \"/first_name\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against strict punc' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_puncs_6 ... FAILED
failures:
failures:
test_puncs_6
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.00s
error: test failed, to rerun pass `--test tests`

View File

@ -1,106 +0,0 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.41s
Running tests/tests.rs (target/debug/deps/tests-0f6b1e496850f0af)
running 1 test
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"name", "job_id", "manager_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"name", "job_id", "manager_id", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"name", "manager_id", "job_id", "type"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "job_id", "type"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id", "type"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"job_id", "type", "name"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"job_id", "nested_or_super_job", "manager_id", "type", "name"}
DEBUG: check_strictness at . Extensible: false. Keys: {"job_id", "nested_or_super_job", "manager_id", "type", "name", "root_job"}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"manager_id", "name", "job_id"}
DEBUG: validate_refs merging res from super_job. Keys: {"manager_id", "name", "job_id", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: validate_object inserted 'nested_or_super_job' at /nested_or_super_job. Keys: {"type", "manager_id", "job_id", "name"}
DEBUG: check_strictness at /root_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /root_job/name. Keys: {}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /root_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /root_job/job_id. Keys: {"name"}
DEBUG: check_strictness at /root_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /root_job/type. Keys: {"name", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"name", "type", "job_id"}
DEBUG: validate_refs merging res from job. Keys: {"name", "type", "job_id"}
DEBUG: check_strictness at /root_job. Extensible: false. Keys: {"type", "name", "job_id"}
DEBUG: validate_object inserted 'root_job' at /root_job. Keys: {"type", "manager_id", "job_id", "name", "nested_or_super_job"}
DEBUG: check_strictness at . Extensible: false. Keys: {"type", "root_job", "manager_id", "job_id", "name", "nested_or_super_job"}
DEBUG: validate_refs merging res from entity. Keys: {}
DEBUG: validate_refs merging res from job. Keys: {}
DEBUG: validate_refs merging res from super_job. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/my_job/name. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name"}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/my_job/type. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"type", "name"}
DEBUG: validate_refs merging res from job. Keys: {"type", "name"}
DEBUG: check_strictness at /nested_or_super_job/my_job. Extensible: false. Keys: {"name", "type"}
DEBUG: validate_object inserted 'my_job' at /nested_or_super_job/my_job. Keys: {"name", "type"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: check_strictness at /nested_or_super_job/name. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'name' at /nested_or_super_job/name. Keys: {}
DEBUG: validate_refs merging res from entity. Keys: {"name"}
DEBUG: check_strictness at /nested_or_super_job/job_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'job_id' at /nested_or_super_job/job_id. Keys: {"name"}
DEBUG: validate_refs merging res from job. Keys: {"name", "job_id"}
DEBUG: check_strictness at /nested_or_super_job/manager_id. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'manager_id' at /nested_or_super_job/manager_id. Keys: {"job_id", "name"}
DEBUG: check_strictness at /nested_or_super_job/type. Extensible: false. Keys: {}
DEBUG: validate_object inserted 'type' at /nested_or_super_job/type. Keys: {"manager_id", "job_id", "name"}
DEBUG: validate_refs merging res from super_job. Keys: {"job_id", "manager_id", "type", "name"}
DEBUG: check_strictness at /nested_or_super_job. Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("polymorphic_org_punc.request") ref=Some("organization.family")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
DEBUG: VALIDATE ROOT: id=Some("strict_org_punc.request") ref=Some("organization")
DEBUG: check_strictness at . Extensible: false. Keys: {}
thread 'test_puncs_6' (15121282) panicked at tests/tests.rs:150:44:
called `Result::unwrap()` on an `Err` value: "[complex punc type matching with oneOf and nested refs] Test 'valid person against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'first_name'\", details: ErrorDetails { path: \"/first_name\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against organization punc (polymorphic)' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]\n[complex punc type matching with oneOf and nested refs] Test 'valid organization against strict punc' failed. Expected: true, Got: true. Errors: [Error { punc: None, code: \"STRICT_PROPERTY_VIOLATION\", message: \"Unexpected property 'id'\", details: ErrorDetails { path: \"/id\" } }]"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test test_puncs_6 ... FAILED
failures:
failures:
test_puncs_6
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 338 filtered out; finished in 0.01s
error: test failed, to rerun pass `--test tests`

View File

@ -113,6 +113,9 @@ impl Compiler {
Self::compile_recursive(Arc::make_mut(s));
}
}
if let Some(add_props) = &mut schema.additional_properties {
Self::compile_recursive(Arc::make_mut(add_props));
}
// ... Recurse logic ...
if let Some(items) = &mut schema.items {
@ -323,6 +326,11 @@ impl Compiler {
Self::compile_index(sub_schema, registry, current_base.clone(), sub);
}
}
if let Some(add_props) = &schema.additional_properties {
let mut sub = child_pointer.clone();
sub.push("additionalProperties".to_string());
Self::compile_index(add_props, registry, current_base.clone(), sub);
}
if let Some(contains) = &schema.contains {
let mut sub = child_pointer.clone();
sub.push("contains".to_string());
@ -379,7 +387,7 @@ impl Compiler {
// Schema struct modifications require &mut.
let mut final_schema = Arc::try_unwrap(root).unwrap_or_else(|arc| (*arc).clone());
final_schema.obj.compiled_schemas = Some(Arc::new(registry));
final_schema.obj.compiled_registry = Some(Arc::new(registry));
Arc::new(final_schema)
}

View File

@ -7,14 +7,13 @@ pub struct Drop {
// 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)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Error>,
}
@ -30,7 +29,15 @@ impl Drop {
pub fn success() -> Self {
Self {
type_: "drop".to_string(),
response: Some(serde_json::json!({ "result": "success" })), // Or appropriate success response
response: Some(serde_json::json!("success")),
errors: vec![],
}
}
pub fn success_with_val(val: Value) -> Self {
Self {
type_: "drop".to_string(),
response: Some(val),
errors: vec![],
}
}
@ -46,8 +53,6 @@ impl Drop {
#[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,

View File

@ -11,14 +11,23 @@ mod schema;
pub mod util;
mod validator;
use crate::registry::REGISTRY;
use crate::schema::Schema;
use serde_json::{Value, json};
use std::sync::{Arc, RwLock};
lazy_static::lazy_static! {
// Global Atomic Swap Container:
// - RwLock: To protect the SWAP of the Option.
// - Option: Because it starts empty.
// - Arc: Because multiple running threads might hold the OLD validator while we swap.
// - Validator: It immutably owns the Registry.
static ref GLOBAL_VALIDATOR: RwLock<Option<Arc<validator::Validator>>> = RwLock::new(None);
}
#[pg_extern(strict)]
fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
let mut registry = REGISTRY.write().unwrap();
registry.clear();
pub fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
// 1. Build a new Registry LOCALLY (on stack)
let mut registry = registry::Registry::new();
// Generate Family Schemas from Types
{
@ -54,8 +63,7 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
});
if let Ok(schema) = serde_json::from_value::<Schema>(schema_json) {
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
registry.insert(id, compiled);
registry.add(schema);
}
}
@ -74,14 +82,8 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
for schema_val in schemas {
// Deserialize into our robust Schema struct to ensure validity/parsing
if let Ok(schema) = serde_json::from_value::<Schema>(schema_val.clone()) {
if let Some(id) = &schema.obj.id {
let id_clone = id.clone();
// Store the compiled Schema in the registry.
// The registry.insert method now handles simple insertion of CompiledSchema
let compiled =
crate::compiler::Compiler::compile(schema, Some(id_clone.clone()));
registry.insert(id_clone, compiled);
}
// Registry handles compilation
registry.add(schema);
}
}
}
@ -94,42 +96,169 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
cache_items(types);
cache_items(puncs); // public/private distinction logic to come later
}
JsonB(json!({ "response": "success" }))
}
#[pg_extern(strict, parallel_safe)]
fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
let drop = validator::Validator::validate(schema_id, &instance.0);
// 2. Wrap in Validator and Arc
let new_validator = validator::Validator::new(registry);
let new_arc = Arc::new(new_validator);
// 3. ATOMIC SWAP
{
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
*lock = Some(new_arc);
}
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
let registry = REGISTRY.read().unwrap();
registry.get(schema_id).is_some()
}
pub fn mask_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
lock.clone()
};
#[pg_extern(strict)]
fn clear_json_schemas() -> JsonB {
let mut registry = REGISTRY.write().unwrap();
registry.clear();
JsonB(json!({ "response": "success" }))
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
// We need a mutable copy of the value to mask it
let mut mutable_instance = instance.0.clone();
match validator.mask(schema_id, &mut mutable_instance) {
Ok(result) => {
// If valid, return the MASKED instance
if result.is_valid() {
let drop = crate::drop::Drop::success_with_val(mutable_instance);
JsonB(serde_json::to_value(drop).unwrap())
} else {
// If invalid, return errors (Schema Validation Errors)
let errors: Vec<crate::drop::Error> = result
.errors
.into_iter()
.map(|e| crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
})
.collect();
let drop = crate::drop::Drop::with_errors(errors);
JsonB(serde_json::to_value(drop).unwrap())
}
}
Err(e) => {
// Schema Not Found or other fatal error
let error = crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
#[pg_extern(strict, parallel_safe)]
fn show_json_schemas() -> JsonB {
let registry = REGISTRY.read().unwrap();
// Debug dump
// In a real scenario we might return the whole map, but for now just success
// or maybe a list of keys
JsonB(json!({ "response": "success", "count": registry.len() }))
pub fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
// 1. Acquire Snapshot
let validator_arc = {
let lock = GLOBAL_VALIDATOR.read().unwrap();
lock.clone()
};
// 2. Validate (Lock-Free)
if let Some(validator) = validator_arc {
match validator.validate(schema_id, &instance.0) {
Ok(result) => {
if result.is_valid() {
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
} else {
let errors: Vec<crate::drop::Error> = result
.errors
.into_iter()
.map(|e| crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
})
.collect();
let drop = crate::drop::Drop::with_errors(errors);
JsonB(serde_json::to_value(drop).unwrap())
}
}
Err(e) => {
let error = crate::drop::Error {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails { path: e.path },
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
} else {
let error = crate::drop::Error {
code: "VALIDATOR_NOT_INITIALIZED".to_string(),
message: "JSON Schemas have not been cached yet. Run cache_json_schemas()".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
},
};
let drop = crate::drop::Drop::with_errors(vec![error]);
JsonB(serde_json::to_value(drop).unwrap())
}
}
#[pg_extern(strict, parallel_safe)]
pub fn json_schema_cached(schema_id: &str) -> bool {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
match validator.validate(schema_id, &serde_json::Value::Null) {
Err(e) if e.code == "SCHEMA_NOT_FOUND" => false,
_ => true,
}
} else {
false
}
}
#[pg_extern(strict)]
pub fn clear_json_schemas() -> JsonB {
let mut lock = GLOBAL_VALIDATOR.write().unwrap();
*lock = None;
let drop = crate::drop::Drop::success();
JsonB(serde_json::to_value(drop).unwrap())
}
#[pg_extern(strict, parallel_safe)]
pub fn show_json_schemas() -> JsonB {
if let Some(validator) = GLOBAL_VALIDATOR.read().unwrap().as_ref() {
let mut keys = validator.get_schema_ids();
keys.sort();
let drop = crate::drop::Drop::success_with_val(json!(keys));
JsonB(serde_json::to_value(drop).unwrap())
} else {
let drop = crate::drop::Drop::success_with_val(json!([]));
JsonB(serde_json::to_value(drop).unwrap())
}
}
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
use pgrx::prelude::*;
include!("tests.rs");
include!("tests/fixtures.rs");
}
#[cfg(test)]

View File

@ -21,6 +21,16 @@ impl Registry {
}
}
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);

View File

@ -33,6 +33,8 @@ pub struct SchemaObject {
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
pub additional_properties: Option<Arc<Schema>>,
pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies
@ -139,7 +141,7 @@ pub struct SchemaObject {
#[serde(skip)]
pub compiled_pattern_properties: Option<Vec<(crate::compiler::CompiledRegex, Arc<Schema>)>>,
#[serde(skip)]
pub compiled_schemas: Option<Arc<crate::registry::Registry>>,
pub compiled_registry: Option<Arc<crate::registry::Registry>>,
}
#[derive(Debug, Clone, Serialize)]

View File

@ -155,6 +155,24 @@ fn test_puncs_7() {
crate::util::run_test_file_at_index(&path, 7).unwrap();
}
#[pg_test]
fn test_additional_properties_0() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_additional_properties_1() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_additional_properties_2() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_exclusive_minimum_0() {
let path = format!("{}/tests/fixtures/exclusiveMinimum.json", env!("CARGO_MANIFEST_DIR"));
@ -1097,6 +1115,30 @@ fn test_pattern_1() {
crate::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_masking_0() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 0).unwrap();
}
#[pg_test]
fn test_masking_1() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 1).unwrap();
}
#[pg_test]
fn test_masking_2() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 2).unwrap();
}
#[pg_test]
fn test_masking_3() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
crate::util::run_test_file_at_index(&path, 3).unwrap();
}
#[pg_test]
fn test_max_properties_0() {
let path = format!("{}/tests/fixtures/maxProperties.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -25,7 +25,7 @@ struct TestCase {
expected: Option<serde_json::Value>,
}
use crate::registry::REGISTRY;
// use crate::registry::REGISTRY; // No longer used directly for tests!
use crate::validator::Validator;
use serde_json::Value;
@ -38,12 +38,6 @@ where
}
pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
// Clear registry to ensure isolation
// {
// let mut registry = REGISTRY.write().unwrap();
// registry.clear();
// }
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)
@ -56,6 +50,7 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
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'
@ -69,12 +64,7 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
if let Ok(schema) =
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
{
// Clone ID upfront to avoid borrow issues
if let Some(id_clone) = schema.obj.id.clone() {
let compiled =
crate::compiler::Compiler::compile(schema, Some(id_clone.clone()));
registry.insert(id_clone, compiled);
}
registry.add(schema);
}
}
}
@ -118,8 +108,7 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
});
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
registry.insert(id, compiled);
registry.add(schema);
}
}
}
@ -134,20 +123,16 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
// 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(schema) => {
let id = schema
.obj
.id
.clone()
.or_else(|| {
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
Some(format!("test:{}:{}", path, index))
})
.unwrap();
let mut registry_ref = &mut registry;
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
registry_ref.insert(id, compiled);
let id = format!("test:{}:{}", path, index);
schema.obj.id = Some(id);
registry.add(schema);
}
}
Err(e) => {
eprintln!(
@ -158,6 +143,9 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
}
}
// 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();
@ -193,18 +181,54 @@ pub fn run_test_file_at_index(path: &str, index: usize) -> Result<(), String> {
}
if let Some(sid) = schema_id {
let result = Validator::validate_with_registry(&sid, &test.data, &registry);
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),
};
if !result.errors.is_empty() != !test.valid {
failures.push(format!(
"[{}] Test '{}' failed. Expected: {}, Got: {}. Errors: {:?}",
group.description,
test.description,
test.valid,
!result.errors.is_empty(), // "Got Invalid?"
result.errors
"[{}] 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.",
@ -227,8 +251,11 @@ pub fn run_test_file(path: &str) -> Result<(), String> {
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 = |items_val: Option<Value>| {
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 {
@ -238,14 +265,7 @@ pub fn run_test_file(path: &str) -> Result<(), String> {
if let Ok(schema) =
serde_json::from_value::<crate::schema::Schema>(schema_val.clone())
{
// Clone ID upfront to avoid borrow issues
if let Some(id_clone) = schema.obj.id.clone() {
let mut registry = REGISTRY.write().unwrap();
// Utilize the new compile method which handles strictness
let compiled =
crate::compiler::Compiler::compile(schema, Some(id_clone.clone()));
registry.insert(id_clone, compiled);
}
registry.add(schema);
}
}
}
@ -291,18 +311,16 @@ pub fn run_test_file(path: &str) -> Result<(), String> {
});
if let Ok(schema) = serde_json::from_value::<crate::schema::Schema>(schema_json) {
let mut registry = REGISTRY.write().unwrap();
let compiled = crate::compiler::Compiler::compile(schema, Some(id.clone()));
registry.insert(id, compiled);
registry.add(schema);
}
}
}
}
// Register 'types', 'enums', and 'puncs' if present (JSPG style)
register_schemas(group.types);
register_schemas(group.enums);
register_schemas(group.puncs);
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
@ -310,41 +328,67 @@ pub fn run_test_file(path: &str) -> Result<(), String> {
// Register main 'schema' if present (Standard style)
if let Some(ref schema_val) = group.schema {
let mut registry = REGISTRY.write().unwrap();
let schema: crate::schema::Schema =
let mut schema: crate::schema::Schema =
serde_json::from_value(schema_val.clone()).expect("Failed to parse test schema");
let compiled = crate::compiler::Compiler::compile(schema, Some(unique_id.clone()));
registry.insert(unique_id.clone(), compiled);
// 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 drop = Validator::validate(&schema_id, &test.data);
let result = validator.validate(&schema_id, &test.data);
if test.valid {
if !drop.errors.is_empty() {
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,
drop.errors
res.errors
);
eprintln!("{}", msg);
failures.push(msg);
}
} else {
if drop.errors.is_empty() {
}
Err(e) => {
let msg = format!(
"Test failed (expected invalid): {}\nSchema: {:?}\nData: {:?}\nErrors: (Empty)",
"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
);
println!("{}", msg);
eprintln!("{}", msg);
failures.push(msg);
}
}
Err(_) => {
// Expected invalid, got error (which implies invalid/failure), so this is PASS.
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -156,6 +156,24 @@ fn test_puncs_7() {
util::run_test_file_at_index(&path, 7).unwrap();
}
#[test]
fn test_additional_properties_0() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 0).unwrap();
}
#[test]
fn test_additional_properties_1() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 1).unwrap();
}
#[test]
fn test_additional_properties_2() {
let path = format!("{}/tests/fixtures/additionalProperties.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 2).unwrap();
}
#[test]
fn test_exclusive_minimum_0() {
let path = format!("{}/tests/fixtures/exclusiveMinimum.json", env!("CARGO_MANIFEST_DIR"));
@ -1098,6 +1116,30 @@ fn test_pattern_1() {
util::run_test_file_at_index(&path, 1).unwrap();
}
#[test]
fn test_masking_0() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 0).unwrap();
}
#[test]
fn test_masking_1() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 1).unwrap();
}
#[test]
fn test_masking_2() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 2).unwrap();
}
#[test]
fn test_masking_3() {
let path = format!("{}/tests/fixtures/masking.json", env!("CARGO_MANIFEST_DIR"));
util::run_test_file_at_index(&path, 3).unwrap();
}
#[test]
fn test_max_properties_0() {
let path = format!("{}/tests/fixtures/maxProperties.json", env!("CARGO_MANIFEST_DIR"));

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

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

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

113
tests/lib.rs Normal file
View File

@ -0,0 +1,113 @@
use jspg::*;
use pgrx::JsonB;
use serde_json::json;
#[test]
fn test_library_api() {
// 1. Initially, schemas are not cached.
assert!(!json_schema_cached("test_schema"));
// Expected uninitialized drop format: errors + null response
let uninitialized_drop = validate_json_schema("test_schema", JsonB(json!({})));
assert_eq!(
uninitialized_drop.0,
json!({
"type": "drop",
"errors": [{
"code": "VALIDATOR_NOT_INITIALIZED",
"message": "JSON Schemas have not been cached yet. Run cache_json_schemas()",
"details": { "path": "" }
}]
})
);
// 2. Cache schemas
let puncs = json!([]);
let types = json!([{
"schemas": [{
"$id": "test_schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}]
}]);
let enums = json!([]);
let cache_drop = cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
assert_eq!(
cache_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 3. Check schemas are cached
assert!(json_schema_cached("test_schema"));
let show_drop = show_json_schemas();
assert_eq!(
show_drop.0,
json!({
"type": "drop",
"response": ["test_schema"]
})
);
// 4. Validate Happy Path
let happy_drop = validate_json_schema("test_schema", JsonB(json!({"name": "Neo"})));
assert_eq!(
happy_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
// 5. Validate Unhappy Path
let unhappy_drop = validate_json_schema("test_schema", JsonB(json!({"wrong": "data"})));
assert_eq!(
unhappy_drop.0,
json!({
"type": "drop",
"errors": [
{
"code": "REQUIRED_FIELD_MISSING",
"message": "Missing name",
"details": { "path": "/name" }
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'",
"details": { "path": "/wrong" }
}
]
})
);
// 6. Mask Happy Path
let mask_drop = mask_json_schema(
"test_schema",
JsonB(json!({"name": "Neo", "extra": "data"})),
);
assert_eq!(
mask_drop.0,
json!({
"type": "drop",
"response": {"name": "Neo"}
})
);
// 7. Clear Schemas
let clear_drop = clear_json_schemas();
assert_eq!(
clear_drop.0,
json!({
"type": "drop",
"response": "success"
})
);
assert!(!json_schema_cached("test_schema"));
}

View File

@ -1 +1 @@
1.0.48
1.0.53