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