json-schema-utils 0.8__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.
- json_schema_utils-0.8.dist-info/METADATA +119 -0
- json_schema_utils-0.8.dist-info/RECORD +15 -0
- json_schema_utils-0.8.dist-info/WHEEL +5 -0
- json_schema_utils-0.8.dist-info/entry_points.txt +7 -0
- json_schema_utils-0.8.dist-info/licenses/LICENSE +1 -0
- json_schema_utils-0.8.dist-info/top_level.txt +1 -0
- jsutils/__init__.py +5 -0
- jsutils/convert.py +934 -0
- jsutils/inline.py +206 -0
- jsutils/recurse.py +90 -0
- jsutils/schemas.py +151 -0
- jsutils/scripts.py +396 -0
- jsutils/simplify.py +580 -0
- jsutils/stats.py +1310 -0
- jsutils/utils.py +44 -0
jsutils/convert.py
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import re
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
from .utils import only
|
|
6
|
+
from urllib.parse import quote, unquote
|
|
7
|
+
|
|
8
|
+
type JsonPath = list[str|int]
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("convert")
|
|
11
|
+
# log.setLevel(logging.DEBUG)
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# BEST EFFORT SCHEMA TO MODEL CONVERSION
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
def toconst(val):
|
|
18
|
+
match val:
|
|
19
|
+
case None:
|
|
20
|
+
return None # or "=null"
|
|
21
|
+
case bool():
|
|
22
|
+
return "=true" if val else "=false"
|
|
23
|
+
case int()|float():
|
|
24
|
+
return "=" + str(val)
|
|
25
|
+
case str():
|
|
26
|
+
return "_" + val
|
|
27
|
+
case list():
|
|
28
|
+
# constant list…
|
|
29
|
+
return {
|
|
30
|
+
"@": [ toconst(v) for v in val ],
|
|
31
|
+
"=": len(val)
|
|
32
|
+
}
|
|
33
|
+
case dict():
|
|
34
|
+
return { f"_{p}": toconst(v) for p, v in val.items() }
|
|
35
|
+
case _:
|
|
36
|
+
raise Exception(f"unexpected value for a constant: {val}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def esc(s):
|
|
40
|
+
"""Escape a string if necessary."""
|
|
41
|
+
if isinstance(s, str) and (len(s) == 0 or s[0] in ("$", "?", "_", "!", "=", "^")):
|
|
42
|
+
return "_" + s
|
|
43
|
+
else:
|
|
44
|
+
return s
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sesc(s):
|
|
48
|
+
if not isinstance(s, str):
|
|
49
|
+
return str(s)
|
|
50
|
+
elif re.search(r"^\w+$", s):
|
|
51
|
+
return s
|
|
52
|
+
else:
|
|
53
|
+
return '"' + s.replace('"', r'\"').replace("\n", r'\n') + '"'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def jpath(path: JsonPath):
|
|
57
|
+
return "." + ".".join(sesc(s) for s in path)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def numberConstraints(schema):
|
|
61
|
+
assert "type" in schema and schema["type"] in ("integer", "number")
|
|
62
|
+
constraints = {}
|
|
63
|
+
if "multipleOf" in schema:
|
|
64
|
+
mo = schema["multipleOf"]
|
|
65
|
+
assert type(mo) in (int, float)
|
|
66
|
+
constraints[".mo"] = mo # extension
|
|
67
|
+
# assert False, "keyword multipleOf is not supported"
|
|
68
|
+
if "minimum" in schema:
|
|
69
|
+
mini = schema["minimum"]
|
|
70
|
+
assert type(mini) in (int, float)
|
|
71
|
+
constraints[">="] = mini
|
|
72
|
+
if "maximum" in schema:
|
|
73
|
+
maxi = schema["maximum"]
|
|
74
|
+
assert type(maxi) in (int, float)
|
|
75
|
+
constraints["<="] = maxi
|
|
76
|
+
if "exclusiveMinimum" in schema:
|
|
77
|
+
mini = schema["exclusiveMinimum"]
|
|
78
|
+
assert type(mini) in (int, float)
|
|
79
|
+
constraints[">"] = mini
|
|
80
|
+
if "exclusiveMaximum" in schema:
|
|
81
|
+
maxi = schema["exclusiveMaximum"]
|
|
82
|
+
assert type(maxi) in (int, float)
|
|
83
|
+
constraints["<"] = maxi
|
|
84
|
+
return constraints
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def buildModel(model, constraints: dict, defs: dict, sharp: dict, is_root = False):
|
|
88
|
+
if constraints or sharp or defs:
|
|
89
|
+
|
|
90
|
+
if constraints:
|
|
91
|
+
m = {"@": model, **constraints}
|
|
92
|
+
elif isinstance(model, dict):
|
|
93
|
+
m = model
|
|
94
|
+
else:
|
|
95
|
+
m = {"@": model}
|
|
96
|
+
|
|
97
|
+
if sharp and "description" in sharp:
|
|
98
|
+
m["#"] = sharp["description"]
|
|
99
|
+
if defs:
|
|
100
|
+
m["$"] = defs
|
|
101
|
+
if is_root and "#" not in m:
|
|
102
|
+
m["#"] = "JSON Model generated from a JSON Schema with json-schema-utils"
|
|
103
|
+
return m
|
|
104
|
+
else:
|
|
105
|
+
if is_root and isinstance(model, dict) and "#" not in model:
|
|
106
|
+
model["#"] = "JSON Model generated from a JSON Schema with json-schema-utils"
|
|
107
|
+
return model
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
META_KEYS = [
|
|
111
|
+
"title", "description", "default", "examples", "deprecated", "readOnly", "writeOnly", "id",
|
|
112
|
+
"$schema", "$id", "$comment", "$dynamicAnchor",
|
|
113
|
+
# OLD?
|
|
114
|
+
"context", "notes",
|
|
115
|
+
# extensions and strange stuff?
|
|
116
|
+
"markdownDescription", "deprecationMessage", "scope", "body", "example", "private",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
IGNORE = META_KEYS + ["$defs", "definitions"]
|
|
120
|
+
|
|
121
|
+
# keywords specific to a type
|
|
122
|
+
SPLITS = {
|
|
123
|
+
"number": ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"],
|
|
124
|
+
"string": ["minLength", "maxLength", "pattern"],
|
|
125
|
+
"array": ["minItems", "maxItems", "uniqueItems", "items", "prefixItems", "contains",
|
|
126
|
+
"minContains", "maxContains"],
|
|
127
|
+
"object": ["properties", "required", "additionalProperties", "minProperties", "maxProperties",
|
|
128
|
+
"patternProperties", "propertyNames"],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
SPLIT = {}
|
|
132
|
+
for k in SPLITS.keys():
|
|
133
|
+
for n in SPLITS[k]:
|
|
134
|
+
SPLIT[n] = k
|
|
135
|
+
# log.warning(f"SPLIT = {SPLIT}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def split_schema(schema: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
139
|
+
assert isinstance(schema, dict) and "type" in schema
|
|
140
|
+
types = schema["type"]
|
|
141
|
+
assert isinstance(types, (list, tuple))
|
|
142
|
+
# log.info(f"splitting on {types}")
|
|
143
|
+
# per type
|
|
144
|
+
schemas = {t: {"type": t} for t in types}
|
|
145
|
+
schemas[""] = {}
|
|
146
|
+
for prop, val in schema.items():
|
|
147
|
+
if prop in IGNORE:
|
|
148
|
+
schemas[""][prop] = val
|
|
149
|
+
elif prop in META_KEYS or prop == "type":
|
|
150
|
+
pass
|
|
151
|
+
elif prop == "format":
|
|
152
|
+
assert isinstance(val, str), "format is a string"
|
|
153
|
+
if "string" in schemas:
|
|
154
|
+
schemas["string"]["format"] = val
|
|
155
|
+
elif "number" in schemas:
|
|
156
|
+
schemas["number"]["format"] = val
|
|
157
|
+
elif "integer" in schemas:
|
|
158
|
+
schemas["integer"]["format"] = val
|
|
159
|
+
else:
|
|
160
|
+
assert False, f"cannot map format to {types}"
|
|
161
|
+
elif prop == "enum":
|
|
162
|
+
# DEAD CODE… enum does not need any check and is already before?
|
|
163
|
+
assert isinstance(val, list), "enum is a list"
|
|
164
|
+
for _, sh in schemas:
|
|
165
|
+
sh["enum"] = []
|
|
166
|
+
for v in val:
|
|
167
|
+
if v is None and "null" in schemas:
|
|
168
|
+
# just drop the enum
|
|
169
|
+
del schemas["null"]["enum"]
|
|
170
|
+
elif isinstance(v, bool) and "boolean" in schemas:
|
|
171
|
+
schemas["boolean"]["enum"].append(v)
|
|
172
|
+
elif isinstance(v, int):
|
|
173
|
+
if "integer" in schemas:
|
|
174
|
+
schemas["integer"]["enum"].append(v)
|
|
175
|
+
if "number" in schemas:
|
|
176
|
+
schemas["number"]["enum"].append(float(v))
|
|
177
|
+
elif isinstance(v, float):
|
|
178
|
+
if "number" in schemas:
|
|
179
|
+
schemas["number"]["enum"].append(v)
|
|
180
|
+
if "integer" in schemas and v - int(v) == 0.0:
|
|
181
|
+
schemas["integer"]["enum"].append(int(v))
|
|
182
|
+
elif isinstance(v, str) and "string" in schemas:
|
|
183
|
+
schemas["string"]["enum"].append(v)
|
|
184
|
+
elif isinstance(v, list) and "array" in schemas:
|
|
185
|
+
schemas["array"]["enum"].append(v)
|
|
186
|
+
elif isinstance(v, dict) and "object" in schemas:
|
|
187
|
+
schemas["object"]["enum"].append(v)
|
|
188
|
+
# else just ignore incompatible value…
|
|
189
|
+
elif prop in SPLIT:
|
|
190
|
+
assert SPLIT[prop] in types
|
|
191
|
+
schemas[SPLIT[prop]][prop] = val
|
|
192
|
+
else:
|
|
193
|
+
assert False, f"cannot map {prop} to a type"
|
|
194
|
+
# log.debug(f"splitted: {schemas}")
|
|
195
|
+
return schemas
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# identifiers
|
|
199
|
+
CURRENT_SCHEMA: str|None = None
|
|
200
|
+
SCHEMA: str|None = None
|
|
201
|
+
IDS: dict[str, dict[str, Any]] = {}
|
|
202
|
+
EXPLICIT_TYPE: bool = False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def reset():
|
|
206
|
+
global CURRENT_SCHEMA, IDS, SCHEMA
|
|
207
|
+
CURRENT_SCHEMA = None
|
|
208
|
+
SCHEMA = None
|
|
209
|
+
IDS = {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_FMT2MODEL = {
|
|
213
|
+
"password": "$STRING", # OpenAPI
|
|
214
|
+
"date": "$DATE",
|
|
215
|
+
"date-time": "$DATETIME",
|
|
216
|
+
"time": "$TIME",
|
|
217
|
+
"duration": "$DURATION",
|
|
218
|
+
"email": "$EMAIL",
|
|
219
|
+
"idn-email": "$EMAIL",
|
|
220
|
+
"hostname": "$HOSTNAME",
|
|
221
|
+
"idn-hostname": "$HOSTNAME",
|
|
222
|
+
"ipv4": "$IP4",
|
|
223
|
+
"ipv6": "$IP6",
|
|
224
|
+
"uri": "$URI",
|
|
225
|
+
"iri": "$URI",
|
|
226
|
+
"iri-reference": "$URI",
|
|
227
|
+
"uri-reference": "$URI",
|
|
228
|
+
"uri-template": "$URI",
|
|
229
|
+
"uuid": "$UUID",
|
|
230
|
+
"json-pointer": "$JSONPT",
|
|
231
|
+
"relative-json-pointer": "$JSONPT",
|
|
232
|
+
"regex": "$REGEX",
|
|
233
|
+
# hmmm…
|
|
234
|
+
"color": "$STRING",
|
|
235
|
+
"phone": "$STRING",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def format2model(fmt: str):
|
|
240
|
+
if fmt in _FMT2MODEL:
|
|
241
|
+
return _FMT2MODEL[fmt]
|
|
242
|
+
else:
|
|
243
|
+
log.warning(f"unknow format: {fmt}")
|
|
244
|
+
# return f"$UNKNOWN:{fmt}"
|
|
245
|
+
return ""
|
|
246
|
+
|
|
247
|
+
def doubt(ok: bool, msg: str, strict: bool):
|
|
248
|
+
if not ok:
|
|
249
|
+
if strict:
|
|
250
|
+
assert False, msg
|
|
251
|
+
else:
|
|
252
|
+
log.warning(msg)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def allOfLayer(schema: dict, operator: str):
|
|
256
|
+
# log.warning(f"ao {operator} in: {schema}")
|
|
257
|
+
schemas = copy.deepcopy(schema[operator])
|
|
258
|
+
del schema[operator]
|
|
259
|
+
# extract ignoreables
|
|
260
|
+
nschema = {}
|
|
261
|
+
for k, v in list(schema.items()):
|
|
262
|
+
if k in IGNORE:
|
|
263
|
+
nschema[k] = v
|
|
264
|
+
del schema[k]
|
|
265
|
+
# forward type just in case for *Of
|
|
266
|
+
if "type" in schema and isinstance(schema["type"], str) and isinstance(schemas, list):
|
|
267
|
+
t = schema["type"]
|
|
268
|
+
for s in schemas:
|
|
269
|
+
if isinstance(s, dict) and "type" not in s:
|
|
270
|
+
s["type"] = t
|
|
271
|
+
nschema["allOf"] = [{operator: schemas}, schema]
|
|
272
|
+
return nschema
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# TODO handle a global defs so as to be able to create new ones
|
|
276
|
+
def schema2model(schema, path: JsonPath = [], strict: bool = True, is_root: bool = True):
|
|
277
|
+
"""Convert a JSON schema to a JSON model assuming a 2020-12 semantics."""
|
|
278
|
+
|
|
279
|
+
global CURRENT_SCHEMA, SCHEMA
|
|
280
|
+
|
|
281
|
+
# 4.3.2 Boolean JSON Schemas
|
|
282
|
+
if isinstance(schema, bool):
|
|
283
|
+
return "$ANY" if schema else "$NONE"
|
|
284
|
+
|
|
285
|
+
spath: str = jpath(path)
|
|
286
|
+
|
|
287
|
+
assert isinstance(schema, dict), f"is an object at [{spath}]"
|
|
288
|
+
|
|
289
|
+
if "$schema" in schema:
|
|
290
|
+
sname = schema["$schema"]
|
|
291
|
+
if CURRENT_SCHEMA is not None and CURRENT_SCHEMA != sname:
|
|
292
|
+
log.warning(f"distinct nested $schema: {sname} at [{spath}]")
|
|
293
|
+
CURRENT_SCHEMA = sname
|
|
294
|
+
|
|
295
|
+
if SCHEMA is None:
|
|
296
|
+
# COPY because schema may be changed… (hmmm…)
|
|
297
|
+
SCHEMA = copy.deepcopy(schema)
|
|
298
|
+
|
|
299
|
+
# handle metadata
|
|
300
|
+
sharp = {}
|
|
301
|
+
for prop in META_KEYS:
|
|
302
|
+
if prop in schema:
|
|
303
|
+
sharp[prop] = schema[prop]
|
|
304
|
+
|
|
305
|
+
# handle defs
|
|
306
|
+
# FIXME should delay conversion...
|
|
307
|
+
defs = {}
|
|
308
|
+
if "$defs" in schema or "definitions" in schema:
|
|
309
|
+
dname = "$defs" if "$defs" in schema else "definitions"
|
|
310
|
+
IDS[dname] = {}
|
|
311
|
+
_defs = schema[dname]
|
|
312
|
+
assert isinstance(_defs, dict)
|
|
313
|
+
for name, val in _defs.items():
|
|
314
|
+
log.info(f"registering {dname}/{name} at [{spath}]")
|
|
315
|
+
# if name is ugly, $ref are encoded, we encode here as well
|
|
316
|
+
if "/" in name or "%" in name:
|
|
317
|
+
encoded = quote(name).replace("~", "~0").replace("/", "~1")
|
|
318
|
+
else:
|
|
319
|
+
encoded = name
|
|
320
|
+
# keep json schema for handling $ref #
|
|
321
|
+
IDS[dname][encoded] = val
|
|
322
|
+
# provide a local converted version as well? not enough??
|
|
323
|
+
defs[encoded] = schema2model(val, path + [dname, name], strict, False)
|
|
324
|
+
# special root handling
|
|
325
|
+
IDS[""] = "$#"
|
|
326
|
+
|
|
327
|
+
# FIXME cleanup OpenAPI extentions "x-*", nullable
|
|
328
|
+
for prop in list(schema.keys()):
|
|
329
|
+
if prop.startswith("x-"):
|
|
330
|
+
log.warning(f"deleting {prop} at [{spath}]")
|
|
331
|
+
del schema[prop]
|
|
332
|
+
if "nullable" in schema:
|
|
333
|
+
nullable = schema["nullable"]
|
|
334
|
+
assert isinstance(nullable, bool)
|
|
335
|
+
if nullable:
|
|
336
|
+
if "type" in schema and isinstance(schema["type"], str):
|
|
337
|
+
schema["type"] = [schema["type"], "null"]
|
|
338
|
+
log.warning(f"ignoring nullable directive at [{spath}]")
|
|
339
|
+
del schema["nullable"]
|
|
340
|
+
|
|
341
|
+
# FIX missing type in some cases
|
|
342
|
+
if "type" not in schema:
|
|
343
|
+
if "properties" in schema or "required" in schema or "additionalProperties" in schema:
|
|
344
|
+
schema["type"] = "object"
|
|
345
|
+
elif "pattern" in schema or "maxLength" in schema or "minLength" in schema:
|
|
346
|
+
schema["type"] = "string"
|
|
347
|
+
elif "items" in schema or "minItems" in schema or "maxItems" in schema:
|
|
348
|
+
schema["type"] = "array"
|
|
349
|
+
|
|
350
|
+
# translate if/then/else to and/xor/not
|
|
351
|
+
# TODO move/replicate in simplify?
|
|
352
|
+
# NOTE this induces some code expansion
|
|
353
|
+
# NOTE generated structures may be simplified later
|
|
354
|
+
if "then" in schema or "else" in schema:
|
|
355
|
+
if "if" not in schema:
|
|
356
|
+
# no if => then/else are ignored (10.2.2)
|
|
357
|
+
log.warning(f"ignoring then/else without if at [{spath}]")
|
|
358
|
+
if "else" in schema:
|
|
359
|
+
del schema["else"]
|
|
360
|
+
if "then" in schema:
|
|
361
|
+
del schema["then"]
|
|
362
|
+
else: # if in schema
|
|
363
|
+
sif = schema["if"]
|
|
364
|
+
del schema["if"]
|
|
365
|
+
# possibly add type to help convertion
|
|
366
|
+
if isinstance(sif, dict) and "type" not in sif:
|
|
367
|
+
for t in SPLITS.keys():
|
|
368
|
+
if only(sif, *SPLITS[t], *IGNORE):
|
|
369
|
+
sif["type"] = t
|
|
370
|
+
break
|
|
371
|
+
if isinstance(sif, dict) and "type" in sif and isinstance(sif["type"], str):
|
|
372
|
+
type_sif = sif["type"]
|
|
373
|
+
else:
|
|
374
|
+
type_sif = None
|
|
375
|
+
if "then" in schema:
|
|
376
|
+
sthen = schema["then"]
|
|
377
|
+
del schema["then"]
|
|
378
|
+
if type_sif and isinstance(sthen, dict) and "type" not in sthen:
|
|
379
|
+
sthen["type"] = type_sif
|
|
380
|
+
else:
|
|
381
|
+
sthen = True
|
|
382
|
+
if "else" in schema:
|
|
383
|
+
selse = schema["else"]
|
|
384
|
+
del schema["else"]
|
|
385
|
+
if type_sif and isinstance(selse, dict) and "type" not in selse:
|
|
386
|
+
selse["type"] = type_sif
|
|
387
|
+
else:
|
|
388
|
+
selse = True
|
|
389
|
+
# W, if: X, then: Y, else: Z => (W and ((X and Y) x?or (not X and Z))
|
|
390
|
+
# separate meta keywords from significant keywords
|
|
391
|
+
subschema = {}
|
|
392
|
+
for k in list(schema.keys()):
|
|
393
|
+
if k not in IGNORE:
|
|
394
|
+
subschema[k] = schema[k]
|
|
395
|
+
del schema[k]
|
|
396
|
+
if "allOf" not in schema:
|
|
397
|
+
schema["allOf"] = []
|
|
398
|
+
schema["allOf"].append(subschema)
|
|
399
|
+
schema["allOf"].append(
|
|
400
|
+
{
|
|
401
|
+
"anyOf": [
|
|
402
|
+
{ "allOf": [ sif, sthen ] },
|
|
403
|
+
{ "allOf": [ { "not": copy.deepcopy(sif) }, selse ] }
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
elif "if" in schema:
|
|
408
|
+
log.warning(f"ignoring lone if at [{spath}]")
|
|
409
|
+
del schema["if"]
|
|
410
|
+
|
|
411
|
+
# FIXME adhoc handling for table-schema.json and ADEME and others
|
|
412
|
+
# FIXME maybe this is not needed anymore?
|
|
413
|
+
if "type" in schema and schema["type"] == "object" and ("anyOf" in schema or "oneOf" in schema):
|
|
414
|
+
log.warning(f"distributing object on anyOf/oneOf at [{spath}]")
|
|
415
|
+
# special case for Ademe
|
|
416
|
+
if "anyOf" in schema:
|
|
417
|
+
assert "oneOf" not in schema
|
|
418
|
+
lof = schema["anyOf"]
|
|
419
|
+
elif "oneOf" in schema:
|
|
420
|
+
assert "anyOf" not in schema
|
|
421
|
+
lof = schema["oneOf"]
|
|
422
|
+
else:
|
|
423
|
+
assert False # dead code
|
|
424
|
+
# transfer type in list
|
|
425
|
+
del schema["type"]
|
|
426
|
+
for s in lof:
|
|
427
|
+
assert isinstance(s, dict)
|
|
428
|
+
s["type"] = "object"
|
|
429
|
+
if "required" in schema:
|
|
430
|
+
required = schema["required"]
|
|
431
|
+
del schema["required"]
|
|
432
|
+
for s in lof:
|
|
433
|
+
if "required" in s:
|
|
434
|
+
s["required"].append(required)
|
|
435
|
+
else:
|
|
436
|
+
s["required"] = required
|
|
437
|
+
if "properties" in schema:
|
|
438
|
+
props = schema["properties"]
|
|
439
|
+
del schema["properties"]
|
|
440
|
+
for s in lof:
|
|
441
|
+
if "properties" in s:
|
|
442
|
+
s["properties"].update(props)
|
|
443
|
+
else:
|
|
444
|
+
s["properties"] = props
|
|
445
|
+
if "additionalProperties" in schema:
|
|
446
|
+
addprop = schema["additionalProperties"]
|
|
447
|
+
del schema["additionalProperties"]
|
|
448
|
+
for s in lof:
|
|
449
|
+
# cold overwrite, should warn
|
|
450
|
+
s["additionalProperties"] = addprop
|
|
451
|
+
if "patternProperties" in schema:
|
|
452
|
+
pp = schema["patternProperties"]
|
|
453
|
+
del schema["patternProperties"]
|
|
454
|
+
for s in lof:
|
|
455
|
+
# cold overwrite, should warn
|
|
456
|
+
s["patternProperties"] = pp
|
|
457
|
+
|
|
458
|
+
if "type" in schema and ("allOf" in schema or "anyOf" in schema or "oneOf" in schema or
|
|
459
|
+
"enum" in schema or "$ref" in schema):
|
|
460
|
+
log.warning("removing type from constructed schema?")
|
|
461
|
+
del schema["type"]
|
|
462
|
+
|
|
463
|
+
# structures
|
|
464
|
+
if "oneOf" in schema:
|
|
465
|
+
choices = schema["oneOf"]
|
|
466
|
+
assert isinstance(choices, list), f"oneOf list at [{spath}]"
|
|
467
|
+
if only(schema, "oneOf", *IGNORE):
|
|
468
|
+
model = {"^": [schema2model(s, path + ["oneOf", i], strict, False)
|
|
469
|
+
for i, s in enumerate(choices)]}
|
|
470
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
471
|
+
else: # try building an "allOf" layer
|
|
472
|
+
log.warning(f"keyword oneOf intermixed with other keywords at [{spath}]")
|
|
473
|
+
ao = allOfLayer(schema, "oneOf")
|
|
474
|
+
return schema2model(ao, path + ["oneOf"], strict, False)
|
|
475
|
+
elif "anyOf" in schema:
|
|
476
|
+
choices = schema["anyOf"]
|
|
477
|
+
assert isinstance(choices, (list, tuple)), f"anyOf list at [{spath}]"
|
|
478
|
+
if only(schema, "anyOf", *IGNORE):
|
|
479
|
+
model = {"|": [schema2model(s, path + ["anyOf", i], strict, False)
|
|
480
|
+
for i, s in enumerate(choices)]}
|
|
481
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
482
|
+
else:
|
|
483
|
+
log.warning(f"keyword anyOf intermixed with other keywords at [{spath}]")
|
|
484
|
+
ao = allOfLayer(schema, "anyOf")
|
|
485
|
+
return schema2model(ao, path + ["anyOf"], strict, False)
|
|
486
|
+
elif "allOf" in schema:
|
|
487
|
+
# NOTE types should be compatible to avoid an empty match
|
|
488
|
+
choices = schema["allOf"]
|
|
489
|
+
assert isinstance(choices, (list, tuple)), f"allOf list at [{spath}]"
|
|
490
|
+
if only(schema, "allOf", *IGNORE):
|
|
491
|
+
model = {"&": [schema2model(s, path + ["allOf", i], strict, False)
|
|
492
|
+
for i, s in enumerate(choices)]}
|
|
493
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
494
|
+
else: # build another allOf layer
|
|
495
|
+
log.warning(f"keyword allOf intermixed with other keywords at [{spath}]")
|
|
496
|
+
ao = allOfLayer(schema, "allOf")
|
|
497
|
+
return schema2model(ao, path + ["allOf"], strict, False)
|
|
498
|
+
elif "not" in schema:
|
|
499
|
+
val = schema["not"]
|
|
500
|
+
assert isinstance(val, dict), "not object at [{spath}]"
|
|
501
|
+
if only(schema, "not", *IGNORE):
|
|
502
|
+
if len(val) == 0:
|
|
503
|
+
model = "$NONE"
|
|
504
|
+
else:
|
|
505
|
+
model = {"^": ["$ANY", schema2model(val, path + ["not"], strict, False)]}
|
|
506
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
507
|
+
else: # add a allOf layer
|
|
508
|
+
log.warning(f"keyword not intermixed with other keywords at [{spath}]")
|
|
509
|
+
ao = allOfLayer(schema, "not")
|
|
510
|
+
return schema2model(ao, path + ["not"], strict, False)
|
|
511
|
+
|
|
512
|
+
# handle simpler schemas
|
|
513
|
+
if "$ref" in schema:
|
|
514
|
+
if only(schema, "$ref", *IGNORE):
|
|
515
|
+
ref = schema["$ref"]
|
|
516
|
+
assert isinstance(ref, str) and len(ref) > 0
|
|
517
|
+
if ref in ("#/", "#"):
|
|
518
|
+
return "$#"
|
|
519
|
+
|
|
520
|
+
name = None
|
|
521
|
+
if ref.startswith("#/$defs/") and only(schema, "$ref", *IGNORE):
|
|
522
|
+
# keep a reference if simple
|
|
523
|
+
name = ref[8:]
|
|
524
|
+
elif ref.startswith("#/definitions/") and only(schema, "$ref", *IGNORE):
|
|
525
|
+
name = ref[14:]
|
|
526
|
+
if name is not None and "/" not in name:
|
|
527
|
+
return buildModel("$" + name, {}, defs, sharp, is_root)
|
|
528
|
+
|
|
529
|
+
# else we have to navigate…
|
|
530
|
+
# FIXME should be useless? should have been simplified?
|
|
531
|
+
if ref.startswith("#/"):
|
|
532
|
+
names = ref[2:].split("/")
|
|
533
|
+
# standard /$def/foo
|
|
534
|
+
if names and names[0] in ("$defs", "definitions"):
|
|
535
|
+
val = IDS
|
|
536
|
+
for name in names:
|
|
537
|
+
assert name in val, f"following path in {ref}: missing {name} ({IDS}) at [{spath}]"
|
|
538
|
+
val = val[name]
|
|
539
|
+
else: # arbitrary path
|
|
540
|
+
val = SCHEMA
|
|
541
|
+
for name in names:
|
|
542
|
+
if "~0" in name or "~1" in name or "%" in name:
|
|
543
|
+
name = unquote(name).replace("~1", "/").replace("~0", "~")
|
|
544
|
+
assert name in val, f"following path in {ref}: missing {name} at [{spath}]"
|
|
545
|
+
val = val[name]
|
|
546
|
+
model = schema2model(val, path + ["$ref"], strict, False)
|
|
547
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
548
|
+
else:
|
|
549
|
+
assert False, f"$ref handling not implemented: {ref}"
|
|
550
|
+
else:
|
|
551
|
+
log.warning(f"$ref intermixed with other keywords at [{spath}]")
|
|
552
|
+
return schema2model(allOfLayer(schema, "$ref"), path, strict, False)
|
|
553
|
+
|
|
554
|
+
elif "type" in schema:
|
|
555
|
+
ts = schema["type"]
|
|
556
|
+
if isinstance(ts, (list, tuple)):
|
|
557
|
+
schemas = split_schema(schema)
|
|
558
|
+
del schemas[""] # remove ignored stuff
|
|
559
|
+
return buildModel(
|
|
560
|
+
{
|
|
561
|
+
"|": [
|
|
562
|
+
schema2model(v, path + ["typeS"], strict, False) for v in schemas.values()
|
|
563
|
+
]
|
|
564
|
+
}, {}, defs, sharp, is_root)
|
|
565
|
+
elif ts == "string" and "const" in schema:
|
|
566
|
+
doubt(only(schema, "type", "const", *IGNORE), f"string const at [{spath}]", strict)
|
|
567
|
+
const = schema["const"]
|
|
568
|
+
return buildModel(f"_{const}", {}, defs, sharp, is_root)
|
|
569
|
+
elif ts == "string":
|
|
570
|
+
doubt(only(schema, "enum", "type", "format", "pattern", "minLength", "maxLength",
|
|
571
|
+
"contentMediaType", "contentEncoding", *IGNORE),
|
|
572
|
+
f"string at [{spath}]", strict)
|
|
573
|
+
model = "$STRING" if EXPLICIT_TYPE else ""
|
|
574
|
+
if "format" in schema:
|
|
575
|
+
fmt = schema["format"]
|
|
576
|
+
if fmt is not None:
|
|
577
|
+
model = format2model(fmt)
|
|
578
|
+
else:
|
|
579
|
+
log.warning(f"ignoring null format at [{spath}]")
|
|
580
|
+
constraints = {}
|
|
581
|
+
if "enum" in schema:
|
|
582
|
+
ve = schema["enum"]
|
|
583
|
+
assert isinstance(ve, list), path
|
|
584
|
+
# TODO check stat *all* items are strings
|
|
585
|
+
if len(ve) == 1:
|
|
586
|
+
assert isinstance(ve[0], str), path
|
|
587
|
+
model = esc(ve[0])
|
|
588
|
+
else:
|
|
589
|
+
model = {"|": [esc(v) for v in ve]}
|
|
590
|
+
if "pattern" in schema:
|
|
591
|
+
pattern = schema["pattern"]
|
|
592
|
+
assert isinstance(pattern, str), f"string pattern at [{spath}]"
|
|
593
|
+
assert model in ("", "$STRING"), f"string pattern for string at [{spath}]"
|
|
594
|
+
model = f"/{pattern}/"
|
|
595
|
+
if "minLength" in schema and "maxLength" in schema and \
|
|
596
|
+
schema["minLength"] == schema["maxLength"]:
|
|
597
|
+
ival = schema["minLength"]
|
|
598
|
+
assert isinstance(ival, int), f"int length at [{spath}]"
|
|
599
|
+
constraints["="] = ival
|
|
600
|
+
else:
|
|
601
|
+
if "minLength" in schema:
|
|
602
|
+
minlen = schema["minLength"]
|
|
603
|
+
assert isinstance(minlen, int), f"int min length at [{spath}]"
|
|
604
|
+
constraints[">="] = minlen
|
|
605
|
+
if "maxLength" in schema:
|
|
606
|
+
maxlen = schema["maxLength"]
|
|
607
|
+
assert isinstance(maxlen, int), f"int max length at [{spath}]"
|
|
608
|
+
constraints["<="] = maxlen
|
|
609
|
+
# if "contentMediaType" in schema:
|
|
610
|
+
# val = schema["contentMediaType"]
|
|
611
|
+
# assert isinstance(val, str), path
|
|
612
|
+
# constraints["mime"] = val
|
|
613
|
+
# if "contentEncoding" in schema:
|
|
614
|
+
# val = schema["contentEncoding"]
|
|
615
|
+
# assert isinstance(val, str), path
|
|
616
|
+
# constraints["encoding"] = val
|
|
617
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
618
|
+
elif ts == "number":
|
|
619
|
+
doubt(only(schema, "type", "format", "multipleOf", "minimum", "maximum",
|
|
620
|
+
"exclusiveMinimum", "exclusiveMaximum", *IGNORE),
|
|
621
|
+
f"number properties at [{spath}]", strict)
|
|
622
|
+
model = "$NUMBER" if EXPLICIT_TYPE else -1.0
|
|
623
|
+
if "format" in schema:
|
|
624
|
+
fmt = schema["format"]
|
|
625
|
+
assert fmt in ("double", "float"), f"bad format {fmt} at [{spath}]"
|
|
626
|
+
if fmt == "double":
|
|
627
|
+
model = "$DOUBLE"
|
|
628
|
+
elif fmt == "float":
|
|
629
|
+
model = "$FLOAT"
|
|
630
|
+
else:
|
|
631
|
+
assert False, f"unexpected number format {fmt} at [{spath}]"
|
|
632
|
+
constraints = numberConstraints(schema)
|
|
633
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
634
|
+
elif ts == "integer" and "const" in schema:
|
|
635
|
+
ival = schema["const"]
|
|
636
|
+
if isinstance(ival, (int, float)) and ival == int(ival):
|
|
637
|
+
model = "={int(ival)}"
|
|
638
|
+
else:
|
|
639
|
+
model = "$NONE"
|
|
640
|
+
elif ts == "integer":
|
|
641
|
+
doubt(only(schema, "type", "format", "multipleOf", "minimum", "maximum",
|
|
642
|
+
"exclusiveMinimum", "exclusiveMaximum", *IGNORE),
|
|
643
|
+
f"integer properties at [{spath}]", strict)
|
|
644
|
+
model = "$INTEGER" if EXPLICIT_TYPE else -1
|
|
645
|
+
# OpenAPI
|
|
646
|
+
if "format" in schema:
|
|
647
|
+
fmt = schema["format"]
|
|
648
|
+
log.warning(f"ignoring format {fmt} at [{spath}]")
|
|
649
|
+
assert fmt in ("int32", "int64"), f"bad format {fmt} at [{spath}]"
|
|
650
|
+
if fmt == "int32":
|
|
651
|
+
model = "$I32"
|
|
652
|
+
elif fmt == "int64":
|
|
653
|
+
model = "$I64"
|
|
654
|
+
else:
|
|
655
|
+
assert False, f"unexpected integer format: {fmt} at [{spath}]"
|
|
656
|
+
constraints = numberConstraints(schema)
|
|
657
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
658
|
+
elif ts == "boolean":
|
|
659
|
+
if "required" in schema:
|
|
660
|
+
log.warning(f"'required' ignored for boolean at [{spath}]")
|
|
661
|
+
doubt(only(schema, "type", "required", *IGNORE),
|
|
662
|
+
f"boolean properties at [{spath}]", strict)
|
|
663
|
+
model = "$BOOL" if EXPLICIT_TYPE else True
|
|
664
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
665
|
+
elif ts == "null":
|
|
666
|
+
model = "$NULL" if EXPLICIT_TYPE else None
|
|
667
|
+
doubt(only(schema, "type", *IGNORE), f"null with props at [{spath}]", strict)
|
|
668
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
669
|
+
|
|
670
|
+
elif ts == "array":
|
|
671
|
+
# fix common misplacements
|
|
672
|
+
if not strict and "items" in schema and isinstance(schema["items"], dict):
|
|
673
|
+
subschema = schema["items"]
|
|
674
|
+
for kw in ("uniqueItems", "minItems", "maxItems"):
|
|
675
|
+
# possible move misplaced uniqueItems
|
|
676
|
+
if kw not in schema and kw in subschema and "type" in subschema and \
|
|
677
|
+
isinstance(subschema["type"], str) and subschema["type"] != "array":
|
|
678
|
+
log.warning(f"moving misplaced {kw} at [{spath}.items]")
|
|
679
|
+
schema[kw] = subschema[kw]
|
|
680
|
+
del subschema[kw]
|
|
681
|
+
# TODO minLength -> minItems
|
|
682
|
+
|
|
683
|
+
# sanity check
|
|
684
|
+
doubt(only(schema, "type", "prefixItems", "items",
|
|
685
|
+
"contains", "minContains", "maxContains",
|
|
686
|
+
"minItems", "maxItems", "uniqueItems", *IGNORE),
|
|
687
|
+
f"array properties at [{spath}]", strict)
|
|
688
|
+
|
|
689
|
+
# build model
|
|
690
|
+
constraints = {}
|
|
691
|
+
if "minItems" in schema and "maxItems" in schema and \
|
|
692
|
+
schema["minItems"] == schema["maxItems"]:
|
|
693
|
+
ival = schema["minItems"]
|
|
694
|
+
assert isinstance(ival, int)
|
|
695
|
+
constraints["="] = ival
|
|
696
|
+
else:
|
|
697
|
+
if "minItems" in schema:
|
|
698
|
+
mini = schema["minItems"]
|
|
699
|
+
assert type(mini) is int, f"int min items at [{spath}]"
|
|
700
|
+
constraints[">="] = mini
|
|
701
|
+
if "maxItems" in schema:
|
|
702
|
+
maxi = schema["maxItems"]
|
|
703
|
+
assert type(maxi) is int, f"int max items at [{spath}]"
|
|
704
|
+
constraints["<="] = maxi
|
|
705
|
+
if "uniqueItems" in schema:
|
|
706
|
+
unique = schema["uniqueItems"]
|
|
707
|
+
assert isinstance(unique, bool), f"boolean unique at [{spath}]"
|
|
708
|
+
if unique:
|
|
709
|
+
constraints["!"] = True
|
|
710
|
+
if "prefixItems" in schema:
|
|
711
|
+
doubt(only(schema, "type", "prefixItems", "items", "minItems", "maxItems",
|
|
712
|
+
"uniqueItems", *IGNORE),
|
|
713
|
+
f"array props with prefixItems at [{spath}]", strict)
|
|
714
|
+
# tuple
|
|
715
|
+
vpi = schema["prefixItems"]
|
|
716
|
+
assert isinstance(vpi, list), f"list prefixItems at [{spath}]"
|
|
717
|
+
model = [
|
|
718
|
+
schema2model(s, path + ["prefixItems", i], strict, False)
|
|
719
|
+
for i, s in enumerate(vpi)
|
|
720
|
+
]
|
|
721
|
+
if "items" in schema:
|
|
722
|
+
# cas prefixItems + items
|
|
723
|
+
if isinstance(schema["items"], bool):
|
|
724
|
+
assert not constraints, f"not implemented yet at [{spath}]"
|
|
725
|
+
if not schema["items"]:
|
|
726
|
+
return buildModel(model, {">=": 0, "<=": len(model)}, defs, sharp, is_root)
|
|
727
|
+
else:
|
|
728
|
+
assert False, f"not implemented yet at [{spath}]"
|
|
729
|
+
# items is a type
|
|
730
|
+
model.append(schema2model(schema["items"], path + ["items"], strict, False))
|
|
731
|
+
if not constraints:
|
|
732
|
+
constraints[">="] = len(model) - 1
|
|
733
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
734
|
+
else:
|
|
735
|
+
model.append("$ANY")
|
|
736
|
+
if not constraints:
|
|
737
|
+
constraints[">="] = len(model) - 1
|
|
738
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
739
|
+
elif "items" in schema:
|
|
740
|
+
doubt(only(schema, "type", "items", "minItems", "maxItems", "uniqueItems",
|
|
741
|
+
*IGNORE), f"array props with items at [{spath}]", strict)
|
|
742
|
+
sitems = schema["items"]
|
|
743
|
+
if isinstance(sitems, list):
|
|
744
|
+
# OLD JSON Schema prefixItems…
|
|
745
|
+
array = [
|
|
746
|
+
schema2model(s, path + ["items", i], strict, False)
|
|
747
|
+
for i, s in enumerate(sitems)
|
|
748
|
+
]
|
|
749
|
+
if strict:
|
|
750
|
+
return array
|
|
751
|
+
else: # trigger varlen tuple…
|
|
752
|
+
array.append("$ANY")
|
|
753
|
+
return { "@": array, ">=": 0 }
|
|
754
|
+
else:
|
|
755
|
+
assert isinstance(sitems, (dict, bool)), f"valid schema at [{spath}]"
|
|
756
|
+
model = [schema2model(schema["items"], path + ["items"], strict, False)]
|
|
757
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
758
|
+
elif "contains" in schema:
|
|
759
|
+
# NO contains/items mixing yet
|
|
760
|
+
assert only(schema, "type", "contains", "minContains", "maxContains",
|
|
761
|
+
"uniqueItems", *IGNORE), f"array props for containts[{spath}]"
|
|
762
|
+
# NOTE contains is not really supported in jm v2, or rather as an extension
|
|
763
|
+
model = {
|
|
764
|
+
"@": ["$ANY"],
|
|
765
|
+
".in": schema2model(schema["contains"], path + ["contains"], strict, False)
|
|
766
|
+
}
|
|
767
|
+
if "minContains" in schema:
|
|
768
|
+
mini = schema["minContains"]
|
|
769
|
+
assert type(mini) is int, f"int min contains at [{spath}]"
|
|
770
|
+
model[">="] = mini
|
|
771
|
+
if "maxContains" in schema:
|
|
772
|
+
maxi = schema["maxContains"]
|
|
773
|
+
assert type(maxi) is int, f"int max contains at [{spath}]"
|
|
774
|
+
model["<="] = maxi
|
|
775
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
776
|
+
else:
|
|
777
|
+
return buildModel(["$ANY"], constraints, defs, sharp, is_root)
|
|
778
|
+
|
|
779
|
+
elif ts == "object":
|
|
780
|
+
|
|
781
|
+
# ignore OpenAPI extension
|
|
782
|
+
if "discriminator" in schema:
|
|
783
|
+
log.warning(f"ignoring discriminator at [{spath}]")
|
|
784
|
+
del schema["discriminator"]
|
|
785
|
+
|
|
786
|
+
# fix additionalProperties misplacement
|
|
787
|
+
if not strict and "properties" in schema and isinstance(schema["properties"], dict) \
|
|
788
|
+
and "additionalProperties" not in schema \
|
|
789
|
+
and "additionalProperties" in schema["properties"] \
|
|
790
|
+
and "properties" not in schema["properties"]:
|
|
791
|
+
log.warning(f"moving misplaced additionalProperties at [{spath}.properties]")
|
|
792
|
+
schema["additionalProperties"] = schema["properties"]["additionalProperties"]
|
|
793
|
+
del schema["properties"]["additionalProperties"]
|
|
794
|
+
|
|
795
|
+
# sanity check
|
|
796
|
+
doubt(only(schema, "type", "properties", "additionalProperties", "required",
|
|
797
|
+
"minProperties", "maxProperties", "patternProperties", "propertyNames",
|
|
798
|
+
*IGNORE), f"object properties at [{spath}]", strict)
|
|
799
|
+
|
|
800
|
+
# build model
|
|
801
|
+
constraints = {}
|
|
802
|
+
model = {}
|
|
803
|
+
if "minProperties" in schema and "maxProperties" in schema and \
|
|
804
|
+
schema["minProperties"] == schema["maxProperties"]:
|
|
805
|
+
ival = schema["maxProperties"]
|
|
806
|
+
assert isinstance(ival, int), f"int # props at [{spath}]"
|
|
807
|
+
else:
|
|
808
|
+
if "minProperties" in schema:
|
|
809
|
+
mini = schema["minProperties"]
|
|
810
|
+
assert type(mini) is int, f"int min props at [{spath}]"
|
|
811
|
+
constraints[">="] = mini
|
|
812
|
+
if "maxProperties" in schema:
|
|
813
|
+
maxi = schema["maxProperties"]
|
|
814
|
+
assert type(maxi) is int, f"int max props at [{spath}]"
|
|
815
|
+
constraints["<="] = maxi
|
|
816
|
+
|
|
817
|
+
if "patternProperties" in schema:
|
|
818
|
+
pats = schema["patternProperties"]
|
|
819
|
+
assert isinstance(pats, dict), f"dict pattern props at [{spath}]"
|
|
820
|
+
for pp in sorted(pats.keys()):
|
|
821
|
+
model[f"/{pp}/"] = \
|
|
822
|
+
schema2model(pats[pp], path + ["patternProperties", pp], strict, False)
|
|
823
|
+
|
|
824
|
+
if "propertyNames" in schema:
|
|
825
|
+
# does not seem very useful?
|
|
826
|
+
pnames = schema["propertyNames"]
|
|
827
|
+
if "additionalProperties" in schema:
|
|
828
|
+
target = schema2model(schema["additionalProperties"], path, strict, False)
|
|
829
|
+
else:
|
|
830
|
+
target = "$ANY"
|
|
831
|
+
|
|
832
|
+
# TODO for other cases, we could create a new reference
|
|
833
|
+
assert only(pnames, "pattern", "type", "format", *IGNORE), \
|
|
834
|
+
f"props for prop names at [{spath}]"
|
|
835
|
+
# if given a type, it must be string
|
|
836
|
+
if "type" in pnames:
|
|
837
|
+
assert pnames["type"] == "string", f"prop name is string at [{spath}]"
|
|
838
|
+
if "pattern" in pnames:
|
|
839
|
+
pat = pnames["pattern"]
|
|
840
|
+
assert isinstance(pat, str), f"pattern is string at [{spath}]"
|
|
841
|
+
model[f"/{pat}/"] = target
|
|
842
|
+
elif "format" in pnames:
|
|
843
|
+
fmt = pnames["format"]
|
|
844
|
+
model[format2model(fmt)] = target
|
|
845
|
+
else:
|
|
846
|
+
model[""] = target
|
|
847
|
+
|
|
848
|
+
if "properties" in schema:
|
|
849
|
+
props = schema["properties"]
|
|
850
|
+
required = schema.get("required", [])
|
|
851
|
+
assert isinstance(props, dict), f"dict properties [{spath}]"
|
|
852
|
+
for k, v in props.items():
|
|
853
|
+
if k in required:
|
|
854
|
+
model[f"_{k}"] = \
|
|
855
|
+
schema2model(v, path + ["properties", f"_{k}"], strict, False)
|
|
856
|
+
else:
|
|
857
|
+
model[f"?{k}"] = \
|
|
858
|
+
schema2model(v, path + ["properties", f"?{k}"], strict, False)
|
|
859
|
+
if "additionalProperties" in schema:
|
|
860
|
+
ap = schema["additionalProperties"]
|
|
861
|
+
if isinstance(ap, bool):
|
|
862
|
+
if ap:
|
|
863
|
+
model[""] = "$ANY"
|
|
864
|
+
# else nothing else is allowed
|
|
865
|
+
elif isinstance(ap, dict):
|
|
866
|
+
model[""] = schema2model(ap, path + ["additionalProperties"], strict, False)
|
|
867
|
+
else:
|
|
868
|
+
assert False, f"not implemented yet at [{spath}]"
|
|
869
|
+
else:
|
|
870
|
+
model[""] = "$ANY"
|
|
871
|
+
|
|
872
|
+
elif "additionalProperties" in schema:
|
|
873
|
+
# "additionalProperties" without "properties" or "patternProperties"
|
|
874
|
+
doubt(only(schema, "type", "additionalProperties", "maxProperties", "minProperties",
|
|
875
|
+
"properties", "patternProperties", *IGNORE),
|
|
876
|
+
f"add prop props at [{spath}]", strict)
|
|
877
|
+
if "properties" not in schema and "additionalProperties" not in schema and strict:
|
|
878
|
+
assert False, "additionalProperties without properties or patternProperties"
|
|
879
|
+
ap = schema["additionalProperties"]
|
|
880
|
+
if isinstance(ap, bool):
|
|
881
|
+
if ap:
|
|
882
|
+
model[""] = "$ANY"
|
|
883
|
+
# else nothing else is allowed
|
|
884
|
+
elif isinstance(ap, dict):
|
|
885
|
+
model[""] = schema2model(ap, path + ["additionalProperties"], strict, False)
|
|
886
|
+
else:
|
|
887
|
+
assert False, f"not implemented yet at [{spath}]"
|
|
888
|
+
|
|
889
|
+
elif "required" in schema:
|
|
890
|
+
# required without properties or additionalProperties
|
|
891
|
+
doubt(only(schema, "type", "required", "maxProperties", "minProperties",
|
|
892
|
+
*IGNORE), f"object props at [{spath}]", strict)
|
|
893
|
+
required = schema["required"]
|
|
894
|
+
assert isinstance(required, list) and all(isinstance(s, str) for s in required)
|
|
895
|
+
model[""] = "$ANY"
|
|
896
|
+
model.update({ f"_{k}": "$ANY" for k in required })
|
|
897
|
+
# what about other props?
|
|
898
|
+
|
|
899
|
+
else:
|
|
900
|
+
doubt(only(schema, "type", "maxProperties", "minProperties", "patternProperties",
|
|
901
|
+
"propertyNames", *IGNORE), f"object props at [{spath}]", strict)
|
|
902
|
+
if "propertyNames" in schema:
|
|
903
|
+
pass
|
|
904
|
+
else:
|
|
905
|
+
model[""] = "$ANY"
|
|
906
|
+
# handle constraints
|
|
907
|
+
return buildModel(model, constraints, defs, sharp, is_root)
|
|
908
|
+
else:
|
|
909
|
+
assert False, f"unexpected type: {ts} at [{spath}]"
|
|
910
|
+
elif "enum" in schema:
|
|
911
|
+
# FIXME check type value compatibility with other keywords, eg type?!
|
|
912
|
+
for prop in ("maxLength", "minLength"):
|
|
913
|
+
if prop in schema:
|
|
914
|
+
log.warning(f"ignoring doubtful {prop} from enum at [{spath}]")
|
|
915
|
+
del schema[prop]
|
|
916
|
+
doubt(only(schema, "enum", *IGNORE),
|
|
917
|
+
f"keyword enum intermixed with other keywords at [{spath}]", strict)
|
|
918
|
+
ve = schema["enum"]
|
|
919
|
+
assert isinstance(ve, list), f"enum list at [{spath}]"
|
|
920
|
+
if len(ve) == 1:
|
|
921
|
+
model = toconst(ve[0])
|
|
922
|
+
else:
|
|
923
|
+
model = {"|": [toconst(v) for v in ve]}
|
|
924
|
+
# vérifier les valeurs?
|
|
925
|
+
return buildModel(model, {}, defs, sharp, is_root)
|
|
926
|
+
elif "const" in schema:
|
|
927
|
+
assert only(schema, "const", *IGNORE), \
|
|
928
|
+
f"keyword const intermixed with other keywords at [{spath}]"
|
|
929
|
+
const = schema["const"]
|
|
930
|
+
# what is the type of the constant? assume a string for NOW, could be anything?
|
|
931
|
+
return buildModel(toconst(const), {}, defs, sharp, is_root)
|
|
932
|
+
else:
|
|
933
|
+
# empty schema
|
|
934
|
+
return buildModel("$ANY", {}, defs, sharp, is_root)
|