flowspec2 1.0.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.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
"""JSON Schema relation and path proofs used by compiler contract validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
|
|
9
|
+
from typing import Any, Final, cast
|
|
10
|
+
|
|
11
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
12
|
+
|
|
13
|
+
from .schema_contracts import schema_reference_target
|
|
14
|
+
|
|
15
|
+
CONTRACT_INVALID: Final[int] = -1
|
|
16
|
+
CONTRACT_UNKNOWN: Final[int] = 0
|
|
17
|
+
CONTRACT_VALID: Final[int] = 1
|
|
18
|
+
_VALUE_TYPE_ATOMS: Final[frozenset[str]] = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"array",
|
|
21
|
+
"boolean",
|
|
22
|
+
"integer",
|
|
23
|
+
"non_integer_number",
|
|
24
|
+
"null",
|
|
25
|
+
"object",
|
|
26
|
+
"string",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
_NumericBoundary = tuple[Decimal, bool]
|
|
30
|
+
_NumericInterval = tuple[_NumericBoundary | None, _NumericBoundary | None]
|
|
31
|
+
_SCHEMA_ANNOTATION_KEYWORDS: Final[frozenset[str]] = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"$comment",
|
|
34
|
+
"default",
|
|
35
|
+
"deprecated",
|
|
36
|
+
"description",
|
|
37
|
+
"examples",
|
|
38
|
+
"readOnly",
|
|
39
|
+
"title",
|
|
40
|
+
"writeOnly",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mutable_contract_json(contract_fragment: Any) -> Any:
|
|
46
|
+
if isinstance(contract_fragment, Mapping):
|
|
47
|
+
return {
|
|
48
|
+
property_name: mutable_contract_json(property_value)
|
|
49
|
+
for property_name, property_value in contract_fragment.items()
|
|
50
|
+
}
|
|
51
|
+
if isinstance(contract_fragment, (list, tuple)):
|
|
52
|
+
return [mutable_contract_json(element) for element in contract_fragment]
|
|
53
|
+
return contract_fragment
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def standalone_schema(
|
|
57
|
+
schema: Any,
|
|
58
|
+
root_schema: Any,
|
|
59
|
+
reference_stack: tuple[str, ...] = (),
|
|
60
|
+
) -> Any:
|
|
61
|
+
"""Inline safe local references so contracts from different roots can be composed."""
|
|
62
|
+
|
|
63
|
+
if isinstance(schema, Mapping):
|
|
64
|
+
standalone_mapping = {
|
|
65
|
+
keyword: standalone_schema(keyword_value, root_schema, reference_stack)
|
|
66
|
+
for keyword, keyword_value in schema.items()
|
|
67
|
+
if keyword != "$ref"
|
|
68
|
+
}
|
|
69
|
+
if (reference := schema.get("$ref")) is None:
|
|
70
|
+
return standalone_mapping
|
|
71
|
+
reference_text = cast(str, reference)
|
|
72
|
+
if reference_text in reference_stack:
|
|
73
|
+
raise ValueError("cyclic local schema reference cannot be materialized")
|
|
74
|
+
referenced_schema = standalone_schema(
|
|
75
|
+
schema_reference_target(root_schema, reference_text),
|
|
76
|
+
root_schema,
|
|
77
|
+
(*reference_stack, reference_text),
|
|
78
|
+
)
|
|
79
|
+
return combined_contract([standalone_mapping or True, referenced_schema])
|
|
80
|
+
if isinstance(schema, (list, tuple)):
|
|
81
|
+
return [standalone_schema(element, root_schema, reference_stack) for element in schema]
|
|
82
|
+
return schema
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def schema_accepts_known_instance(
|
|
86
|
+
schema: Any,
|
|
87
|
+
root_schema: Any,
|
|
88
|
+
instance: object,
|
|
89
|
+
) -> bool:
|
|
90
|
+
mutable_root_schema = mutable_contract_json(root_schema)
|
|
91
|
+
validator = Draft202012Validator(
|
|
92
|
+
mutable_root_schema,
|
|
93
|
+
format_checker=FormatChecker(),
|
|
94
|
+
)
|
|
95
|
+
return validator.evolve(schema=mutable_contract_json(schema)).is_valid(cast(Any, instance))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def combined_contract(contract_fragments: list[Any], keyword: str = "allOf") -> Any:
|
|
99
|
+
effective_fragments = [
|
|
100
|
+
contract_fragment
|
|
101
|
+
for contract_fragment in contract_fragments
|
|
102
|
+
if contract_fragment is not True
|
|
103
|
+
]
|
|
104
|
+
if not effective_fragments:
|
|
105
|
+
return True
|
|
106
|
+
if len(effective_fragments) == 1:
|
|
107
|
+
return effective_fragments[0]
|
|
108
|
+
return {keyword: effective_fragments}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def schema_property_contract(
|
|
112
|
+
schema: Any,
|
|
113
|
+
property_name: str,
|
|
114
|
+
root_schema: Any,
|
|
115
|
+
bound_keys: frozenset[str],
|
|
116
|
+
reference_stack: tuple[str, ...] = (),
|
|
117
|
+
) -> Any:
|
|
118
|
+
if schema is False:
|
|
119
|
+
return False
|
|
120
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
property_contracts: list[Any] = []
|
|
124
|
+
properties = schema.get("properties")
|
|
125
|
+
pattern_properties = schema.get("patternProperties")
|
|
126
|
+
has_property_dispatch = (
|
|
127
|
+
isinstance(properties, Mapping)
|
|
128
|
+
or isinstance(pattern_properties, Mapping)
|
|
129
|
+
or "additionalProperties" in schema
|
|
130
|
+
)
|
|
131
|
+
if has_property_dispatch:
|
|
132
|
+
matching_contracts: list[Any] = []
|
|
133
|
+
if isinstance(properties, Mapping) and property_name in properties:
|
|
134
|
+
matching_contracts.append(properties[property_name])
|
|
135
|
+
if isinstance(pattern_properties, Mapping):
|
|
136
|
+
matching_contracts.extend(
|
|
137
|
+
property_contract
|
|
138
|
+
for property_pattern, property_contract in pattern_properties.items()
|
|
139
|
+
if re.search(property_pattern, property_name) is not None
|
|
140
|
+
)
|
|
141
|
+
if not matching_contracts:
|
|
142
|
+
matching_contracts.append(schema.get("additionalProperties", True))
|
|
143
|
+
property_contracts.append(combined_contract(matching_contracts))
|
|
144
|
+
|
|
145
|
+
if (reference := schema.get("$ref")) is not None:
|
|
146
|
+
reference_text = cast(str, reference)
|
|
147
|
+
property_contracts.append(
|
|
148
|
+
True
|
|
149
|
+
if reference_text in reference_stack
|
|
150
|
+
else schema_property_contract(
|
|
151
|
+
schema_reference_target(root_schema, reference_text),
|
|
152
|
+
property_name,
|
|
153
|
+
root_schema,
|
|
154
|
+
bound_keys,
|
|
155
|
+
(*reference_stack, reference_text),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
159
|
+
property_contracts.extend(
|
|
160
|
+
schema_property_contract(
|
|
161
|
+
branch,
|
|
162
|
+
property_name,
|
|
163
|
+
root_schema,
|
|
164
|
+
bound_keys,
|
|
165
|
+
reference_stack,
|
|
166
|
+
)
|
|
167
|
+
for branch in all_of
|
|
168
|
+
)
|
|
169
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
170
|
+
branches = schema.get(union_keyword)
|
|
171
|
+
if isinstance(branches, (list, tuple)):
|
|
172
|
+
property_contracts.append(
|
|
173
|
+
combined_contract(
|
|
174
|
+
[
|
|
175
|
+
schema_property_contract(
|
|
176
|
+
branch,
|
|
177
|
+
property_name,
|
|
178
|
+
root_schema,
|
|
179
|
+
bound_keys,
|
|
180
|
+
reference_stack,
|
|
181
|
+
)
|
|
182
|
+
for branch in branches
|
|
183
|
+
],
|
|
184
|
+
"anyOf",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
if "if" in schema:
|
|
188
|
+
property_contracts.append(
|
|
189
|
+
combined_contract(
|
|
190
|
+
[
|
|
191
|
+
schema_property_contract(
|
|
192
|
+
schema.get("then", True),
|
|
193
|
+
property_name,
|
|
194
|
+
root_schema,
|
|
195
|
+
bound_keys,
|
|
196
|
+
reference_stack,
|
|
197
|
+
),
|
|
198
|
+
schema_property_contract(
|
|
199
|
+
schema.get("else", True),
|
|
200
|
+
property_name,
|
|
201
|
+
root_schema,
|
|
202
|
+
bound_keys,
|
|
203
|
+
reference_stack,
|
|
204
|
+
),
|
|
205
|
+
],
|
|
206
|
+
"anyOf",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
dependent_schemas = schema.get("dependentSchemas")
|
|
210
|
+
if isinstance(dependent_schemas, Mapping):
|
|
211
|
+
property_contracts.extend(
|
|
212
|
+
schema_property_contract(
|
|
213
|
+
dependent_schema,
|
|
214
|
+
property_name,
|
|
215
|
+
root_schema,
|
|
216
|
+
bound_keys,
|
|
217
|
+
reference_stack,
|
|
218
|
+
)
|
|
219
|
+
for trigger_property, dependent_schema in dependent_schemas.items()
|
|
220
|
+
if trigger_property in bound_keys
|
|
221
|
+
)
|
|
222
|
+
return combined_contract(property_contracts)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _instance_type_atom(schema_instance: Any) -> str:
|
|
226
|
+
if isinstance(schema_instance, Mapping):
|
|
227
|
+
return "object"
|
|
228
|
+
if isinstance(schema_instance, list):
|
|
229
|
+
return "array"
|
|
230
|
+
if isinstance(schema_instance, bool):
|
|
231
|
+
return "boolean"
|
|
232
|
+
if schema_instance is None:
|
|
233
|
+
return "null"
|
|
234
|
+
if isinstance(schema_instance, int) or (
|
|
235
|
+
isinstance(schema_instance, float) and schema_instance.is_integer()
|
|
236
|
+
):
|
|
237
|
+
return "integer"
|
|
238
|
+
if isinstance(schema_instance, float):
|
|
239
|
+
return "non_integer_number"
|
|
240
|
+
return "string"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _atoms_for_schema_type(schema_type: str) -> frozenset[str]:
|
|
244
|
+
if schema_type == "number":
|
|
245
|
+
return frozenset({"integer", "non_integer_number"})
|
|
246
|
+
if schema_type == "integer":
|
|
247
|
+
return frozenset({"integer"})
|
|
248
|
+
return frozenset({schema_type})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def schema_type_atoms(
|
|
252
|
+
schema: Any,
|
|
253
|
+
root_schema: Any,
|
|
254
|
+
reference_stack: tuple[str, ...] = (),
|
|
255
|
+
) -> frozenset[str]:
|
|
256
|
+
if schema is False:
|
|
257
|
+
return frozenset()
|
|
258
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
259
|
+
return _VALUE_TYPE_ATOMS
|
|
260
|
+
|
|
261
|
+
possible_atoms = _VALUE_TYPE_ATOMS
|
|
262
|
+
if (schema_types := schema.get("type")) is not None:
|
|
263
|
+
declared_types = (schema_types,) if isinstance(schema_types, str) else tuple(schema_types)
|
|
264
|
+
possible_atoms &= frozenset().union(
|
|
265
|
+
*(_atoms_for_schema_type(schema_type) for schema_type in declared_types)
|
|
266
|
+
)
|
|
267
|
+
if "const" in schema:
|
|
268
|
+
possible_atoms &= frozenset({_instance_type_atom(schema["const"])})
|
|
269
|
+
if isinstance((enum_members := schema.get("enum")), (list, tuple)):
|
|
270
|
+
possible_atoms &= frozenset(
|
|
271
|
+
_instance_type_atom(enum_member) for enum_member in enum_members
|
|
272
|
+
)
|
|
273
|
+
if (reference := schema.get("$ref")) is not None:
|
|
274
|
+
reference_text = cast(str, reference)
|
|
275
|
+
if reference_text not in reference_stack:
|
|
276
|
+
possible_atoms &= schema_type_atoms(
|
|
277
|
+
schema_reference_target(root_schema, reference_text),
|
|
278
|
+
root_schema,
|
|
279
|
+
(*reference_stack, reference_text),
|
|
280
|
+
)
|
|
281
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
282
|
+
for branch in all_of:
|
|
283
|
+
possible_atoms &= schema_type_atoms(branch, root_schema, reference_stack)
|
|
284
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
285
|
+
branches = schema.get(union_keyword)
|
|
286
|
+
if isinstance(branches, (list, tuple)):
|
|
287
|
+
branch_atoms = frozenset().union(
|
|
288
|
+
*(schema_type_atoms(branch, root_schema, reference_stack) for branch in branches)
|
|
289
|
+
)
|
|
290
|
+
possible_atoms &= branch_atoms
|
|
291
|
+
if "if" in schema:
|
|
292
|
+
conditional_atoms = schema_type_atoms(
|
|
293
|
+
schema.get("then", True), root_schema, reference_stack
|
|
294
|
+
) | schema_type_atoms(schema.get("else", True), root_schema, reference_stack)
|
|
295
|
+
possible_atoms &= conditional_atoms
|
|
296
|
+
return possible_atoms
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def canonical_contract_instance(schema_instance: Any) -> str:
|
|
300
|
+
return json.dumps(
|
|
301
|
+
schema_instance,
|
|
302
|
+
allow_nan=False,
|
|
303
|
+
ensure_ascii=False,
|
|
304
|
+
separators=(",", ":"),
|
|
305
|
+
sort_keys=True,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def finite_schema_instances(
|
|
310
|
+
schema: Any,
|
|
311
|
+
root_schema: Any,
|
|
312
|
+
reference_stack: tuple[str, ...] = (),
|
|
313
|
+
) -> dict[str, Any] | None:
|
|
314
|
+
if schema is False:
|
|
315
|
+
return {}
|
|
316
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
finite_instances: dict[str, Any] | None = None
|
|
320
|
+
|
|
321
|
+
def intersect_instances(candidate_instances: dict[str, Any]) -> None:
|
|
322
|
+
nonlocal finite_instances
|
|
323
|
+
finite_instances = (
|
|
324
|
+
candidate_instances
|
|
325
|
+
if finite_instances is None
|
|
326
|
+
else {
|
|
327
|
+
serialized_instance: schema_instance
|
|
328
|
+
for serialized_instance, schema_instance in finite_instances.items()
|
|
329
|
+
if serialized_instance in candidate_instances
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if "const" in schema:
|
|
334
|
+
constant_instance = schema["const"]
|
|
335
|
+
intersect_instances({canonical_contract_instance(constant_instance): constant_instance})
|
|
336
|
+
if isinstance((enum_members := schema.get("enum")), (list, tuple)):
|
|
337
|
+
intersect_instances(
|
|
338
|
+
{canonical_contract_instance(enum_member): enum_member for enum_member in enum_members}
|
|
339
|
+
)
|
|
340
|
+
if (schema_types := schema.get("type")) is not None:
|
|
341
|
+
declared_types = {schema_types} if isinstance(schema_types, str) else set(schema_types)
|
|
342
|
+
if declared_types and declared_types <= {"boolean", "null"}:
|
|
343
|
+
finite_type_instances = [
|
|
344
|
+
*([None] if "null" in declared_types else []),
|
|
345
|
+
*([False, True] if "boolean" in declared_types else []),
|
|
346
|
+
]
|
|
347
|
+
intersect_instances(
|
|
348
|
+
{
|
|
349
|
+
canonical_contract_instance(type_instance): type_instance
|
|
350
|
+
for type_instance in finite_type_instances
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
if (reference := schema.get("$ref")) is not None:
|
|
354
|
+
reference_text = cast(str, reference)
|
|
355
|
+
if (
|
|
356
|
+
reference_text not in reference_stack
|
|
357
|
+
and (
|
|
358
|
+
referenced_instances := finite_schema_instances(
|
|
359
|
+
schema_reference_target(root_schema, reference_text),
|
|
360
|
+
root_schema,
|
|
361
|
+
(*reference_stack, reference_text),
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
is not None
|
|
365
|
+
):
|
|
366
|
+
intersect_instances(referenced_instances)
|
|
367
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
368
|
+
for branch in all_of:
|
|
369
|
+
if (
|
|
370
|
+
branch_instances := finite_schema_instances(branch, root_schema, reference_stack)
|
|
371
|
+
) is not None:
|
|
372
|
+
intersect_instances(branch_instances)
|
|
373
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
374
|
+
branches = schema.get(union_keyword)
|
|
375
|
+
if not isinstance(branches, (list, tuple)):
|
|
376
|
+
continue
|
|
377
|
+
branch_instance_sets = [
|
|
378
|
+
finite_schema_instances(branch, root_schema, reference_stack) for branch in branches
|
|
379
|
+
]
|
|
380
|
+
if all(branch_instances is not None for branch_instances in branch_instance_sets):
|
|
381
|
+
intersect_instances(
|
|
382
|
+
{
|
|
383
|
+
serialized_instance: schema_instance
|
|
384
|
+
for branch_instances in branch_instance_sets
|
|
385
|
+
if branch_instances is not None
|
|
386
|
+
for serialized_instance, schema_instance in branch_instances.items()
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
if "if" in schema:
|
|
390
|
+
conditional_instance_sets = [
|
|
391
|
+
finite_schema_instances(
|
|
392
|
+
schema.get(conditional_keyword, True),
|
|
393
|
+
root_schema,
|
|
394
|
+
reference_stack,
|
|
395
|
+
)
|
|
396
|
+
for conditional_keyword in ("then", "else")
|
|
397
|
+
]
|
|
398
|
+
if all(
|
|
399
|
+
conditional_instances is not None for conditional_instances in conditional_instance_sets
|
|
400
|
+
):
|
|
401
|
+
intersect_instances(
|
|
402
|
+
{
|
|
403
|
+
serialized_instance: schema_instance
|
|
404
|
+
for conditional_instances in conditional_instance_sets
|
|
405
|
+
if conditional_instances is not None
|
|
406
|
+
for serialized_instance, schema_instance in conditional_instances.items()
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if finite_instances is None:
|
|
411
|
+
return None
|
|
412
|
+
return {
|
|
413
|
+
serialized_instance: schema_instance
|
|
414
|
+
for serialized_instance, schema_instance in finite_instances.items()
|
|
415
|
+
if schema_accepts_known_instance(schema, root_schema, schema_instance)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _stronger_lower_boundary(
|
|
420
|
+
current_boundary: _NumericBoundary | None,
|
|
421
|
+
candidate_boundary: _NumericBoundary,
|
|
422
|
+
) -> _NumericBoundary:
|
|
423
|
+
if current_boundary is None or candidate_boundary[0] > current_boundary[0]:
|
|
424
|
+
return candidate_boundary
|
|
425
|
+
if candidate_boundary[0] == current_boundary[0]:
|
|
426
|
+
return candidate_boundary[0], current_boundary[1] and candidate_boundary[1]
|
|
427
|
+
return current_boundary
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _stronger_upper_boundary(
|
|
431
|
+
current_boundary: _NumericBoundary | None,
|
|
432
|
+
candidate_boundary: _NumericBoundary,
|
|
433
|
+
) -> _NumericBoundary:
|
|
434
|
+
if current_boundary is None or candidate_boundary[0] < current_boundary[0]:
|
|
435
|
+
return candidate_boundary
|
|
436
|
+
if candidate_boundary[0] == current_boundary[0]:
|
|
437
|
+
return candidate_boundary[0], current_boundary[1] and candidate_boundary[1]
|
|
438
|
+
return current_boundary
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _schema_numeric_interval(
|
|
442
|
+
schema: Any,
|
|
443
|
+
root_schema: Any,
|
|
444
|
+
reference_stack: tuple[str, ...] = (),
|
|
445
|
+
) -> _NumericInterval:
|
|
446
|
+
if not isinstance(schema, Mapping):
|
|
447
|
+
return None, None
|
|
448
|
+
lower_boundary: _NumericBoundary | None = None
|
|
449
|
+
upper_boundary: _NumericBoundary | None = None
|
|
450
|
+
if isinstance((minimum := schema.get("minimum")), (int, float)):
|
|
451
|
+
lower_boundary = _stronger_lower_boundary(lower_boundary, (Decimal(str(minimum)), True))
|
|
452
|
+
if isinstance((exclusive_minimum := schema.get("exclusiveMinimum")), (int, float)):
|
|
453
|
+
lower_boundary = _stronger_lower_boundary(
|
|
454
|
+
lower_boundary, (Decimal(str(exclusive_minimum)), False)
|
|
455
|
+
)
|
|
456
|
+
if isinstance((maximum := schema.get("maximum")), (int, float)):
|
|
457
|
+
upper_boundary = _stronger_upper_boundary(upper_boundary, (Decimal(str(maximum)), True))
|
|
458
|
+
if isinstance((exclusive_maximum := schema.get("exclusiveMaximum")), (int, float)):
|
|
459
|
+
upper_boundary = _stronger_upper_boundary(
|
|
460
|
+
upper_boundary, (Decimal(str(exclusive_maximum)), False)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
nested_schemas: list[Any] = []
|
|
464
|
+
if (reference := schema.get("$ref")) is not None:
|
|
465
|
+
reference_text = cast(str, reference)
|
|
466
|
+
if reference_text not in reference_stack:
|
|
467
|
+
nested_schemas.append(schema_reference_target(root_schema, reference_text))
|
|
468
|
+
reference_stack = (*reference_stack, reference_text)
|
|
469
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
470
|
+
nested_schemas.extend(all_of)
|
|
471
|
+
for nested_schema in nested_schemas:
|
|
472
|
+
nested_lower, nested_upper = _schema_numeric_interval(
|
|
473
|
+
nested_schema, root_schema, reference_stack
|
|
474
|
+
)
|
|
475
|
+
if nested_lower is not None:
|
|
476
|
+
lower_boundary = _stronger_lower_boundary(lower_boundary, nested_lower)
|
|
477
|
+
if nested_upper is not None:
|
|
478
|
+
upper_boundary = _stronger_upper_boundary(upper_boundary, nested_upper)
|
|
479
|
+
return lower_boundary, upper_boundary
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _numeric_intervals_overlap(
|
|
483
|
+
source_interval: _NumericInterval,
|
|
484
|
+
parameter_interval: _NumericInterval,
|
|
485
|
+
*,
|
|
486
|
+
integer_only: bool,
|
|
487
|
+
) -> bool:
|
|
488
|
+
lower_boundary: _NumericBoundary | None = None
|
|
489
|
+
upper_boundary: _NumericBoundary | None = None
|
|
490
|
+
for candidate_lower in (source_interval[0], parameter_interval[0]):
|
|
491
|
+
if candidate_lower is not None:
|
|
492
|
+
lower_boundary = _stronger_lower_boundary(lower_boundary, candidate_lower)
|
|
493
|
+
for candidate_upper in (source_interval[1], parameter_interval[1]):
|
|
494
|
+
if candidate_upper is not None:
|
|
495
|
+
upper_boundary = _stronger_upper_boundary(upper_boundary, candidate_upper)
|
|
496
|
+
if lower_boundary is not None and upper_boundary is not None:
|
|
497
|
+
if lower_boundary[0] > upper_boundary[0]:
|
|
498
|
+
return False
|
|
499
|
+
if lower_boundary[0] == upper_boundary[0] and not (lower_boundary[1] and upper_boundary[1]):
|
|
500
|
+
return False
|
|
501
|
+
if not integer_only:
|
|
502
|
+
return True
|
|
503
|
+
|
|
504
|
+
minimum_integer = (
|
|
505
|
+
None
|
|
506
|
+
if lower_boundary is None
|
|
507
|
+
else int(lower_boundary[0].to_integral_value(rounding=ROUND_CEILING))
|
|
508
|
+
+ (
|
|
509
|
+
1
|
|
510
|
+
if not lower_boundary[1] and lower_boundary[0] == lower_boundary[0].to_integral_value()
|
|
511
|
+
else 0
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
maximum_integer = (
|
|
515
|
+
None
|
|
516
|
+
if upper_boundary is None
|
|
517
|
+
else int(upper_boundary[0].to_integral_value(rounding=ROUND_FLOOR))
|
|
518
|
+
- (
|
|
519
|
+
1
|
|
520
|
+
if not upper_boundary[1] and upper_boundary[0] == upper_boundary[0].to_integral_value()
|
|
521
|
+
else 0
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
return minimum_integer is None or maximum_integer is None or minimum_integer <= maximum_integer
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _schemas_are_clearly_disjoint(
|
|
528
|
+
source_schema: Any,
|
|
529
|
+
source_root_schema: Any,
|
|
530
|
+
parameter_schema: Any,
|
|
531
|
+
parameter_root_schema: Any,
|
|
532
|
+
) -> bool:
|
|
533
|
+
source_instances = finite_schema_instances(source_schema, source_root_schema)
|
|
534
|
+
if source_instances is not None:
|
|
535
|
+
return not any(
|
|
536
|
+
schema_accepts_known_instance(parameter_schema, parameter_root_schema, source_instance)
|
|
537
|
+
for source_instance in source_instances.values()
|
|
538
|
+
)
|
|
539
|
+
parameter_instances = finite_schema_instances(parameter_schema, parameter_root_schema)
|
|
540
|
+
if parameter_instances is not None:
|
|
541
|
+
return not any(
|
|
542
|
+
schema_accepts_known_instance(source_schema, source_root_schema, parameter_instance)
|
|
543
|
+
for parameter_instance in parameter_instances.values()
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
shared_type_atoms = schema_type_atoms(source_schema, source_root_schema) & schema_type_atoms(
|
|
547
|
+
parameter_schema, parameter_root_schema
|
|
548
|
+
)
|
|
549
|
+
if not shared_type_atoms:
|
|
550
|
+
return True
|
|
551
|
+
numeric_atoms = frozenset({"integer", "non_integer_number"})
|
|
552
|
+
if shared_type_atoms - numeric_atoms:
|
|
553
|
+
return False
|
|
554
|
+
return not _numeric_intervals_overlap(
|
|
555
|
+
_schema_numeric_interval(source_schema, source_root_schema),
|
|
556
|
+
_schema_numeric_interval(parameter_schema, parameter_root_schema),
|
|
557
|
+
integer_only=shared_type_atoms == {"integer"},
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _schema_assertion_json(schema: Any) -> Any:
|
|
562
|
+
if isinstance(schema, Mapping):
|
|
563
|
+
return {
|
|
564
|
+
keyword: _schema_assertion_json(keyword_value)
|
|
565
|
+
for keyword, keyword_value in schema.items()
|
|
566
|
+
if keyword not in _SCHEMA_ANNOTATION_KEYWORDS
|
|
567
|
+
}
|
|
568
|
+
if isinstance(schema, (list, tuple)):
|
|
569
|
+
return [_schema_assertion_json(element) for element in schema]
|
|
570
|
+
return schema
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def schema_without_keyword(schema: Mapping[str, Any], keyword: str) -> Any:
|
|
574
|
+
remaining_schema = {
|
|
575
|
+
schema_keyword: schema_value
|
|
576
|
+
for schema_keyword, schema_value in schema.items()
|
|
577
|
+
if schema_keyword != keyword
|
|
578
|
+
}
|
|
579
|
+
return remaining_schema or True
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _schema_length_bound(
|
|
583
|
+
schema: Any,
|
|
584
|
+
root_schema: Any,
|
|
585
|
+
keyword: str,
|
|
586
|
+
reference_stack: tuple[str, ...] = (),
|
|
587
|
+
) -> int | None:
|
|
588
|
+
if not isinstance(schema, Mapping):
|
|
589
|
+
return None
|
|
590
|
+
candidate_bounds: list[int | None] = [
|
|
591
|
+
cast(int, schema[keyword])
|
|
592
|
+
if isinstance(schema.get(keyword), int) and not isinstance(schema[keyword], bool)
|
|
593
|
+
else None
|
|
594
|
+
]
|
|
595
|
+
if (reference := schema.get("$ref")) is not None:
|
|
596
|
+
reference_text = cast(str, reference)
|
|
597
|
+
if reference_text not in reference_stack:
|
|
598
|
+
candidate_bounds.append(
|
|
599
|
+
_schema_length_bound(
|
|
600
|
+
schema_reference_target(root_schema, reference_text),
|
|
601
|
+
root_schema,
|
|
602
|
+
keyword,
|
|
603
|
+
(*reference_stack, reference_text),
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
607
|
+
candidate_bounds.extend(
|
|
608
|
+
_schema_length_bound(branch, root_schema, keyword, reference_stack) for branch in all_of
|
|
609
|
+
)
|
|
610
|
+
concrete_bounds = [bound for bound in candidate_bounds if bound is not None]
|
|
611
|
+
if not concrete_bounds:
|
|
612
|
+
return None
|
|
613
|
+
return max(concrete_bounds) if keyword == "minLength" else min(concrete_bounds)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _schema_object_count_bound(
|
|
617
|
+
schema: Any,
|
|
618
|
+
root_schema: Any,
|
|
619
|
+
keyword: str,
|
|
620
|
+
reference_stack: tuple[str, ...] = (),
|
|
621
|
+
) -> int | None:
|
|
622
|
+
if not isinstance(schema, Mapping):
|
|
623
|
+
return None
|
|
624
|
+
candidate_bounds: list[int | None] = [
|
|
625
|
+
cast(int, schema[keyword])
|
|
626
|
+
if isinstance(schema.get(keyword), int) and not isinstance(schema[keyword], bool)
|
|
627
|
+
else None
|
|
628
|
+
]
|
|
629
|
+
if keyword == "minProperties":
|
|
630
|
+
candidate_bounds.append(
|
|
631
|
+
len(
|
|
632
|
+
{
|
|
633
|
+
property_name
|
|
634
|
+
for property_name in schema.get("required", ())
|
|
635
|
+
if isinstance(property_name, str)
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
if (reference := schema.get("$ref")) is not None:
|
|
640
|
+
reference_text = cast(str, reference)
|
|
641
|
+
if reference_text not in reference_stack:
|
|
642
|
+
candidate_bounds.append(
|
|
643
|
+
_schema_object_count_bound(
|
|
644
|
+
schema_reference_target(root_schema, reference_text),
|
|
645
|
+
root_schema,
|
|
646
|
+
keyword,
|
|
647
|
+
(*reference_stack, reference_text),
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
651
|
+
candidate_bounds.extend(
|
|
652
|
+
_schema_object_count_bound(branch, root_schema, keyword, reference_stack)
|
|
653
|
+
for branch in all_of
|
|
654
|
+
)
|
|
655
|
+
concrete_bounds = [bound for bound in candidate_bounds if bound is not None]
|
|
656
|
+
if not concrete_bounds:
|
|
657
|
+
return None
|
|
658
|
+
return max(concrete_bounds) if keyword == "minProperties" else min(concrete_bounds)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _schema_string_assertions(
|
|
662
|
+
schema: Any,
|
|
663
|
+
root_schema: Any,
|
|
664
|
+
keyword: str,
|
|
665
|
+
reference_stack: tuple[str, ...] = (),
|
|
666
|
+
) -> frozenset[str]:
|
|
667
|
+
if not isinstance(schema, Mapping):
|
|
668
|
+
return frozenset()
|
|
669
|
+
assertions = {assertion for assertion in (schema.get(keyword),) if isinstance(assertion, str)}
|
|
670
|
+
if (reference := schema.get("$ref")) is not None:
|
|
671
|
+
reference_text = cast(str, reference)
|
|
672
|
+
if reference_text not in reference_stack:
|
|
673
|
+
assertions.update(
|
|
674
|
+
_schema_string_assertions(
|
|
675
|
+
schema_reference_target(root_schema, reference_text),
|
|
676
|
+
root_schema,
|
|
677
|
+
keyword,
|
|
678
|
+
(*reference_stack, reference_text),
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
682
|
+
for branch in all_of:
|
|
683
|
+
assertions.update(
|
|
684
|
+
_schema_string_assertions(branch, root_schema, keyword, reference_stack)
|
|
685
|
+
)
|
|
686
|
+
return frozenset(assertions)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _lower_boundary_contains(
|
|
690
|
+
source_boundary: _NumericBoundary | None,
|
|
691
|
+
target_boundary: _NumericBoundary | None,
|
|
692
|
+
) -> bool:
|
|
693
|
+
if target_boundary is None:
|
|
694
|
+
return True
|
|
695
|
+
if source_boundary is None or source_boundary[0] < target_boundary[0]:
|
|
696
|
+
return False
|
|
697
|
+
if source_boundary[0] > target_boundary[0]:
|
|
698
|
+
return True
|
|
699
|
+
return target_boundary[1] or not source_boundary[1]
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _upper_boundary_contains(
|
|
703
|
+
source_boundary: _NumericBoundary | None,
|
|
704
|
+
target_boundary: _NumericBoundary | None,
|
|
705
|
+
) -> bool:
|
|
706
|
+
if target_boundary is None:
|
|
707
|
+
return True
|
|
708
|
+
if source_boundary is None or source_boundary[0] > target_boundary[0]:
|
|
709
|
+
return False
|
|
710
|
+
if source_boundary[0] < target_boundary[0]:
|
|
711
|
+
return True
|
|
712
|
+
return target_boundary[1] or not source_boundary[1]
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _multiple_of_is_subset(source_multiple: Any, target_multiple: Any) -> bool:
|
|
716
|
+
if not isinstance(target_multiple, (int, float)) or isinstance(target_multiple, bool):
|
|
717
|
+
return True
|
|
718
|
+
if not isinstance(source_multiple, (int, float)) or isinstance(source_multiple, bool):
|
|
719
|
+
return False
|
|
720
|
+
source_decimal = Decimal(str(source_multiple))
|
|
721
|
+
target_decimal = Decimal(str(target_multiple))
|
|
722
|
+
return target_decimal > 0 and source_decimal > 0 and source_decimal % target_decimal == 0
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def schema_is_proven_subset(
|
|
726
|
+
source_schema: Any,
|
|
727
|
+
source_root_schema: Any,
|
|
728
|
+
target_schema: Any,
|
|
729
|
+
target_root_schema: Any,
|
|
730
|
+
) -> bool:
|
|
731
|
+
"""Conservatively prove JSON-Schema language inclusion for flow value contracts."""
|
|
732
|
+
|
|
733
|
+
if target_schema is True or source_schema is False:
|
|
734
|
+
return True
|
|
735
|
+
if target_schema is False:
|
|
736
|
+
return finite_schema_instances(source_schema, source_root_schema) == {}
|
|
737
|
+
if _schema_assertion_json(source_schema) == _schema_assertion_json(target_schema):
|
|
738
|
+
return True
|
|
739
|
+
|
|
740
|
+
source_instances = finite_schema_instances(source_schema, source_root_schema)
|
|
741
|
+
if source_instances is not None:
|
|
742
|
+
return all(
|
|
743
|
+
schema_accepts_known_instance(target_schema, target_root_schema, source_instance)
|
|
744
|
+
for source_instance in source_instances.values()
|
|
745
|
+
)
|
|
746
|
+
if not isinstance(source_schema, Mapping) or not isinstance(target_schema, Mapping):
|
|
747
|
+
return False
|
|
748
|
+
|
|
749
|
+
if (source_reference := source_schema.get("$ref")) is not None:
|
|
750
|
+
source_without_reference = schema_without_keyword(source_schema, "$ref")
|
|
751
|
+
if source_without_reference is True:
|
|
752
|
+
return schema_is_proven_subset(
|
|
753
|
+
schema_reference_target(source_root_schema, cast(str, source_reference)),
|
|
754
|
+
source_root_schema,
|
|
755
|
+
target_schema,
|
|
756
|
+
target_root_schema,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
760
|
+
source_branches = source_schema.get(union_keyword)
|
|
761
|
+
if not isinstance(source_branches, (list, tuple)):
|
|
762
|
+
continue
|
|
763
|
+
source_without_union = schema_without_keyword(source_schema, union_keyword)
|
|
764
|
+
return all(
|
|
765
|
+
schema_is_proven_subset(
|
|
766
|
+
combined_contract([source_without_union, source_branch]),
|
|
767
|
+
source_root_schema,
|
|
768
|
+
target_schema,
|
|
769
|
+
target_root_schema,
|
|
770
|
+
)
|
|
771
|
+
for source_branch in source_branches
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if (target_reference := target_schema.get("$ref")) is not None:
|
|
775
|
+
return schema_is_proven_subset(
|
|
776
|
+
source_schema,
|
|
777
|
+
source_root_schema,
|
|
778
|
+
schema_without_keyword(target_schema, "$ref"),
|
|
779
|
+
target_root_schema,
|
|
780
|
+
) and schema_is_proven_subset(
|
|
781
|
+
source_schema,
|
|
782
|
+
source_root_schema,
|
|
783
|
+
schema_reference_target(target_root_schema, cast(str, target_reference)),
|
|
784
|
+
target_root_schema,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
if isinstance((target_all_of := target_schema.get("allOf")), (list, tuple)):
|
|
788
|
+
return schema_is_proven_subset(
|
|
789
|
+
source_schema,
|
|
790
|
+
source_root_schema,
|
|
791
|
+
schema_without_keyword(target_schema, "allOf"),
|
|
792
|
+
target_root_schema,
|
|
793
|
+
) and all(
|
|
794
|
+
schema_is_proven_subset(
|
|
795
|
+
source_schema,
|
|
796
|
+
source_root_schema,
|
|
797
|
+
target_branch,
|
|
798
|
+
target_root_schema,
|
|
799
|
+
)
|
|
800
|
+
for target_branch in target_all_of
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
804
|
+
target_branches = target_schema.get(union_keyword)
|
|
805
|
+
if not isinstance(target_branches, (list, tuple)):
|
|
806
|
+
continue
|
|
807
|
+
if not schema_is_proven_subset(
|
|
808
|
+
source_schema,
|
|
809
|
+
source_root_schema,
|
|
810
|
+
schema_without_keyword(target_schema, union_keyword),
|
|
811
|
+
target_root_schema,
|
|
812
|
+
):
|
|
813
|
+
return False
|
|
814
|
+
for branch_index, target_branch in enumerate(target_branches):
|
|
815
|
+
if not schema_is_proven_subset(
|
|
816
|
+
source_schema,
|
|
817
|
+
source_root_schema,
|
|
818
|
+
target_branch,
|
|
819
|
+
target_root_schema,
|
|
820
|
+
):
|
|
821
|
+
continue
|
|
822
|
+
if union_keyword == "anyOf" or all(
|
|
823
|
+
_schemas_are_clearly_disjoint(
|
|
824
|
+
source_schema,
|
|
825
|
+
source_root_schema,
|
|
826
|
+
other_branch,
|
|
827
|
+
target_root_schema,
|
|
828
|
+
)
|
|
829
|
+
for other_index, other_branch in enumerate(target_branches)
|
|
830
|
+
if other_index != branch_index
|
|
831
|
+
):
|
|
832
|
+
return True
|
|
833
|
+
return False
|
|
834
|
+
|
|
835
|
+
if any(keyword in target_schema for keyword in ("if", "not")):
|
|
836
|
+
return False
|
|
837
|
+
if "const" in target_schema or "enum" in target_schema:
|
|
838
|
+
return False
|
|
839
|
+
|
|
840
|
+
source_atoms = schema_type_atoms(source_schema, source_root_schema)
|
|
841
|
+
target_atoms = schema_type_atoms(target_schema, target_root_schema)
|
|
842
|
+
if not source_atoms <= target_atoms:
|
|
843
|
+
return False
|
|
844
|
+
|
|
845
|
+
numeric_atoms = frozenset({"integer", "non_integer_number"})
|
|
846
|
+
if source_atoms & numeric_atoms:
|
|
847
|
+
source_interval = _schema_numeric_interval(source_schema, source_root_schema)
|
|
848
|
+
target_interval = _schema_numeric_interval(target_schema, target_root_schema)
|
|
849
|
+
if not (
|
|
850
|
+
_lower_boundary_contains(source_interval[0], target_interval[0])
|
|
851
|
+
and _upper_boundary_contains(source_interval[1], target_interval[1])
|
|
852
|
+
and _multiple_of_is_subset(
|
|
853
|
+
source_schema.get("multipleOf"), target_schema.get("multipleOf")
|
|
854
|
+
)
|
|
855
|
+
):
|
|
856
|
+
return False
|
|
857
|
+
|
|
858
|
+
if "string" in source_atoms:
|
|
859
|
+
source_minimum_length = _schema_length_bound(source_schema, source_root_schema, "minLength")
|
|
860
|
+
target_minimum_length = _schema_length_bound(target_schema, target_root_schema, "minLength")
|
|
861
|
+
if target_minimum_length is not None and (
|
|
862
|
+
source_minimum_length is None or source_minimum_length < target_minimum_length
|
|
863
|
+
):
|
|
864
|
+
return False
|
|
865
|
+
source_maximum_length = _schema_length_bound(source_schema, source_root_schema, "maxLength")
|
|
866
|
+
target_maximum_length = _schema_length_bound(target_schema, target_root_schema, "maxLength")
|
|
867
|
+
if target_maximum_length is not None and (
|
|
868
|
+
source_maximum_length is None or source_maximum_length > target_maximum_length
|
|
869
|
+
):
|
|
870
|
+
return False
|
|
871
|
+
for string_keyword in ("format", "pattern"):
|
|
872
|
+
if not _schema_string_assertions(
|
|
873
|
+
target_schema, target_root_schema, string_keyword
|
|
874
|
+
) <= _schema_string_assertions(source_schema, source_root_schema, string_keyword):
|
|
875
|
+
return False
|
|
876
|
+
|
|
877
|
+
structured_atoms = frozenset({"array", "object"})
|
|
878
|
+
if source_atoms & structured_atoms:
|
|
879
|
+
if "object" in source_atoms:
|
|
880
|
+
source_minimum_properties = _schema_object_count_bound(
|
|
881
|
+
source_schema, source_root_schema, "minProperties"
|
|
882
|
+
)
|
|
883
|
+
target_minimum_properties = _schema_object_count_bound(
|
|
884
|
+
target_schema, target_root_schema, "minProperties"
|
|
885
|
+
)
|
|
886
|
+
if target_minimum_properties is not None and (
|
|
887
|
+
source_minimum_properties is None
|
|
888
|
+
or source_minimum_properties < target_minimum_properties
|
|
889
|
+
):
|
|
890
|
+
return False
|
|
891
|
+
source_maximum_properties = _schema_object_count_bound(
|
|
892
|
+
source_schema, source_root_schema, "maxProperties"
|
|
893
|
+
)
|
|
894
|
+
target_maximum_properties = _schema_object_count_bound(
|
|
895
|
+
target_schema, target_root_schema, "maxProperties"
|
|
896
|
+
)
|
|
897
|
+
if target_maximum_properties is not None and (
|
|
898
|
+
source_maximum_properties is None
|
|
899
|
+
or source_maximum_properties > target_maximum_properties
|
|
900
|
+
):
|
|
901
|
+
return False
|
|
902
|
+
structured_assertions = set(target_schema) - {
|
|
903
|
+
*_SCHEMA_ANNOTATION_KEYWORDS,
|
|
904
|
+
"$anchor",
|
|
905
|
+
"$defs",
|
|
906
|
+
"$id",
|
|
907
|
+
"$schema",
|
|
908
|
+
"maxProperties",
|
|
909
|
+
"minProperties",
|
|
910
|
+
"type",
|
|
911
|
+
}
|
|
912
|
+
if structured_assertions:
|
|
913
|
+
return False
|
|
914
|
+
return True
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _projection_all(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
|
|
918
|
+
return (
|
|
919
|
+
all(projection[0] for projection in projections),
|
|
920
|
+
all(projection[1] for projection in projections),
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def _projection_any(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
|
|
925
|
+
return (
|
|
926
|
+
any(projection[0] for projection in projections),
|
|
927
|
+
any(projection[1] for projection in projections),
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _projection_not(projection: tuple[bool, bool]) -> tuple[bool, bool]:
|
|
932
|
+
can_match, must_match = projection
|
|
933
|
+
return not must_match, not can_match
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _projection_one_of(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
|
|
937
|
+
possible_projections = [projection for projection in projections if projection[0]]
|
|
938
|
+
guaranteed_matches = sum(projection[1] for projection in possible_projections)
|
|
939
|
+
return (
|
|
940
|
+
bool(possible_projections) and guaranteed_matches < 2,
|
|
941
|
+
guaranteed_matches == 1 and len(possible_projections) == 1,
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _unknown_value_projection(
|
|
946
|
+
schema: Any,
|
|
947
|
+
root_schema: Any,
|
|
948
|
+
reference_stack: tuple[str, ...] = (),
|
|
949
|
+
) -> tuple[bool, bool]:
|
|
950
|
+
if schema is False:
|
|
951
|
+
return False, False
|
|
952
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
953
|
+
return True, True
|
|
954
|
+
|
|
955
|
+
projections: list[tuple[bool, bool]] = []
|
|
956
|
+
value_keywords = set(schema) - {
|
|
957
|
+
"$anchor",
|
|
958
|
+
"$comment",
|
|
959
|
+
"$defs",
|
|
960
|
+
"$id",
|
|
961
|
+
"$ref",
|
|
962
|
+
"$schema",
|
|
963
|
+
"allOf",
|
|
964
|
+
"anyOf",
|
|
965
|
+
"definitions",
|
|
966
|
+
"description",
|
|
967
|
+
"else",
|
|
968
|
+
"examples",
|
|
969
|
+
"if",
|
|
970
|
+
"oneOf",
|
|
971
|
+
"then",
|
|
972
|
+
"title",
|
|
973
|
+
}
|
|
974
|
+
if value_keywords:
|
|
975
|
+
projections.append((True, False))
|
|
976
|
+
|
|
977
|
+
if (reference := schema.get("$ref")) is not None:
|
|
978
|
+
reference_text = cast(str, reference)
|
|
979
|
+
if reference_text in reference_stack:
|
|
980
|
+
projections.append((True, False))
|
|
981
|
+
else:
|
|
982
|
+
projections.append(
|
|
983
|
+
_unknown_value_projection(
|
|
984
|
+
schema_reference_target(root_schema, reference_text),
|
|
985
|
+
root_schema,
|
|
986
|
+
(*reference_stack, reference_text),
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
990
|
+
projections.append(
|
|
991
|
+
_projection_all(
|
|
992
|
+
[
|
|
993
|
+
_unknown_value_projection(branch, root_schema, reference_stack)
|
|
994
|
+
for branch in all_of
|
|
995
|
+
]
|
|
996
|
+
)
|
|
997
|
+
)
|
|
998
|
+
if isinstance((any_of := schema.get("anyOf")), (list, tuple)):
|
|
999
|
+
projections.append(
|
|
1000
|
+
_projection_any(
|
|
1001
|
+
[
|
|
1002
|
+
_unknown_value_projection(branch, root_schema, reference_stack)
|
|
1003
|
+
for branch in any_of
|
|
1004
|
+
]
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
if isinstance((one_of := schema.get("oneOf")), (list, tuple)):
|
|
1008
|
+
projections.append(
|
|
1009
|
+
_projection_one_of(
|
|
1010
|
+
[
|
|
1011
|
+
_unknown_value_projection(branch, root_schema, reference_stack)
|
|
1012
|
+
for branch in one_of
|
|
1013
|
+
]
|
|
1014
|
+
)
|
|
1015
|
+
)
|
|
1016
|
+
if "not" in schema:
|
|
1017
|
+
projections.append(
|
|
1018
|
+
_projection_not(_unknown_value_projection(schema["not"], root_schema, reference_stack))
|
|
1019
|
+
)
|
|
1020
|
+
if "if" in schema:
|
|
1021
|
+
condition_projection = _unknown_value_projection(schema["if"], root_schema, reference_stack)
|
|
1022
|
+
conditional_projection = _projection_any(
|
|
1023
|
+
[
|
|
1024
|
+
_projection_all(
|
|
1025
|
+
[
|
|
1026
|
+
condition_projection,
|
|
1027
|
+
_unknown_value_projection(
|
|
1028
|
+
schema.get("then", True), root_schema, reference_stack
|
|
1029
|
+
),
|
|
1030
|
+
]
|
|
1031
|
+
),
|
|
1032
|
+
_projection_all(
|
|
1033
|
+
[
|
|
1034
|
+
_projection_not(condition_projection),
|
|
1035
|
+
_unknown_value_projection(
|
|
1036
|
+
schema.get("else", True), root_schema, reference_stack
|
|
1037
|
+
),
|
|
1038
|
+
]
|
|
1039
|
+
),
|
|
1040
|
+
]
|
|
1041
|
+
)
|
|
1042
|
+
projections.append(conditional_projection)
|
|
1043
|
+
return _projection_all(projections) if projections else (True, True)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def key_projection_match(
|
|
1047
|
+
schema: Any,
|
|
1048
|
+
root_schema: Any,
|
|
1049
|
+
bound_keys: frozenset[str],
|
|
1050
|
+
reference_stack: tuple[str, ...] = (),
|
|
1051
|
+
) -> tuple[bool, bool]:
|
|
1052
|
+
if schema is False:
|
|
1053
|
+
return False, False
|
|
1054
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
1055
|
+
return True, True
|
|
1056
|
+
|
|
1057
|
+
projections: list[tuple[bool, bool]] = []
|
|
1058
|
+
schema_types = schema.get("type")
|
|
1059
|
+
if schema_types is not None:
|
|
1060
|
+
supports_object = schema_types == "object" or (
|
|
1061
|
+
isinstance(schema_types, (list, tuple)) and "object" in schema_types
|
|
1062
|
+
)
|
|
1063
|
+
projections.append((supports_object, supports_object))
|
|
1064
|
+
|
|
1065
|
+
if "const" in schema:
|
|
1066
|
+
constant_object = schema["const"]
|
|
1067
|
+
constant_keys_match = (
|
|
1068
|
+
isinstance(constant_object, Mapping)
|
|
1069
|
+
and frozenset(str(property_name) for property_name in constant_object) == bound_keys
|
|
1070
|
+
)
|
|
1071
|
+
projections.append((constant_keys_match, False))
|
|
1072
|
+
if isinstance((enum_members := schema.get("enum")), (list, tuple)):
|
|
1073
|
+
matching_enum_member = any(
|
|
1074
|
+
isinstance(enum_member, Mapping)
|
|
1075
|
+
and frozenset(str(property_name) for property_name in enum_member) == bound_keys
|
|
1076
|
+
for enum_member in enum_members
|
|
1077
|
+
)
|
|
1078
|
+
projections.append((matching_enum_member, False))
|
|
1079
|
+
|
|
1080
|
+
required_properties = {
|
|
1081
|
+
property_name
|
|
1082
|
+
for property_name in schema.get("required", ())
|
|
1083
|
+
if isinstance(property_name, str)
|
|
1084
|
+
}
|
|
1085
|
+
if required_properties:
|
|
1086
|
+
has_required_properties = required_properties <= bound_keys
|
|
1087
|
+
projections.append((has_required_properties, has_required_properties))
|
|
1088
|
+
if isinstance((minimum_properties := schema.get("minProperties")), int):
|
|
1089
|
+
meets_minimum = len(bound_keys) >= minimum_properties
|
|
1090
|
+
projections.append((meets_minimum, meets_minimum))
|
|
1091
|
+
if isinstance((maximum_properties := schema.get("maxProperties")), int):
|
|
1092
|
+
meets_maximum = len(bound_keys) <= maximum_properties
|
|
1093
|
+
projections.append((meets_maximum, meets_maximum))
|
|
1094
|
+
if (property_names := schema.get("propertyNames")) is not None:
|
|
1095
|
+
valid_property_names = all(
|
|
1096
|
+
schema_accepts_known_instance(property_names, root_schema, bound_key)
|
|
1097
|
+
for bound_key in bound_keys
|
|
1098
|
+
)
|
|
1099
|
+
projections.append((valid_property_names, valid_property_names))
|
|
1100
|
+
|
|
1101
|
+
properties = schema.get("properties")
|
|
1102
|
+
pattern_properties = schema.get("patternProperties")
|
|
1103
|
+
additional_properties = schema.get("additionalProperties", True)
|
|
1104
|
+
if isinstance(properties, Mapping) or isinstance(pattern_properties, Mapping):
|
|
1105
|
+
property_projections: list[tuple[bool, bool]] = []
|
|
1106
|
+
for bound_key in bound_keys:
|
|
1107
|
+
matching_property_schemas: list[Any] = []
|
|
1108
|
+
if isinstance(properties, Mapping) and bound_key in properties:
|
|
1109
|
+
matching_property_schemas.append(properties[bound_key])
|
|
1110
|
+
if isinstance(pattern_properties, Mapping):
|
|
1111
|
+
matching_property_schemas.extend(
|
|
1112
|
+
property_schema
|
|
1113
|
+
for property_pattern, property_schema in pattern_properties.items()
|
|
1114
|
+
if re.search(property_pattern, bound_key) is not None
|
|
1115
|
+
)
|
|
1116
|
+
if not matching_property_schemas:
|
|
1117
|
+
if additional_properties is False:
|
|
1118
|
+
property_projections.append((False, False))
|
|
1119
|
+
continue
|
|
1120
|
+
matching_property_schemas.append(additional_properties)
|
|
1121
|
+
property_projections.append(
|
|
1122
|
+
_projection_all(
|
|
1123
|
+
[
|
|
1124
|
+
_unknown_value_projection(property_schema, root_schema, reference_stack)
|
|
1125
|
+
for property_schema in matching_property_schemas
|
|
1126
|
+
]
|
|
1127
|
+
)
|
|
1128
|
+
)
|
|
1129
|
+
if property_projections:
|
|
1130
|
+
projections.append(_projection_all(property_projections))
|
|
1131
|
+
elif additional_properties is False and bound_keys:
|
|
1132
|
+
projections.append((False, False))
|
|
1133
|
+
|
|
1134
|
+
dependent_required = schema.get("dependentRequired")
|
|
1135
|
+
if isinstance(dependent_required, Mapping):
|
|
1136
|
+
for trigger_property, dependent_properties in dependent_required.items():
|
|
1137
|
+
if trigger_property not in bound_keys:
|
|
1138
|
+
continue
|
|
1139
|
+
dependencies_present = all(
|
|
1140
|
+
dependent_property in bound_keys for dependent_property in dependent_properties
|
|
1141
|
+
)
|
|
1142
|
+
projections.append((dependencies_present, dependencies_present))
|
|
1143
|
+
dependent_schemas = schema.get("dependentSchemas")
|
|
1144
|
+
if isinstance(dependent_schemas, Mapping):
|
|
1145
|
+
projections.extend(
|
|
1146
|
+
key_projection_match(
|
|
1147
|
+
dependent_schema,
|
|
1148
|
+
root_schema,
|
|
1149
|
+
bound_keys,
|
|
1150
|
+
reference_stack,
|
|
1151
|
+
)
|
|
1152
|
+
for trigger_property, dependent_schema in dependent_schemas.items()
|
|
1153
|
+
if trigger_property in bound_keys
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1157
|
+
reference_text = cast(str, reference)
|
|
1158
|
+
if reference_text in reference_stack:
|
|
1159
|
+
projections.append((True, False))
|
|
1160
|
+
else:
|
|
1161
|
+
projections.append(
|
|
1162
|
+
key_projection_match(
|
|
1163
|
+
schema_reference_target(root_schema, reference_text),
|
|
1164
|
+
root_schema,
|
|
1165
|
+
bound_keys,
|
|
1166
|
+
(*reference_stack, reference_text),
|
|
1167
|
+
)
|
|
1168
|
+
)
|
|
1169
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
1170
|
+
projections.append(
|
|
1171
|
+
_projection_all(
|
|
1172
|
+
[
|
|
1173
|
+
key_projection_match(branch, root_schema, bound_keys, reference_stack)
|
|
1174
|
+
for branch in all_of
|
|
1175
|
+
]
|
|
1176
|
+
)
|
|
1177
|
+
)
|
|
1178
|
+
if isinstance((any_of := schema.get("anyOf")), (list, tuple)):
|
|
1179
|
+
projections.append(
|
|
1180
|
+
_projection_any(
|
|
1181
|
+
[
|
|
1182
|
+
key_projection_match(branch, root_schema, bound_keys, reference_stack)
|
|
1183
|
+
for branch in any_of
|
|
1184
|
+
]
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
if isinstance((one_of := schema.get("oneOf")), (list, tuple)):
|
|
1188
|
+
projections.append(
|
|
1189
|
+
_projection_one_of(
|
|
1190
|
+
[
|
|
1191
|
+
key_projection_match(branch, root_schema, bound_keys, reference_stack)
|
|
1192
|
+
for branch in one_of
|
|
1193
|
+
]
|
|
1194
|
+
)
|
|
1195
|
+
)
|
|
1196
|
+
if "not" in schema:
|
|
1197
|
+
projections.append(
|
|
1198
|
+
_projection_not(
|
|
1199
|
+
key_projection_match(schema["not"], root_schema, bound_keys, reference_stack)
|
|
1200
|
+
)
|
|
1201
|
+
)
|
|
1202
|
+
if "if" in schema:
|
|
1203
|
+
condition_projection = key_projection_match(
|
|
1204
|
+
schema["if"], root_schema, bound_keys, reference_stack
|
|
1205
|
+
)
|
|
1206
|
+
projections.append(
|
|
1207
|
+
_projection_any(
|
|
1208
|
+
[
|
|
1209
|
+
_projection_all(
|
|
1210
|
+
[
|
|
1211
|
+
condition_projection,
|
|
1212
|
+
key_projection_match(
|
|
1213
|
+
schema.get("then", True),
|
|
1214
|
+
root_schema,
|
|
1215
|
+
bound_keys,
|
|
1216
|
+
reference_stack,
|
|
1217
|
+
),
|
|
1218
|
+
]
|
|
1219
|
+
),
|
|
1220
|
+
_projection_all(
|
|
1221
|
+
[
|
|
1222
|
+
_projection_not(condition_projection),
|
|
1223
|
+
key_projection_match(
|
|
1224
|
+
schema.get("else", True),
|
|
1225
|
+
root_schema,
|
|
1226
|
+
bound_keys,
|
|
1227
|
+
reference_stack,
|
|
1228
|
+
),
|
|
1229
|
+
]
|
|
1230
|
+
),
|
|
1231
|
+
]
|
|
1232
|
+
)
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
return _projection_all(projections) if projections else (True, True)
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def schema_property_status(
|
|
1239
|
+
schema: Any,
|
|
1240
|
+
property_name: str,
|
|
1241
|
+
root_schema: Any | None = None,
|
|
1242
|
+
reference_stack: tuple[str, ...] = (),
|
|
1243
|
+
) -> int:
|
|
1244
|
+
resolved_root_schema = schema if root_schema is None else root_schema
|
|
1245
|
+
if schema is False:
|
|
1246
|
+
return CONTRACT_INVALID
|
|
1247
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
1248
|
+
return CONTRACT_UNKNOWN
|
|
1249
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1250
|
+
reference_text = cast(str, reference)
|
|
1251
|
+
if reference_text in reference_stack:
|
|
1252
|
+
return CONTRACT_UNKNOWN
|
|
1253
|
+
return schema_property_status(
|
|
1254
|
+
schema_reference_target(resolved_root_schema, reference_text),
|
|
1255
|
+
property_name,
|
|
1256
|
+
resolved_root_schema,
|
|
1257
|
+
(*reference_stack, reference_text),
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
all_of = schema.get("allOf")
|
|
1261
|
+
if isinstance(all_of, (list, tuple)):
|
|
1262
|
+
statuses = [
|
|
1263
|
+
schema_property_status(branch, property_name, resolved_root_schema, reference_stack)
|
|
1264
|
+
for branch in all_of
|
|
1265
|
+
]
|
|
1266
|
+
if CONTRACT_INVALID in statuses:
|
|
1267
|
+
return CONTRACT_INVALID
|
|
1268
|
+
if CONTRACT_VALID in statuses:
|
|
1269
|
+
return CONTRACT_VALID
|
|
1270
|
+
|
|
1271
|
+
for union_keyword in ("oneOf", "anyOf"):
|
|
1272
|
+
branches = schema.get(union_keyword)
|
|
1273
|
+
if not isinstance(branches, (list, tuple)):
|
|
1274
|
+
continue
|
|
1275
|
+
statuses = [
|
|
1276
|
+
schema_property_status(branch, property_name, resolved_root_schema, reference_stack)
|
|
1277
|
+
for branch in branches
|
|
1278
|
+
]
|
|
1279
|
+
if CONTRACT_VALID in statuses:
|
|
1280
|
+
return CONTRACT_VALID
|
|
1281
|
+
if statuses and all(status == CONTRACT_INVALID for status in statuses):
|
|
1282
|
+
return CONTRACT_INVALID
|
|
1283
|
+
return CONTRACT_UNKNOWN
|
|
1284
|
+
|
|
1285
|
+
schema_types = schema.get("type")
|
|
1286
|
+
if isinstance(schema_types, str) and schema_types != "object":
|
|
1287
|
+
return CONTRACT_INVALID
|
|
1288
|
+
if isinstance(schema_types, (list, tuple)) and "object" not in schema_types:
|
|
1289
|
+
return CONTRACT_INVALID
|
|
1290
|
+
|
|
1291
|
+
properties = schema.get("properties")
|
|
1292
|
+
if isinstance(properties, Mapping) and property_name in properties:
|
|
1293
|
+
return CONTRACT_VALID
|
|
1294
|
+
pattern_properties = schema.get("patternProperties")
|
|
1295
|
+
if isinstance(pattern_properties, Mapping) and any(
|
|
1296
|
+
re.search(pattern, property_name) is not None for pattern in pattern_properties
|
|
1297
|
+
):
|
|
1298
|
+
return CONTRACT_VALID
|
|
1299
|
+
additional_properties = schema.get("additionalProperties", True)
|
|
1300
|
+
if additional_properties is False:
|
|
1301
|
+
return CONTRACT_INVALID
|
|
1302
|
+
return CONTRACT_VALID if isinstance(additional_properties, Mapping) else CONTRACT_UNKNOWN
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def required_schema_properties(
|
|
1306
|
+
schema: Any,
|
|
1307
|
+
root_schema: Any | None = None,
|
|
1308
|
+
reference_stack: tuple[str, ...] = (),
|
|
1309
|
+
) -> set[str]:
|
|
1310
|
+
resolved_root_schema = schema if root_schema is None else root_schema
|
|
1311
|
+
if not isinstance(schema, Mapping):
|
|
1312
|
+
return set()
|
|
1313
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1314
|
+
reference_text = cast(str, reference)
|
|
1315
|
+
if reference_text in reference_stack:
|
|
1316
|
+
return set()
|
|
1317
|
+
return required_schema_properties(
|
|
1318
|
+
schema_reference_target(resolved_root_schema, reference_text),
|
|
1319
|
+
resolved_root_schema,
|
|
1320
|
+
(*reference_stack, reference_text),
|
|
1321
|
+
)
|
|
1322
|
+
required_properties = {
|
|
1323
|
+
property_name
|
|
1324
|
+
for property_name in schema.get("required", ())
|
|
1325
|
+
if isinstance(property_name, str)
|
|
1326
|
+
}
|
|
1327
|
+
all_of = schema.get("allOf")
|
|
1328
|
+
if isinstance(all_of, (list, tuple)):
|
|
1329
|
+
for branch in all_of:
|
|
1330
|
+
required_properties.update(
|
|
1331
|
+
required_schema_properties(branch, resolved_root_schema, reference_stack)
|
|
1332
|
+
)
|
|
1333
|
+
for union_keyword in ("oneOf", "anyOf"):
|
|
1334
|
+
branches = schema.get(union_keyword)
|
|
1335
|
+
if not isinstance(branches, (list, tuple)) or not branches:
|
|
1336
|
+
continue
|
|
1337
|
+
branch_requirements = [
|
|
1338
|
+
required_schema_properties(branch, resolved_root_schema, reference_stack)
|
|
1339
|
+
for branch in branches
|
|
1340
|
+
]
|
|
1341
|
+
required_properties.update(set.intersection(*branch_requirements))
|
|
1342
|
+
return required_properties
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def schema_path_status(
|
|
1346
|
+
schema: Any,
|
|
1347
|
+
path_segments: tuple[str, ...],
|
|
1348
|
+
root_schema: Any | None = None,
|
|
1349
|
+
reference_stack: tuple[str, ...] = (),
|
|
1350
|
+
) -> int:
|
|
1351
|
+
resolved_root_schema = schema if root_schema is None else root_schema
|
|
1352
|
+
if schema is False:
|
|
1353
|
+
return CONTRACT_INVALID
|
|
1354
|
+
if not path_segments:
|
|
1355
|
+
return CONTRACT_VALID
|
|
1356
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
1357
|
+
return CONTRACT_UNKNOWN
|
|
1358
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1359
|
+
reference_text = cast(str, reference)
|
|
1360
|
+
if reference_text in reference_stack:
|
|
1361
|
+
return CONTRACT_UNKNOWN
|
|
1362
|
+
return schema_path_status(
|
|
1363
|
+
schema_reference_target(resolved_root_schema, reference_text),
|
|
1364
|
+
path_segments,
|
|
1365
|
+
resolved_root_schema,
|
|
1366
|
+
(*reference_stack, reference_text),
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
all_of = schema.get("allOf")
|
|
1370
|
+
if isinstance(all_of, (list, tuple)):
|
|
1371
|
+
statuses = [
|
|
1372
|
+
schema_path_status(branch, path_segments, resolved_root_schema, reference_stack)
|
|
1373
|
+
for branch in all_of
|
|
1374
|
+
]
|
|
1375
|
+
if CONTRACT_INVALID in statuses:
|
|
1376
|
+
return CONTRACT_INVALID
|
|
1377
|
+
if CONTRACT_VALID in statuses:
|
|
1378
|
+
return CONTRACT_VALID
|
|
1379
|
+
|
|
1380
|
+
for union_keyword in ("oneOf", "anyOf"):
|
|
1381
|
+
branches = schema.get(union_keyword)
|
|
1382
|
+
if not isinstance(branches, (list, tuple)):
|
|
1383
|
+
continue
|
|
1384
|
+
statuses = [
|
|
1385
|
+
schema_path_status(branch, path_segments, resolved_root_schema, reference_stack)
|
|
1386
|
+
for branch in branches
|
|
1387
|
+
]
|
|
1388
|
+
if CONTRACT_VALID in statuses:
|
|
1389
|
+
return CONTRACT_VALID
|
|
1390
|
+
if statuses and all(status == CONTRACT_INVALID for status in statuses):
|
|
1391
|
+
return CONTRACT_INVALID
|
|
1392
|
+
return CONTRACT_UNKNOWN
|
|
1393
|
+
|
|
1394
|
+
path_segment, remaining_segments = path_segments[0], path_segments[1:]
|
|
1395
|
+
schema_types = schema.get("type")
|
|
1396
|
+
supports_array = schema_types == "array" or (
|
|
1397
|
+
isinstance(schema_types, (list, tuple)) and "array" in schema_types
|
|
1398
|
+
)
|
|
1399
|
+
supports_object = (
|
|
1400
|
+
schema_types in (None, "object")
|
|
1401
|
+
or isinstance(schema.get("properties"), Mapping)
|
|
1402
|
+
or (isinstance(schema_types, (list, tuple)) and "object" in schema_types)
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
if supports_array and path_segment.isdigit():
|
|
1406
|
+
items = schema.get("items", True)
|
|
1407
|
+
return schema_path_status(items, remaining_segments, resolved_root_schema, reference_stack)
|
|
1408
|
+
if supports_object:
|
|
1409
|
+
properties = schema.get("properties")
|
|
1410
|
+
if isinstance(properties, Mapping) and path_segment in properties:
|
|
1411
|
+
return schema_path_status(
|
|
1412
|
+
properties[path_segment],
|
|
1413
|
+
remaining_segments,
|
|
1414
|
+
resolved_root_schema,
|
|
1415
|
+
reference_stack,
|
|
1416
|
+
)
|
|
1417
|
+
pattern_properties = schema.get("patternProperties")
|
|
1418
|
+
if isinstance(pattern_properties, Mapping):
|
|
1419
|
+
matching_schemas = [
|
|
1420
|
+
nested_schema
|
|
1421
|
+
for pattern, nested_schema in pattern_properties.items()
|
|
1422
|
+
if re.search(pattern, path_segment) is not None
|
|
1423
|
+
]
|
|
1424
|
+
if matching_schemas:
|
|
1425
|
+
statuses = [
|
|
1426
|
+
schema_path_status(
|
|
1427
|
+
nested_schema,
|
|
1428
|
+
remaining_segments,
|
|
1429
|
+
resolved_root_schema,
|
|
1430
|
+
reference_stack,
|
|
1431
|
+
)
|
|
1432
|
+
for nested_schema in matching_schemas
|
|
1433
|
+
]
|
|
1434
|
+
if CONTRACT_INVALID in statuses:
|
|
1435
|
+
return CONTRACT_INVALID
|
|
1436
|
+
if all(status == CONTRACT_VALID for status in statuses):
|
|
1437
|
+
return CONTRACT_VALID
|
|
1438
|
+
return CONTRACT_UNKNOWN
|
|
1439
|
+
additional_properties = schema.get("additionalProperties", True)
|
|
1440
|
+
if additional_properties is False:
|
|
1441
|
+
return CONTRACT_INVALID
|
|
1442
|
+
if isinstance(additional_properties, Mapping):
|
|
1443
|
+
return schema_path_status(
|
|
1444
|
+
additional_properties,
|
|
1445
|
+
remaining_segments,
|
|
1446
|
+
resolved_root_schema,
|
|
1447
|
+
reference_stack,
|
|
1448
|
+
)
|
|
1449
|
+
return CONTRACT_UNKNOWN
|
|
1450
|
+
return CONTRACT_INVALID
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def _combine_schema_alternatives(
|
|
1454
|
+
left_alternatives: list[list[Any]],
|
|
1455
|
+
right_alternatives: list[list[Any]],
|
|
1456
|
+
) -> list[list[Any]]:
|
|
1457
|
+
return [
|
|
1458
|
+
[*left_alternative, *right_alternative]
|
|
1459
|
+
for left_alternative in left_alternatives
|
|
1460
|
+
for right_alternative in right_alternatives
|
|
1461
|
+
]
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def schema_conjunctive_alternatives(
|
|
1465
|
+
schema: Any,
|
|
1466
|
+
root_schema: Any,
|
|
1467
|
+
reference_stack: tuple[str, ...] = (),
|
|
1468
|
+
) -> list[list[Any]]:
|
|
1469
|
+
if schema is False:
|
|
1470
|
+
return []
|
|
1471
|
+
if schema is True or not isinstance(schema, Mapping):
|
|
1472
|
+
return [[]]
|
|
1473
|
+
|
|
1474
|
+
applicator_keywords = {
|
|
1475
|
+
"$ref",
|
|
1476
|
+
"allOf",
|
|
1477
|
+
"anyOf",
|
|
1478
|
+
"else",
|
|
1479
|
+
"if",
|
|
1480
|
+
"oneOf",
|
|
1481
|
+
"then",
|
|
1482
|
+
}
|
|
1483
|
+
direct_schema = {
|
|
1484
|
+
property_name: property_contract
|
|
1485
|
+
for property_name, property_contract in schema.items()
|
|
1486
|
+
if property_name not in applicator_keywords
|
|
1487
|
+
}
|
|
1488
|
+
alternatives: list[list[Any]] = [[direct_schema]] if direct_schema else [[]]
|
|
1489
|
+
|
|
1490
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1491
|
+
reference_text = cast(str, reference)
|
|
1492
|
+
if reference_text not in reference_stack:
|
|
1493
|
+
alternatives = _combine_schema_alternatives(
|
|
1494
|
+
alternatives,
|
|
1495
|
+
schema_conjunctive_alternatives(
|
|
1496
|
+
schema_reference_target(root_schema, reference_text),
|
|
1497
|
+
root_schema,
|
|
1498
|
+
(*reference_stack, reference_text),
|
|
1499
|
+
),
|
|
1500
|
+
)
|
|
1501
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)):
|
|
1502
|
+
for branch in all_of:
|
|
1503
|
+
alternatives = _combine_schema_alternatives(
|
|
1504
|
+
alternatives,
|
|
1505
|
+
schema_conjunctive_alternatives(branch, root_schema, reference_stack),
|
|
1506
|
+
)
|
|
1507
|
+
for union_keyword in ("anyOf", "oneOf"):
|
|
1508
|
+
branches = schema.get(union_keyword)
|
|
1509
|
+
if not isinstance(branches, (list, tuple)):
|
|
1510
|
+
continue
|
|
1511
|
+
union_alternatives = [
|
|
1512
|
+
branch_alternative
|
|
1513
|
+
for branch in branches
|
|
1514
|
+
for branch_alternative in schema_conjunctive_alternatives(
|
|
1515
|
+
branch, root_schema, reference_stack
|
|
1516
|
+
)
|
|
1517
|
+
]
|
|
1518
|
+
alternatives = _combine_schema_alternatives(alternatives, union_alternatives)
|
|
1519
|
+
if "if" in schema:
|
|
1520
|
+
condition_schema = schema["if"]
|
|
1521
|
+
conditional_alternatives = [
|
|
1522
|
+
*(
|
|
1523
|
+
_combine_schema_alternatives(
|
|
1524
|
+
schema_conjunctive_alternatives(condition_schema, root_schema, reference_stack),
|
|
1525
|
+
schema_conjunctive_alternatives(
|
|
1526
|
+
schema.get("then", True), root_schema, reference_stack
|
|
1527
|
+
),
|
|
1528
|
+
)
|
|
1529
|
+
),
|
|
1530
|
+
*(
|
|
1531
|
+
_combine_schema_alternatives(
|
|
1532
|
+
[[{"not": condition_schema}]],
|
|
1533
|
+
schema_conjunctive_alternatives(
|
|
1534
|
+
schema.get("else", True), root_schema, reference_stack
|
|
1535
|
+
),
|
|
1536
|
+
)
|
|
1537
|
+
),
|
|
1538
|
+
]
|
|
1539
|
+
alternatives = _combine_schema_alternatives(alternatives, conditional_alternatives)
|
|
1540
|
+
return alternatives
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
def _alternative_required_properties(
|
|
1544
|
+
schema_fragments: list[Any],
|
|
1545
|
+
root_schema: Any,
|
|
1546
|
+
) -> frozenset[str]:
|
|
1547
|
+
required_properties = {
|
|
1548
|
+
property_name
|
|
1549
|
+
for schema_fragment in schema_fragments
|
|
1550
|
+
if isinstance(schema_fragment, Mapping)
|
|
1551
|
+
for property_name in schema_fragment.get("required", ())
|
|
1552
|
+
if isinstance(property_name, str)
|
|
1553
|
+
}
|
|
1554
|
+
requirements_changed = True
|
|
1555
|
+
while requirements_changed:
|
|
1556
|
+
requirements_changed = False
|
|
1557
|
+
for schema_fragment in schema_fragments:
|
|
1558
|
+
if not isinstance(schema_fragment, Mapping):
|
|
1559
|
+
continue
|
|
1560
|
+
dependent_required = schema_fragment.get("dependentRequired")
|
|
1561
|
+
if isinstance(dependent_required, Mapping):
|
|
1562
|
+
for trigger_property, dependent_properties in dependent_required.items():
|
|
1563
|
+
if trigger_property not in required_properties:
|
|
1564
|
+
continue
|
|
1565
|
+
previous_size = len(required_properties)
|
|
1566
|
+
required_properties.update(
|
|
1567
|
+
property_name
|
|
1568
|
+
for property_name in dependent_properties
|
|
1569
|
+
if isinstance(property_name, str)
|
|
1570
|
+
)
|
|
1571
|
+
requirements_changed = (
|
|
1572
|
+
requirements_changed or len(required_properties) > previous_size
|
|
1573
|
+
)
|
|
1574
|
+
dependent_schemas = schema_fragment.get("dependentSchemas")
|
|
1575
|
+
if not isinstance(dependent_schemas, Mapping):
|
|
1576
|
+
continue
|
|
1577
|
+
for trigger_property, dependent_schema in dependent_schemas.items():
|
|
1578
|
+
if trigger_property not in required_properties:
|
|
1579
|
+
continue
|
|
1580
|
+
previous_size = len(required_properties)
|
|
1581
|
+
required_properties.update(
|
|
1582
|
+
required_schema_properties(dependent_schema, root_schema)
|
|
1583
|
+
)
|
|
1584
|
+
requirements_changed = (
|
|
1585
|
+
requirements_changed or len(required_properties) > previous_size
|
|
1586
|
+
)
|
|
1587
|
+
return frozenset(required_properties)
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def _alternative_property_contract(
|
|
1591
|
+
schema_fragments: list[Any],
|
|
1592
|
+
property_name: str,
|
|
1593
|
+
root_schema: Any,
|
|
1594
|
+
required_properties: frozenset[str],
|
|
1595
|
+
) -> Any:
|
|
1596
|
+
return combined_contract(
|
|
1597
|
+
[
|
|
1598
|
+
schema_property_contract(
|
|
1599
|
+
schema_fragment,
|
|
1600
|
+
property_name,
|
|
1601
|
+
root_schema,
|
|
1602
|
+
required_properties,
|
|
1603
|
+
)
|
|
1604
|
+
for schema_fragment in schema_fragments
|
|
1605
|
+
]
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def _schema_is_unconditional(
|
|
1610
|
+
schema: Any,
|
|
1611
|
+
root_schema: Any,
|
|
1612
|
+
reference_stack: tuple[str, ...] = (),
|
|
1613
|
+
) -> bool:
|
|
1614
|
+
if schema is True:
|
|
1615
|
+
return True
|
|
1616
|
+
if schema is False or not isinstance(schema, Mapping):
|
|
1617
|
+
return False
|
|
1618
|
+
assertion_keywords = set(schema) - {
|
|
1619
|
+
"$anchor",
|
|
1620
|
+
"$comment",
|
|
1621
|
+
"$defs",
|
|
1622
|
+
"$id",
|
|
1623
|
+
"$schema",
|
|
1624
|
+
"description",
|
|
1625
|
+
"examples",
|
|
1626
|
+
"title",
|
|
1627
|
+
}
|
|
1628
|
+
if not assertion_keywords:
|
|
1629
|
+
return True
|
|
1630
|
+
if assertion_keywords == {"$ref"}:
|
|
1631
|
+
reference_text = cast(str, schema["$ref"])
|
|
1632
|
+
return reference_text in reference_stack or _schema_is_unconditional(
|
|
1633
|
+
schema_reference_target(root_schema, reference_text),
|
|
1634
|
+
root_schema,
|
|
1635
|
+
(*reference_stack, reference_text),
|
|
1636
|
+
)
|
|
1637
|
+
if assertion_keywords == {"allOf"} and isinstance(
|
|
1638
|
+
(all_of := schema.get("allOf")), (list, tuple)
|
|
1639
|
+
):
|
|
1640
|
+
return all(
|
|
1641
|
+
_schema_is_unconditional(branch, root_schema, reference_stack) for branch in all_of
|
|
1642
|
+
)
|
|
1643
|
+
if assertion_keywords == {"anyOf"} and isinstance(
|
|
1644
|
+
(any_of := schema.get("anyOf")), (list, tuple)
|
|
1645
|
+
):
|
|
1646
|
+
return any(
|
|
1647
|
+
_schema_is_unconditional(branch, root_schema, reference_stack) for branch in any_of
|
|
1648
|
+
)
|
|
1649
|
+
return False
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
def _schema_is_guaranteed_for_known_object(
|
|
1653
|
+
schema: Any,
|
|
1654
|
+
root_schema: Any,
|
|
1655
|
+
known_properties: Mapping[str, Any],
|
|
1656
|
+
required_properties: frozenset[str],
|
|
1657
|
+
reference_stack: tuple[str, ...] = (),
|
|
1658
|
+
) -> bool:
|
|
1659
|
+
if schema is True:
|
|
1660
|
+
return True
|
|
1661
|
+
if schema is False or not isinstance(schema, Mapping):
|
|
1662
|
+
return False
|
|
1663
|
+
|
|
1664
|
+
schema_types = schema.get("type")
|
|
1665
|
+
if schema_types is not None and not (
|
|
1666
|
+
schema_types == "object"
|
|
1667
|
+
or isinstance(schema_types, (list, tuple))
|
|
1668
|
+
and "object" in schema_types
|
|
1669
|
+
):
|
|
1670
|
+
return False
|
|
1671
|
+
if "const" in schema or "enum" in schema:
|
|
1672
|
+
return False
|
|
1673
|
+
declared_required = {
|
|
1674
|
+
property_name
|
|
1675
|
+
for property_name in schema.get("required", ())
|
|
1676
|
+
if isinstance(property_name, str)
|
|
1677
|
+
}
|
|
1678
|
+
if not declared_required <= required_properties:
|
|
1679
|
+
return False
|
|
1680
|
+
if isinstance((minimum_properties := schema.get("minProperties")), int) and (
|
|
1681
|
+
len(required_properties) < minimum_properties
|
|
1682
|
+
):
|
|
1683
|
+
return False
|
|
1684
|
+
if "maxProperties" in schema:
|
|
1685
|
+
return False
|
|
1686
|
+
if schema.get("additionalProperties") is False:
|
|
1687
|
+
return False
|
|
1688
|
+
if "propertyNames" in schema:
|
|
1689
|
+
return False
|
|
1690
|
+
if any(
|
|
1691
|
+
schema.get(dependency_keyword)
|
|
1692
|
+
for dependency_keyword in ("dependentRequired", "dependentSchemas")
|
|
1693
|
+
):
|
|
1694
|
+
return False
|
|
1695
|
+
|
|
1696
|
+
properties = schema.get("properties")
|
|
1697
|
+
if isinstance(properties, Mapping):
|
|
1698
|
+
for property_name, property_contract in properties.items():
|
|
1699
|
+
if property_name in known_properties:
|
|
1700
|
+
if not schema_accepts_known_instance(
|
|
1701
|
+
property_contract,
|
|
1702
|
+
root_schema,
|
|
1703
|
+
known_properties[property_name],
|
|
1704
|
+
):
|
|
1705
|
+
return False
|
|
1706
|
+
elif not _schema_is_unconditional(property_contract, root_schema):
|
|
1707
|
+
return False
|
|
1708
|
+
pattern_properties = schema.get("patternProperties")
|
|
1709
|
+
if isinstance(pattern_properties, Mapping) and any(
|
|
1710
|
+
not _schema_is_unconditional(property_contract, root_schema)
|
|
1711
|
+
for property_contract in pattern_properties.values()
|
|
1712
|
+
):
|
|
1713
|
+
return False
|
|
1714
|
+
|
|
1715
|
+
if (reference := schema.get("$ref")) is not None:
|
|
1716
|
+
reference_text = cast(str, reference)
|
|
1717
|
+
if reference_text not in reference_stack and not _schema_is_guaranteed_for_known_object(
|
|
1718
|
+
schema_reference_target(root_schema, reference_text),
|
|
1719
|
+
root_schema,
|
|
1720
|
+
known_properties,
|
|
1721
|
+
required_properties,
|
|
1722
|
+
(*reference_stack, reference_text),
|
|
1723
|
+
):
|
|
1724
|
+
return False
|
|
1725
|
+
if isinstance((all_of := schema.get("allOf")), (list, tuple)) and not all(
|
|
1726
|
+
_schema_is_guaranteed_for_known_object(
|
|
1727
|
+
branch,
|
|
1728
|
+
root_schema,
|
|
1729
|
+
known_properties,
|
|
1730
|
+
required_properties,
|
|
1731
|
+
reference_stack,
|
|
1732
|
+
)
|
|
1733
|
+
for branch in all_of
|
|
1734
|
+
):
|
|
1735
|
+
return False
|
|
1736
|
+
if isinstance((any_of := schema.get("anyOf")), (list, tuple)) and not any(
|
|
1737
|
+
_schema_is_guaranteed_for_known_object(
|
|
1738
|
+
branch,
|
|
1739
|
+
root_schema,
|
|
1740
|
+
known_properties,
|
|
1741
|
+
required_properties,
|
|
1742
|
+
reference_stack,
|
|
1743
|
+
)
|
|
1744
|
+
for branch in any_of
|
|
1745
|
+
):
|
|
1746
|
+
return False
|
|
1747
|
+
if any(composition_keyword in schema for composition_keyword in ("oneOf", "not", "if")):
|
|
1748
|
+
return False
|
|
1749
|
+
return True
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def alternative_is_success_compatible(
|
|
1753
|
+
schema_fragments: list[Any],
|
|
1754
|
+
root_schema: Any,
|
|
1755
|
+
) -> bool:
|
|
1756
|
+
required_properties = _alternative_required_properties(schema_fragments, root_schema)
|
|
1757
|
+
if "status" not in required_properties:
|
|
1758
|
+
return True
|
|
1759
|
+
status_contract = _alternative_property_contract(
|
|
1760
|
+
schema_fragments,
|
|
1761
|
+
"status",
|
|
1762
|
+
root_schema,
|
|
1763
|
+
required_properties,
|
|
1764
|
+
)
|
|
1765
|
+
if not schema_accepts_known_instance(status_contract, root_schema, "success"):
|
|
1766
|
+
return False
|
|
1767
|
+
known_properties = {"status": "success"}
|
|
1768
|
+
return not any(
|
|
1769
|
+
isinstance(schema_fragment, Mapping)
|
|
1770
|
+
and (negated_schema := schema_fragment.get("not")) is not None
|
|
1771
|
+
and _schema_is_guaranteed_for_known_object(
|
|
1772
|
+
negated_schema,
|
|
1773
|
+
root_schema,
|
|
1774
|
+
known_properties,
|
|
1775
|
+
required_properties,
|
|
1776
|
+
)
|
|
1777
|
+
for schema_fragment in schema_fragments
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _alternative_requires_path(
|
|
1782
|
+
schema_fragments: list[Any],
|
|
1783
|
+
path_segments: tuple[str, ...],
|
|
1784
|
+
root_schema: Any,
|
|
1785
|
+
) -> bool:
|
|
1786
|
+
required_properties = _alternative_required_properties(schema_fragments, root_schema)
|
|
1787
|
+
path_segment, remaining_segments = path_segments[0], path_segments[1:]
|
|
1788
|
+
if path_segment not in required_properties:
|
|
1789
|
+
return False
|
|
1790
|
+
if not remaining_segments:
|
|
1791
|
+
return True
|
|
1792
|
+
nested_contract = _alternative_property_contract(
|
|
1793
|
+
schema_fragments,
|
|
1794
|
+
path_segment,
|
|
1795
|
+
root_schema,
|
|
1796
|
+
required_properties,
|
|
1797
|
+
)
|
|
1798
|
+
return schema_requires_path_in_every_alternative(
|
|
1799
|
+
nested_contract,
|
|
1800
|
+
remaining_segments,
|
|
1801
|
+
root_schema,
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def schema_requires_path_in_every_alternative(
|
|
1806
|
+
schema: Any,
|
|
1807
|
+
path_segments: tuple[str, ...],
|
|
1808
|
+
root_schema: Any,
|
|
1809
|
+
) -> bool:
|
|
1810
|
+
alternatives = schema_conjunctive_alternatives(schema, root_schema)
|
|
1811
|
+
return bool(alternatives) and all(
|
|
1812
|
+
_alternative_requires_path(
|
|
1813
|
+
schema_fragments,
|
|
1814
|
+
path_segments,
|
|
1815
|
+
root_schema,
|
|
1816
|
+
)
|
|
1817
|
+
for schema_fragments in alternatives
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
def schema_requires_path_for_every_success(
|
|
1822
|
+
schema: Any,
|
|
1823
|
+
path_segments: tuple[str, ...],
|
|
1824
|
+
) -> bool:
|
|
1825
|
+
alternatives = schema_conjunctive_alternatives(schema, schema)
|
|
1826
|
+
return all(
|
|
1827
|
+
not alternative_is_success_compatible(schema_fragments, schema)
|
|
1828
|
+
or _alternative_requires_path(schema_fragments, path_segments, schema)
|
|
1829
|
+
for schema_fragments in alternatives
|
|
1830
|
+
)
|