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]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-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 code_len = len(self._code)
260 self.generate_func_code_block(not_definition, self._variable, self._variable_name, clear_variables=True)
261 if len(self._code) == code_len:
262 self.l('pass')
263 self.l('except JsonSchemaValueException: pass')
264 with self.l('else:'):
265 self.exc('{name} must NOT match a disallowed definition', rule='not')
266
267 def generate_min_length(self):
268 with self.l('if isinstance({variable}, str):'):
269 self.create_variable_with_length()
270 if not isinstance(self._definition['minLength'], (int, float)):
271 raise JsonSchemaDefinitionException('minLength must be a number')
272 with self.l('if {variable}_len < {minLength}:'):
273 self.exc('{name} must be longer than or equal to {minLength} characters', rule='minLength')
274
275 def generate_max_length(self):
276 with self.l('if isinstance({variable}, str):'):
277 self.create_variable_with_length()
278 if not isinstance(self._definition['maxLength'], (int, float)):
279 raise JsonSchemaDefinitionException('maxLength must be a number')
280 with self.l('if {variable}_len > {maxLength}:'):
281 self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength')
282
283 def generate_pattern(self):
284 with self.l('if isinstance({variable}, str):'):
285 pattern = self._definition['pattern']
286 safe_pattern = pattern.replace('\\', '\\\\').replace('"', '\\"')
287 end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern)
288 self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern)
289 with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)):
290 self.exc('{name} must match pattern {}', safe_pattern, rule='pattern')
291
292 def generate_format(self):
293 """
294 Means that value have to be in specified format. For example date, email or other.
295
296 .. code-block:: python
297
298 {'format': 'email'}
299
300 Valid value for this definition is user@example.com but not @username
301 """
302 if not self._use_formats:
303 return
304 format_ = self._definition['format']
305 if format_ not in self._custom_formats and format_ not in self.FORMAT_REGEXS and format_ != 'regex':
306 return
307 with self.l('if isinstance({variable}, str):'):
308 # Checking custom formats - user is allowed to override default formats.
309 if format_ in self._custom_formats:
310 custom_format = self._custom_formats[format_]
311 if isinstance(custom_format, str):
312 self._generate_format(format_, format_ + '_re_pattern', custom_format)
313 else:
314 with self.l('if not custom_formats["{}"]({variable}):', format_):
315 self.exc('{name} must be {}', format_, rule='format')
316 elif format_ in self.FORMAT_REGEXS:
317 format_regex = self.FORMAT_REGEXS[format_]
318 self._generate_format(format_, format_ + '_re_pattern', format_regex)
319 # Format regex is used only in meta schemas.
320 elif format_ == 'regex':
321 self._extra_imports_lines = ['import re']
322 with self.l('try:', optimize=False):
323 self.l('re.compile({variable})')
324 with self.l('except Exception:'):
325 self.exc('{name} must be a valid regex', rule='format')
326
327
328 def _generate_format(self, format_name, regexp_name, regexp):
329 if self._definition['format'] == format_name:
330 if not regexp_name in self._compile_regexps:
331 self._compile_regexps[regexp_name] = re.compile(regexp)
332 with self.l('if not REGEX_PATTERNS["{}"].match({variable}):', regexp_name):
333 self.exc('{name} must be {}', format_name, rule='format')
334
335 def generate_minimum(self):
336 with self.l('if isinstance({variable}, (int, float, Decimal)):'):
337 if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)):
338 raise JsonSchemaDefinitionException('minimum must be a number')
339 if self._definition.get('exclusiveMinimum', False):
340 with self.l('if {variable} <= {minimum}:'):
341 self.exc('{name} must be bigger than {minimum}', rule='minimum')
342 else:
343 with self.l('if {variable} < {minimum}:'):
344 self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum')
345
346 def generate_maximum(self):
347 with self.l('if isinstance({variable}, (int, float, Decimal)):'):
348 if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)):
349 raise JsonSchemaDefinitionException('maximum must be a number')
350 if self._definition.get('exclusiveMaximum', False):
351 with self.l('if {variable} >= {maximum}:'):
352 self.exc('{name} must be smaller than {maximum}', rule='maximum')
353 else:
354 with self.l('if {variable} > {maximum}:'):
355 self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum')
356
357 def generate_multiple_of(self):
358 with self.l('if isinstance({variable}, (int, float, Decimal)):'):
359 if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)):
360 raise JsonSchemaDefinitionException('multipleOf must be a number')
361 # For proper multiplication check of floats we need to use decimals,
362 # because for example 19.01 / 0.01 = 1901.0000000000002.
363 if isinstance(self._definition['multipleOf'], float):
364 self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))')
365 else:
366 self.l('quotient = {variable} / {multipleOf}')
367 with self.l('if int(quotient) != quotient:'):
368 self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf')
369 # For example, 1e308 / 0.123456789
370 with self.l('if {variable} / {multipleOf} == float("inf"):'):
371 self.exc('inifinity reached', rule='multipleOf')
372
373 def generate_min_items(self):
374 self.create_variable_is_list()
375 with self.l('if {variable}_is_list:'):
376 if not isinstance(self._definition['minItems'], (int, float)):
377 raise JsonSchemaDefinitionException('minItems must be a number')
378 self.create_variable_with_length()
379 with self.l('if {variable}_len < {minItems}:'):
380 self.exc('{name} must contain at least {minItems} items', rule='minItems')
381
382 def generate_max_items(self):
383 self.create_variable_is_list()
384 with self.l('if {variable}_is_list:'):
385 if not isinstance(self._definition['maxItems'], (int, float)):
386 raise JsonSchemaDefinitionException('maxItems must be a number')
387 self.create_variable_with_length()
388 with self.l('if {variable}_len > {maxItems}:'):
389 self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems')
390
391 def generate_unique_items(self):
392 """
393 With Python 3.4 module ``timeit`` recommended this solutions:
394
395 .. code-block:: python
396
397 >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000)
398 0.5839540958404541
399 >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000)
400 0.7094449996948242
401 >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000)
402 2.0819358825683594
403 >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000)
404 2.1439831256866455
405 """
406 unique_definition = self._definition['uniqueItems']
407 if not unique_definition:
408 return
409
410 self.create_variable_is_list()
411 with self.l('if {variable}_is_list:'):
412 self.l(
413 'def fn(var): '
414 'return frozenset(dict((k, fn(v)) '
415 'for k, v in var.items()).items()) '
416 'if hasattr(var, "items") else tuple(fn(v) '
417 'for v in var) '
418 'if isinstance(var, (dict, list)) else str(var) '
419 'if isinstance(var, bool) else var')
420 self.create_variable_with_length()
421 with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'):
422 self.exc('{name} must contain unique items', rule='uniqueItems')
423
424 def generate_items(self):
425 """
426 Means array is valid only when all items are valid by this definition.
427
428 .. code-block:: python
429
430 {
431 'items': [
432 {'type': 'integer'},
433 {'type': 'string'},
434 ],
435 }
436
437 Valid arrays are those with integers or strings, nothing else.
438
439 Since draft 06 definition can be also boolean. True means nothing, False
440 means everything is invalid.
441 """
442 items_definition = self._definition['items']
443 if items_definition is True:
444 return
445
446 self.create_variable_is_list()
447 with self.l('if {variable}_is_list:'):
448 self.create_variable_with_length()
449 if items_definition is False:
450 with self.l('if {variable}:'):
451 self.exc('{name} must not be there', rule='items')
452 elif isinstance(items_definition, list):
453 for idx, item_definition in enumerate(items_definition):
454 with self.l('if {variable}_len > {}:', idx):
455 self.l('{variable}__{0} = {variable}[{0}]', idx)
456 self.generate_func_code_block(
457 item_definition,
458 '{}__{}'.format(self._variable, idx),
459 '{}[{}]'.format(self._variable_name, idx),
460 )
461 if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition:
462 self.l('else: {variable}.append({})', repr(item_definition['default']))
463
464 if 'additionalItems' in self._definition:
465 if self._definition['additionalItems'] is False:
466 with self.l('if {variable}_len > {}:', len(items_definition)):
467 self.exc('{name} must contain only specified items', rule='items')
468 else:
469 with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)):
470 code_len = len(self._code)
471 self.generate_func_code_block(
472 self._definition['additionalItems'],
473 '{}_item'.format(self._variable),
474 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
475 )
476 if len(self._code) == code_len:
477 self.l('pass')
478 else:
479 if items_definition:
480 with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'):
481 code_len = len(self._code)
482 self.generate_func_code_block(
483 items_definition,
484 '{}_item'.format(self._variable),
485 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
486 )
487 if len(self._code) == code_len:
488 self.l('pass')
489
490 def generate_min_properties(self):
491 self.create_variable_is_dict()
492 with self.l('if {variable}_is_dict:'):
493 if not isinstance(self._definition['minProperties'], (int, float)):
494 raise JsonSchemaDefinitionException('minProperties must be a number')
495 self.create_variable_with_length()
496 with self.l('if {variable}_len < {minProperties}:'):
497 self.exc('{name} must contain at least {minProperties} properties', rule='minProperties')
498
499 def generate_max_properties(self):
500 self.create_variable_is_dict()
501 with self.l('if {variable}_is_dict:'):
502 if not isinstance(self._definition['maxProperties'], (int, float)):
503 raise JsonSchemaDefinitionException('maxProperties must be a number')
504 self.create_variable_with_length()
505 with self.l('if {variable}_len > {maxProperties}:'):
506 self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties')
507
508 def generate_required(self):
509 self.create_variable_is_dict()
510 with self.l('if {variable}_is_dict:'):
511 if not isinstance(self._definition['required'], (list, tuple)):
512 raise JsonSchemaDefinitionException('required must be an array')
513 if len(self._definition['required']) != len(set(self._definition['required'])):
514 raise JsonSchemaDefinitionException('required must contain unique elements')
515 if not self._definition.get('additionalProperties', True):
516 not_possible = [
517 prop
518 for prop in self._definition['required']
519 if
520 prop not in self._definition.get('properties', {})
521 and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {}))
522 ]
523 if not_possible:
524 raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible))
525 self.l('{variable}__missing_keys = set({required}) - {variable}.keys()')
526 with self.l('if {variable}__missing_keys:'):
527 dynamic = 'str(sorted({variable}__missing_keys)) + " properties"'
528 self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic)
529
530 def generate_properties(self):
531 """
532 Means object with defined keys.
533
534 .. code-block:: python
535
536 {
537 'properties': {
538 'key': {'type': 'number'},
539 },
540 }
541
542 Valid object is containing key called 'key' and value any number.
543 """
544 self.create_variable_is_dict()
545 with self.l('if {variable}_is_dict:'):
546 self.create_variable_keys()
547 for key, prop_definition in self._definition['properties'].items():
548 key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key)
549 if not isinstance(prop_definition, (dict, bool)):
550 raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name))
551 with self.l('if "{}" in {variable}_keys:', self.e(key)):
552 self.l('{variable}_keys.remove("{}")', self.e(key))
553 self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key))
554 self.generate_func_code_block(
555 prop_definition,
556 '{}__{}'.format(self._variable, key_name),
557 '{}.{}'.format(self._variable_name, self.e(key)),
558 clear_variables=True,
559 )
560 if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition:
561 self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default']))
562
563 def generate_pattern_properties(self):
564 """
565 Means object with defined keys as patterns.
566
567 .. code-block:: python
568
569 {
570 'patternProperties': {
571 '^x': {'type': 'number'},
572 },
573 }
574
575 Valid object is containing key starting with a 'x' and value any number.
576 """
577 self.create_variable_is_dict()
578 with self.l('if {variable}_is_dict:'):
579 self.create_variable_keys()
580 pattern_prop_definition = self._definition['patternProperties']
581 if pattern_prop_definition == {}:
582 return
583 for pattern, definition in pattern_prop_definition.items():
584 self._compile_regexps[pattern] = re.compile(pattern)
585 with self.l('for {variable}_key, {variable}_val in {variable}.items():'):
586 for pattern, definition in self._definition['patternProperties'].items():
587 with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)):
588 with self.l('if {variable}_key in {variable}_keys:'):
589 self.l('{variable}_keys.remove({variable}_key)')
590 self.generate_func_code_block(
591 definition,
592 '{}_val'.format(self._variable),
593 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
594 clear_variables=True,
595 )
596
597 def generate_additional_properties(self):
598 """
599 Means object with keys with values defined by definition.
600
601 .. code-block:: python
602
603 {
604 'properties': {
605 'key': {'type': 'number'},
606 }
607 'additionalProperties': {'type': 'string'},
608 }
609
610 Valid object is containing key called 'key' and it's value any number and
611 any other key with any string.
612 """
613 self.create_variable_is_dict()
614 with self.l('if {variable}_is_dict:'):
615 self.create_variable_keys()
616 add_prop_definition = self._definition["additionalProperties"]
617 if add_prop_definition is True or add_prop_definition == {}:
618 return
619 if add_prop_definition:
620 properties_keys = list(self._definition.get("properties", {}).keys())
621 with self.l('for {variable}_key in {variable}_keys:'):
622 with self.l('if {variable}_key not in {}:', properties_keys):
623 self.l('{variable}_value = {variable}.get({variable}_key)')
624 self.generate_func_code_block(
625 add_prop_definition,
626 '{}_value'.format(self._variable),
627 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
628 )
629 else:
630 with self.l('if {variable}_keys:'):
631 self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties')
632
633 def generate_dependencies(self):
634 """
635 Means when object has property, it needs to have also other property.
636
637 .. code-block:: python
638
639 {
640 'dependencies': {
641 'bar': ['foo'],
642 },
643 }
644
645 Valid object is containing only foo, both bar and foo or none of them, but not
646 object with only bar.
647
648 Since draft 06 definition can be boolean or empty array. True and empty array
649 means nothing, False means that key cannot be there at all.
650 """
651 self.create_variable_is_dict()
652 with self.l('if {variable}_is_dict:'):
653 is_empty = True
654 for key, values in self._definition["dependencies"].items():
655 if values == [] or values is True:
656 continue
657 is_empty = False
658 with self.l('if "{}" in {variable}:', self.e(key)):
659 if values is False:
660 self.exc('{} in {name} must not be there', key, rule='dependencies')
661 elif isinstance(values, list):
662 for value in values:
663 with self.l('if "{}" not in {variable}:', self.e(value)):
664 self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies')
665 else:
666 code_len = len(self._code)
667 self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True)
668 if len(self._code) == code_len:
669 self.l('pass')
670 if is_empty:
671 self.l('pass')