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

330 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]?)\.){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, 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 self.generate_func_code_block(not_definition, self._variable, self._variable_name) 

260 self.l('except JsonSchemaValueException: pass') 

261 with self.l('else:'): 

262 self.exc('{name} must NOT match a disallowed definition', rule='not') 

263 

264 def generate_min_length(self): 

265 with self.l('if isinstance({variable}, str):'): 

266 self.create_variable_with_length() 

267 if not isinstance(self._definition['minLength'], (int, float)): 

268 raise JsonSchemaDefinitionException('minLength must be a number') 

269 with self.l('if {variable}_len < {minLength}:'): 

270 self.exc('{name} must be longer than or equal to {minLength} characters', rule='minLength') 

271 

272 def generate_max_length(self): 

273 with self.l('if isinstance({variable}, str):'): 

274 self.create_variable_with_length() 

275 if not isinstance(self._definition['maxLength'], (int, float)): 

276 raise JsonSchemaDefinitionException('maxLength must be a number') 

277 with self.l('if {variable}_len > {maxLength}:'): 

278 self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength') 

279 

280 def generate_pattern(self): 

281 with self.l('if isinstance({variable}, str):'): 

282 pattern = self._definition['pattern'] 

283 safe_pattern = pattern.replace('\\', '\\\\').replace('"', '\\"') 

284 end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern) 

285 self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern) 

286 with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)): 

287 self.exc('{name} must match pattern {}', safe_pattern, rule='pattern') 

288 

289 def generate_format(self): 

290 """ 

291 Means that value have to be in specified format. For example date, email or other. 

292 

293 .. code-block:: python 

294 

295 {'format': 'email'} 

296 

297 Valid value for this definition is user@example.com but not @username 

298 """ 

299 if not self._use_formats: 

300 return 

301 format_ = self._definition['format'] 

302 if format_ not in self._custom_formats and format_ not in self.FORMAT_REGEXS and format_ != 'regex': 

303 return 

304 with self.l('if isinstance({variable}, str):'): 

305 # Checking custom formats - user is allowed to override default formats. 

306 if format_ in self._custom_formats: 

307 custom_format = self._custom_formats[format_] 

308 if isinstance(custom_format, str): 

309 self._generate_format(format_, format_ + '_re_pattern', custom_format) 

310 else: 

311 with self.l('if not custom_formats["{}"]({variable}):', format_): 

312 self.exc('{name} must be {}', format_, rule='format') 

313 elif format_ in self.FORMAT_REGEXS: 

314 format_regex = self.FORMAT_REGEXS[format_] 

315 self._generate_format(format_, format_ + '_re_pattern', format_regex) 

316 # Format regex is used only in meta schemas. 

317 elif format_ == 'regex': 

318 self._extra_imports_lines = ['import re'] 

319 with self.l('try:', optimize=False): 

320 self.l('re.compile({variable})') 

321 with self.l('except Exception:'): 

322 self.exc('{name} must be a valid regex', rule='format') 

323 

324 

325 def _generate_format(self, format_name, regexp_name, regexp): 

326 if self._definition['format'] == format_name: 

327 if not regexp_name in self._compile_regexps: 

328 self._compile_regexps[regexp_name] = re.compile(regexp) 

329 with self.l('if not REGEX_PATTERNS["{}"].match({variable}):', regexp_name): 

330 self.exc('{name} must be {}', format_name, rule='format') 

331 

332 def generate_minimum(self): 

333 with self.l('if isinstance({variable}, (int, float, Decimal)):'): 

334 if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)): 

335 raise JsonSchemaDefinitionException('minimum must be a number') 

336 if self._definition.get('exclusiveMinimum', False): 

337 with self.l('if {variable} <= {minimum}:'): 

338 self.exc('{name} must be bigger than {minimum}', rule='minimum') 

339 else: 

340 with self.l('if {variable} < {minimum}:'): 

341 self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum') 

342 

343 def generate_maximum(self): 

344 with self.l('if isinstance({variable}, (int, float, Decimal)):'): 

