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

290 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +0000

1import decimal 

2import re 

3 

4from .exceptions import JsonSchemaDefinitionException 

5from .generator import CodeGenerator, enforce_list 

6 

7 

8JSON_TYPE_TO_PYTHON_TYPE = { 

9 'null': 'NoneType', 

10 'boolean': 'bool', 

11 'number': 'int, float, Decimal', 

12 'integer': 'int', 

13 'string': 'str', 

14 'array': 'list, tuple', 

15 'object': 'dict', 

16} 

17 

18DOLLAR_FINDER = re.compile(r"(?<!\\)\$") # Finds any un-escaped $ (including inside []-sets) 

19 

20 

21# pylint: disable=too-many-instance-attributes,too-many-public-methods 

22class CodeGeneratorDraft04(CodeGenerator): 

23 # pylint: disable=line-too-long 

24 # I was thinking about using ipaddress module instead of regexps for example, but it's big 

25 # difference in performance. With a module I got this difference: over 100 ms with a module 

26 # vs. 9 ms with a regex! Other modules are also ineffective or not available in standard 

27 # library. Some regexps are not 100% precise but good enough, fast and without dependencies. 

28 FORMAT_REGEXS = { 

29 'date-time': r'^\d{4}-[01]\d-[0-3]\d(t|T)[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:[+-][0-2]\d:[0-5]\d|[+-][0-2]\d[0-5]\d|z|Z)\Z', 

30 'email': r'^[^@]+@[^@]+\.[^@]+\Z', 

31 'hostname': r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9])\Z', 

32 'ipv4': r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\Z', 

33 'ipv6': r'^(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)\Z', 

34 'uri': r'^\w+:(\/?\/?)[^\s]+\Z', 

35 } 

36 

37 def __init__(self, definition, resolver=None, formats={}, use_default=True): 

38 super().__init__(definition, resolver) 

39 self._custom_formats = formats 

40 self._use_default = use_default 

41 self._json_keywords_to_function.update(( 

42 ('type', self.generate_type), 

43 ('enum', self.generate_enum), 

44 ('allOf', self.generate_all_of), 

45 ('anyOf', self.generate_any_of), 

46 ('oneOf', self.generate_one_of), 

47 ('not', self.generate_not), 

48 ('minLength', self.generate_min_length), 

49 ('maxLength', self.generate_max_length), 

50 ('pattern', self.generate_pattern), 

51 ('format', self.generate_format), 

52 ('minimum', self.generate_minimum), 

53 ('maximum', self.generate_maximum), 

54 ('multipleOf', self.generate_multiple_of), 

55 ('minItems', self.generate_min_items), 

56 ('maxItems', self.generate_max_items), 

57 ('uniqueItems', self.generate_unique_items), 

58 ('items', self.generate_items), 

59 ('minProperties', self.generate_min_properties), 

60 ('maxProperties', self.generate_max_properties), 

61 ('required', self.generate_required), 

62 # Check dependencies before properties generates default values. 

63 ('dependencies', self.generate_dependencies), 

64 ('properties', self.generate_properties), 

65 ('patternProperties', self.generate_pattern_properties), 

66 ('additionalProperties', self.generate_additional_properties), 

67 )) 

68 self._any_or_one_of_count = 0 

69 

70 @property 

71 def global_state(self): 

72 res = super().global_state 

73 res['custom_formats'] = self._custom_formats 

74 return res 

75 

76 def generate_type(self): 

77 """ 

78 Validation of type. Can be one type or list of types. 

79 

80 .. code-block:: python 

81 

82 {'type': 'string'} 

83 {'type': ['string', 'number']} 

84 """ 

85 types = enforce_list(self._definition['type']) 

86 try: 

87 python_types = ', '.join(JSON_TYPE_TO_PYTHON_TYPE[t] for t in types) 

88 except KeyError as exc: 

89 raise JsonSchemaDefinitionException('Unknown type: {}'.format(exc)) 

90 

91 extra = '' 

92 if ('number' in types or 'integer' in types) and 'boolean' not in types: 

93 extra = ' or isinstance({variable}, bool)'.format(variable=self._variable) 

94 

95 with self.l('if not isinstance({variable}, ({})){}:', python_types, extra): 

96 self.exc('{name} must be {}', ' or '.join(types), rule='type') 

97 

98 def generate_enum(self): 

99 """ 

100 Means that only value specified in the enum is valid. 

101 

102 .. code-block:: python 

103 

104 { 

105 'enum': ['a', 'b'], 

106 } 

107 """ 

108 enum = self._definition['enum'] 

109 if not isinstance(enum, (list, tuple)): 

110 raise JsonSchemaDefinitionException('enum must be an array') 

