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):
38 super().__init__(definition, resolver, detailed_exceptions)
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 self.exc('{name} must NOT match a disallowed definition', rule='not')
215 else:
216 with self.l('try:', optimize=False):
217 self.generate_func_code_block(not_definition, self._variable, self._variable_name)
218 self.l('except JsonSchemaValueException: pass')
219 with self.l('else:'):
220 self.exc('{name} must NOT match a disallowed definition', rule='not')
221
222 def generate_min_length(self):
223 with self.l('if isinstance({variable}, str):'):
224 self.create_variable_with_length()
225 if not isinstance(self._definition['minLength'], (int, float)):
226 raise JsonSchemaDefinitionException('minLength must be a number')
227 with self.l('if {variable}_len < {minLength}:'):
228 self.exc('{name} must be longer than or equal to {minLength} characters', rule='minLength')
229
230 def generate_max_length(self):
231 with self.l('if isinstance({variable}, str):'):
232 self.create_variable_with_length()
233 if not isinstance(self._definition['maxLength'], (int, float)):
234 raise JsonSchemaDefinitionException('maxLength must be a number')
235 with self.l('if {variable}_len > {maxLength}:'):
236 self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength')
237
238 def generate_pattern(self):
239 with self.l('if isinstance({variable}, str):'):
240 pattern = self._definition['pattern']
241 safe_pattern = pattern.replace('\\', '\\\\').replace('"', '\\"')
242 end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern)
243 self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern)
244 with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)):
245 self.exc('{name} must match pattern {}', safe_pattern, rule='pattern')
246
247 def generate_format(self):
248 """
249 Means that value have to be in specified format. For example date, email or other.
250
251 .. code-block:: python
252
253 {'format': 'email'}
254
255 Valid value for this definition is user@example.com but not @username
256 """
257 if not self._use_formats:
258 return
259 with self.l('if isinstance({variable}, str):'):
260 format_ = self._definition['format']
261 # Checking custom formats - user is allowed to override default formats.
262 if format_ in self._custom_formats:
263 custom_format = self._custom_formats[format_]
264 if isinstance(custom_format, str):
265 self._generate_format(format_, format_ + '_re_pattern', custom_format)
266 else:
267 with self.l('if not custom_formats["{}"]({variable}):', format_):
268 self.exc('{name} must be {}', format_, rule='format')
269 elif format_ in self.FORMAT_REGEXS:
270 format_regex = self.FORMAT_REGEXS[format_]
271 self._generate_format(format_, format_ + '_re_pattern', format_regex)
272 # Format regex is used only in meta schemas.
273 elif format_ == 'regex':
274 self._extra_imports_lines = ['import re']
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 # For example, 1e308 / 0.123456789
325 with self.l('if {variable} / {multipleOf} == float("inf"):'):
326 self.exc('inifinity reached', rule='multipleOf')
327
328 def generate_min_items(self):
329 self.create_variable_is_list()
330 with self.l('if {variable}_is_list:'):
331 if not isinstance(self._definition['minItems'], (int, float)):
332 raise JsonSchemaDefinitionException('minItems must be a number')
333 self.create_variable_with_length()
334 with self.l('if {variable}_len < {minItems}:'):
335 self.exc('{name} must contain at least {minItems} items', rule='minItems')
336
337 def generate_max_items(self):
338 self.create_variable_is_list()
339 with self.l('if {variable}_is_list:'):
340 if not isinstance(self._definition['maxItems'], (int, float)):
341 raise JsonSchemaDefinitionException('maxItems must be a number')
342 self.create_variable_with_length()
343 with self.l('if {variable}_len > {maxItems}:'):
344 self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems')
345
346 def generate_unique_items(self):
347 """
348 With Python 3.4 module ``timeit`` recommended this solutions:
349
350 .. code-block:: python
351
352 >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000)
353 0.5839540958404541
354 >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000)
355 0.7094449996948242
356 >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000)
357 2.0819358825683594
358 >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000)
359 2.1439831256866455
360 """
361 unique_definition = self._definition['uniqueItems']
362 if not unique_definition:
363 return
364
365 self.create_variable_is_list()
366 with self.l('if {variable}_is_list:'):
367 self.l(
368 'def fn(var): '
369 'return frozenset(dict((k, fn(v)) '
370 'for k, v in var.items()).items()) '
371 'if hasattr(var, "items") else tuple(fn(v) '
372 'for v in var) '
373 'if isinstance(var, (dict, list)) else str(var) '
374 'if isinstance(var, bool) else var')
375 self.create_variable_with_length()
376 with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'):
377 self.exc('{name} must contain unique items', rule='uniqueItems')
378
379 def generate_items(self):
380 """
381 Means array is valid only when all items are valid by this definition.
382
383 .. code-block:: python
384
385 {
386 'items': [
387 {'type': 'integer'},
388 {'type': 'string'},
389 ],
390 }
391
392 Valid arrays are those with integers or strings, nothing else.
393
394 Since draft 06 definition can be also boolean. True means nothing, False
395 means everything is invalid.
396 """
397 items_definition = self._definition['items']
398 if items_definition is True:
399 return
400
401 self.create_variable_is_list()
402 with self.l('if {variable}_is_list:'):
403 self.create_variable_with_length()
404 if items_definition is False:
405 with self.l('if {variable}:'):
406 self.exc('{name} must not be there', rule='items')
407 elif isinstance(items_definition, list):
408 for idx, item_definition in enumerate(items_definition):
409 with self.l('if {variable}_len > {}:', idx):
410 self.l('{variable}__{0} = {variable}[{0}]', idx)
411 self.generate_func_code_block(
412 item_definition,
413 '{}__{}'.format(self._variable, idx),
414 '{}[{}]'.format(self._variable_name, idx),
415 )
416 if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition:
417 self.l('else: {variable}.append({})', repr(item_definition['default']))
418
419 if 'additionalItems' in self._definition:
420 if self._definition['additionalItems'] is False:
421 with self.l('if {variable}_len > {}:', len(items_definition)):
422 self.exc('{name} must contain only specified items', rule='items')
423 else:
424 with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)):
425 count = self.generate_func_code_block(
426 self._definition['additionalItems'],
427 '{}_item'.format(self._variable),
428 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
429 )
430 if count == 0:
431 self.l('pass')
432 else:
433 if items_definition:
434 with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'):
435 count = self.generate_func_code_block(
436 items_definition,
437 '{}_item'.format(self._variable),
438 '{}[{{{}_x}}]'.format(self._variable_name, self._variable),
439 )
440 if count == 0:
441 self.l('pass')
442
443 def generate_min_properties(self):
444 self.create_variable_is_dict()
445 with self.l('if {variable}_is_dict:'):
446 if not isinstance(self._definition['minProperties'], (int, float)):
447 raise JsonSchemaDefinitionException('minProperties must be a number')
448 self.create_variable_with_length()
449 with self.l('if {variable}_len < {minProperties}:'):
450 self.exc('{name} must contain at least {minProperties} properties', rule='minProperties')
451
452 def generate_max_properties(self):
453 self.create_variable_is_dict()
454 with self.l('if {variable}_is_dict:'):
455 if not isinstance(self._definition['maxProperties'], (int, float)):
456 raise JsonSchemaDefinitionException('maxProperties must be a number')
457 self.create_variable_with_length()
458 with self.l('if {variable}_len > {maxProperties}:'):
459 self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties')
460
461 def generate_required(self):
462 self.create_variable_is_dict()
463 with self.l('if {variable}_is_dict:'):
464 if not isinstance(self._definition['required'], (list, tuple)):
465 raise JsonSchemaDefinitionException('required must be an array')
466 if len(self._definition['required']) != len(set(self._definition['required'])):
467 raise JsonSchemaDefinitionException('required must contain unique elements')
468 if not self._definition.get('additionalProperties', True):
469 not_possible = [
470 prop
471 for prop in self._definition['required']
472 if
473 prop not in self._definition.get('properties', {})
474 and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {}))
475 ]
476 if not_possible:
477 raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible))
478 self.l('{variable}__missing_keys = set({required}) - {variable}.keys()')
479 with self.l('if {variable}__missing_keys:'):
480 dynamic = 'str(sorted({variable}__missing_keys)) + " properties"'
481 self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic)
482
483 def generate_properties(self):
484 """
485 Means object with defined keys.
486
487 .. code-block:: python
488
489 {
490 'properties': {
491 'key': {'type': 'number'},
492 },
493 }
494
495 Valid object is containing key called 'key' and value any number.
496 """
497 self.create_variable_is_dict()
498 with self.l('if {variable}_is_dict:'):
499 self.create_variable_keys()
500 for key, prop_definition in self._definition['properties'].items():
501 key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key)
502 if not isinstance(prop_definition, (dict, bool)):
503 raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name))
504 with self.l('if "{}" in {variable}_keys:', self.e(key)):
505 self.l('{variable}_keys.remove("{}")', self.e(key))
506 self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key))
507 self.generate_func_code_block(
508 prop_definition,
509 '{}__{}'.format(self._variable, key_name),
510 '{}.{}'.format(self._variable_name, self.e(key)),
511 clear_variables=True,
512 )
513 if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition:
514 self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default']))
515
516 def generate_pattern_properties(self):
517 """
518 Means object with defined keys as patterns.
519
520 .. code-block:: python
521
522 {
523 'patternProperties': {
524 '^x': {'type': 'number'},
525 },
526 }
527
528 Valid object is containing key starting with a 'x' and value any number.
529 """
530 self.create_variable_is_dict()
531 with self.l('if {variable}_is_dict:'):
532 self.create_variable_keys()
533 for pattern, definition in self._definition['patternProperties'].items():
534 self._compile_regexps[pattern] = re.compile(pattern)
535 with self.l('for {variable}_key, {variable}_val in {variable}.items():'):
536 for pattern, definition in self._definition['patternProperties'].items():
537 with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)):
538 with self.l('if {variable}_key in {variable}_keys:'):
539 self.l('{variable}_keys.remove({variable}_key)')
540 self.generate_func_code_block(
541 definition,
542 '{}_val'.format(self._variable),
543 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
544 clear_variables=True,
545 )
546
547 def generate_additional_properties(self):
548 """
549 Means object with keys with values defined by definition.
550
551 .. code-block:: python
552
553 {
554 'properties': {
555 'key': {'type': 'number'},
556 }
557 'additionalProperties': {'type': 'string'},
558 }
559
560 Valid object is containing key called 'key' and it's value any number and
561 any other key with any string.
562 """
563 self.create_variable_is_dict()
564 with self.l('if {variable}_is_dict:'):
565 self.create_variable_keys()
566 add_prop_definition = self._definition["additionalProperties"]
567 if add_prop_definition is True or add_prop_definition == {}:
568 return
569 if add_prop_definition:
570 properties_keys = list(self._definition.get("properties", {}).keys())
571 with self.l('for {variable}_key in {variable}_keys:'):
572 with self.l('if {variable}_key not in {}:', properties_keys):
573 self.l('{variable}_value = {variable}.get({variable}_key)')
574 self.generate_func_code_block(
575 add_prop_definition,
576 '{}_value'.format(self._variable),
577 '{}.{{{}_key}}'.format(self._variable_name, self._variable),
578 )
579 else:
580 with self.l('if {variable}_keys:'):
581 self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties')
582
583 def generate_dependencies(self):
584 """
585 Means when object has property, it needs to have also other property.
586
587 .. code-block:: python
588
589 {
590 'dependencies': {
591 'bar': ['foo'],
592 },
593 }
594
595 Valid object is containing only foo, both bar and foo or none of them, but not
596 object with only bar.
597
598 Since draft 06 definition can be boolean or empty array. True and empty array
599 means nothing, False means that key cannot be there at all.
600 """
601 self.create_variable_is_dict()
602 with self.l('if {variable}_is_dict:'):
603 is_empty = True
604 for key, values in self._definition["dependencies"].items():
605 if values == [] or values is True:
606 continue
607 is_empty = False
608 with self.l('if "{}" in {variable}:', self.e(key)):
609 if values is False:
610 self.exc('{} in {name} must not be there', key, rule='dependencies')
611 elif isinstance(values, list):
612 for value in values:
613 with self.l('if "{}" not in {variable}:', self.e(value)):
614 self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies')
615 else:
616 self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True)
617 if is_empty:
618 self.l('pass')