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