json-schema-engine 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/CHANGELOG.md +31 -1
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/PKG-INFO +56 -41
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/README.md +55 -40
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/pyproject.toml +1 -1
- json_schema_engine-0.0.3/src/json_schema_engine/compiler/__init__.py +250 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/emit.py +77 -0
- json_schema_engine-0.0.3/src/json_schema_engine/compiler/plan.py +780 -0
- json_schema_engine-0.0.3/src/json_schema_engine/compiler/runtime.py +227 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/runtime_compile.py +9 -1
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/__init__.py +114 -8
- json_schema_engine-0.0.3/src/json_schema_engine/compiler/serialize/body.py +1116 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/context.py +77 -2
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/serialize/units.py +49 -9
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/standalone.py +4 -0
- json_schema_engine-0.0.3/src/json_schema_engine/core/channel_ops.py +152 -0
- json_schema_engine-0.0.3/src/json_schema_engine/core/coverage.py +58 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/dialect.py +9 -1
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/engine.py +84 -58
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/evaluator.py +34 -16
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/json_model.py +7 -2
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator.py +11 -4
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator_array.py +64 -19
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/applicator_object.py +50 -14
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/content.py +8 -4
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/core.py +58 -6
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/format.py +7 -2
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/legacy.py +37 -7
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/unevaluated.py +93 -40
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/validation.py +21 -7
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/lowering.py +243 -18
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/registry.py +70 -0
- json_schema_engine-0.0.2/src/json_schema_engine/compiler/__init__.py +0 -113
- json_schema_engine-0.0.2/src/json_schema_engine/compiler/plan.py +0 -415
- json_schema_engine-0.0.2/src/json_schema_engine/compiler/runtime.py +0 -114
- json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/body.py +0 -524
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/.gitignore +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/LICENSE +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/errors.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/compiler/py.typed +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/__init__.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/channel.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/cursor.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/errors.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/formats.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/__init__.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/_ids.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect2019.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect2020.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialect7.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/dialects.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/keywords/meta_data.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/loader.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/applicator.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/content.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/core.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/format.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/meta-data.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/schema.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2019-09/validation.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/applicator.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/content.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/core.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/format-annotation.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/format-assertion.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/meta-data.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/schema.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/unevaluated.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/2020-12/validation.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/__init__.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/draft-06/schema.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/metaschemas/draft-07/schema.json +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/output.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/positions.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/py.typed +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/records.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/ref.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/regex.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/result.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/core/uri.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/__init__.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/_abnf.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/datetime_.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/idna_.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/misc.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/net.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/pointer.py +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/py.typed +0 -0
- {json_schema_engine-0.0.2 → json_schema_engine-0.0.3}/src/json_schema_engine/formats/uri.py +0 -0
|
@@ -5,7 +5,37 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
|
5
5
|
versions follow [SemVer](https://semver.org/) with the 0.x caveat that
|
|
6
6
|
minor versions may change public API.
|
|
7
7
|
|
|
8
|
-
## [0.0.
|
|
8
|
+
## [0.0.3] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `compile_evaluator`: compiles a registered root schema into an evaluator
|
|
13
|
+
serving every output format the interpreter does (errors, annotations,
|
|
14
|
+
dropped records, the application trace), not only the verdict `flag`
|
|
15
|
+
level.
|
|
16
|
+
- Plan-time resolution of `$dynamicRef`/`$recursiveRef`: a reference site
|
|
17
|
+
whose target is the same on every path that can reach it compiles as an
|
|
18
|
+
ordinary static edge instead of falling back to the interpreter;
|
|
19
|
+
`explain_compilation` reports such sites through `resolved_dynamic_sites`.
|
|
20
|
+
- Runtime coverage tracking: an `unevaluated*` consumer whose evaluated
|
|
21
|
+
coverage depends on runtime branching (an `anyOf`/`oneOf` alternative, an
|
|
22
|
+
`if`'s condition) compiles directly instead of islanding, folding a
|
|
23
|
+
runtime coverage channel instead of a static licence.
|
|
24
|
+
- Public `json_schema_engine.core.lowering`: the compiler lowering IR a
|
|
25
|
+
custom keyword's `lower()` is built from, previously private to the
|
|
26
|
+
engine's own keyword modules.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `first_duplicate_pair` returns a list.
|
|
31
|
+
- Compiled error messages now match the interpreter's for `oneOf`,
|
|
32
|
+
`contains`, `uniqueItems`, and `type`.
|
|
33
|
+
- A dialect refusing unknown keywords no longer compiles those units
|
|
34
|
+
silently: an unknown keyword under such a dialect now falls back to the
|
|
35
|
+
interpreter like any other unlowerable case, rather than being planned
|
|
36
|
+
as if the keyword were absent.
|
|
37
|
+
|
|
38
|
+
## [0.0.2] - 2026-09-21
|
|
9
39
|
|
|
10
40
|
The first functional release. Everything below is new relative to the
|
|
11
41
|
0.0.1 name reservation.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: json-schema-engine
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: A spec-complete, annotation-first JSON Schema engine for Python: interpreter, compiler, formats, every standard output format.
|
|
5
5
|
Project-URL: Homepage, https://github.com/handrews/py-json-schema-engine
|
|
6
6
|
Project-URL: Repository, https://github.com/handrews/py-json-schema-engine
|
|
@@ -39,20 +39,16 @@ relevance model.
|
|
|
39
39
|
|
|
40
40
|
Produced by Henry Andrews via Claude Code.
|
|
41
41
|
|
|
42
|
-
**Status: `0.0.
|
|
42
|
+
**Status: `0.0.3` is fully compliant except for draft-04 support.**
|
|
43
|
+
|
|
44
|
+
See [CHANGELOG.md](CHANGELOG.md) for the current release's contents.
|
|
45
|
+
[DESIGN.md](DESIGN.md) is the design contract and carries the milestone
|
|
46
|
+
status.
|
|
43
47
|
|
|
44
48
|
All `0.0.x` releases will have AI-written documentation. Version
|
|
45
49
|
`0.1.0` will indicate that the documentation has been audited and revised
|
|
46
50
|
by a human.
|
|
47
51
|
|
|
48
|
-
As of `0.0.2`, this package carries the
|
|
49
|
-
interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
|
|
50
|
-
standard output format, the compiler tier (a flag validator and standalone
|
|
51
|
-
modules, M6–M7), and the `json_schema_engine.formats` package — green on
|
|
52
|
-
every official test-suite file for those drafts through both tiers, on the
|
|
53
|
-
official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
|
|
54
|
-
contract and carries the milestone status.
|
|
55
|
-
|
|
56
52
|
The regular-expression translator lives in its own package,
|
|
57
53
|
[`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
|
|
58
54
|
for Python, with JavaScript semantics, no dependency on this engine.
|
|
@@ -93,12 +89,14 @@ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
|
|
|
93
89
|
siblings while a 2019-09 one does not.
|
|
94
90
|
|
|
95
91
|
Every standard output format is available by name: `flag` (the default),
|
|
96
|
-
`basic`, `detailed`, and `verbose` from the
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
`basic`, `detailed`, and `verbose` from the
|
|
93
|
+
[IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
|
|
94
|
+
output sections, and `list` and `hierarchical` from the
|
|
95
|
+
[machines-oriented proposal](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md).
|
|
96
|
+
Annotations are a separate control (`annotations=True`, or an
|
|
97
|
+
`AnnotationSelection`), `verbose=True` asks `list`/`hierarchical` for the
|
|
98
|
+
verbose level with irrelevant records marked as dropped, and `trace=True`
|
|
99
|
+
adds the application tree with error indexes into `result.errors`.
|
|
102
100
|
|
|
103
101
|
```python
|
|
104
102
|
result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
|
|
@@ -129,9 +127,9 @@ attach a `source` location to every error and annotation, and
|
|
|
129
127
|
## Compile
|
|
130
128
|
|
|
131
129
|
The compiler tier turns a registered schema into a Python function. It is
|
|
132
|
-
not a second implementation: any subschema it cannot emit (a `$dynamicRef
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
not a second implementation: any subschema it cannot emit (a `$dynamicRef`
|
|
131
|
+
whose target differs by path, an in-place cycle) calls back into the
|
|
132
|
+
interpreter, so a compiled validator is exactly
|
|
135
133
|
as correct as `Engine.evaluate` and never less complete. Tier choice is a
|
|
136
134
|
performance decision, not a semantic one.
|
|
137
135
|
|
|
@@ -154,9 +152,25 @@ only the standard library and this package's pure helpers; it refuses,
|
|
|
154
152
|
with `StandaloneUnsupportedError`, a schema that would need the
|
|
155
153
|
interpreter at evaluation time. Compiled code assumes plain data as
|
|
156
154
|
`json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
|
|
157
|
-
`None`); subclasses of those types belong to the interpreter.
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
`None`); subclasses of those types belong to the interpreter.
|
|
156
|
+
|
|
157
|
+
`compile_evaluator` compiles a schema into an evaluator serving every
|
|
158
|
+
output format the interpreter does — errors, annotations, dropped
|
|
159
|
+
records, and the application trace, not only the verdict. The annotation
|
|
160
|
+
selection is fixed at compile time; every other control (`output`,
|
|
161
|
+
`error_params`, `verbose`, `trace`, `positions`) is chosen per call, same
|
|
162
|
+
as `Engine.evaluate`.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from json_schema_engine.compiler import compile_evaluator
|
|
166
|
+
|
|
167
|
+
evaluator = compile_evaluator(engine, uri, annotations=True)
|
|
168
|
+
compiled_result = evaluator.evaluate({}, output="list", error_params=True)
|
|
169
|
+
interpreted_result = engine.evaluate(
|
|
170
|
+
uri, {}, output="list", error_params=True, annotations=True
|
|
171
|
+
)
|
|
172
|
+
assert compiled_result.errors == interpreted_result.errors
|
|
173
|
+
```
|
|
160
174
|
|
|
161
175
|
## Formats
|
|
162
176
|
|
|
@@ -258,10 +272,6 @@ large arrays of distinct values do not incur quadratic cost, while genuine
|
|
|
258
272
|
duplicates — including numbers equal across `int`/`float` and objects that
|
|
259
273
|
differ only in member order — are still reported.
|
|
260
274
|
|
|
261
|
-
**No prototype hazard.** Python dicts have no prototype chain, so there is
|
|
262
|
-
nothing for a hostile property name to pollute: `__proto__`, `constructor`,
|
|
263
|
-
and similar reserved-looking names evaluate as ordinary properties.
|
|
264
|
-
|
|
265
275
|
## Development
|
|
266
276
|
|
|
267
277
|
```sh
|
|
@@ -290,21 +300,26 @@ uv run python scripts/bowtie_check.py
|
|
|
290
300
|
uv run python scripts/bench.py --budget-ms 250 --filter user
|
|
291
301
|
```
|
|
292
302
|
|
|
293
|
-
`scripts/bench.py` times
|
|
294
|
-
|
|
295
|
-
over seven corpora in
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
`scripts/bench.py` times every jse tier (the interpreter, the compiler's
|
|
304
|
+
flag validator and evaluator, and the standalone artifact) against two
|
|
305
|
+
competitors (fastjsonschema, jsonschema) over seven corpora in
|
|
306
|
+
`packages/bench`: three small hand-authored schemas, the official OpenAPI
|
|
307
|
+
3.1 schema against a real document, a generated API-payload corpus, and
|
|
308
|
+
two 2000-record corpora. Every jse tier is timed against every corpus,
|
|
309
|
+
`jse standalone` against `oas-document` included, since its `$dynamicRef`
|
|
310
|
+
sites resolve at plan time and leave no interpreted unit. Two subjects
|
|
311
|
+
measure the record-producing tier against the verdict-only tiers:
|
|
312
|
+
`jse interpreter list` and `jse compiled evaluator (list)`, both timing
|
|
313
|
+
`output="list"`. The bench is report-only (it enforces no performance
|
|
314
|
+
threshold) and, per the IP policy below, runs the competitors only, never
|
|
315
|
+
reading or porting their source. `--filter` takes a regex over
|
|
316
|
+
corpus/subject/partition names; omit `--out` to skip writing JSON;
|
|
317
|
+
`--compare BEFORE AFTER` prints a before/after comparison of two results
|
|
318
|
+
files. The committed run lives at `packages/bench/results/results.json`
|
|
319
|
+
(`--budget-ms 250`). The interpreter is the reference semantics, so ratios
|
|
320
|
+
are informational, not a compatibility claim.
|
|
321
|
+
[`packages/bench/README.md`](packages/bench/README.md) covers corpus
|
|
322
|
+
provenance and licensing, methodology, and the recorded exclusions.
|
|
308
323
|
|
|
309
324
|
## IP policy
|
|
310
325
|
|
|
@@ -12,20 +12,16 @@ relevance model.
|
|
|
12
12
|
|
|
13
13
|
Produced by Henry Andrews via Claude Code.
|
|
14
14
|
|
|
15
|
-
**Status: `0.0.
|
|
15
|
+
**Status: `0.0.3` is fully compliant except for draft-04 support.**
|
|
16
|
+
|
|
17
|
+
See [CHANGELOG.md](CHANGELOG.md) for the current release's contents.
|
|
18
|
+
[DESIGN.md](DESIGN.md) is the design contract and carries the milestone
|
|
19
|
+
status.
|
|
16
20
|
|
|
17
21
|
All `0.0.x` releases will have AI-written documentation. Version
|
|
18
22
|
`0.1.0` will indicate that the documentation has been audited and revised
|
|
19
23
|
by a human.
|
|
20
24
|
|
|
21
|
-
As of `0.0.2`, this package carries the
|
|
22
|
-
interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
|
|
23
|
-
standard output format, the compiler tier (a flag validator and standalone
|
|
24
|
-
modules, M6–M7), and the `json_schema_engine.formats` package — green on
|
|
25
|
-
every official test-suite file for those drafts through both tiers, on the
|
|
26
|
-
official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
|
|
27
|
-
contract and carries the milestone status.
|
|
28
|
-
|
|
29
25
|
The regular-expression translator lives in its own package,
|
|
30
26
|
[`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
|
|
31
27
|
for Python, with JavaScript semantics, no dependency on this engine.
|
|
@@ -66,12 +62,14 @@ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
|
|
|
66
62
|
siblings while a 2019-09 one does not.
|
|
67
63
|
|
|
68
64
|
Every standard output format is available by name: `flag` (the default),
|
|
69
|
-
`basic`, `detailed`, and `verbose` from the
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
`basic`, `detailed`, and `verbose` from the
|
|
66
|
+
[IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
|
|
67
|
+
output sections, and `list` and `hierarchical` from the
|
|
68
|
+
[machines-oriented proposal](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md).
|
|
69
|
+
Annotations are a separate control (`annotations=True`, or an
|
|
70
|
+
`AnnotationSelection`), `verbose=True` asks `list`/`hierarchical` for the
|
|
71
|
+
verbose level with irrelevant records marked as dropped, and `trace=True`
|
|
72
|
+
adds the application tree with error indexes into `result.errors`.
|
|
75
73
|
|
|
76
74
|
```python
|
|
77
75
|
result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
|
|
@@ -102,9 +100,9 @@ attach a `source` location to every error and annotation, and
|
|
|
102
100
|
## Compile
|
|
103
101
|
|
|
104
102
|
The compiler tier turns a registered schema into a Python function. It is
|
|
105
|
-
not a second implementation: any subschema it cannot emit (a `$dynamicRef
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
not a second implementation: any subschema it cannot emit (a `$dynamicRef`
|
|
104
|
+
whose target differs by path, an in-place cycle) calls back into the
|
|
105
|
+
interpreter, so a compiled validator is exactly
|
|
108
106
|
as correct as `Engine.evaluate` and never less complete. Tier choice is a
|
|
109
107
|
performance decision, not a semantic one.
|
|
110
108
|
|
|
@@ -127,9 +125,25 @@ only the standard library and this package's pure helpers; it refuses,
|
|
|
127
125
|
with `StandaloneUnsupportedError`, a schema that would need the
|
|
128
126
|
interpreter at evaluation time. Compiled code assumes plain data as
|
|
129
127
|
`json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
|
|
130
|
-
`None`); subclasses of those types belong to the interpreter.
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
`None`); subclasses of those types belong to the interpreter.
|
|
129
|
+
|
|
130
|
+
`compile_evaluator` compiles a schema into an evaluator serving every
|
|
131
|
+
output format the interpreter does — errors, annotations, dropped
|
|
132
|
+
records, and the application trace, not only the verdict. The annotation
|
|
133
|
+
selection is fixed at compile time; every other control (`output`,
|
|
134
|
+
`error_params`, `verbose`, `trace`, `positions`) is chosen per call, same
|
|
135
|
+
as `Engine.evaluate`.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from json_schema_engine.compiler import compile_evaluator
|
|
139
|
+
|
|
140
|
+
evaluator = compile_evaluator(engine, uri, annotations=True)
|
|
141
|
+
compiled_result = evaluator.evaluate({}, output="list", error_params=True)
|
|
142
|
+
interpreted_result = engine.evaluate(
|
|
143
|
+
uri, {}, output="list", error_params=True, annotations=True
|
|
144
|
+
)
|
|
145
|
+
assert compiled_result.errors == interpreted_result.errors
|
|
146
|
+
```
|
|
133
147
|
|
|
134
148
|
## Formats
|
|
135
149
|
|
|
@@ -231,10 +245,6 @@ large arrays of distinct values do not incur quadratic cost, while genuine
|
|
|
231
245
|
duplicates — including numbers equal across `int`/`float` and objects that
|
|
232
246
|
differ only in member order — are still reported.
|
|
233
247
|
|
|
234
|
-
**No prototype hazard.** Python dicts have no prototype chain, so there is
|
|
235
|
-
nothing for a hostile property name to pollute: `__proto__`, `constructor`,
|
|
236
|
-
and similar reserved-looking names evaluate as ordinary properties.
|
|
237
|
-
|
|
238
248
|
## Development
|
|
239
249
|
|
|
240
250
|
```sh
|
|
@@ -263,21 +273,26 @@ uv run python scripts/bowtie_check.py
|
|
|
263
273
|
uv run python scripts/bench.py --budget-ms 250 --filter user
|
|
264
274
|
```
|
|
265
275
|
|
|
266
|
-
`scripts/bench.py` times
|
|
267
|
-
|
|
268
|
-
over seven corpora in
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
`
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
`scripts/bench.py` times every jse tier (the interpreter, the compiler's
|
|
277
|
+
flag validator and evaluator, and the standalone artifact) against two
|
|
278
|
+
competitors (fastjsonschema, jsonschema) over seven corpora in
|
|
279
|
+
`packages/bench`: three small hand-authored schemas, the official OpenAPI
|
|
280
|
+
3.1 schema against a real document, a generated API-payload corpus, and
|
|
281
|
+
two 2000-record corpora. Every jse tier is timed against every corpus,
|
|
282
|
+
`jse standalone` against `oas-document` included, since its `$dynamicRef`
|
|
283
|
+
sites resolve at plan time and leave no interpreted unit. Two subjects
|
|
284
|
+
measure the record-producing tier against the verdict-only tiers:
|
|
285
|
+
`jse interpreter list` and `jse compiled evaluator (list)`, both timing
|
|
286
|
+
`output="list"`. The bench is report-only (it enforces no performance
|
|
287
|
+
threshold) and, per the IP policy below, runs the competitors only, never
|
|
288
|
+
reading or porting their source. `--filter` takes a regex over
|
|
289
|
+
corpus/subject/partition names; omit `--out` to skip writing JSON;
|
|
290
|
+
`--compare BEFORE AFTER` prints a before/after comparison of two results
|
|
291
|
+
files. The committed run lives at `packages/bench/results/results.json`
|
|
292
|
+
(`--budget-ms 250`). The interpreter is the reference semantics, so ratios
|
|
293
|
+
are informational, not a compatibility claim.
|
|
294
|
+
[`packages/bench/README.md`](packages/bench/README.md) covers corpus
|
|
295
|
+
provenance and licensing, methodology, and the recorded exclusions.
|
|
281
296
|
|
|
282
297
|
## IP policy
|
|
283
298
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "json-schema-engine"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
description = "A spec-complete, annotation-first JSON Schema engine for Python: interpreter, compiler, formats, every standard output format."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# The compiler tier's public surface (DESIGN.md D1, D9, D10; M6): compile a
|
|
2
|
+
# registered schema into a flag validator, or emit it as a standalone
|
|
3
|
+
# module. The interpreter is the reference semantics; any subschema the
|
|
4
|
+
# compiler cannot emit trampolines back into it, so an artifact is exactly
|
|
5
|
+
# as correct as `Engine.evaluate` and never less complete — tier choice is
|
|
6
|
+
# a performance decision, not a semantic one.
|
|
7
|
+
#
|
|
8
|
+
# An artifact binds a snapshot of the schema and dialect registries taken
|
|
9
|
+
# at compile time: compile after registration is complete.
|
|
10
|
+
#
|
|
11
|
+
# Dependency direction: imports the planner, serializer, runtime, and
|
|
12
|
+
# instantiation modules, plus core's engine façade. `json_schema_engine.core`
|
|
13
|
+
# never imports this package (P5).
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from json_schema_engine.compiler import emit as e
|
|
20
|
+
from json_schema_engine.compiler.errors import (
|
|
21
|
+
FormatTableError,
|
|
22
|
+
StandaloneUnsupportedError,
|
|
23
|
+
)
|
|
24
|
+
from json_schema_engine.compiler.plan import (
|
|
25
|
+
CompilationExplanation,
|
|
26
|
+
CompilationPlan,
|
|
27
|
+
DynamicResolution,
|
|
28
|
+
FallbackCause,
|
|
29
|
+
PlannedApplication,
|
|
30
|
+
PlannedUnit,
|
|
31
|
+
ResolvedDynamicSite,
|
|
32
|
+
build_plan,
|
|
33
|
+
build_plan_over,
|
|
34
|
+
explain_compilation,
|
|
35
|
+
)
|
|
36
|
+
from json_schema_engine.compiler.runtime import make_namespace, make_runtime
|
|
37
|
+
from json_schema_engine.compiler.runtime_compile import instantiate, instantiate_entry
|
|
38
|
+
from json_schema_engine.compiler.serialize import Flags, assemble, serialize_plan
|
|
39
|
+
from json_schema_engine.compiler.standalone import emit_standalone
|
|
40
|
+
from json_schema_engine.core.channel_ops import cut_annotations
|
|
41
|
+
from json_schema_engine.core.engine import Engine, assemble_evaluation
|
|
42
|
+
from json_schema_engine.core.evaluator import EvalState
|
|
43
|
+
from json_schema_engine.core.json_model import JsonValue
|
|
44
|
+
from json_schema_engine.core.output import AnnotationsOption, make_record_predicate
|
|
45
|
+
from json_schema_engine.core.result import OutputFormat, Result, resolve_output_demand
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"CompilationExplanation",
|
|
49
|
+
"CompilationPlan",
|
|
50
|
+
"CompiledEvaluator",
|
|
51
|
+
"CompiledValidator",
|
|
52
|
+
"DynamicResolution",
|
|
53
|
+
"FallbackCause",
|
|
54
|
+
"FormatTableError",
|
|
55
|
+
"PlannedApplication",
|
|
56
|
+
"PlannedUnit",
|
|
57
|
+
"ResolvedDynamicSite",
|
|
58
|
+
"StandaloneUnsupportedError",
|
|
59
|
+
"build_plan",
|
|
60
|
+
"compile_evaluator",
|
|
61
|
+
"compile_validator",
|
|
62
|
+
"emit_standalone",
|
|
63
|
+
"explain_compilation",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class CompiledValidator:
|
|
69
|
+
"""A compiled flag validator: `validate(instance) -> bool`, the plan it
|
|
70
|
+
was built from, the emitted module, and its source (for diagnostics
|
|
71
|
+
and goldens; `ast.unparse` of `module`)."""
|
|
72
|
+
|
|
73
|
+
validate: Callable[[JsonValue], bool]
|
|
74
|
+
plan: CompilationPlan
|
|
75
|
+
module: ast.Module
|
|
76
|
+
source: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def compile_validator(
|
|
80
|
+
engine: Engine,
|
|
81
|
+
schema_uri: str,
|
|
82
|
+
*,
|
|
83
|
+
max_depth: int | None = None,
|
|
84
|
+
conservative: bool = False,
|
|
85
|
+
) -> CompiledValidator:
|
|
86
|
+
"""Compile a registered root schema into a verdict-only validator.
|
|
87
|
+
|
|
88
|
+
`max_depth` defaults to the engine's; `conservative` turns the
|
|
89
|
+
emitter's optimizations off (no inlining, no set specialization) — the
|
|
90
|
+
differential fuzzer referees both configurations.
|
|
91
|
+
"""
|
|
92
|
+
registry = engine.schemas.snapshot()
|
|
93
|
+
plan = build_plan_over(registry, schema_uri)
|
|
94
|
+
flags = Flags(inline=not conservative, specialize_sets=not conservative)
|
|
95
|
+
serialized = serialize_plan(plan, registry, flags)
|
|
96
|
+
prologue = [
|
|
97
|
+
e.assign(
|
|
98
|
+
name,
|
|
99
|
+
e.subscript(e.attr(e.load(e.RUNTIME), "re"), e.const(source)),
|
|
100
|
+
)
|
|
101
|
+
for name, source in serialized.regexes
|
|
102
|
+
]
|
|
103
|
+
prologue.extend(
|
|
104
|
+
e.assign(
|
|
105
|
+
name,
|
|
106
|
+
e.subscript(e.attr(e.load(e.RUNTIME), "formats"), e.const(format_name)),
|
|
107
|
+
)
|
|
108
|
+
for name, format_name in serialized.formats
|
|
109
|
+
)
|
|
110
|
+
module = assemble(prologue, serialized)
|
|
111
|
+
budget = engine.max_depth if max_depth is None else max_depth
|
|
112
|
+
runtime = make_runtime(
|
|
113
|
+
registry,
|
|
114
|
+
engine.regex_cache,
|
|
115
|
+
plan.patterns,
|
|
116
|
+
budget,
|
|
117
|
+
formats=plan.formats,
|
|
118
|
+
format_table=engine.formats,
|
|
119
|
+
coverage_ids=plan.coverage_ids,
|
|
120
|
+
)
|
|
121
|
+
validate = instantiate(
|
|
122
|
+
module, make_namespace(runtime, [t.ref for t in plan.targets])
|
|
123
|
+
)
|
|
124
|
+
return CompiledValidator(validate, plan, module, ast.unparse(module))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _record_nothing(keyword_name: str, vocabulary_uri: str | None) -> bool:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True, slots=True)
|
|
132
|
+
class CompiledEvaluator:
|
|
133
|
+
"""A compiled evaluator (M9): `evaluate(instance, *, output, error_params,
|
|
134
|
+
verbose, trace, positions) -> Result`, the same result `Engine.evaluate`
|
|
135
|
+
returns for that demand. The annotation selection is fixed at compile
|
|
136
|
+
time (ruled-out annotations are never recorded); every other output
|
|
137
|
+
control is chosen per evaluation. Records and the application trace
|
|
138
|
+
are written to a core `EvalState` the artifact shares with any
|
|
139
|
+
interpreted island, and rendered by the interpreter's own renderers.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
evaluate: Callable[..., Result]
|
|
143
|
+
plan: CompilationPlan
|
|
144
|
+
module: ast.Module
|
|
145
|
+
source: str
|
|
146
|
+
annotations: AnnotationsOption
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compile_evaluator(
|
|
150
|
+
engine: Engine,
|
|
151
|
+
schema_uri: str,
|
|
152
|
+
*,
|
|
153
|
+
annotations: AnnotationsOption = False,
|
|
154
|
+
max_depth: int | None = None,
|
|
155
|
+
conservative: bool = False,
|
|
156
|
+
) -> CompiledEvaluator:
|
|
157
|
+
"""Compile a registered root schema into an evaluator serving every
|
|
158
|
+
output format but the verdict-only `flag`.
|
|
159
|
+
|
|
160
|
+
Every consumer is tracked at runtime (a static coverage models only
|
|
161
|
+
the parent-success path, and an evaluator continues past a failed
|
|
162
|
+
sibling), nothing inlines, and every branch runs (§4 rule 7), so the
|
|
163
|
+
errors, annotations, dropped records, and trace equal the interpreter's.
|
|
164
|
+
"""
|
|
165
|
+
registry = engine.schemas.snapshot()
|
|
166
|
+
plan = build_plan_over(registry, schema_uri, track_all=True)
|
|
167
|
+
flags = Flags(inline=False, specialize_sets=not conservative, mode="evaluator")
|
|
168
|
+
record = make_record_predicate(annotations)
|
|
169
|
+
serialized = serialize_plan(plan, registry, flags, record=record)
|
|
170
|
+
prologue = [
|
|
171
|
+
e.assign(
|
|
172
|
+
name,
|
|
173
|
+
e.subscript(e.attr(e.load(e.RUNTIME), "re"), e.const(source)),
|
|
174
|
+
)
|
|
175
|
+
for name, source in serialized.regexes
|
|
176
|
+
]
|
|
177
|
+
prologue.extend(
|
|
178
|
+
e.assign(
|
|
179
|
+
name,
|
|
180
|
+
e.subscript(e.attr(e.load(e.RUNTIME), "formats"), e.const(format_name)),
|
|
181
|
+
)
|
|
182
|
+
for name, format_name in serialized.formats
|
|
183
|
+
)
|
|
184
|
+
prologue.extend(
|
|
185
|
+
e.assign(name, e.subscript(e.load(e.SITES), e.const(index)))
|
|
186
|
+
for index, (name, _) in enumerate(serialized.sites)
|
|
187
|
+
)
|
|
188
|
+
module = assemble(prologue, serialized)
|
|
189
|
+
budget = engine.max_depth if max_depth is None else max_depth
|
|
190
|
+
runtime = make_runtime(
|
|
191
|
+
registry,
|
|
192
|
+
engine.regex_cache,
|
|
193
|
+
plan.patterns,
|
|
194
|
+
budget,
|
|
195
|
+
formats=plan.formats,
|
|
196
|
+
format_table=engine.formats,
|
|
197
|
+
coverage_ids=plan.coverage_ids,
|
|
198
|
+
)
|
|
199
|
+
entry = instantiate_entry(
|
|
200
|
+
module,
|
|
201
|
+
make_namespace(
|
|
202
|
+
runtime,
|
|
203
|
+
[t.ref for t in plan.targets],
|
|
204
|
+
[site for _, site in serialized.sites],
|
|
205
|
+
),
|
|
206
|
+
e.EVALUATE,
|
|
207
|
+
)
|
|
208
|
+
root_location = registry.root_ref(schema_uri).location
|
|
209
|
+
compile_regex = engine.regex_cache.compile
|
|
210
|
+
should_record = record if record is not None else _record_nothing
|
|
211
|
+
|
|
212
|
+
def evaluate(
|
|
213
|
+
instance: JsonValue,
|
|
214
|
+
*,
|
|
215
|
+
output: str | OutputFormat = OutputFormat.LIST,
|
|
216
|
+
error_params: bool = False,
|
|
217
|
+
verbose: bool | None = None,
|
|
218
|
+
trace: bool = False,
|
|
219
|
+
positions: bool = False,
|
|
220
|
+
) -> Result:
|
|
221
|
+
demand = resolve_output_demand(
|
|
222
|
+
output=output,
|
|
223
|
+
annotations=annotations,
|
|
224
|
+
error_params=error_params,
|
|
225
|
+
verbose=verbose,
|
|
226
|
+
trace=trace,
|
|
227
|
+
positions=positions,
|
|
228
|
+
)
|
|
229
|
+
state = EvalState(
|
|
230
|
+
registry,
|
|
231
|
+
compile_regex,
|
|
232
|
+
should_record=should_record,
|
|
233
|
+
max_depth=budget,
|
|
234
|
+
tracing=demand.tracing,
|
|
235
|
+
)
|
|
236
|
+
valid = bool(entry(instance, state))
|
|
237
|
+
if not valid:
|
|
238
|
+
# The root application's records never merge (rule 3).
|
|
239
|
+
cut_annotations(state, 0)
|
|
240
|
+
return assemble_evaluation(
|
|
241
|
+
state,
|
|
242
|
+
valid,
|
|
243
|
+
demand,
|
|
244
|
+
annotations,
|
|
245
|
+
root_location,
|
|
246
|
+
trace,
|
|
247
|
+
locate=engine.locate if positions else None,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return CompiledEvaluator(evaluate, plan, module, ast.unparse(module), annotations)
|