345 if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)): 

346 raise JsonSchemaDefinitionException('maximum must be a number') 

347 if self._definition.get('exclusiveMaximum', False): 

348 with self.l('if {variable} >= {maximum}:'): 

349 self.exc('{name} must be smaller than {maximum}', rule='maximum') 

350 else: 

351 with self.l('if {variable} > {maximum}:'): 

352 self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum') 

353 

354 def generate_multiple_of(self): 

355 with self.l('if isinstance({variable}, (int, float, Decimal)):'): 

356 if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)): 

357 raise JsonSchemaDefinitionException('multipleOf must be a number') 

358 # For proper multiplication check of floats we need to use decimals, 

359 # because for example 19.01 / 0.01 = 1901.0000000000002. 

360 if isinstance(self._definition['multipleOf'], float): 

361 self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))') 

362 else: 

363 self.l('quotient = {variable} / {multipleOf}') 

364 with self.l('if int(quotient) != quotient:'): 

365 self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') 

366 # For example, 1e308 / 0.123456789 

367 with self.l('if {variable} / {multipleOf} == float("inf"):'): 

368 self.exc('inifinity reached', rule='multipleOf') 

369 

370 def generate_min_items(self): 

371 self.create_variable_is_list() 

372 with self.l('if {variable}_is_list:'): 

373 if not isinstance(self._definition['minItems'], (int, float)): 

374 raise JsonSchemaDefinitionException('minItems must be a number') 

375 self.create_variable_with_length() 

376 with self.l('if {variable}_len < {minItems}:'): 

377 self.exc('{name} must contain at least {minItems} items', rule='minItems') 

378 

379 def generate_max_items(self): 

380 self.create_variable_is_list() 

381 with self.l('if {variable}_is_list:'): 

382 if not isinstance(self._definition['maxItems'], (int, float)): 

383 raise JsonSchemaDefinitionException('maxItems must be a number') 

384 self.create_variable_with_length() 

385 with self.l('if {variable}_len > {maxItems}:'): 

386 self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems') 

387 

388 def generate_unique_items(self): 

389 """ 

390 With Python 3.4 module ``timeit`` recommended this solutions: 

391 

392 .. code-block:: python 

393 

394 >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000) 

395 0.5839540958404541 

396 >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000) 

397 0.7094449996948242 

398 >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000) 

399 2.0819358825683594 

400 >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000) 

401 2.1439831256866455 

402 """ 

403 unique_definition = self._definition['uniqueItems'] 

404 if not unique_definition: 

405 return 

406 

407 self.create_variable_is_list() 

408 with self.l('if {variable}_is_list:'): 

409 self.l( 

410 'def fn(var): ' 

411 'return frozenset(dict((k, fn(v)) ' 

412 'for k, v in var.items()).items()) ' 

413 'if hasattr(var, "items") else tuple(fn(v) ' 

414 'for v in var) ' 

415 'if isinstance(var, (dict, list)) else str(var) ' 

416 'if isinstance(var, bool) else var') 

417 self.create_variable_with_length() 

418 with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'): 

419 self.exc('{name} must contain unique items', rule='uniqueItems') 

420 

421 def generate_items(self): 

422 """ 

423 Means array is valid only when all items are valid by this definition. 

424 

425 .. code-block:: python 

426 

427 { 

428 'items': [ 

429 {'type': 'integer'}, 

430 {'type': 'string'}, 

431 ], 

432 } 

433 

434 Valid arrays are those with integers or strings, nothing else. 

435 

436 Since draft 06 definition can be also boolean. True means nothing, False 

437 means everything is invalid. 

438 """ 

439 items_definition = self._definition['items'] 

440 if items_definition is True: 

441 return 

442 

443 self.create_variable_is_list() 

444 with self.l('if {variable}_is_list:'): 

445 self.create_variable_with_length() 

446 if items_definition is False: 

447 with self.l('if {variable}:'): 

448 self.exc('{name} must not be there', rule='items') 

449 elif isinstance(items_definition, list): 

