Compare commits

...

30 Commits

Author SHA1 Message Date
ddc86f1ba0 version: 1.0.176 2026-07-07 22:53:59 -04:00
562e52e0eb queryer: deterministic result order — array aggregations emit ORDER BY entity.created_at, entity.id (creation order, id tiebreak; every entity-family type has the entity alias), fixing run-to-run row-order variance (flaky reads like invoice-lines order, unstable grids). Also: the snapshot formatter silently DROPPED aggregate ORDER BY clauses when re-rendering parsed SQL — now rendered (format_function_clauses), so snapshots can't hide ordering changes. 1280 tests green. 2026-07-07 22:53:48 -04:00
569eb1d2ea version: 1.0.175 2026-07-07 21:38:14 -04:00
6cebce3740 queryer: array containment fires on punc-response paths — compile_array hands the entity node the ITEMS PROXY as its schema (no properties), so property-shape checks that read node.schema silently missed; fall back to the type's own schema (Type.schemas[name]). Fixture adds get_people punc + containment/cast filters through it. Live case: get_vendors trade filter still compiled =::jsonb after 1.0.174. 2026-07-07 21:38:08 -04:00
d3de3961c1 version: 1.0.174 2026-07-07 21:18:37 -04:00
d0ae6eeddd queryer: array-property detection handles the compiled list form — engine-compiled schemas normalize to type:["array"] (SchemaTypeOrArray::Multiple), so the containment branch only fired for hand-written Single("array") schemas and live jsonb array filters fell through to =::jsonb ('Token invalid' at execution). Accept both encodings (array, [array], [array,null]). Fixture pins the list form (labels). Found live: agreego vendor.trade filter. 2026-07-07 21:18:31 -04:00
e741a7197f version: 1.0.173 2026-07-07 20:55:10 -04:00
cf3cabd1f5 queryer: typed filter conditions for float and jsonb columns — (a) float4/float8 (the pg typnames that actually arrive in field_types; 'real'/'double precision' never do) now cast the parameter ::numeric, fixing 'double precision >= text' on $gt/$gte/etc; (b) jsonb columns compile explicitly: array-valued properties (tag lists) get CONTAINMENT semantics ($eq→?, $ne→NOT ?, $of→?|, $nof→NOT ?|), non-array jsonb compares ::jsonb for $eq/$ne, and unsupported operators are a loud QUERY_COMPILATION_FAILED naming the property instead of a runtime SQL type error ('jsonb = text'). compile_filter_conditions now returns Result. New fixture case (float8 $gte + jsonb array $eq/$of); 1279 tests green. Unblocks agreego TestListVendors_FilterByTrade/FilterByRating. 2026-07-07 20:54:52 -04:00
d608f5e963 filter synthesis: omit Field-Backed JSONB Bubbles from Composed Filter References — only Table-Backed boundaries have a synthesized .filter to proxy to; a dangling proxy reference breaks downstream code generators consuming the exported registry. Fixture pins a bubble-typed property: omitted from the filter, raw schemas promoted, no phantom .filter. 2026-07-07 13:39:03 -04:00
95600551a7 version: 1.0.172 2026-07-07 12:45:00 -04:00
c18a8c1561 remove icon from sidebar 2026-07-07 12:44:49 -04:00
8ed588ca61 version: 1.0.171 2026-07-07 12:42:56 -04:00
edd0dd4763 jspg cleanup 2026-07-07 12:42:05 -04:00
abf1d12e43 test: mock matcher understands UNION existence reads; revive dead update-path mocks
parse_and_match_mocks matched mock.type against only the first FROM table —
always 'entity' for hierarchy existence reads — so every merger update-case
mock was silently inert and those cases snapshotted the create path. Now each
UNION arm is matched independently (a mock matching any arm returns once,
mirroring UNION dedup) and the type check accepts any FROM/JOIN table.

