corvus-json-schema 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,260 @@
1
+ """A high-performance JSON Schema evaluator (draft 4, 6, 7, 2019-09 and 2020-12) that compiles schemas to
2
+ specialised Python, with results collection and annotations. A port of the Corvus.Text.Json V5 runtime evaluator, by
3
+ way of its TypeScript port.
4
+
5
+ >>> import corvus_json_schema as cjs
6
+ >>> validate = cjs.compile({"type": "object", "required": ["id"], "properties": {"id": {"type": "integer"}}})
7
+ >>> validate({"id": 3})
8
+ True
9
+ >>> validate({"id": "3"})
10
+ False
11
+
12
+ Instances are the values ``json.loads`` produces (``dict``, ``list``, ``str``, ``int``, ``float``, ``bool`` and
13
+ ``None``); a validator tests their exact types, so subclasses such as ``OrderedDict`` are not objects to it.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import builtins
19
+ import json
20
+ import os
21
+ import re
22
+ import threading
23
+ import types
24
+ from collections.abc import Mapping
25
+ from dataclasses import replace
26
+ from typing import Any, Protocol, cast
27
+
28
+ from . import runtime
29
+ from .codegen import CodeGenerator, GeneratedCode
30
+ from .collecting import CollectingProgram, evaluate_with_collector, serialize_program
31
+ from .compiler import CompiledSchema, SchemaCompiler, resolve_annotations
32
+ from .dialect import Dialect
33
+ from .options import (
34
+ CompileOptions,
35
+ DocumentResolver,
36
+ FormatValidator,
37
+ SchemaCompilationError,
38
+ SchemaEvaluationDepthError,
39
+ )
40
+ from .results import (
41
+ Annotation,
42
+ JsonSchemaResultsCollector,
43
+ ResultsLevel,
44
+ SchemaResult,
45
+ collect_annotations,
46
+ enumerate_annotations,
47
+ schema_location_fragment,
48
+ )
49
+
50
+ __version__ = "0.1.0"
51
+
52
+ __all__ = [
53
+ "Annotation",
54
+ "CompileOptions",
55
+ "Dialect",
56
+ "DocumentResolver",
57
+ "FormatValidator",
58
+ "JsonSchemaResultsCollector",
59
+ "ResultsLevel",
60
+ "SchemaCompilationError",
61
+ "SchemaEvaluationDepthError",
62
+ "SchemaResult",
63
+ "Validator",
64
+ "collect_annotations",
65
+ "compile",
66
+ "enumerate_annotations",
67
+ "generate_module",
68
+ "schema_location_fragment",
69
+ ]
70
+
71
+
72
+ class Validator(Protocol):
73
+ """A compiled validator: returns True when the instance is valid against the schema."""
74
+
75
+ def __call__(self, instance: Any) -> bool: ...
76
+
77
+ def evaluate(self, instance: Any, collector: JsonSchemaResultsCollector | None = None) -> bool:
78
+ """Evaluates the instance, reporting results to the collector when one is given (every keyword is evaluated
79
+ and reported, at the collector's level); without a collector this is the validator itself."""
80
+ ...
81
+
82
+ @property
83
+ def source(self) -> str:
84
+ """The generated Python (the body of the validator's module)."""
85
+ ...
86
+
87
+
88
+ def _options(options: CompileOptions | None, overrides: Mapping[str, Any]) -> CompileOptions:
89
+ base = options if options is not None else CompileOptions()
90
+ return replace(base, **overrides) if overrides else base
91
+
92
+
93
+ def _parse_schema(schema: Any) -> Any:
94
+ return json.loads(schema) if isinstance(schema, (str, bytes, bytearray)) else schema
95
+
96
+
97
+ def _generate(schema: Any, options: CompileOptions) -> tuple[GeneratedCode, CompiledSchema]:
98
+ program = SchemaCompiler.compile(_parse_schema(schema), options)
99
+ return CodeGenerator(program).generate(), program
100
+
101
+
102
+ def _collecting_program(program: CompiledSchema) -> CollectingProgram:
103
+ resolve_annotations(program)
104
+ return CollectingProgram(
105
+ program.nodes, program.root, program.uses_dynamic_scope, program.options.max_depth, program.options.formats
106
+ )
107
+
108
+
109
+ def _assemble(g: GeneratedCode, functions: bool = True) -> str:
110
+ s = ""
111
+ if g.uses_dynamic_scope:
112
+ s += "DS = []\n"
113
+ if g.uses_depth:
114
+ s += f"depth = 0\nMAXDEPTH = {g.max_depth}\n"
115
+ s += (g.declarations if functions else g.constants) + "\n"
116
+ if g.uses_dynamic_scope or g.uses_depth:
117
+ # The dynamic scope and the depth count are module state: one evaluation at a time (re-entrant, for a custom
118
+ # format that validates with the same validator).
119
+ s += "LOCK = THREADING.RLock()\n"
120
+ s += "def validate(x):\n"
121
+ s += " global depth\n" if g.uses_depth else ""
122
+ s += " with LOCK:\n"
123
+ if g.uses_depth:
124
+ s += " saved = depth\n depth = 0\n"
125
+ if g.uses_dynamic_scope:
126
+ s += f" scope = DS[:]\n DS.clear()\n DS.append({g.root_resource})\n"
127
+ s += " try:\n"
128
+ s += f" return {g.root}(x)\n"
129
+ s += " finally:\n"
130
+ restore = []
131
+ if g.uses_depth:
132
+ restore.append(" depth = saved\n")
133
+ if g.uses_dynamic_scope:
134
+ restore.append(" DS[:] = scope\n")
135
+ s += "".join(restore)
136
+ else:
137
+ s += f"validate = {g.root}\n"
138
+ return s
139
+
140
+
141
+ # Programs with more source than this (characters) compile each function on its first call.
142
+ LAZY_MIN_SOURCE = int(os.environ.get("CORVUS_PY_LAZY_MIN", 64 * 1024))
143
+
144
+
145
+ def _stub_template(*args: Any) -> Any:
146
+ return MATERIALISE("@", args) # type: ignore[name-defined] # noqa: F821 - a global of the generated module
147
+
148
+
149
+ def _install_lazy_functions(namespace: dict[str, Any], functions: list[tuple[str, str]]) -> None:
150
+ """Defines each function as a stub that, on its first call, compiles the function's source and takes its code:
151
+ the stub object becomes the function, so references already taken (dispatch tables) need no update."""
152
+ sources = dict(functions)
153
+ template = _stub_template.__code__
154
+
155
+ def materialise(name: str, args: tuple[Any, ...]) -> Any:
156
+ stub = namespace[name]
157
+ module = builtins.compile(sources[name], "<corvus-json-schema>", "exec")
158
+ stub.__code__ = next(c for c in module.co_consts if isinstance(c, types.CodeType))
159
+ return stub(*args)
160
+
161
+ namespace["MATERIALISE"] = materialise
162
+ for name, _ in functions:
163
+ code = template.replace(co_consts=tuple(name if c == "@" else c for c in template.co_consts), co_name=name)
164
+ namespace[name] = types.FunctionType(code, namespace, name)
165
+
166
+
167
+ def _namespace(custom_formats: list[Any]) -> dict[str, Any]:
168
+ return {
169
+ "__name__": "corvus_json_schema.generated",
170
+ "R": runtime,
171
+ "F": custom_formats,
172
+ "JSON": json,
173
+ "RE": re,
174
+ "THREADING": threading,
175
+ "NoneType": types.NoneType,
176
+ }
177
+
178
+
179
+ def compile(schema: Any, options: CompileOptions | None = None, **kwargs: Any) -> Validator:
180
+ """Compiles a schema (a parsed JSON value, or JSON text) into a validator.
181
+
182
+ The schema is loaded, its references are resolved and analysed, and the result is emitted as specialised Python
183
+ and compiled once. Options are given as a ``CompileOptions`` or as its fields by keyword (``default_dialect``,
184
+ ``assert_format``, ``formats``, ``resolve_document``, ``base_uri``, ``entry_point``, ``max_depth`` and so on).
185
+ """
186
+ opts = _options(options, kwargs)
187
+ g, program = _generate(schema, opts)
188
+ source = _assemble(g)
189
+ namespace = _namespace(g.custom_formats)
190
+ try:
191
+ if len(source) <= LAZY_MIN_SOURCE:
192
+ exec(builtins.compile(source, "<corvus-json-schema>", "exec"), namespace)
193
+ else:
194
+ # A large program: each function is compiled on its first call, so functions no instance reaches (much of
195
+ # a large schema, for most instances) are never compiled.
196
+ _install_lazy_functions(namespace, g.functions)
197
+ exec(builtins.compile(_assemble(g, functions=False), "<corvus-json-schema>", "exec"), namespace)
198
+ except SyntaxError as e:
199
+ raise SchemaCompilationError(f"The schema produced invalid code: {e}") from e
200
+ validate = namespace["validate"]
201
+ # The collecting program (annotations included) is built on the first evaluation with a collector.
202
+ collecting: list[CollectingProgram] = []
203
+
204
+ def evaluate(instance: Any, collector: JsonSchemaResultsCollector | None = None) -> bool:
205
+ if collector is None:
206
+ return bool(validate(instance))
207
+ if not collecting:
208
+ collecting.append(_collecting_program(program))
209
+ return evaluate_with_collector(collecting[0], instance, collector)
210
+
211
+ # The generated function itself is the validator, so a call costs nothing beyond the checks. When the module keeps
212
+ # evaluation state (a dynamic scope or a depth count), `validate` is already a wrapper that resets it.
213
+ validate.source = source
214
+ validate.evaluate = evaluate
215
+ return cast(Validator, validate)
216
+
217
+
218
+ def generate_module(
219
+ schema: Any,
220
+ options: CompileOptions | None = None,
221
+ *,
222
+ runtime_import: str = "corvus_json_schema.runtime",
223
+ collecting: bool = True,
224
+ collecting_import: str = "corvus_json_schema.collecting",
225
+ **kwargs: Any,
226
+ ) -> str:
227
+ """Generates a standalone Python module for the schema, defining ``validate(instance) -> bool``.
228
+
229
+ The module depends only on the runtime helpers (``corvus_json_schema.runtime``), so the schema compiler is not
230
+ needed where it runs. With ``collecting`` (the default) it also defines ``evaluate(instance, collector=None)``,
231
+ embedding the program image for results collection. Custom format functions cannot be serialised; supply them to
232
+ ``compile`` instead.
233
+ """
234
+ opts = _options(options, kwargs)
235
+ g, program = _generate(schema, opts)
236
+ if g.custom_formats:
237
+ raise SchemaCompilationError("Custom format functions cannot be emitted into a standalone module.")
238
+ text = "# Generated by corvus-json-schema. Do not edit.\n"
239
+ text += "# ruff: noqa\n# fmt: off\n"
240
+ text += "import json as JSON\nimport re as RE\nimport threading as THREADING\nfrom types import NoneType\n\n"
241
+ text += f"import {runtime_import} as R\n"
242
+ if collecting:
243
+ text += f"from {collecting_import} import evaluate_with_collector, load_program\n"
244
+ text += "\n" + _assemble(g)
245
+ text += "\n__all__ = ['validate'" + (", 'evaluate'" if collecting else "") + "]\n"
246
+ if collecting:
247
+ image = serialize_program(_collecting_program(program))
248
+ text += (
249
+ f"\n_image = {image!r}\n"
250
+ "_program = None\n\n\n"
251
+ "def evaluate(instance, collector=None):\n"
252
+ ' """Evaluates the instance, reporting to the results collector when one is given."""\n'
253
+ " global _program\n"
254
+ " if collector is None:\n"
255
+ " return validate(instance)\n"
256
+ " if _program is None:\n"
257
+ " _program = load_program(_image)\n"
258
+ " return evaluate_with_collector(_program, instance, collector)\n"
259
+ )
260
+ return text
@@ -0,0 +1,27 @@
1
+ # <auto-generated> by scripts/embed_metaschemas.py from src/Corvus.Text.Json/metaschema. Do not edit.
2
+
3
+ """The standard metaschemas, keyed by their canonical URI (no trailing empty fragment)."""
4
+
5
+ METASCHEMAS: dict[str, str] = {
6
+ "http://json-schema.org/draft-04/schema": '{"id":"http://json-schema.org/draft-04/schema#","$schema":"http://json-schema.org/draft-04/schema#","description":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"positiveInteger":{"type":"integer","minimum":0},"positiveIntegerDefault0":{"allOf":[{"$ref":"#/definitions/positiveInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}},"type":"object","properties":{"id":{"type":"string"},"$schema":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"multipleOf":{"type":"number","minimum":0,"exclusiveMinimum":true},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"boolean","default":false},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"boolean","default":false},"maxLength":{"$ref":"#/definitions/positiveInteger"},"minLength":{"$ref":"#/definitions/positiveIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/positiveInteger"},"minItems":{"$ref":"#/definitions/positiveIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxProperties":{"$ref":"#/definitions/positiveInteger"},"minProperties":{"$ref":"#/definitions/positiveIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"anyOf":[{"type":"boolean"},{"$ref":"#"}],"default":{}},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"dependencies":{"exclusiveMaximum":["maximum"],"exclusiveMinimum":["minimum"]},"default":{}}',
7
+ "http://json-schema.org/draft-06/schema": '{"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}',
8
+ "http://json-schema.org/draft-07/schema": '{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}',
9
+ "https://json-schema.org/draft/2019-09/schema": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/schema","$vocabulary":{"https://json-schema.org/draft/2019-09/vocab/core":true,"https://json-schema.org/draft/2019-09/vocab/applicator":true,"https://json-schema.org/draft/2019-09/vocab/validation":true,"https://json-schema.org/draft/2019-09/vocab/meta-data":true,"https://json-schema.org/draft/2019-09/vocab/format":false,"https://json-schema.org/draft/2019-09/vocab/content":true},"$recursiveAnchor":true,"title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format"},{"$ref":"meta/content"}],"type":["object","boolean"],"properties":{"definitions":{"$comment":"While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.","type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"dependencies":{"$comment":"\\"dependencies\\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \\"dependentSchemas\\" and \\"dependentRequired\\"","type":"object","additionalProperties":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"meta/validation#/$defs/stringArray"}]}}}}',
10
+ "https://json-schema.org/draft/2020-12/schema": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/schema","$vocabulary":{"https://json-schema.org/draft/2020-12/vocab/core":true,"https://json-schema.org/draft/2020-12/vocab/applicator":true,"https://json-schema.org/draft/2020-12/vocab/unevaluated":true,"https://json-schema.org/draft/2020-12/vocab/validation":true,"https://json-schema.org/draft/2020-12/vocab/meta-data":true,"https://json-schema.org/draft/2020-12/vocab/format-annotation":true,"https://json-schema.org/draft/2020-12/vocab/content":true},"$dynamicAnchor":"meta","title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/unevaluated"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format-annotation"},{"$ref":"meta/content"}],"type":["object","boolean"],"$comment":"This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.","properties":{"definitions":{"$comment":"\\"definitions\\" has been replaced by \\"$defs\\".","type":"object","additionalProperties":{"$dynamicRef":"#meta"},"deprecated":true,"default":{}},"dependencies":{"$comment":"\\"dependencies\\" has been split and replaced by \\"dependentSchemas\\" and \\"dependentRequired\\" in order to serve their differing semantics.","type":"object","additionalProperties":{"anyOf":[{"$dynamicRef":"#meta"},{"$ref":"meta/validation#/$defs/stringArray"}]},"deprecated":true,"default":{}},"$recursiveAnchor":{"$comment":"\\"$recursiveAnchor\\" has been replaced by \\"$dynamicAnchor\\".","$ref":"meta/core#/$defs/anchorString","deprecated":true},"$recursiveRef":{"$comment":"\\"$recursiveRef\\" has been replaced by \\"$dynamicRef\\".","$ref":"meta/core#/$defs/uriReferenceString","deprecated":true}}}',
11
+ "https://json-schema.org/draft/2019-09/meta/applicator": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/applicator","$recursiveAnchor":true,"title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"additionalItems":{"$recursiveRef":"#"},"unevaluatedItems":{"$recursiveRef":"#"},"items":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"#/$defs/schemaArray"}]},"contains":{"$recursiveRef":"#"},"additionalProperties":{"$recursiveRef":"#"},"unevaluatedProperties":{"$recursiveRef":"#"},"properties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$recursiveRef":"#"}},"propertyNames":{"$recursiveRef":"#"},"if":{"$recursiveRef":"#"},"then":{"$recursiveRef":"#"},"else":{"$recursiveRef":"#"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$recursiveRef":"#"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$recursiveRef":"#"}}}}',
12
+ "https://json-schema.org/draft/2019-09/meta/content": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/content","$recursiveAnchor":true,"title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"contentSchema":{"$recursiveRef":"#"}}}',
13
+ "https://json-schema.org/draft/2019-09/meta/core": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/core","$recursiveAnchor":true,"title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"type":"string","format":"uri"},"$anchor":{"type":"string","pattern":"^[A-Za-z][-A-Za-z0-9.:_]*$"},"$ref":{"type":"string","format":"uri-reference"},"$recursiveRef":{"type":"string","format":"uri-reference"},"$recursiveAnchor":{"type":"boolean","default":false},"$vocabulary":{"type":"object","propertyNames":{"type":"string","format":"uri"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}}}}',
14
+ "https://json-schema.org/draft/2019-09/meta/format": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/format","$recursiveAnchor":true,"title":"Format vocabulary meta-schema","type":["object","boolean"],"properties":{"format":{"type":"string"}}}',
15
+ "https://json-schema.org/draft/2019-09/meta/hyper-schema": '{"$schema":"https://json-schema.org/draft/2019-09/hyper-schema","$id":"https://json-schema.org/draft/2019-09/meta/hyper-schema","$recursiveAnchor":true,"title":"JSON Hyper-Schema Vocabulary Schema","type":["object","boolean"],"properties":{"base":{"type":"string","format":"uri-template"},"links":{"type":"array","items":{"$ref":"https://json-schema.org/draft/2019-09/links"}}},"links":[{"rel":"self","href":"{+%24id}"}]}',
16
+ "https://json-schema.org/draft/2019-09/meta/meta-data": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/meta-data","$recursiveAnchor":true,"title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}',
17
+ "https://json-schema.org/draft/2019-09/meta/validation": '{"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/validation","$recursiveAnchor":true,"title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}},"const":true,"enum":{"type":"array","items":true},"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}',
18
+ "https://json-schema.org/draft/2020-12/meta/applicator": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/applicator","$dynamicAnchor":"meta","title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"prefixItems":{"$ref":"#/$defs/schemaArray"},"items":{"$dynamicRef":"#meta"},"contains":{"$dynamicRef":"#meta"},"additionalProperties":{"$dynamicRef":"#meta"},"properties":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"default":{}},"propertyNames":{"$dynamicRef":"#meta"},"if":{"$dynamicRef":"#meta"},"then":{"$dynamicRef":"#meta"},"else":{"$dynamicRef":"#meta"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$dynamicRef":"#meta"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$dynamicRef":"#meta"}}}}',
19
+ "https://json-schema.org/draft/2020-12/meta/content": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/content","$dynamicAnchor":"meta","title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentEncoding":{"type":"string"},"contentMediaType":{"type":"string"},"contentSchema":{"$dynamicRef":"#meta"}}}',
20
+ "https://json-schema.org/draft/2020-12/meta/core": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/core","$dynamicAnchor":"meta","title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"$ref":"#/$defs/uriReferenceString","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"$ref":"#/$defs/uriString"},"$ref":{"$ref":"#/$defs/uriReferenceString"},"$anchor":{"$ref":"#/$defs/anchorString"},"$dynamicRef":{"$ref":"#/$defs/uriReferenceString"},"$dynamicAnchor":{"$ref":"#/$defs/anchorString"},"$vocabulary":{"type":"object","propertyNames":{"$ref":"#/$defs/uriString"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"}}},"$defs":{"anchorString":{"type":"string","pattern":"^[A-Za-z_][-A-Za-z0-9._]*$"},"uriString":{"type":"string","format":"uri"},"uriReferenceString":{"type":"string","format":"uri-reference"}}}',
21
+ "https://json-schema.org/draft/2020-12/meta/format-annotation": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/format-annotation","$dynamicAnchor":"meta","title":"Format vocabulary meta-schema for annotation results","type":["object","boolean"],"properties":{"format":{"type":"string"}}}',
22
+ "https://json-schema.org/draft/2020-12/meta/format-assertion": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/format-assertion","$dynamicAnchor":"meta","title":"Format vocabulary meta-schema for assertion results","type":["object","boolean"],"properties":{"format":{"type":"string"}}}',
23
+ "https://json-schema.org/draft/2020-12/meta/hyper-schema": '{"$schema":"https://json-schema.org/draft/2020-12/hyper-schema","$id":"https://json-schema.org/draft/2020-12/meta/hyper-schema","$dynamicAnchor":"meta","title":"JSON Hyper-Schema Vocabulary Schema","type":["object","boolean"],"properties":{"base":{"type":"string","format":"uri-template"},"links":{"type":"array","items":{"$ref":"https://json-schema.org/draft/2020-12/links"}}},"links":[{"rel":"self","href":"{+%24id}"}]}',
24
+ "https://json-schema.org/draft/2020-12/meta/meta-data": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/meta-data","$dynamicAnchor":"meta","title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}',
25
+ "https://json-schema.org/draft/2020-12/meta/unevaluated": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/unevaluated","$dynamicAnchor":"meta","title":"Unevaluated applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"unevaluatedItems":{"$dynamicRef":"#meta"},"unevaluatedProperties":{"$dynamicRef":"#meta"}}}',
26
+ "https://json-schema.org/draft/2020-12/meta/validation": '{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/validation","$dynamicAnchor":"meta","title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]},"const":true,"enum":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}',
27
+ }
@@ -0,0 +1,95 @@
1
+ """corvus-json-schema: emit a standalone validator module for a schema, or validate JSON documents against a schema.
2
+
3
+ corvus-json-schema generate <schema.json> [-o <out.py>] [--runtime-import <module>] [options]
4
+ corvus-json-schema validate <schema.json> <instance.json>... [options]
5
+
6
+ Options: --default-dialect <4|6|7|2019-09|2020-12>, --assert-format, --base-uri <uri>, --entry-point <ref>,
7
+ --schema-dir <dir> (resolve remote $refs from local files named after the URI's last path segment).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from urllib.parse import unquote, urlsplit
18
+
19
+ from . import Dialect, compile, generate_module
20
+
21
+ DIALECTS = {
22
+ "4": Dialect.DRAFT4,
23
+ "6": Dialect.DRAFT6,
24
+ "7": Dialect.DRAFT7,
25
+ "2019-09": Dialect.DRAFT201909,
26
+ "2020-12": Dialect.DRAFT202012,
27
+ }
28
+
29
+
30
+ def _read_json(file: str) -> Any:
31
+ return json.loads(Path(file).read_text(encoding="utf-8"))
32
+
33
+
34
+ def main(argv: list[str] | None = None) -> int:
35
+ parser = argparse.ArgumentParser(
36
+ prog="corvus-json-schema", description=__doc__.split("\n\n")[0] if __doc__ else None
37
+ )
38
+ parser.add_argument("command", choices=["generate", "validate"])
39
+ parser.add_argument("files", nargs="+")
40
+ parser.add_argument("-o", "--output", help="the file to write the generated module to (generate)")
41
+ parser.add_argument(
42
+ "--runtime-import", default="corvus_json_schema.runtime", help="the module of the runtime helpers"
43
+ )
44
+ parser.add_argument("--default-dialect", help="dialect for schemas without $schema (default 2020-12)")
45
+ parser.add_argument("--assert-format", action="store_true", help='assert "format" (otherwise it is an annotation)')
46
+ parser.add_argument("--base-uri", help="base URI of the root schema")
47
+ parser.add_argument("--entry-point", help="evaluate from a subschema, e.g. #/$defs/item")
48
+ parser.add_argument("--schema-dir", help="resolve referenced documents from files in this directory")
49
+ args = parser.parse_args(argv)
50
+
51
+ options: dict[str, Any] = {}
52
+ if args.default_dialect is not None:
53
+ name = args.default_dialect.removeprefix("draft").removeprefix("-")
54
+ if name not in DIALECTS:
55
+ parser.error(f"unknown dialect '{args.default_dialect}'")
56
+ options["default_dialect"] = DIALECTS[name]
57
+ if args.assert_format:
58
+ options["assert_format"] = True
59
+ if args.base_uri is not None:
60
+ options["base_uri"] = args.base_uri
61
+ if args.entry_point is not None:
62
+ options["entry_point"] = args.entry_point
63
+ if args.schema_dir is not None:
64
+ directory = Path(args.schema_dir)
65
+
66
+ def resolve(uri: str) -> Any:
67
+ file = directory / unquote(urlsplit(uri).path.split("/")[-1])
68
+ return json.loads(file.read_text(encoding="utf-8")) if file.is_file() else None
69
+
70
+ options["resolve_document"] = resolve
71
+
72
+ if args.command == "generate":
73
+ if len(args.files) != 1:
74
+ parser.error("generate takes one schema file")
75
+ source = generate_module(_read_json(args.files[0]), runtime_import=args.runtime_import, **options)
76
+ if args.output is None:
77
+ sys.stdout.write(source)
78
+ else:
79
+ Path(args.output).write_text(source, encoding="utf-8")
80
+ return 0
81
+
82
+ if len(args.files) < 2:
83
+ parser.error("validate takes a schema file and at least one instance file")
84
+ validate = compile(_read_json(args.files[0]), **options)
85
+ failed = 0
86
+ for file in args.files[1:]:
87
+ valid = validate(_read_json(file))
88
+ if not valid:
89
+ failed += 1
90
+ print(f"{file}: {'valid' if valid else 'invalid'}")
91
+ return 0 if failed == 0 else 1
92
+
93
+
94
+ if __name__ == "__main__":
95
+ sys.exit(main())