450 for idx, item_definition in enumerate(items_definition): 

451 with self.l('if {variable}_len > {}:', idx): 

452 self.l('{variable}__{0} = {variable}[{0}]', idx) 

453 self.generate_func_code_block( 

454 item_definition, 

455 '{}__{}'.format(self._variable, idx), 

456 '{}[{}]'.format(self._variable_name, idx), 

457 ) 

458 if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition: 

459 self.l('else: {variable}.append({})', repr(item_definition['default'])) 

460 

461 if 'additionalItems' in self._definition: 

462 if self._definition['additionalItems'] is False: 

463 with self.l('if {variable}_len > {}:', len(items_definition)): 

464 self.exc('{name} must contain only specified items', rule='items') 

465 else: 

466 with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)): 

467 count = self.generate_func_code_block( 

468 self._definition['additionalItems'], 

469 '{}_item'.format(self._variable), 

470 '{}[{{{}_x}}]'.format(self._variable_name, self._variable), 

471 ) 

472 if not count: 

473 self.l('pass') 

474 else: 

475 if items_definition: 

476 with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'): 

477 count = self.generate_func_code_block( 

478 items_definition, 

479 '{}_item'.format(self._variable), 

480 '{}[{{{}_x}}]'.format(self._variable_name, self._variable), 

481 ) 

482 if not count: 

483 self.l('pass') 

484 

485 def generate_min_properties(self): 

486 self.create_variable_is_dict() 

487 with self.l('if {variable}_is_dict:'): 

488 if not isinstance(self._definition['minProperties'], (int, float)): 

489 raise JsonSchemaDefinitionException('minProperties must be a number') 

490 self.create_variable_with_length() 

491 with self.l('if {variable}_len < {minProperties}:'): 

492 self.exc('{name} must contain at least {minProperties} properties', rule='minProperties') 

493 

494 def generate_max_properties(self): 

495 self.create_variable_is_dict() 

496 with self.l('if {variable}_is_dict:'): 

497 if not isinstance(self._definition['maxProperties'], (int, float)): 

498 raise JsonSchemaDefinitionException('maxProperties must be a number') 

499 self.create_variable_with_length() 

500 with self.l('if {variable}_len > {maxProperties}:'): 

501 self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties') 

502 

503 def generate_required(self): 

504 self.create_variable_is_dict() 

505 with self.l('if {variable}_is_dict:'): 

506 if not isinstance(self._definition['required'], (list, tuple)): 

507 raise JsonSchemaDefinitionException('required must be an array') 

508 if len(self._definition['required']) != len(set(self._definition['required'])): 

509 raise JsonSchemaDefinitionException('required must contain unique elements') 

510 if not self._definition.get('additionalProperties', True): 

511 not_possible = [ 

512 prop 

513 for prop in self._definition['required'] 

514 if 

515 prop not in self._definition.get('properties', {}) 

516 and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {})) 

517 ] 

518 if not_possible: 

519 raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible)) 

520 self.l('{variable}__missing_keys = set({required}) - {variable}.keys()') 

521 with self.l('if {variable}__missing_keys:'): 

522 dynamic = 'str(sorted({variable}__missing_keys)) + " properties"' 

523 self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic) 

524 

525 def generate_properties(self): 

526 """ 

527 Means object with defined keys. 

528 

529 .. code-block:: python 

530 

531 { 

532 'properties': { 

533 'key': {'type': 'number'}, 

534 }, 

535 } 

536 

537 Valid object is containing key called 'key' and value any number. 

538 """ 

539 self.create_variable_is_dict() 

540 with self.l('if {variable}_is_dict:'): 

541 self.create_variable_keys() 

542 for key, prop_definition in self._definition['properties'].items(): 

543 key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key) 

544 if not isinstance(prop_definition, (dict, bool)): 

545 raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name)) 

546 with self.l('if "{}" in {variable}_keys:', self.e(key)): 

547 self.l('{variable}_keys.remove("{}")', self.e(key)) 

548 self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key)) 