Regenerated the five revived cases (their snapshots now show real update /
no-op replace / archive semantics) and added the two previously untestable
probe-disagreement cases: same-row dedup (updates, no error) and id-vs-lookup
conflict (TOO_MANY_LOOKUP_ROWS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:05:21 -04:00
a885713563 version: 1.0.170 2026-07-06 13:05:16 -04:00
d9b4f417f6 merger existence read: UNION the id and lk-lookup probes instead of OR-ing them across the hierarchy join — the OR is un-indexable and full-scans the subtype table on every child merge (one scan per relationship-edge write; surfaced by Castleberry onboarding); each UNION arm uses its own index (pk / lk_) and dedup preserves TOO_MANY_LOOKUP_ROWS semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:52:29 -04:00
eae39f92d6 version: 1.0.169 2026-07-04 18:27:57 -04:00
2895f06074 page.personal is an opaque filter template (the 'mine' predicate), not a punc name — same read, scope-derived filter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:27:51 -04:00
c1c16bc814 version: 1.0.168 2026-07-04 18:21:36 -04:00
5885552192 database: page carries 'personal' (the personal-scope sibling punc) and sidebar carries 'icon' — previously stripped by the whitelist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:21:30 -04:00
dc033296d7 version: 1.0.167 2026-07-03 20:04:28 -04:00
03a871bc1a filter synthesis: named value types get their own synthesized, referenced filter
Supersedes the inline-structural approach (0d282cc): inlining erased the value
type's name, so identical nested shapes (weekly_hours at two paths) generated
duplicate leaf types downstream and Dart barrel exports collided. Now a named
non-table value type's filter is synthesized ONCE (like table-backed boundaries)
and property references point at it — mirroring how the entity side generates
one class per named type. Same filter-by-fields capability; laziness also
removes the structural-recursion hazard. A named type with no compilable
structure still gets no filter and is omitted rather than dangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:04:16 -04:00
8aa15873b0 version: 1.0.166 2026-07-03 19:24:16 -04:00
0d282cc930 filter synthesis: compile named non-table value types structurally
A property typed as a named value type (a schema-only config object like
an operating-hours schedule) previously got a dangling {type}.filter
reference — no filter is ever synthesized for a non-table-backed schema,
so the whole parent filter failed downstream (PROXY_TYPE_RESOLUTION_FAILED;
the punc generator emitted an empty filter type).

Naming a value type is a reuse choice, not a semantics choice: it now
compiles structurally into the parent filter, exactly like an inline
object, recursively (including array items). Table-backed boundaries keep
the lazy {type}.filter reference. A named type with no compilable
structure is omitted instead of dangling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:23:54 -04:00
581fc8e0c0 flow update 2026-07-03 01:33:00 -04:00
6f0bff8dc7 version: 1.0.165 2026-07-02 23:43:33 -04:00
99c69e27ab minor dict improvements and flow update 2026-07-02 23:43:23 -04:00
e490e0d844 version: 1.0.164 2026-06-23 20:17:35 -04:00
e026e82f65 test(jspg): rename items fixture to array and resolve unused HashMap warning
- Rename fixtures/items.json to fixtures/array.json to better reflect array testing constraints.
- Update reference paths in src/tests/fixtures.rs and across other fixture JSON files.
- Remove unused HashMap import in src/validator/rules/dict.rs to resolve the compiler warning.
2026-06-23 20:17:03 -04:00
a1fb9ef650 jspg checkpoint 2026-06-23 19:48:38 -04:00
60 changed files with 2372 additions and 5351 deletions

View File

@ -47,7 +47,22 @@ The core execution engines natively enforce these boundaries:
* **Merger**: Strictly bounded to the `Type` Realm. It is philosophically impossible and mathematically illegal to attempt to UPSERT an API endpoint.
* **Queryer**: Routes recursively. Safely evaluates API boundary inputs directly from the `Punc` realm, while tracing underlying table targets back through the `Type` realm to physically compile SQL `SELECT` statements.
### Types of Types
### Basic and Structural Types
JSPG models data using a simplified subset of standard JSON Schema types, categorized into primitives and structural types:
* **Primitives**:
* `string`: Standard string, supporting standard keywords (e.g., `pattern`, `minLength`, `maxLength`) and custom formats (`uuid`, `date-time`, `email`). Empty strings (`""`) are leniently treated as "present but unset".
* `number` & `integer`: Numeric and integer values (e.g., supporting `minimum`, `maximum`).
* `boolean`: Boolean `true` or `false` values.
* `null`: Represents database `NULL` or unset fields.
* **Structural Types**:
* `object`: Represents a structured object with a fixed list of static `properties`. Objects are **strict by default** (raising `STRICT_PROPERTY_VIOLATION` for properties not defined in the schema) unless marked with `"extensible": true`.
* `array`: A homogeneous, one-dimensional collection where every element is validated against the `items` schema. Positional/tuple array validation is not supported, and array strictness checks do not apply.
* `dict`: A dynamic key-value map. It utilizes the `keys` keyword to validate key strings (typically via `pattern`) and the `items` keyword to validate the values. It completely replaces the legacy `additionalProperties` and `patternProperties` keywords. Extensibility is implicitly managed via dynamic keys, so strictness checking does not apply.
### Types of Types (Complex & Referential Types)
Beyond basic primitives and structures, JSPG maps schemas to their storage and database-referential boundaries:
* **Table-Backed (Entity Types)**: Primarily defined in root `types` schemas. These represent physical Postgres tables.
* They are implicitly registered in the Global Registry using their precise key name mapped from the database compilation phase.
* The schema conceptually requires a `type` discriminator at runtime so the engine knows what physical variation to interact with.
@ -162,9 +177,11 @@ It evaluates as an **Independent Declarative Rules Engine**. Every `Case` block
```
### Strict by Default & Extensibility
* **Strictness**: By default, any property not explicitly defined in the schema causes a validation error (effectively enforcing `additionalProperties: false` globally).
* **Extensibility (`extensible: true`)**: To allow a free-for-all of undefined properties, schemas must explicitly declare `"extensible": true`.
* **Structured Additional Properties**: If `additionalProperties: {...}` is defined as a schema, arbitrary keys are allowed so long as their values match the defined type constraint.
* **Strictness**: Strictness is a first-class feature for object schemas. By default, any property present in the JSON instance that is not explicitly defined in the schema's `properties` (or inherited, etc.) causes a `STRICT_PROPERTY_VIOLATION` validation error.
* **Extensibility (`extensible: true`)**: This keyword is valid **only** at the `type: "object"` level. By setting `"extensible": true`, you bypass the strictness checking on the object, allowing arbitrary undefined properties to pass.
* **Type: dict and type: array**: These types are dynamic/homogeneous:
* **`dict`**: Defined with `keys` and `items` schemas. Its keys and values are dynamic, so strictness checking does not apply. `"extensible"` is not applicable at the `dict` level.
* **`array`**: Homogeneous collection of items matching the `items` schema. Validation is homogeneous, so strictness checking does not apply. `"extensible"` is not applicable at the `array` level (tuple-like `prefixItems` are removed).
* **Inheritance Boundaries**: Strictness resets when crossing non-primitive `type` boundaries. A schema extending a strict parent remains strict unless it explicitly overrides with `"extensible": true`.
### Format Leniency for Empty Strings
@ -206,7 +223,7 @@ Traits are reusable, non-generating schema fragments used to share properties an
}
```
* **Resolution and Merging**: During `Database::new()`, includes are resolved and merged at the raw JSON level:
* **`properties` / `patternProperties`**: Map keys from the host schema override/shadow included traits.
* **`properties`**: Map keys from the host schema override/shadow included traits.
* **`required` / `display`**: Lists are merged and deduped.
* **`dependencies`**: Merged by combining and deduping lists.
* **Scalars / Arrays / Items**: Host definitions completely override included traits.

View File

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

590
fixtures/array.json Normal file
View File

@ -0,0 +1,590 @@
[
{
"description": "array type without items allows any items",
"database": {
"types": [
{
"name": "array_0_0",
"schemas": {
"array_0_0": {
"type": "array"
}
}
}
]
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"schema_id": "array_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with items is valid",
"data": [
1,
"foo"
],
"schema_id": "array_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with arbitrary objects is valid since items is not constrained",
"data": [
{
"extra_prop": 1
}
],
"schema_id": "array_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items with boolean schema (true)",
"database": {
"types": [
{
"name": "array_1_0",
"schemas": {
"array_1_0": {
"items": true
}
}
}
]
},
"tests": [
{
"description": "any array is valid",
"data": [
1,
"foo",
true
],
"schema_id": "array_1_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "items with boolean schema (false)",
"database": {
"types": [
{
"name": "array_2_0",
"schemas": {
"array_2_0": {
"items": false
}
}
}
]
},
"tests": [
{
"description": "empty array is valid",
"data": [],
"schema_id": "array_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "any non-empty array is invalid",
"data": [
1,
"foo"
],
"schema_id": "array_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "FALSE_SCHEMA",
"details": {
"path": "0",
"schema": "array_2_0"
}
},
{
"code": "FALSE_SCHEMA",
"details": {
"path": "1",
"schema": "array_2_0"
}
}
]
}
}
]
},
{
"description": "a schema given for items",
"database": {
"types": [
{
"name": "array_3_0",
"schemas": {
"array_3_0": {
"items": {
"type": "integer"
}
}
}
}
]
},
"tests": [
{
"description": "valid items",
"data": [
1,
2,
3
],
"schema_id": "array_3_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "wrong type of items",
"data": [
1,
"x"
],
"schema_id": "array_3_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "integer"
},
"details": {
"path": "1",
"schema": "array_3_0"
}
}
]
}
},
{
"description": "non-arrays are invalid",
"data": {
"foo": "bar"
},
"schema_id": "array_3_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_PROPERTY_VIOLATION",
"values": {
"property_name": "foo"
},
"details": {
"path": "foo",
"schema": "array_3_0"
}
}
]
}
},
{
"description": "JavaScript pseudo-arrays are invalid",
"data": {
"0": "invalid",
"length": 1
},
"schema_id": "array_3_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_PROPERTY_VIOLATION",
"values": {
"property_name": "0"
},
"details": {
"path": "0",
"schema": "array_3_0"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"values": {
"property_name": "length"
},
"details": {
"path": "length",
"schema": "array_3_0"
}
}
]
}
},
{
"description": "primitive strings are valid since items is ignored on primitives",
"data": "hello",
"schema_id": "array_3_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "nested items",
"database": {
"types": [
{
"name": "array_4_0",
"schemas": {
"array_4_0": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
}
}
}
}
]
},
"tests": [
{
"description": "valid nested array",
"data": [
[
[
[
1
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"schema_id": "array_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nested array with invalid type",
"data": [
[
[
[
"1"
]
],
[
[
2
],
[
3
]
]
],
[
[
[
4
],
[
5
],
[
6
]
]
]
],
"schema_id": "array_4_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "number"
},
"details": {
"path": "0/0/0/0",
"schema": "array_4_0"
}
}
]
}
},
{
"description": "not deep enough",
"data": [
[
[
1
],
[
2
],
[
3
]
],
[
[
4
],
[
5
],
[
6
]
]
],
"schema_id": "array_4_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "0/0/0",
"schema": "array_4_0"
}
},
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "0/1/0",
"schema": "array_4_0"
}
},
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "0/2/0",
"schema": "array_4_0"
}
},
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "1/0/0",
"schema": "array_4_0"
}
},
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "1/1/0",
"schema": "array_4_0"
}
},
{
"code": "INVALID_TYPE",
"values": {
"expected": "array"
},
"details": {
"path": "1/2/0",
"schema": "array_4_0"
}
}
]
}
}
]
},
{
"description": "items with null instance elements",
"database": {
"types": [
{
"name": "array_5_0",
"schemas": {
"array_5_0": {
"items": {
"type": "null"
}
}
}
}
]
},
"tests": [
{
"description": "allows null elements",
"data": [
null
],
"schema_id": "array_5_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "array: items extensible",
"database": {
"types": [
{
"name": "array_6_0",
"schemas": {
"array_6_0": {
"type": "array",
"items": {
"extensible": true
}
}
}
}
]
},
"tests": [
{
"description": "array with items is valid (items explicitly allowed to be anything extensible)",
"data": [
1,
"foo",
{}
],
"schema_id": "array_6_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "array: items strict",
"database": {
"types": [
{
"name": "array_7_0",
"schemas": {
"array_7_0": {
"type": "array",
"items": {
"type": "object",
"extensible": false
}
}
}
}
]
},
"tests": [
{
"description": "array with strict object items is valid",
"data": [
{}
],
"schema_id": "array_7_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "array with invalid strict object items (extra property)",
"data": [
{
"extra": 1
}
],
"schema_id": "array_7_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_PROPERTY_VIOLATION",
"values": {
"property_name": "extra"
},
"details": {
"path": "0/extra",
"schema": "array_7_0"
}
}
]
}
}
]
}
]

