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

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

302 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): 

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