549 self.generate_func_code_block( 

550 prop_definition, 

551 '{}__{}'.format(self._variable, key_name), 

552 '{}.{}'.format(self._variable_name, self.e(key)), 

553 clear_variables=True, 

554 ) 

555 if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition: 

556 self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default'])) 

557 

558 def generate_pattern_properties(self): 

559 """ 

560 Means object with defined keys as patterns. 

561 

562 .. code-block:: python 

563 

564 { 

565 'patternProperties': { 

566 '^x': {'type': 'number'}, 

567 }, 

568 } 

569 

570 Valid object is containing key starting with a 'x' and value any number. 

571 """ 

572 self.create_variable_is_dict() 

573 with self.l('if {variable}_is_dict:'): 

574 self.create_variable_keys() 

575 pattern_prop_definition = self._definition['patternProperties'] 

576 if pattern_prop_definition == {}: 

577 return 

578 for pattern, definition in pattern_prop_definition.items(): 

579 self._compile_regexps[pattern] = re.compile(pattern) 

580 with self.l('for {variable}_key, {variable}_val in {variable}.items():'): 

581 for pattern, definition in self._definition['patternProperties'].items(): 

582 with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)): 

583 with self.l('if {variable}_key in {variable}_keys:'): 

584 self.l('{variable}_keys.remove({variable}_key)') 

585 self.generate_func_code_block( 

586 definition, 

587 '{}_val'.format(self._variable), 

588 '{}.{{{}_key}}'.format(self._variable_name, self._variable), 

589 clear_variables=True, 

590 ) 

591 

592 def generate_additional_properties(self): 

593 """ 

594 Means object with keys with values defined by definition. 

595 

596 .. code-block:: python 

597 

598 { 

599 'properties': { 

600 'key': {'type': 'number'}, 

601 } 

602 'additionalProperties': {'type': 'string'}, 

603 } 

604 

605 Valid object is containing key called 'key' and it's value any number and 

606 any other key with any string. 

607 """ 

608 self.create_variable_is_dict() 

609 with self.l('if {variable}_is_dict:'): 

610 self.create_variable_keys() 

611 add_prop_definition = self._definition["additionalProperties"] 

612 if add_prop_definition is True or add_prop_definition == {}: 

613 return 

614 if add_prop_definition: 

615 properties_keys = list(self._definition.get("properties", {}).keys()) 

616 with self.l('for {variable}_key in {variable}_keys:'): 

617 with self.l('if {variable}_key not in {}:', properties_keys): 

618 self.l('{variable}_value = {variable}.get({variable}_key)') 

619 self.generate_func_code_block( 

620 add_prop_definition, 

621 '{}_value'.format(self._variable), 

622 '{}.{{{}_key}}'.format(self._variable_name, self._variable), 

623 ) 

624 else: 

625 with self.l('if {variable}_keys:'): 

626 self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties') 

627 

628 def generate_dependencies(self): 

629 """ 

630 Means when object has property, it needs to have also other property. 

631 

632 .. code-block:: python 

633 

634 { 

635 'dependencies': { 

636 'bar': ['foo'], 

637 }, 

638 } 

639 

640 Valid object is containing only foo, both bar and foo or none of them, but not 

641 object with only bar. 

642 

643 Since draft 06 definition can be boolean or empty array. True and empty array 

644 means nothing, False means that key cannot be there at all. 

645 """ 

646 self.create_variable_is_dict() 

647 with self.l('if {variable}_is_dict:'): 

648 is_empty = True 

649 for key, values in self._definition["dependencies"].items(): 

650 if values == [] or values is True: 

651 continue 

652 is_empty = False 

653 with self.l('if "{}" in {variable}:', self.e(key)): 

654 if values is False: 

655 self.exc('{} in {name} must not be there', key, rule='dependencies') 

656 elif isinstance(values, list): 

657 for value in values: 

658 with self.l('if "{}" not in {variable}:', self.e(value)): 

659 self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies') 

660 else: 

661 self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True) 

662 if is_empty: 

663 self.l('pass')