View File

@ -268,16 +268,6 @@
"path": "",
"schema": "booleanSchema_1_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "booleanSchema_1_0"
}
}
]
}

View File

@ -156,26 +156,6 @@
"path": "",
"schema": "const_1_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_1_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "1"
},
"details": {
"path": "1",
"schema": "const_1_0"
}
}
]
}
@ -233,16 +213,6 @@
"path": "",
"schema": "const_2_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_2_0"
}
}
]
}
@ -268,36 +238,6 @@
"path": "",
"schema": "const_2_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_2_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "1"
},
"details": {
"path": "1",
"schema": "const_2_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "2"
},
"details": {
"path": "2",
"schema": "const_2_0"
}
}
]
}
@ -534,16 +474,6 @@
"path": "",
"schema": "const_6_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_6_0"
}
}
]
}
@ -567,16 +497,6 @@
"path": "",
"schema": "const_6_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_6_0"
}
}
]
}
@ -630,16 +550,6 @@
"path": "",
"schema": "const_7_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_7_0"
}
}
]
}
@ -663,16 +573,6 @@
"path": "",
"schema": "const_7_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "const_7_0"
}
}
]
}

View File

@ -72,8 +72,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -268,23 +268,13 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "1"
"limit": "1",
"count": "0"
},
"details": {
"path": "",
"schema": "contains_3_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "contains_3_0"
}
}
]
}
@ -383,8 +373,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"value": "3",
"multiple_of": "2"
"multiple_of": "2",
"value": "3"
},
"details": {
"path": "0",
@ -431,8 +421,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -453,8 +443,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"multiple_of": "2",
"value": "5"
"value": "5",
"multiple_of": "2"
},
"details": {
"path": "1",
@ -599,7 +589,7 @@
},
"tests": [
{
"description": "extra items cause failure",
"description": "extra items do not cause failure",
"data": [
1,
2
@ -607,19 +597,7 @@
"schema_id": "contains_8_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "1"
},
"details": {
"path": "1",
"schema": "contains_8_0"
}
}
]
"success": true
}
},
{

View File

@ -62,8 +62,8 @@
"code": "EDGE_MISSING",
"values": {
"parent_type": "org",
"child_type": "user",
"property_name": "missing_users"
"property_name": "missing_users",
"child_type": "user"
},
"details": {
"path": "full.org/missing_users",
@ -152,8 +152,8 @@
"code": "EDGE_MISSING",
"values": {
"child_type": "child",
"property_name": "children",
"parent_type": "parent"
"parent_type": "parent",
"property_name": "children"
},
"details": {
"path": "full.parent/children",
@ -254,9 +254,9 @@
{
"code": "AMBIGUOUS_TYPE_RELATIONS",
"values": {
"parent_type": "invoice",
"child_type": "activity",
"property_name": "activities",
"parent_type": "invoice"
"property_name": "activities"
},
"details": {
"path": "full.invoice/activities",
@ -470,8 +470,8 @@
{
"code": "DATABASE_TYPE_PARSE_FAILED",
"values": {
"type": "failure",
"reason": "invalid type: sequence, expected a string"
"reason": "invalid type: sequence, expected a string",
"type": "failure"
},
"details": {
"context": "failure"

View File

@ -65,8 +65,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "foo",
"property_name": "bar"
"property_name": "bar",
"required_property": "foo"
},
"details": {
"path": "",
@ -227,8 +227,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"property_name": "quux",
"required_property": "bar"
"required_property": "bar",
"property_name": "quux"
},
"details": {
"path": "",
@ -362,8 +362,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "foo\rbar",
"property_name": "foo\nbar"
"property_name": "foo\nbar",
"required_property": "foo\rbar"
},
"details": {
"path": "",
@ -579,19 +579,7 @@
"schema_id": "schema_schema1",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "schema_schema1"
}
}
]
"success": true
}
},
{
@ -841,8 +829,8 @@
{
"code": "MIN_PROPERTIES_VIOLATED",
"values": {
"count": "2",
"limit": "4"
"limit": "4",
"count": "2"
},
"details": {
"path": "",

284
fixtures/dict.json Normal file
View File

@ -0,0 +1,284 @@
[
{
"description": "basic dict type validation",
"database": {
"types": [
{
"name": "dict_basic",
"schemas": {
"dict_basic": {
"type": "dict"
}
}
}
]
},
"tests": [
{
"description": "valid empty dict",
"data": {},
"schema_id": "dict_basic",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "valid simple dict",
"data": {
"key1": "value1",
"key2": 123,
"key3": true
},
"schema_id": "dict_basic",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid type - array is not dict",
"data": [
"a",
"b"
],
"schema_id": "dict_basic",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "dict"
},
"details": {
"path": "",
"schema": "dict_basic"
}
}
]
}
},
{
"description": "invalid type - string is not dict",
"data": "not a dict",
"schema_id": "dict_basic",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "dict"
},
"details": {
"path": "",
"schema": "dict_basic"
}
}
]
}
}
]
},
{
"description": "dict keys validation",
"database": {
"types": [
{
"name": "dict_keys",
"schemas": {
"dict_keys": {
"type": "dict",
"keys": {
"type": "string",
"pattern": "^[a-z]{3}$"
}
}
}
}
]
},
"tests": [
{
"description": "valid 3-letter lowercase keys",
"data": {
"abc": 1,
"xyz": "test"
},
"schema_id": "dict_keys",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid key format",
"data": {
"abc": 1,
"abcd": 2
},
"schema_id": "dict_keys",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "PATTERN_VIOLATED",
"values": {
"pattern": "^[a-z]{3}$",
"value": "abcd"
},
"details": {
"path": "keys/abcd",
"schema": "dict_keys"
}
}
]
}
}
]
},
{
"description": "dict items (values) validation",
"database": {
"types": [
{
"name": "dict_items",
"schemas": {
"dict_items": {
"type": "dict",
"items": {
"type": "integer"
}
}
}
}
]
},
"tests": [
{
"description": "valid integer values",
"data": {
"a": 1,
"b": 100
},
"schema_id": "dict_items",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid value type",
"data": {
"a": 1,
"b": "string value"
},
"schema_id": "dict_items",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "integer"
},
"details": {
"path": "b",
"schema": "dict_items"
}
}
]
}
}
]
},
{
"description": "dict keys and items validation combined",
"database": {
"types": [
{
"name": "dict_combined",
"schemas": {
"dict_combined": {
"type": "dict",
"keys": {
"type": "string",
"pattern": "^[0-9]+$"
},
"items": {
"type": "boolean"
}
}
}
}
]
},
"tests": [
{
"description": "valid numeric keys and boolean values",
"data": {
"123": true,
"456": false
},
"schema_id": "dict_combined",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "invalid key and valid value",
"data": {
"123": true,
"abc": false
},
"schema_id": "dict_combined",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "PATTERN_VIOLATED",
"values": {
"pattern": "^[0-9]+$",
"value": "abc"
},
"details": {
"path": "keys/abc",
"schema": "dict_combined"
}
}
]
}
},
{
"description": "valid key and invalid value",
"data": {
"123": "not a boolean"
},
"schema_id": "dict_combined",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"values": {
"expected": "boolean"
},
"details": {
"path": "123",
"schema": "dict_combined"
}
}
]
}
}
]
}
]

View File

