Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fastjsonschema/draft04.py: 11%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

338 statements  

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')