111 with self.l('if {variable} not in {enum}:'): 

112 self.exc('{name} must be one of {}', self.e(enum), rule='enum') 

113 

114 def generate_all_of(self): 

115 """ 

116 Means that value have to be valid by all of those definitions. It's like put it in 

117 one big definition. 

118 

119 .. code-block:: python 

120 

121 { 

122 'allOf': [ 

123 {'type': 'number'}, 

124 {'minimum': 5}, 

125 ], 

126 } 

127 

128 Valid values for this definition are 5, 6, 7, ... but not 4 or 'abc' for example. 

129 """ 

130 for definition_item in self._definition['allOf']: 

131 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 

132 

133 def generate_any_of(self): 

134 """ 

135 Means that value have to be valid by any of those definitions. It can also be valid 

136 by all of them. 

137 

138 .. code-block:: python 

139 

140 { 

141 'anyOf': [ 

142 {'type': 'number', 'minimum': 10}, 

143 {'type': 'number', 'maximum': 5}, 

144 ], 

145 } 

146 

147 Valid values for this definition are 3, 4, 5, 10, 11, ... but not 8 for example. 

148 """ 

149 self._any_or_one_of_count += 1 

150 count = self._any_or_one_of_count 

151 self.l('{variable}_any_of_count{count} = 0', count=count) 

152 for definition_item in self._definition['anyOf']: 

153 # When we know it's passing (at least once), we do not need to do another expensive try-except. 

154 with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False): 

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

156 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 

157 self.l('{variable}_any_of_count{count} += 1', count=count) 

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

159 

160 with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False): 

161 self.exc('{name} cannot be validated by any definition', rule='anyOf') 

162 

163 def generate_one_of(self): 

164 """ 

165 Means that value have to be valid by only one of those definitions. It can't be valid 

166 by two or more of them. 

167 

168 .. code-block:: python 

169 

170 { 

171 'oneOf': [ 

172 {'type': 'number', 'multipleOf': 3}, 

173 {'type': 'number', 'multipleOf': 5}, 

174 ], 

175 } 

176 

177 Valid values for this definition are 3, 5, 6, ... but not 15 for example. 

178 """ 

179 self._any_or_one_of_count += 1 

180 count = self._any_or_one_of_count 

181 self.l('{variable}_one_of_count{count} = 0', count=count) 

182 for definition_item in self._definition['oneOf']: 

183 # When we know it's failing (one of means exactly once), we do not need to do another expensive try-except. 

184 with self.l('if {variable}_one_of_count{count} < 2:', count=count, optimize=False): 

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

186 self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 

187 self.l('{variable}_one_of_count{count} += 1', count=count) 

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

189 

190 with self.l('if {variable}_one_of_count{count} != 1:', count=count): 

191 dynamic = '" (" + str({variable}_one_of_count{}) + " matches found)"' 

192 self.exc('{name} must be valid exactly by one definition', count, append_to_msg=dynamic, rule='oneOf') 

193 

194 def generate_not(self): 

195 """ 

196 Means that value have not to be valid by this definition. 

197 

198 .. code-block:: python 

199 

200 {'not': {'type': 'null'}} 

201 

202 Valid values for this definition are 'hello', 42, {} ... but not None. 

203 

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

205 means everything is invalid. 

206 """ 

207 not_definition = self._definition['not'] 

208 if not_definition is True: 

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

210 elif not_definition is False: 

211 return 

212 elif not not_definition: 

213 with self.l('if {}:', self._variable): 

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

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

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 with self.l('if isinstance({variable}, str):'): 

258 format_ = self._definition['format'] 

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

260 if format_ in self._custom_formats: 

261 custom_format = self._custom_formats[format_] 

262 if isinstance(custom_format, str): 

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

264 else: 

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

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

267 elif format_ in self.FORMAT_REGEXS: 

268 format_regex = self.FORMAT_REGEXS[format_] 

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

270 # Format regex is used only in meta schemas. 

271 elif format_ == 'regex': 

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

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

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

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

276 else: 

277 raise JsonSchemaDefinitionException('Unknown format: {}'.format(format_)) 

278 

279 

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

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

282 if not regexp_name in self._compile_regexps: 

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

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

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

286 

287 def generate_minimum(self): 

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

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

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

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

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

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

294 else: 

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

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

297 

298 def generate_maximum(self): 

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

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

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

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

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

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

305 else: 

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

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

308 

309 def generate_multiple_of(self): 

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

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

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

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

314 # because for example 19.01 / 0.01 = 1901.0000000000002. 

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

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

317 else: 

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

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

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

321 

322 def generate_min_items(self): 

323 self.create_variable_is_list() 

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

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

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