@ -164,8 +164,8 @@
{
"code": "DYNAMIC_TYPE_RESOLUTION_FAILED",
"values": {
"discriminator": "kind",
"pointer": "$kind.filter"
"pointer": "$kind.filter",
"discriminator": "kind"
},
"details": {
"path": "filter",

View File

@ -560,16 +560,6 @@
"path": "",
"schema": "enum_6_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_6_0"
}
}
]
}
@ -593,16 +583,6 @@
"path": "",
"schema": "enum_6_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_6_0"
}
}
]
}
@ -728,16 +708,6 @@
"path": "",
"schema": "enum_8_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_8_0"
}
}
]
}
@ -761,16 +731,6 @@
"path": "",
"schema": "enum_8_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_8_0"
}
}
]
}
@ -873,16 +833,6 @@
"path": "",
"schema": "enum_10_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_10_0"
}
}
]
}
@ -1007,16 +957,6 @@
"path": "",
"schema": "enum_12_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "enum_12_0"
}
}
]
}

View File

@ -34,8 +34,8 @@
{
"code": "EXCLUSIVE_MAXIMUM_VIOLATED",
"values": {
"value": "3",
"limit": "3"
"limit": "3",
"value": "3"
},
"details": {
"path": "",

View File

@ -34,8 +34,8 @@
{
"code": "EXCLUSIVE_MINIMUM_VIOLATED",
"values": {
"value": "1.1",
"limit": "1.1"
"limit": "1.1",
"value": "1.1"
},
"details": {
"path": "",
@ -56,8 +56,8 @@
{
"code": "EXCLUSIVE_MINIMUM_VIOLATED",
"values": {
"limit": "1.1",
"value": "0.6"
"value": "0.6",
"limit": "1.1"
},
"details": {
"path": "",

View File

@ -87,6 +87,34 @@
"type": "string"
}
}
},
"schedule": {
"type": [
"opening_hours",
"null"
]
}
}
},
"opening_hours": {
"type": "object",
"properties": {
"open": {
"type": "string"
},
"seasons": {
"type": "array",
"items": {
"type": "season"
}
}
}
},
"season": {
"type": "object",
"properties": {
"label": {
"type": "string"
}
}
}
@ -483,7 +511,9 @@
]
}
}
}
},
"opening_hours": {},
"season": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,8 +106,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "2",
"limit": "1"
"limit": "1",
"count": "2"
},
"details": {
"path": "",
@ -144,8 +144,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "2",
"limit": "1"
"limit": "1",
"count": "2"
},
"details": {
"path": "",
@ -284,8 +284,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "4",
"limit": "3"
"limit": "3",
"count": "4"
},
"details": {
"path": "",

View File

@ -7,8 +7,7 @@
"name": "maxItems_0_0",
"schemas": {
"maxItems_0_0": {
"maxItems": 2,
"extensible": true
"maxItems": 2
}
}
}
@ -53,8 +52,8 @@
{
"code": "MAX_ITEMS_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",
@ -83,8 +82,7 @@
"name": "maxItems_1_0",
"schemas": {
"maxItems_1_0": {
"maxItems": 2,
"extensible": true
"maxItems": 2
}
}
}
@ -117,8 +115,8 @@
{
"code": "MAX_ITEMS_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",
@ -138,8 +136,7 @@
"name": "maxItems_2_0",
"schemas": {
"maxItems_2_0": {
"maxItems": 2,
"extensible": true
"maxItems": 2
}
}
}
@ -161,8 +158,8 @@
{
"code": "MAX_ITEMS_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",
@ -109,8 +109,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",

View File

@ -139,8 +139,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",
@ -190,8 +190,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"count": "1",
"limit": "0"
"limit": "0",
"count": "1"
},
"details": {
"path": "",
@ -234,8 +234,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",

View File

@ -118,8 +118,8 @@
{
"code": "MAXIMUM_VIOLATED",
"values": {
"value": "300.5",
"limit": "300"
"limit": "300",
"value": "300.5"
},
"details": {
"path": "",

View File

@ -239,8 +239,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"property_name": "trigger",
"required_property": "base_dep"
"required_property": "base_dep",
"property_name": "trigger"
},
"details": {
"path": "",
@ -264,8 +264,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "child_dep",
"property_name": "trigger"
"property_name": "trigger",
"required_property": "child_dep"
},
"details": {
"path": "",

View File

@ -1310,9 +1310,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
@ -1463,12 +1463,18 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
" AND \"date_of_birth\" = '{{timestamp}}'",
" AND \"pronouns\" = 'they/them'))"
@ -1616,12 +1622,18 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
" AND \"date_of_birth\" = '{{timestamp}}'",
" AND \"pronouns\" = 'they/them'))"
@ -1759,9 +1771,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:mocks.0.id}}')"
],
@ -1882,9 +1894,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '123')"
],
@ -2221,10 +2233,14 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order\" t2 ON ",
"LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = 'abc'",
" OR (\"id\" = 'abc'))"
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id",
"WHERE",
" (\"id\" = 'abc'))"
],
[
"INSERT INTO agreego.\"entity\" (",
@ -3024,9 +3040,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = 'abc-archived')"
],
@ -3377,7 +3393,7 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order_line\" t2 ON ",
"LEFT JOIN agreego.\"order_line\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.lines.0.id}}')"
],
@ -3501,10 +3517,14 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"invoice\" t2 ON ",
"LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"id\" = '{{uuid:data.id}}'))"
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id",
"WHERE",
" (\"id\" = '{{uuid:data.id}}'))"
],
[
"INSERT INTO agreego.\"entity\" (",
@ -3618,7 +3638,7 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"account\" t2 ON ",
"LEFT JOIN agreego.\"account\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}')"
],

View File

@ -169,8 +169,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "2"
"limit": "2",
"count": "0"
},
"details": {
"path": "",
@ -193,8 +193,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",
@ -218,8 +218,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",
@ -301,8 +301,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",
@ -357,8 +357,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "2"
"limit": "2",
"count": "0"
},
"details": {
"path": "",
@ -407,8 +407,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",
@ -487,8 +487,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "1",
"limit": "3"
"limit": "3",
"count": "1"
},
"details": {
"path": "",
@ -538,8 +538,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "3",
"count": "2"
"count": "2",
"limit": "3"
},
"details": {
"path": "",
@ -549,8 +549,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "2"
"count": "2",
"limit": "1"
},
"details": {
"path": "",

View File

@ -7,8 +7,7 @@
"name": "minItems_0_0",
"schemas": {
"minItems_0_0": {
"minItems": 1,
"extensible": true
"minItems": 1
}
}
}
@ -79,8 +78,7 @@
"name": "minItems_1_0",
"schemas": {
"minItems_1_0": {
"minItems": 1,
"extensible": true
"minItems": 1
}
}
}

View File

@ -43,8 +43,8 @@
{
"code": "MIN_LENGTH_VIOLATED",
"values": {
"count": "1",
"limit": "2"
"limit": "2",
"count": "1"
},
"details": {
"path": "",
@ -74,8 +74,8 @@
{
"code": "MIN_LENGTH_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",

View File

@ -128,8 +128,8 @@
{
"code": "MIN_PROPERTIES_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MINIMUM_VIOLATED",
"values": {
"value": "0.6",
"limit": "1.1"
"limit": "1.1",
"value": "0.6"
},
"details": {
"path": "",

View File

@ -100,8 +100,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"multiple_of": "1.5",
"value": "35"
"value": "35",
"multiple_of": "1.5"
},
"details": {
"path": "",

View File

@ -407,16 +407,6 @@
"path": "",
"schema": "not_4_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "not_4_0"
}
}
]
}
@ -610,16 +600,6 @@
"path": "",
"schema": "not_5_0"
}
},
{
"code": "STRICT_ITEM_VIOLATION",
"values": {
"index": "0"
},
"details": {
"path": "0",
"schema": "not_5_0"
}
}
]
}

View File

