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