1import decimal
2import re
3
4from .exceptions import JsonSchemaDefinitionException
5from .generator import VALIDATION_EXCEPTIONS, CodeGenerator, enforce_list, repr_default
6
7
8JSON_TYPE_TO_PYTHON_TYPE = {
9 'null': 'NoneType',
10 'boolean': 'bool',
11 'number': 'int, float, Decimal',
12 'integer': 'int',
13 'string': 'str',
14 'array': 'list, tuple',
15 'object': 'dict',
16}
17
18DOLLAR_FINDER = re.compile(r"(?<!\\)\$") # Finds any un-escaped $ (including inside []-sets)
19
20
21# Calendar date: the day must exist in its month, and 29 February only in a leap year.
22DATE_REGEX = (
23 r'\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8]))'
24 r'|(?:\d\d(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29'
25)
26
27
28# pylint: disable=too-many-instance-attributes,too-many-public-methods
29class CodeGeneratorDraft04(CodeGenerator):
30 # pylint: disable=line-too-long
31 # I was thinking about using ipaddress module instead of regexps for example, but it's big
32 # difference in performance. With a module I got this difference: over 100 ms with a module
33 # vs. 9 ms with a regex! Other modules are also ineffective or not available in standard
34 # library. Some regexps are not 100% precise but good enough, fast and without dependencies.
35 FORMAT_REGEXS = {
36 'date-time': r'^(?:' + DATE_REGEX + r')(t|T)(?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[+-](?:[01]\d|2[0-3]):[0-5]\d|[+-](?:[01]\d|2[0-3])[0-5]\d|z|Z)\Z',
37 'email': r'^(?!.*\.\..*@)[^@.][^@]*(?<!\.)@[^@]+\.[^@]+\Z',
38 'hostname': r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9])\Z',
39 'ipv4': r'^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\Z',
40 'ipv6': r'^(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)\Z',
41 'uri': r'^\w+:(\/?\/?)[^\s]+\Z',
42 }
43
44 def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
45 super().__init__(definition, resolver, detailed_exceptions, fast_fail)
46 self._custom_formats = formats
47 self._use_formats = use_formats
48 self._use_default = use_default
49 self._json_keywords_to_function.update((
50 ('type', self.generate_type),
51 ('enum', self.generate_enum),
52 ('allOf', self.generate_all_of),
53 ('anyOf', self.generate_any_of),
54 ('oneOf', self.generate_one_of),
55 ('not', self.generate_not),
56 ('minLength', self.generate_min_length),
57 ('maxLength', self.generate_max_length),
58 ('pattern', self.generate_pattern),
59 ('format', self.generate_format),
60 ('minimum', self.generate_minimum),
61 ('maximum', self.generate_maximum),
62 ('multipleOf', self.generate_multiple_of),
63 ('minItems', self.generate_min_items),
64 ('maxItems', self.generate_max_items),
65 ('uniqueItems', self.generate_unique_items),
66 ('items', self.generate_items),
67 ('minProperties', self.generate_min_properties),
68 ('maxProperties', self.generate_max_properties),
69 ('required', self.generate_required),
70 # Check dependencies before properties generates default values.
71 ('dependencies', self.generate_dependencies),
72 ('properties', self.generate_properties),
73 ('patternProperties', self.generate_pattern_properties),
74 ('additionalProperties', self.generate_additional_properties),
75 ))
76 self._any_or_one_of_count = 0
77
78 @property
79 def global_state(self):
80 res = super().global_state
81 res['custom_formats'] = self._custom_formats
82 return res
83
84 def generate_type(self):
85 """
86 Validation of type. Can be one type or list of types.
87
88 .. code-block:: python
89
90 {'type': 'string'}
91 {'type': ['string', 'number']}
92 """
93 types = enforce_list(self._definition['type'])
94 try:
95 python_types = ', '.join(JSON_TYPE_TO_PYTHON_TYPE[t] for t in types)
96 except KeyError as exc:
97 raise JsonSchemaDefinitionException('Unknown type') from exc
98
99 extra = ''
100 if ('number' in types or 'integer' in types) and 'boolean' not in types:
101 extra = ' or isinstance({variable}, bool)'.format(variable=self._variable)
102
103 with self.l('if not isinstance({variable}, ({})){}:', python_types, extra):
104 self.exc('{name} must be {}', ' or '.join(types), rule='type')
105
106 def generate_enum(self):
107 """
108 Means that only value specified in the enum is valid.
109
110 .. code-block:: python
111
112 {
113 'enum': ['a', 'b'],
114 }
115 """
116 enum = self._definition['enum']
117 if not isinstance(enum, (list, tuple)):
118 raise JsonSchemaDefinitionException('enum must be an array')
119 matches = ' or '.join(self._enum_value_matches(self._variable, value) for value in enum)
120 if matches:
121 with self.l('if not ({}):', matches):
122 self.exc('{name} must be one of {}', self.e(enum), rule='enum')
123 else:
124 with self.l('if True:'):
125 self.exc('{name} must be one of {}', self.e(enum), rule='enum')
126
127 def _enum_value_matches(self, var, value):
128 if isinstance(value, bool):
129 return 'isinstance({var}, bool) and {var} is {val}'.format(var=var, val=repr(value))
130 if isinstance(value, (int, float)) and not isinstance(value, bool):
131 return (
132 'isinstance({var}, (int, float)) and not isinstance({var}, bool) and {var} == {val}'
133 ).format(var=var, val=repr(value))
134 if value is None:
135 return '{var} is None'.format(var=var)
136 if isinstance(value, str):
137 return 'isinstance({var}, str) and {var} == {val}'.format(var=var, val=repr(value))
138 if isinstance(value, dict):
139 if not value:
140 return 'isinstance({var}, dict) and not {var}'.format(var=var)
141 key_checks = ' and '.join(
142 '{key!r} in {var} and {match}'.format(
143 key=key,
144 var=var,
145 match=self._enum_value_matches('{var}[{key!r}]'.format(var=var, key=key), item),
146 )
147 for key, item in value.items()
148 )
149 return 'isinstance({var}, dict) and len({var}) == {size} and {checks}'.format(
150 var=var, size=len(value), checks=key_checks,
151 )
152 if isinstance(value, (list, tuple)):
153 if not value:
154 return 'isinstance({var}, (list, tuple)) and not {var}'.format(var=var)
155 item_checks = ' and '.join(
156 self._enum_value_matches('{var}[{index}]'.format(var=var, index=index), item)
157 for index, item in enumerate(value)
158 )
159 return 'isinstance({var}, (list, tuple)) and len({var}) == {size} and {checks}'.format(
160 var=var, size=len(value), checks=item_checks,
161 )
162 return '{var} == {val}'.format(var=var, val=repr(value))
163
164 def generate_all_of(self):
165 """
166 Means that value have to be valid by all of those definitions. It's like put it in
167 one big definition.
168
169 .. code-block:: python
170
171 {
172 'allOf': [
173 {'type': 'number'},
174 {'minimum': 5},
175 ],
176 }
177
178 Valid values for this definition are 5, 6, 7, ... but not 4 or 'abc' for example.
179 """
180 for definition_item in self._definition['allOf']:
181 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True)
182
183 def generate_any_of(self):
184 """
185 Means that value have to be valid by any of those definitions. It can also be valid
186 by all of them.
187
188 .. code-block:: python
189
190 {
191 'anyOf': [
192 {'type': 'number', 'minimum': 10},
193 {'type': 'number', 'maximum': 5},
194 ],
195 }
196
197 Valid values for this definition are 3, 4, 5, 10, 11, ... but not 8 for example.
198 """
199 self._any_or_one_of_count += 1
200 count = self._any_or_one_of_count
201 self.l('{variable}_any_of_count{count} = 0', count=count)
202 for definition_item in self._definition['anyOf']:
203 # When we know it's passing (at least once), we do not need to do another expensive try-except.
204 with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False):
205 with self.l('try:', optimize=False):
206 with self.trial_validation():
207 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True)
208 self.l('{variable}_any_of_count{count} += 1', count=count)
209 self.l('except {}: pass', VALIDATION_EXCEPTIONS)
210
211 with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False):
212 self.exc('{name} cannot be validated by any definition', rule='anyOf')
213
214 def generate_one_of(self):
215 """
216 Means that value have to be valid by only one of those definitions. It can't be valid
217 by two or more of them.
218
219 .. code-block:: python
220
221 {
222 'oneOf': [
223 {'type': 'number', 'multipleOf': 3},
224 {'type': 'number', 'multipleOf': 5},
225 ],
226 }
227
228 Valid values for this definition are 3, 5, 6, ... but not 15 for example.
229 """
230 self._any_or_one_of_count += 1
231 count = self._any_or_one_of_count
232 self.l('{variable}_one_of_count{count} = 0', count=count)
233 for definition_item in self._definition['oneOf']:
234 # When we know it's failing (one of means exactly once), we do not need to do another expensive try-except.
235 with self.l('if {variable}_one_of_count{count} < 2:', count=count, optimize=False):
236 with self.l('try:', optimize=False):
237 with self.trial_validation():
238 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True)
239 self.l('{variable}_one_of_count{count} += 1', count=count)
240 self.l('except {}: pass', VALIDATION_EXCEPTIONS)
241
242 with self.l('if {variable}_one_of_count{count} != 1:', count=count):
243 dynamic = '" (" + str({variable}_one_of_count{}) + " matches found)"'
244 self.exc('{name} must be valid exactly by one definition', count, append_to_msg=dynamic, rule='oneOf')
245
246 def generate_not(self):
247 """
248 Means that value have not to be valid by this definition.
249
250 .. code-block:: python
251
252 {'not': {'type': 'null'}}
253
254 Valid values for this definition are 'hello', 42, {} ... but not None.
255
256 Since draft 06 definition can be boolean. False means nothing, True
257 means everything is invalid.
258 """
259 not_definition = self._definition['not']
260 if not_definition is True:
261 self.exc('{name} must not be there', rule='not')
262 elif not_definition is False:
263 return
264 elif not not_definition:
265 self.exc('{name} must NOT match a disallowed definition', rule='not')
266 else:
267 with self.l('try:', optimize=False):
268 code_len = len(self._code)
269 with self.trial_validation():
270 self.generate_func_code_block(not_definition, self._variable, self._variable_name, clear_variables=True)
271 if len(self._code) == code_len:
272 self.l('pass')
273 self.l('except {}: pass', VALIDATION_EXCEPTIONS)
274 with self.l('else:'):
275 self.exc('{name} must NOT match a disallowed definition', rule='not')
276
277 def generate_min_length(self):
278 with self.l('if isinstance({variable}, str):'):
279 self.create_variable_with_length()
280 if not isinstance(self._definition['minLength'], (int, float)):
281 raise JsonSchemaDefinitionException('minLength must be a number')
282 with self.l('if {variable}_len < {minLength}:'):
283 self.exc('{name} must be longer than or equal to {minLength} characters', rule='minLength')
284
285 def generate_max_length(self):
286 with self.l('if isinstance({variable}, str):'):
287 self.create_variable_with_length()
288 if not isinstance(self._definition['maxLength'], (int, float)):
289 raise JsonSchemaDefinitionException('maxLength must be a number')
290 with self.l('if {variable}_len > {maxLength}:'):
291 self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength')
292
293 def generate_pattern(self):
294 with self.l('if isinstance({variable}, str):'):
295 pattern = self._definition['pattern']
296 safe_pattern = self.e(pattern)
297 end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern)
298 self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern)
299 with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)):
300 self.exc('{name} must match pattern {}', safe_pattern, rule='pattern')
301
302 def generate_format(self):
303 """
304 Means that value have to be in specified format. For example date, email or other.
305
306 .. code-block:: python
307
308 {'format': 'email'}
309
310 Valid value for this definition is user@example.com but not @username
311 """
312 if not self._use_formats:
313 return
314 format_ = self._definition['format']
315 if format_ not in self._custom_formats and format_ not in self.FORMAT_REGEXS and format_ != 'regex':
316 return
317 with self.l('if isinstance({variable}, str):'):
318 # Checking custom formats - user is allowed to override default formats.
319 if format_ in self._custom_formats:
320 custom_format = self._custom_formats[format_]
321 if isinstance(custom_format, str):
322 self._generate_format(format_, format_ + '_re_pattern', custom_format)
323 else:
324 with self.l('if not custom_formats["{}"]({variable}):', format_):
325 self.exc('{name} must be {}', format_, rule='format')
326 elif format_ in self.FORMAT_REGEXS:
327 format_regex = self.FORMAT_REGEXS[format_]
328 self._generate_format(format_, format_ + '_re_pattern', format_regex)
329 # Format regex is used only in meta schemas.
330 elif format_ == 'regex':
331 self._extra_imports_lines = ['import re']
332 with self.l('try:', optimize=False):
333 self.l('re.compile({variable})')
334 with self.l('except Exception:'):
335 self.exc('{name} must be a valid regex', rule='format')
336
337
338 def _generate_format(self, format_name, regexp_name, regexp):
339 if self._definition['format'] == format_name:
340 if not regexp_name in self._compile_regexps:
341 self._compile_regexps[regexp_name] = re.compile(regexp)
342 with self.l('if not REGEX_PATTERNS["{}"].match({variable}):', regexp_name):
343 self.exc('{name} must be {}', format_name, rule='format')
344
345 def generate_minimum(self):
346 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
347 if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)):
348 raise JsonSchemaDefinitionException('minimum must be a number')
349 if self._definition.get('exclusiveMinimum', False):
350 with self.l('if {variable} <= {minimum}:'):
351 self.exc('{name} must be bigger than {minimum}', rule='minimum')
352 else:
353 with self.l('if {variable} < {minimum}:'):
354 self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum')
355
356 def generate_maximum(self):
357 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
358 if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)):
359 raise JsonSchemaDefinitionException('maximum must be a number')
360 if self._definition.get('exclusiveMaximum', False):
361 with self.l('if {variable} >= {maximum}:'):
362 self.exc('{name} must be smaller than {maximum}', rule='maximum')
363 else:
364 with self.l('if {variable} > {maximum}:'):
365 self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum')
366
367 def generate_multiple_of(self):
368 with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'):
369 if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)):
370 raise JsonSchemaDefinitionException('multipleOf must be a number')
371 # Integer division through a float loses precision or can overflow.
372 with self.l('if isinstance({variable}, int) and isinstance({multipleOf}, int):'):
373 with self.l('if {variable} % {multipleOf} != 0:'):
374 self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf')
375 with self.l('else:'):
376 # For proper multiplication check of floats we need to use decimals,
377 # because for example 19.01 / 0.01 = 1901.0000000000002.
378 if isinstance(self._definition['multipleOf'], float):
379 self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))')
380 else:
381 self.l('quotient = {variable} / {multipleOf}')
382 with self.l('if int(quotient) != quotient:'):
383 self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf')
384 # For example, 1e308 / 0.123456789
385 with self.l('if {variable} / {multipleOf} == float("inf"):'):
386 self.exc('inifinity reached', rule='multipleOf')
387
388 def generate_min_items(self):
389 self.create_variable_is_list()
390 with self.l('if {variable}_is_list:'):
391 if not isinstance(self._definition['minItems'], (int, float)):
392 raise JsonSchemaDefinitionException('minItems must be a number')
393 self.create_variable_with_length()
394 with self.l('if {variable}_len < {minItems}:'):
395 self.exc('{name} must contain at least {minItems} items', rule='minItems')
396
397 def generate_max_items(self):
398 self.create_variable_is_list()
399 with self.l('if {variable}_is_list:'):
400 if not isinstance(self._definition['maxItems'], (int, float)):
401 raise JsonSchemaDefinitionException('maxItems must be a number')
402 self.create_variable_with_length()
403 with self.l('if {variable}_len > {maxItems}:'):
404 self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems')
405
406 def generate_unique_items(self):
407 """
408 With Python 3.4 module ``timeit`` recommended this solutions:
409
410 .. code-block:: python
411
412 >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000)
413 0.5839540958404541
414 >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000)
415 0.7094449996948242
416 >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000)
417 2.0819358825683594
418 >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000)
419 2.1439831256866455
420 """
421 unique_definition = self._definition['uniqueItems']
422 if not unique_definition:
423 return
424
425 self.create_variable_is_list()
426 with self.l('if {variable}_is_list:'):
427 self.l(
428 'def fn(var): '
429 'return frozenset(dict((k, fn(v)) '
430 'for k, v in var.items()).items()) '
431 'if hasattr(var, "items") else tuple(fn(v) '
432 'for v in var) '
433 'if isinstance(var, (dict, list)) else (bool, var) '
434 'if isinstance(var, bool) else var')
435 self.create_variable_with_length()
436 with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'):
437 self.exc('{name} must contain unique items', rule='uniqueItems')
438
439 def generate_items(self):
440 """
441 Means array is valid only when all items are valid by this definition.
442
443 .. code-block:: python
444
445 {
446 'items': [
447 {'type': 'integer'},
448 {'type': 'string'},
449 ],
450 }
451
452 Valid arrays are those with integers or strings, nothing else.
453
454 Since draft 06 definition can be also boolean. True means nothing, False
455 means everything is invalid.
456 """
457 items_definition = self._definition['items']
458 if items_definition is True:
459 return
460
461 self.create_variable_is_list()
462 with self.l('if {variable}_is_list:'):
463 self.create_variable_with_length()
464 if items_definition is False:
465 with self.l('if {variable}:'):
466 self.exc('{name} must not be there', rule='items')
467 elif isinstance(items_definition, list):
468 for idx, item_definition in enumerate(items_definition):
469 with self.l('if {variable}_len > {}:', idx):
470 self.l('{variable}__{0} = {variable}[{0}]', idx)
471 self.generate_func_code_block(
472 item_definition,
473 '{}__{}'.format(self._variable, idx),
474 '{}[{}]'.format(self._variable_name, idx),
475 )
476 if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition:
477 self.l('else: {variable}.append({})', repr_default(item_definition['default']))
478
479 if 'additionalItems' in self._definition:
480 if self._definition['additionalItems'] is False:
481 with self.l('if {variable}_len > {}:', len(items_definition)):
482 self.exc('{name} must contain only specified items', rule='items')
483 else:
484 with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)):
485 code_len = len(self._code)
486 self.generate_func_code_block(
487 self._definition['additionalItems'],
488 '{}_item'.format(self._variable),
489 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
490 )
491 if len(self._code) == code_len:
492 self.l('pass')
493 else:
494 if items_definition:
495 with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'):
496 code_len = len(self._code)
497 self.generate_func_code_block(
498 items_definition,
499 '{}_item'.format(self._variable),
500 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
501 )
502 if len(self._code) == code_len:
503 self.l('pass')
504
505 def generate_min_properties(self):
506 self.create_variable_is_dict()
507 with self.l('if {variable}_is_dict:'):
508 if not isinstance(self._definition['minProperties'], (int, float)):
509 raise JsonSchemaDefinitionException('minProperties must be a number')
510 self.create_variable_with_length()
511 with self.l('if {variable}_len < {minProperties}:'):
512 self.exc('{name} must contain at least {minProperties} properties', rule='minProperties')
513
514 def generate_max_properties(self):
515 self.create_variable_is_dict()
516 with self.l('if {variable}_is_dict:'):
517 if not isinstance(self._definition['maxProperties'], (int, float)):
518 raise JsonSchemaDefinitionException('maxProperties must be a number')
519 self.create_variable_with_length()
520 with self.l('if {variable}_len > {maxProperties}:'):
521 self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties')
522
523 def generate_required(self):
524 self.create_variable_is_dict()
525 with self.l('if {variable}_is_dict:'):
526 if not isinstance(self._definition['required'], (list, tuple)):
527 raise JsonSchemaDefinitionException('required must be an array')
528 if len(self._definition['required']) != len(set(self._definition['required'])):
529 raise JsonSchemaDefinitionException('required must contain unique elements')
530 if not self._definition.get('additionalProperties', True):
531 not_possible = [
532 prop
533 for prop in self._definition['required']
534 if
535 prop not in self._definition.get('properties', {})
536 and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {}))
537 ]
538 if not_possible:
539 raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible))
540 self.l('{variable}__missing_keys = set({required}) - {variable}.keys()')
541 with self.l('if {variable}__missing_keys:'):
542 dynamic = 'str(sorted({variable}__missing_keys)) + " properties"'
543 self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic)
544
545 def generate_properties(self):
546 """
547 Means object with defined keys.
548
549 .. code-block:: python
550
551 {
552 'properties': {
553 'key': {'type': 'number'},
554 },
555 }
556
557 Valid object is containing key called 'key' and value any number.
558 """
559 self.create_variable_is_dict()
560 with self.l('if {variable}_is_dict:'):
561 self.create_variable_keys()
562 for key, prop_definition in self._definition['properties'].items():
563 key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key)
564 if not isinstance(prop_definition, (dict, bool)):
565 raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name))
566 with self.l('if "{}" in {variable}_keys:', self.e(key)):
567 self.l('{variable}_keys.remove("{}")', self.e(key))
568 self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key))
569 self.generate_func_code_block(
570 prop_definition,
571 '{}__{}'.format(self._variable, key_name),
572 '{}.{}'.format(self._variable_name, self.e(key).replace('{', '{{').replace('}', '}}')),
573 clear_variables=True,
574 )
575 if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition:
576 self.l('else: {variable}["{}"] = {}', self.e(key), repr_default(prop_definition['default']))
577
578 def generate_pattern_properties(self):
579 """
580 Means object with defined keys as patterns.
581
582 .. code-block:: python
583
584 {
585 'patternProperties': {
586 '^x': {'type': 'number'},
587 },
588 }
589
590 Valid object is containing key starting with a 'x' and value any number.
591 """
592 self.create_variable_is_dict()
593 with self.l('if {variable}_is_dict:'):
594 self.create_variable_keys()
595 pattern_prop_definition = self._definition['patternProperties']
596 if pattern_prop_definition == {}:
597 return
598 for pattern, definition in pattern_prop_definition.items():
599 self._compile_regexps[pattern] = re.compile(pattern)
600 with self.l('for {variable}_key, {variable}_val in {variable}.items():'):
601 for pattern, definition in self._definition['patternProperties'].items():
602 with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)):
603 with self.l('if {variable}_key in {variable}_keys:'):
604 self.l('{variable}_keys.remove({variable}_key)')
605 self.generate_func_code_block(
606 definition,
607 '{}_val'.format(self._variable),
608 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
609 clear_variables=True,
610 )
611
612 def generate_additional_properties(self):
613 """
614 Means object with keys with values defined by definition.
615
616 .. code-block:: python
617
618 {
619 'properties': {
620 'key': {'type': 'number'},
621 }
622 'additionalProperties': {'type': 'string'},
623 }
624
625 Valid object is containing key called 'key' and it's value any number and
626 any other key with any string.
627 """
628 self.create_variable_is_dict()
629 with self.l('if {variable}_is_dict:'):
630 self.create_variable_keys()
631 add_prop_definition = self._definition["additionalProperties"]
632 if add_prop_definition is True or add_prop_definition == {}:
633 return
634 if add_prop_definition:
635 properties_keys = list(self._definition.get("properties", {}).keys())
636 with self.l('for {variable}_key in {variable}_keys:'):
637 with self.l('if {variable}_key not in {}:', properties_keys):
638 self.l('{variable}_value = {variable}.get({variable}_key)')
639 self.generate_func_code_block(
640 add_prop_definition,
641 '{}_value'.format(self._variable),
642 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
643 )
644 else:
645 with self.l('if {variable}_keys:'):
646 self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties')
647
648 def generate_dependencies(self):
649 """
650 Means when object has property, it needs to have also other property.
651
652 .. code-block:: python
653
654 {
655 'dependencies': {
656 'bar': ['foo'],
657 },
658 }
659
660 Valid object is containing only foo, both bar and foo or none of them, but not
661 object with only bar.
662
663 Since draft 06 definition can be boolean or empty array. True and empty array
664 means nothing, False means that key cannot be there at all.
665 """
666 self.create_variable_is_dict()
667 with self.l('if {variable}_is_dict:'):
668 is_empty = True
669 for key, values in self._definition["dependencies"].items():
670 if values == [] or values is True:
671 continue
672 is_empty = False
673 with self.l('if "{}" in {variable}:', self.e(key)):
674 if values is False:
675 self.exc('{} in {name} must not be there', self.e(key), rule='dependencies')
676 elif isinstance(values, list):
677 for value in values:
678 with self.l('if "{}" not in {variable}:', self.e(value)):
679 self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies')
680 else:
681 code_len = len(self._code)
682 self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True)
683 if len(self._code) == code_len:
684 self.l('pass')
685 if is_empty:
686 self.l('pass')