@ -156,8 +156,8 @@
{
"code": "MAXIMUM_VIOLATED",
"values": {
"value": "60",
"limit": "50"
"limit": "50",
"value": "60"
},
"details": {
"path": "max",

View File

@ -34,6 +34,7 @@
{
"code": "PATTERN_VIOLATED",
"values": {
"value": "abc",
"pattern": "^a*$"
},
"details": {

View File

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

View File

@ -148,8 +148,8 @@
{
"code": "NO_FAMILY_MATCH",
"values": {
"discriminator": "type",
"value": "alien"
"value": "alien",
"discriminator": "type"
},
"details": {
"path": "",
@ -284,8 +284,8 @@
{
"code": "NO_FAMILY_MATCH",
"values": {
"discriminator": "type",
"value": "bot"
"value": "bot",
"discriminator": "type"
},
"details": {
"path": "",

View File

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

View File

@ -489,16 +489,6 @@
"path": "constructor",
"schema": "properties_4_0"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"values": {
"property_name": "length"
},
"details": {
"path": "constructor/length",
"schema": "properties_4_0"
}
}
]
}

View File

@ -141,7 +141,8 @@
{
"code": "PATTERN_VIOLATED",
"values": {
"pattern": "^a+$"
"pattern": "^a+$",
"value": "aaA"
},
"details": {
"path": "",

File diff suppressed because it is too large Load Diff

View File

@ -316,536 +316,6 @@
}
]
}
},
{
"description": "non-unique array of more than two arrays is invalid",
"data": [
[
"foo"
],
[
"bar"
],
[
"foo"
]
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_0_0"
}
}
]
}
},
{
"description": "1 and true are unique",
"data": [
1,
true
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "0 and false are unique",
"data": [
0,
false
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[1] and [true] are unique",
"data": [
[
1
],
[
true
]
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[0] and [false] are unique",
"data": [
[
0
],
[
false
]
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nested [1] and [true] are unique",
"data": [
[
[
1
],
"foo"
],
[
[
true
],
"foo"
]
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "nested [0] and [false] are unique",
"data": [
[
[
0
],
"foo"
],
[
[
false
],
"foo"
]
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "unique heterogeneous types are valid",
"data": [
{},
[
1
],
true,
null,
1,
"{}"
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-unique heterogeneous types are invalid",
"data": [
{},
[
1
],
true,
null,
{},
1
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_0_0"
}
}
]
}
},
{
"description": "different objects are unique",
"data": [
{
"a": 1,
"b": 2
},
{
"a": 2,
"b": 1
}
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "objects are non-unique despite key order",
"data": [
{
"a": 1,
"b": 2
},
{
"b": 2,
"a": 1
}
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_0_0"
}
}
]
}
},
{
"description": "{\"a\": false} and {\"a\": 0} are unique",
"data": [
{
"a": false
},
{
"a": 0
}
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "{\"a\": true} and {\"a\": 1} are unique",
"data": [
{
"a": true
},
{
"a": 1
}
],
"schema_id": "uniqueItems_0_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "uniqueItems with an array of items",
"database": {
"types": [
{
"name": "uniqueItems_1_0",
"schemas": {
"uniqueItems_1_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"extensible": true
}
}
}
]
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[false, false] from items array is not valid",
"data": [
false,
false
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_1_0"
}
}
]
}
},
{
"description": "[true, true] from items array is not valid",
"data": [
true,
true
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_1_0"
}
}
]
}
},
{
"description": "unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"bar"
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"bar"
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-unique array extended from [false, true] is not valid",
"data": [
false,
true,
"foo",
"foo"
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_1_0"
}
}
]
}
},
{
"description": "non-unique array extended from [true, false] is not valid",
"data": [
true,
false,
"foo",
"foo"
],
"schema_id": "uniqueItems_1_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_1_0"
}
}
]
}
}
]
},
{
"description": "uniqueItems with an array of items and additionalItems=false",
"database": {
"types": [
{
"name": "uniqueItems_2_0",
"schemas": {
"uniqueItems_2_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": true,
"items": false
}
}
}
]
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"schema_id": "uniqueItems_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"schema_id": "uniqueItems_2_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[false, false] from items array is not valid",
"data": [
false,
false
],
"schema_id": "uniqueItems_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_2_0"
}
}
]
}
},
{
"description": "[true, true] from items array is not valid",
"data": [
true,
true
],
"schema_id": "uniqueItems_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "UNIQUE_ITEMS_VIOLATED",
"details": {
"path": "",
"schema": "uniqueItems_2_0"
}
}
]
}
},
{
"description": "extra items are invalid even if unique",
"data": [
false,
true,
null
],
"schema_id": "uniqueItems_2_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "FALSE_SCHEMA",
"details": {
"path": "2",
"schema": "uniqueItems_2_0"
}
}
]
}
}
]
},
@ -1099,232 +569,6 @@
}
]
},
{
"description": "uniqueItems=false with an array of items",
"database": {
"types": [
{
"name": "uniqueItems_4_0",
"schemas": {
"uniqueItems_4_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"extensible": true
}
}
}
]
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[false, false] from items array is valid",
"data": [
false,
false
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, true] from items array is valid",
"data": [
true,
true
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"bar"
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"bar"
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-unique array extended from [false, true] is valid",
"data": [
false,
true,
"foo",
"foo"
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "non-unique array extended from [true, false] is valid",
"data": [
true,
false,
"foo",
"foo"
],
"schema_id": "uniqueItems_4_0",
"action": "validate",
"expect": {
"success": true
}
}
]
},
{
"description": "uniqueItems=false with an array of items and additionalItems=false",
"database": {
"types": [
{
"name": "uniqueItems_5_0",
"schemas": {
"uniqueItems_5_0": {
"prefixItems": [
{
"type": "boolean"
},
{
"type": "boolean"
}
],
"uniqueItems": false,
"items": false
}
}
}
]
},
"tests": [
{
"description": "[false, true] from items array is valid",
"data": [
false,
true
],
"schema_id": "uniqueItems_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, false] from items array is valid",
"data": [
true,
false
],
"schema_id": "uniqueItems_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[false, false] from items array is valid",
"data": [
false,
false
],
"schema_id": "uniqueItems_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "[true, true] from items array is valid",
"data": [
true,
true
],
"schema_id": "uniqueItems_5_0",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "extra items are invalid even if unique",
"data": [
false,
true,
null
],
"schema_id": "uniqueItems_5_0",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "FALSE_SCHEMA",
"details": {
"path": "2",
"schema": "uniqueItems_5_0"
}
}
]
}
}
]
},
{
"description": "extensible: true allows extra items in uniqueItems",
"database": {

2
flows

Submodule flows updated: 0d9bd8644e...89748a246e

View File

@ -25,9 +25,9 @@ impl Schema {
("identifier".to_string(), id.to_string()),
])),
details: ErrorDetails {
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
},
});
return;
@ -84,13 +84,6 @@ impl Schema {
}
}
if let Some(pattern_props) = &schema_arc.obj.pattern_properties {
for (k, v) in pattern_props.iter() {
let next_path = format!("{}/{}", path, k);
Self::collect_schemas(v, root_id, next_path, to_insert, errors);
}
}
let mut map_arr = |arr: &Vec<Arc<Schema>>, sub: &str| {
for (i, v) in arr.iter().enumerate() {
Self::collect_schemas(
@ -103,10 +96,6 @@ impl Schema {
}
};
if let Some(arr) = &schema_arc.obj.prefix_items {
map_arr(arr, "prefixItems");
}
if let Some(arr) = &schema_arc.obj.one_of {
map_arr(arr, "oneOf");
}
@ -123,11 +112,6 @@ impl Schema {
}
};
map_opt(
&schema_arc.obj.additional_properties,
false,
"additionalProperties",
);
map_opt(&schema_arc.obj.items, true, "items");
map_opt(&schema_arc.obj.not, false, "not");
map_opt(&schema_arc.obj.contains, false, "contains");

View File

@ -151,6 +151,7 @@ impl Schema {
"number" => Some(vec!["number.condition".to_string()]),
"boolean" => Some(vec!["boolean.condition".to_string()]),
"object" => None, // Inline structures are ignored in Composed References
"dict" => None, // Dynamic dictionary maps are ignored in Composed References
"array" => {
if let Some(items) = &schema.obj.items {
return Self::resolve_filter_type(items, db);
@ -164,8 +165,16 @@ impl Schema {
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)])
} else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
Some(vec![format!("{}.filter", custom)])
// Only a Table-Backed boundary has a synthesized Composed Filter to proxy to.
// A Field-Backed JSONB Bubble has none — omit it like an inline object rather
// than emit a dangling proxy reference, which breaks eager consumers of the
// exported registry (downstream code generators).
let base = custom.split('.').next_back().unwrap_or(custom);
if db.types.contains_key(base) {
Some(vec![format!("{}.filter", custom)])
} else {
None
}
}
}
}