327 self.create_variable_with_length() 

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

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

330 

331 def generate_max_items(self): 

332 self.create_variable_is_list() 

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

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

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

336 self.create_variable_with_length() 

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

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

339 

340 def generate_unique_items(self): 

341 """ 

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

343 

344 .. code-block:: python 

345 

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

347 0.5839540958404541 

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

349 0.7094449996948242 

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

351 2.0819358825683594 

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

353 2.1439831256866455 

354 """ 

355 unique_definition = self._definition['uniqueItems'] 

356 if not unique_definition: 

357 return 

358 

359 self.create_variable_is_list() 

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

361 self.l( 

362 'def fn(var): ' 

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

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

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

366 'for v in var) ' 

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

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

369 self.create_variable_with_length() 

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

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

372 

373 def generate_items(self): 

374 """ 

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

376 

377 .. code-block:: python 

378 

379 { 

380 'items': [ 

381 {'type': 'integer'}, 

382 {'type': 'string'}, 

383 ], 

384 } 

385 

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

387 

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

389 means everything is invalid. 

390 """ 

391 items_definition = self._definition['items'] 

392 if items_definition is True: 

393 return 

394 

395 self.create_variable_is_list() 

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

397 self.create_variable_with_length() 

398 if items_definition is False: 

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

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

401 elif isinstance(items_definition, list): 

402 for idx, item_definition in enumerate(items_definition): 

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

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

405 self.generate_func_code_block( 

406 item_definition, 

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

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

409 ) 

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

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

412 

413 if 'additionalItems' in self._definition: 

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

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

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

417 else: 

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

419 count = self.generate_func_code_block( 

420 self._definition['additionalItems'], 

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

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

423 ) 

424 if count == 0: 

425 self.l('pass') 

426 else: 

427 if items_definition: 

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

429 count = self.generate_func_code_block( 

430 items_definition, 

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

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

433 ) 

434 if count == 0: 

435 self.l('pass') 

436 

437 def generate_min_properties(self): 

438 self.create_variable_is_dict() 

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

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

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

442 self.create_variable_with_length() 

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

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

445 

446 def generate_max_properties(self): 

447 self.create_variable_is_dict() 

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

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

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

451 self.create_variable_with_length() 

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

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

454 

455 def generate_required(self): 

456 self.create_variable_is_dict() 

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

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

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

460 self.create_variable_with_length() 

461 with self.l('if not all(prop in {variable} for prop in {required}):'): 

462 self.exc('{name} must contain {} properties', self.e(self._definition['required']), rule='required') 

463 

464 def generate_properties(self): 

465 """ 

466 Means object with defined keys. 

467 

468 .. code-block:: python 

469 

470 { 

471 'properties': { 

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

473 }, 

474 } 

475 

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

477 """ 

478 self.create_variable_is_dict() 

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

480 self.create_variable_keys() 

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

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

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

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

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

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

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

488 self.generate_func_code_block( 

489 prop_definition, 

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

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

492 clear_variables=True, 

493 ) 

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

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

496 

497 def generate_pattern_properties(self): 

498 """ 

499 Means object with defined keys as patterns. 

500 

501 .. code-block:: python 

502 

503 { 

504 'patternProperties': { 

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

506 }, 

507 } 

508 

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

510 """ 

511 self.create_variable_is_dict() 

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

513 self.create_variable_keys() 

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

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

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

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

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

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

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

521 self.generate_func_code_block( 

522 definition, 

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

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

525 clear_variables=True, 

526 ) 

527 

528 def generate_additional_properties(self): 

529 """ 

530 Means object with keys with values defined by definition. 

531 

532 .. code-block:: python 

533 

534 { 

535 'properties': { 

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

537 } 

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

539 } 

540 

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

542 any other key with any string. 

543 """ 

544 self.create_variable_is_dict() 

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

546 self.create_variable_keys() 

547 add_prop_definition = self._definition["additionalProperties"] 

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

549 return 

550 if add_prop_definition: 

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

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

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

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

555 self.generate_func_code_block( 

556 add_prop_definition, 

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

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

559 ) 

560 else: 

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

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

563 

564 def generate_dependencies(self): 

565 """ 

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

567 

568 .. code-block:: python 

569 

570 { 

571 'dependencies': { 

572 'bar': ['foo'], 

573 }, 

574 } 

575 

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

577 object with only bar. 

578 

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

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

581 """ 

582 self.create_variable_is_dict() 

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

584 is_empty = True 

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

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

587 continue 

588 is_empty = False 

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

590 if values is False: 

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

592 elif isinstance(values, list): 

593 for value in values: 

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

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

596 else: 

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

598 if is_empty: 

599 self.l('pass')