View File

@ -39,18 +39,6 @@ impl Schema {
}
}
if let Some(pattern_props) = &self.obj.pattern_properties {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
let _ = self.obj.compiled_pattern_properties.set(compiled);
}
}
let mut props = IndexMap::new();
// 1. Resolve INHERITANCE dependencies first
@ -78,10 +66,10 @@ impl Schema {
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
values: Some(HashMap::from([("types".to_string(), types.join(", "))])),
details: ErrorDetails {
path: Some(path.clone()),
schema: Some(root_id.to_string()),
..Default::default()
}
path: Some(path.clone()),
schema: Some(root_id.to_string()),
..Default::default()
},
});
}
@ -137,35 +125,21 @@ impl Schema {
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(items) = &self.obj.items {
items.compile(db, root_id, format!("{}/items", path), errors);
}
if let Some(pattern_props) = &self.obj.pattern_properties {
for (k, child) in pattern_props {
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(additional_props) = &self.obj.additional_properties {
additional_props.compile(
db,
root_id,
format!("{}/additionalProperties", path),
errors,
);
}
if let Some(one_of) = &self.obj.one_of {
for (i, child) in one_of.iter().enumerate() {
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), errors);
}
}
if let Some(arr) = &self.obj.prefix_items {
for (i, child) in arr.iter().enumerate() {
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors);
}
}
if let Some(child) = &self.obj.not {
child.compile(db, root_id, format!("{}/not", path), errors);
}
if let Some(child) = &self.obj.contains {
child.compile(db, root_id, format!("{}/contains", path), errors);
}

View File

@ -81,7 +81,6 @@ fn resolve_in_place(
let mut merged_required = HashSet::new();
let mut merged_display = HashSet::new();
let mut merged_dependencies = serde_json::Map::new();
let mut merged_pattern_props = serde_json::Map::new();
// Read current values first to let host override included properties
if let Some(req) = current.get("required").and_then(|v| v.as_array()) {
@ -103,11 +102,7 @@ fn resolve_in_place(
merged_dependencies.insert(k.clone(), v.clone());
}
}
if let Some(pat_props) = current.get("patternProperties").and_then(|v| v.as_object()) {
for (k, v) in pat_props {
merged_pattern_props.insert(k.clone(), v.clone());
}
}
if let Some(props) = current.get("properties").and_then(|v| v.as_object()) {
for (k, v) in props {
merged_props.insert(k.clone(), v.clone());
@ -119,7 +114,10 @@ fn resolve_in_place(
if visited.contains(inc_name) {
errors.push(Error {
code: "CIRCULAR_INCLUDE_DETECTED".to_string(),
values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])),
values: Some(HashMap::from([(
"include".to_string(),
inc_name.to_string(),
)])),
details: ErrorDetails {
schema: Some(schema_id.to_string()),
path: Some(path.to_string()),
@ -156,18 +154,6 @@ fn resolve_in_place(
}
}
// Merge patternProperties (host overrides trait)
if let Some(target_pat_props) = resolved_target
.get("patternProperties")
.and_then(|v| v.as_object())
{
for (k, v) in target_pat_props {
if !merged_pattern_props.contains_key(k) {
merged_pattern_props.insert(k.clone(), v.clone());
}
}
}
// Merge required
if let Some(target_req) = resolved_target.get("required").and_then(|v| v.as_array()) {
for r in target_req {
@ -218,7 +204,6 @@ fn resolve_in_place(
if let Some(obj) = current.as_object_mut() {
for (k, v) in resolved_target.as_object().unwrap() {
if k != "properties"
&& k != "patternProperties"
&& k != "required"
&& k != "display"
&& k != "dependencies"
@ -233,7 +218,10 @@ fn resolve_in_place(
} else {
errors.push(Error {
code: "TRAIT_NOT_FOUND".to_string(),
values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])),
values: Some(HashMap::from([(
"include".to_string(),
inc_name.to_string(),
)])),
details: ErrorDetails {
schema: Some(schema_id.to_string()),
path: Some(path.to_string()),
@ -248,12 +236,7 @@ fn resolve_in_place(
if !merged_props.is_empty() {
obj.insert("properties".to_string(), Value::Object(merged_props));
}
if !merged_pattern_props.is_empty() {
obj.insert(
"patternProperties".to_string(),
Value::Object(merged_pattern_props),
);
}
if !merged_required.is_empty() {
let mut req_vec: Vec<Value> = merged_required.into_iter().map(Value::String).collect();
req_vec.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
@ -289,22 +272,7 @@ fn resolve_in_place(
);
}
}
if let Some(pat_props) = obj
.get_mut("patternProperties")
.and_then(|v| v.as_object_mut())
{
for (k, v) in pat_props {
resolve_in_place(
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/{}", path, k),
visited,
);
}
}
if let Some(items) = obj.get_mut("items") {
resolve_in_place(
items,
@ -316,30 +284,6 @@ fn resolve_in_place(
visited,
);
}
if let Some(prefix_items) = obj.get_mut("prefixItems").and_then(|v| v.as_array_mut()) {
for (i, v) in prefix_items.iter_mut().enumerate() {
resolve_in_place(
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/prefixItems/{}", path, i),
visited,
);
}
}
if let Some(additional_props) = obj.get_mut("additionalProperties") {
resolve_in_place(
additional_props,
traits,
schemas,
errors,
schema_id,
&format!("{}/additionalProperties", path),
visited,
);
}
if let Some(one_of) = obj.get_mut("oneOf").and_then(|v| v.as_array_mut()) {
for (i, v) in one_of.iter_mut().enumerate() {
resolve_in_place(

View File

@ -32,12 +32,10 @@ pub struct SchemaObject {
// Object Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<IndexMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
#[serde(rename = "keys")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_properties: Option<IndexMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Arc<Schema>>,
pub keys: Option<Arc<Schema>>,
#[serde(rename = "family")]
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
@ -53,9 +51,6 @@ pub struct SchemaObject {
#[serde(rename = "items")]
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation
#[serde(rename = "minLength")]
@ -189,8 +184,6 @@ pub struct SchemaObject {
pub compiled_format: OnceLock<CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: OnceLock<CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
}
/// Represents a compiled format validator
@ -263,7 +256,7 @@ where
pub fn is_primitive_type(t: &str) -> bool {
matches!(
t,
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null" | "dict"
)
}

View File

@ -1,6 +1,7 @@
use crate::database::action::Action;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
@ -13,6 +14,11 @@ pub struct Page {
pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>,
/// The personal-scope filter template ("one surface, two scopes"): the
/// "mine" predicate the page merges into its filter when the app is in
/// personal scope, e.g. {"reserved_by": "$me"}. Opaque to the engine.
#[serde(skip_serializing_if = "Option::is_none")]
pub personal: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

@ -26,12 +26,10 @@ impl Schema {
/// Returns true if the schema acts purely as a type pointer (composition without overriding constraints)
pub fn is_proxy(&self) -> bool {
self.obj.properties.is_none()
&& self.obj.pattern_properties.is_none()
&& self.obj.additional_properties.is_none()
&& self.obj.keys.is_none()
&& self.obj.required.is_none()
&& self.obj.dependencies.is_none()
&& self.obj.items.is_none()
&& self.obj.prefix_items.is_none()
&& self.obj.contains.is_none()
&& self.obj.format.is_none()
&& self.obj.enum_.is_none()
@ -68,12 +66,10 @@ impl<'de> Deserialize<'de> for Schema {
// restrict additional properties natively
let is_empty = obj.type_.is_none()
&& obj.properties.is_none()
&& obj.pattern_properties.is_none()
&& obj.additional_properties.is_none()
&& obj.keys.is_none()
&& obj.required.is_none()
&& obj.dependencies.is_none()
&& obj.items.is_none()
&& obj.prefix_items.is_none()
&& obj.contains.is_none()
&& obj.format.is_none()
&& obj.enum_.is_none()

View File

@ -744,9 +744,15 @@ impl Merger {
return Ok(None);
}
let where_clause = format!("WHERE {}", where_parts.join(" OR "));
let final_sql = format!("{} {}", fetch_sql_template, where_clause);
let final_sql = if where_parts.len() == 1 {
format!("{} WHERE {}", fetch_sql_template, where_parts[0])
} else {
where_parts
.iter()
.map(|p| format!("{} WHERE {}", fetch_sql_template, p))
.collect::<Vec<_>>()
.join(" UNION ")
};
let fetched = match self.db.query(&final_sql, None) {
Ok(Value::Array(table)) => {

View File

@ -234,7 +234,17 @@ impl<'a> Compiler<'a> {
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
let selection = if is_array {
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
// Deterministic order: aggregation over an unordered heap made result
// order vary run-to-run (flaky reads, unstable grids). Creation order
// with id as the tiebreaker; the entity alias exists for every
// entity-family type.
match table_aliases.get("entity") {
Some(entity_alias) => format!(
"COALESCE(jsonb_agg({} ORDER BY {}.created_at, {}.id), '[]'::jsonb)",
jsonb_obj_sql, entity_alias, entity_alias
),
None => format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql),
}
} else {
jsonb_obj_sql
};
@ -549,7 +559,7 @@ impl<'a> Compiler<'a> {
where_clauses.push(format!("NOT {}.archived", entity_alias));
}
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses)?;
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
let start_len = where_clauses.len();
@ -689,6 +699,11 @@ impl<'a> Compiler<'a> {
|| pg_type.contains("int")
|| pg_type == "real"
|| pg_type == "double precision"
// pg catalog typnames for real / double precision — the SQL
// names above never appear in field_types (they come from
// pg_type.typname), so these are what actually arrives
|| pg_type == "float4"
|| pg_type == "float8"
{
cast = "::numeric";
} else if pg_type == "text" || pg_type.contains("char") {
@ -715,7 +730,7 @@ impl<'a> Compiler<'a> {
node: &Node,
base_alias: &str,
where_clauses: &mut Vec<String>,
) {
) -> Result<(), String> {
for (i, filter_key) in self.filter_keys.iter().enumerate() {
let mut parts = filter_key.split(':');
let full_field_path = parts.next().unwrap_or(filter_key);
@ -745,6 +760,95 @@ impl<'a> Compiler<'a> {
let param_index = i + 1;
let p_val = format!("${}#>>'{{}}'", param_index);
// jsonb columns never type-check against the text parameter, and for
// array-valued properties (tag lists) the meaning of a condition is
// CONTAINMENT, not equality. Compile them explicitly; reject the rest
// loudly at compile time instead of failing at execution.
let is_jsonb = r#type
.field_types
.as_ref()
.and_then(|v| v.as_object())
.and_then(|ft| ft.get(field_name))
.and_then(|v| v.as_str())
== Some("jsonb");
if is_jsonb {
// The node schema may be a punc-response proxy without properties —
// fall back to the type's own schema. Compiled schemas normalize to
// the list form ("type": ["array"]), hand-written ones may use the
// single form — accept both.
let prop_schema = node
.schema
.obj
.properties
.as_ref()
.and_then(|p| p.get(field_name))
.cloned()
.or_else(|| {
r#type
.schemas
.get(&r#type.name)
.and_then(|s| s.obj.properties.as_ref())
.and_then(|p| p.get(field_name))
.cloned()
});
let is_array_prop = prop_schema
.map(|ps| match &ps.obj.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => t == "array",
Some(crate::database::object::SchemaTypeOrArray::Multiple(ts)) => {
ts.iter().any(|t| t == "array") && ts.iter().all(|t| t == "array" || t == "null")
}
None => false,
})
.unwrap_or(false);
if is_array_prop {
match op {
// "the array contains this value"
"$eq" => where_clauses.push(format!(
"{}.{} ? ({})",
filter_alias, field_name, p_val
)),
"$ne" => where_clauses.push(format!(
"NOT ({}.{} ? ({}))",
filter_alias, field_name, p_val
)),
// "the array contains ANY of these values"
"$of" => where_clauses.push(format!(
"{}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb))",
filter_alias, field_name, p_val
)),
"$nof" => where_clauses.push(format!(
"NOT ({}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb)))",
filter_alias, field_name, p_val
)),
other => {
return Err(format!(
"operator {} is not supported on array property '{}' (jsonb containment supports $eq/$ne/$of/$nof)",
other, field_name
))
}
}
} else {
match op {
"$eq" => where_clauses.push(format!(
"{}.{} = ({})::jsonb",
filter_alias, field_name, p_val
)),
"$ne" => where_clauses.push(format!(
"{}.{} != ({})::jsonb",
filter_alias, field_name, p_val
)),
other => {
return Err(format!(
"operator {} is not supported on jsonb property '{}' (only $eq/$ne)",
other, field_name
))
}
}
}
continue;
}
if op == "$of" || op == "$nof" {
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
let subquery = format!(
@ -796,6 +900,7 @@ impl<'a> Compiler<'a> {
));
}
}
Ok(())
}
fn compile_relation_conditions(

File diff suppressed because it is too large Load Diff

View File

@ -179,14 +179,35 @@ impl SqlFormatter {
}
fn format_query(&mut self, query: &Query) {
match &*query.body {
self.format_set_expr(&query.body);
}
fn format_set_expr(&mut self, set_expr: &SetExpr) {
match set_expr {
SetExpr::Select(select) => self.format_select(select),
SetExpr::Query(inner_query) => {
self.push_str("(");
self.format_query(inner_query);
self.push_str(")");
}
_ => self.push_str(&query.to_string()),
SetExpr::SetOperation { op, left, right, set_quantifier } => {
self.format_set_expr(left);
let op_str = match op {
sqlparser::ast::SetOperator::Union => "UNION",
sqlparser::ast::SetOperator::Intersect => "INTERSECT",
sqlparser::ast::SetOperator::Except => "EXCEPT",
_ => "UNION",
};
let quant_str = match set_quantifier {
sqlparser::ast::SetQuantifier::All => " ALL",
sqlparser::ast::SetQuantifier::Distinct => " DISTINCT",
sqlparser::ast::SetQuantifier::None => "",
_ => "",
};
self.push_line(&format!("{}{} ", op_str, quant_str));
self.format_set_expr(right);
}
_ => self.push_str(&set_expr.to_string()),
}
}
@ -236,20 +257,22 @@ impl SqlFormatter {
}
fn format_join(&mut self, join: &Join) {
let op = match &join.join_operator {
let op_str = match &join.join_operator {
JoinOperator::Inner(_) => "JOIN",
JoinOperator::LeftOuter(_) => "LEFT JOIN",
JoinOperator::Left(_) => "LEFT JOIN",
JoinOperator::Right(_) => "RIGHT JOIN",
_ => "JOIN",
};
self.push_str(&format!("{} {} ON ", op, join.relation));
self.push_str(&format!("{} {}", op_str, join.relation));
match &join.join_operator {
JoinOperator::Inner(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::LeftOuter(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::Join(JoinConstraint::On(expr)) => self.format_expr(expr),
_ => {
println!("FALLBACK JOIN OP: {:?}", join.join_operator);
JoinOperator::Inner(JoinConstraint::On(expr))
| JoinOperator::Left(JoinConstraint::On(expr))
| JoinOperator::Right(JoinConstraint::On(expr)) => {
self.push_str(" ON ");
self.format_expr(expr);
}
_ => {}
}
}
@ -366,6 +389,7 @@ impl SqlFormatter {
i += 2;
}
self.indent -= 2;
self.format_function_clauses(list);
self.push_line(")");
} else {
for (i, arg) in list.args.iter().enumerate() {
@ -373,6 +397,7 @@ impl SqlFormatter {
self.format_function_arg(arg);
self.push_str(comma);
}
self.format_function_clauses(list);
self.push_str(")");
}
} else {
@ -380,6 +405,25 @@ impl SqlFormatter {
}
}
// Aggregate clauses (e.g. jsonb_agg(x ORDER BY y)) — without this the
// snapshot silently drops the ORDER BY the compiler emits.
fn format_function_clauses(&mut self, list: &sqlparser::ast::FunctionArgumentList) {
for clause in &list.clauses {
if let sqlparser::ast::FunctionArgumentClause::OrderBy(order) = clause {
self.push_str(" ORDER BY ");
for (i, ob) in order.iter().enumerate() {
if i > 0 {
self.push_str(", ");
}
self.format_expr(&ob.expr);
if let Some(asc) = ob.options.asc {
self.push_str(if asc { " ASC" } else { " DESC" });
}
}
}
}
}
fn format_function_arg(&mut self, arg: &FunctionArg) {
match arg {
FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr(expr)) => self.format_expr(expr),

View File

@ -37,6 +37,7 @@ impl Validator {
"number" => val.is_number(),
"integer" => is_integer(val),
"object" => val.is_object(),
"dict" => val.is_object(),
"array" => val.is_array(),
_ => true,
}

View File

@ -99,42 +99,10 @@ impl<'a> ValidationContext<'a> {
}
let len = arr.len();
let mut validation_index = 0;
if let Some(ref prefix) = self.schema.prefix_items {
for (i, sub_schema) in prefix.iter().enumerate() {
if i < len {
if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
let is_topological = sub_schema.obj.requires_uuid_path(self.db);
if is_topological {
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
}
let derived = self.derive(
sub_schema,
child_instance,
&item_path,
HashSet::new(),
self.extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_indices.insert(i);
validation_index += 1;
}
}
}
}
if let Some(ref items_schema) = self.schema.items {
let is_topological = items_schema.obj.requires_uuid_path(self.db);
for i in validation_index..len {
for i in 0..len {
if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
if is_topological {

View File

@ -0,0 +1,76 @@
use std::collections::HashSet;
use serde_json::Value;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
impl<'a> ValidationContext<'a> {
pub(crate) fn validate_dict(
&self,
result: &mut ValidationResult,
) -> Result<bool, ValidationError> {
let current = self.instance;
if let Some(obj) = current.as_object() {
let is_dict = match &self.schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => t == "dict",
Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) => types.contains(&"dict".to_string()),
None => false,
};
if is_dict {
// Mark all keys as evaluated since a dictionary's keys are all dynamic parameters, not structured properties
result.evaluated_keys.extend(obj.keys().cloned());
// Validate keys
if let Some(ref keys_schema) = self.schema.keys {
for key in obj.keys() {
let _new_path = self.join_path(&format!("keys/{}", key));
let val_str = Value::String(key.to_string());
let ctx = self.derive(
keys_schema,
&val_str,
&_new_path,
HashSet::new(),
self.extensible,
self.reporter,
None,
);
result.merge(ctx.validate()?);
}
}
// Validate items (values)
if let Some(ref items_schema) = self.schema.items {
for (key, child_instance) in obj {
let new_path = self.join_path(key);
let is_ref = match &items_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(ref_t)) => {
!crate::database::object::is_primitive_type(ref_t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
items_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
}
}
}
}
Ok(true)
}
}

View File

@ -1,5 +1,7 @@
use std::collections::HashMap;
use crate::database::object::{is_primitive_type, SchemaTypeOrArray};
use crate::database::schema::Schema;
use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult;
@ -16,6 +18,49 @@ impl<'a> ValidationContext<'a> {
Ok(true)
}
pub(crate) fn schema_expects_primitive(&self, schema: &Schema, primitive: &str) -> bool {
match &schema.type_ {
None => true, // Any type is allowed
Some(SchemaTypeOrArray::Single(t)) => {
if t == primitive {
true
} else if !is_primitive_type(t) {
if primitive == "object" {
true
} else {
// Custom type - resolve recursively
if let Some(parent) = self.db.schemas.get(t) {
self.schema_expects_primitive(parent, primitive)
} else {
false
}
}
} else {
false
}
}
Some(SchemaTypeOrArray::Multiple(types)) => {
types.iter().any(|t| {
if t == primitive {
true
} else if !is_primitive_type(t) {
if primitive == "object" {
true
} else {
if let Some(parent) = self.db.schemas.get(t) {
self.schema_expects_primitive(parent, primitive)
} else {
false
}
}
} else {
false
}
})
}
}
}
pub(crate) fn validate_strictness(
&self,
result: &mut ValidationResult,
@ -24,38 +69,18 @@ impl<'a> ValidationContext<'a> {
return Ok(true);
}
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),
values: Some(HashMap::from([
("property_name".to_string(), key.to_string()),
])),
path: self.join_path(key),
});
}
}
}
if let Some(arr) = self.instance.as_array() {
for i in 0..arr.len() {
if !result.evaluated_indices.contains(&i) {
let mut item_path = self.join_path(&i.to_string());
if let Some(child_instance) = arr.get(i) {
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
if self.schema_expects_primitive(self.schema, "object") || self.schema_expects_primitive(self.schema, "dict") {
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),
values: Some(HashMap::from([
("property_name".to_string(), key.to_string()),
])),
path: self.join_path(key),
});
}
result.errors.push(ValidationError {
code: "STRICT_ITEM_VIOLATION".to_string(),
values: Some(HashMap::from([
("index".to_string(), i.to_string()),
])),
path: item_path,
});
}
}
}

View File

@ -6,6 +6,7 @@ use std::collections::HashMap;
pub mod array;
pub mod cases;
pub mod core;
pub mod dict;
pub mod extensible;
pub mod format;
pub mod not;
@ -43,6 +44,7 @@ impl<'a> ValidationContext<'a> {
// Complex Structures
self.validate_object(&mut result)?;
self.validate_array(&mut result)?;
self.validate_dict(&mut result)?;
// Multipliers & Conditionals
if !self.validate_one_of(&mut result)? {

View File

@ -211,80 +211,8 @@ impl<'a> ValidationContext<'a> {
}
}
if let Some(compiled_pp) = self.schema.compiled_pattern_properties.get() {
for (compiled_re, sub_schema) in compiled_pp {
for (key, child_instance) in obj {
if compiled_re.0.is_match(key) {
let new_path = self.join_path(key);
let is_ref = match &sub_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
!crate::database::object::is_primitive_type(t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
sub_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
}
if let Some(ref additional_schema) = self.schema.additional_properties {
for (key, child_instance) in obj {
let mut locally_matched = false;
if let Some(props) = &self.schema.properties
&& props.contains_key(&key.to_string())
{
locally_matched = true;
}
if !locally_matched
&& let Some(compiled_pp) = self.schema.compiled_pattern_properties.get()
{
for (compiled_re, _) in compiled_pp {
if compiled_re.0.is_match(key) {
locally_matched = true;
break;
}
}
}
if !locally_matched {
let new_path = self.join_path(key);
let is_ref = match &additional_schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
!crate::database::object::is_primitive_type(t)
}
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
let derived = self.derive(
additional_schema,
child_instance,
&new_path,
HashSet::new(),
next_extensible,
false,
Some(self.instance),
);
let item_res = derived.validate()?;
result.merge(item_res);
result.evaluated_keys.insert(key.to_string());
}
}
}
if let Some(ref property_names) = self.schema.property_names {
for key in obj.keys() {

View File

@ -14,7 +14,6 @@ impl<'a> ValidationContext<'a> {
let conflicts = self.schema.type_.is_some()
|| self.schema.properties.is_some()
|| self.schema.required.is_some()
|| self.schema.additional_properties.is_some()
|| self.schema.items.is_some()
|| self.schema.cases.is_some()
|| self.schema.one_of.is_some()

View File

@ -42,6 +42,7 @@ impl<'a> ValidationContext<'a> {
code: "PATTERN_VIOLATED".to_string(),
values: Some(HashMap::from([
("pattern".to_string(), self.schema.pattern.clone().unwrap_or_default()),
("value".to_string(), s.to_string()),
])),
path: self.path.to_string(),
});
@ -54,6 +55,7 @@ impl<'a> ValidationContext<'a> {
code: "PATTERN_VIOLATED".to_string(),
values: Some(HashMap::from([
("pattern".to_string(), pattern.to_string()),
("value".to_string(), s.to_string()),
])),
path: self.path.to_string(),
});

View File

@ -1 +1 @@
1.0.163
1.0.176