schema.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. from __future__ import absolute_import
  2. from collections import (Callable, Hashable, Iterable, Mapping,
  3. MutableMapping, Sequence)
  4. from copy import copy
  5. from cerberus import errors
  6. from cerberus.platform import _str_type
  7. from cerberus.utils import (get_Validator_class, validator_factory,
  8. mapping_hash, TypeDefinition)
  9. class _Abort(Exception):
  10. pass
  11. class SchemaError(Exception):
  12. """ Raised when the validation schema is missing, has the wrong format or
  13. contains errors. """
  14. pass
  15. class DefinitionSchema(MutableMapping):
  16. """ A dict-subclass for caching of validated schemas. """
  17. def __new__(cls, *args, **kwargs):
  18. if 'SchemaValidator' not in globals():
  19. global SchemaValidator
  20. SchemaValidator = validator_factory('SchemaValidator',
  21. SchemaValidatorMixin)
  22. types_mapping = SchemaValidator.types_mapping.copy()
  23. types_mapping.update({
  24. 'callable': TypeDefinition('callable', (Callable,), ()),
  25. 'hashable': TypeDefinition('hashable', (Hashable,), ())
  26. })
  27. SchemaValidator.types_mapping = types_mapping
  28. return super(DefinitionSchema, cls).__new__(cls)
  29. def __init__(self, validator, schema={}):
  30. """
  31. :param validator: An instance of Validator-(sub-)class that uses this
  32. schema.
  33. :param schema: A definition-schema as ``dict``. Defaults to an empty
  34. one.
  35. """
  36. if not isinstance(validator, get_Validator_class()):
  37. raise RuntimeError('validator argument must be a Validator-'
  38. 'instance.')
  39. self.validator = validator
  40. if isinstance(schema, _str_type):
  41. schema = validator.schema_registry.get(schema, schema)
  42. if not isinstance(schema, Mapping):
  43. try:
  44. schema = dict(schema)
  45. except Exception:
  46. raise SchemaError(
  47. errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema))
  48. self.validation_schema = SchemaValidationSchema(validator)
  49. self.schema_validator = SchemaValidator(
  50. None, allow_unknown=self.validation_schema,
  51. error_handler=errors.SchemaErrorHandler,
  52. target_schema=schema, target_validator=validator)
  53. schema = self.expand(schema)
  54. self.validate(schema)
  55. self.schema = schema
  56. def __delitem__(self, key):
  57. _new_schema = self.schema.copy()
  58. try:
  59. del _new_schema[key]
  60. except ValueError:
  61. raise SchemaError("Schema has no field '%s' defined" % key)
  62. except Exception as e:
  63. raise e
  64. else:
  65. del self.schema[key]
  66. def __getitem__(self, item):
  67. return self.schema[item]
  68. def __iter__(self):
  69. return iter(self.schema)
  70. def __len__(self):
  71. return len(self.schema)
  72. def __repr__(self):
  73. return str(self)
  74. def __setitem__(self, key, value):
  75. value = self.expand({0: value})[0]
  76. self.validate({key: value})
  77. self.schema[key] = value
  78. def __str__(self):
  79. return str(self.schema)
  80. def copy(self):
  81. return self.__class__(self.validator, self.schema.copy())
  82. @classmethod
  83. def expand(cls, schema):
  84. try:
  85. schema = cls._expand_logical_shortcuts(schema)
  86. schema = cls._expand_subschemas(schema)
  87. except Exception:
  88. pass
  89. return schema
  90. @classmethod
  91. def _expand_logical_shortcuts(cls, schema):
  92. """ Expand agglutinated rules in a definition-schema.
  93. :param schema: The schema-definition to expand.
  94. :return: The expanded schema-definition.
  95. """
  96. def is_of_rule(x):
  97. return isinstance(x, _str_type) and \
  98. x.startswith(('allof_', 'anyof_', 'noneof_', 'oneof_'))
  99. for field in schema:
  100. for of_rule in (x for x in schema[field] if is_of_rule(x)):
  101. operator, rule = of_rule.split('_')
  102. schema[field].update({operator: []})
  103. for value in schema[field][of_rule]:
  104. schema[field][operator].append({rule: value})
  105. del schema[field][of_rule]
  106. return schema
  107. @classmethod
  108. def _expand_subschemas(cls, schema):
  109. def has_schema_rule():
  110. return isinstance(schema[field], Mapping) and \
  111. 'schema' in schema[field]
  112. def has_mapping_schema():
  113. """ Tries to determine heuristically if the schema-constraints are
  114. aimed to mappings. """
  115. try:
  116. return all(isinstance(x, Mapping) for x
  117. in schema[field]['schema'].values())
  118. except TypeError:
  119. return False
  120. for field in schema:
  121. if not has_schema_rule():
  122. pass
  123. elif has_mapping_schema():
  124. schema[field]['schema'] = cls.expand(schema[field]['schema'])
  125. else: # assumes schema-constraints for a sequence
  126. schema[field]['schema'] = \
  127. cls.expand({0: schema[field]['schema']})[0]
  128. for rule in ('keyschema', 'valueschema'):
  129. if rule in schema[field]:
  130. schema[field][rule] = \
  131. cls.expand({0: schema[field][rule]})[0]
  132. for rule in ('allof', 'anyof', 'items', 'noneof', 'oneof'):
  133. if rule in schema[field]:
  134. if not isinstance(schema[field][rule], Sequence):
  135. continue
  136. new_rules_definition = []
  137. for item in schema[field][rule]:
  138. new_rules_definition.append(cls.expand({0: item})[0])
  139. schema[field][rule] = new_rules_definition
  140. return schema
  141. def update(self, schema):
  142. try:
  143. schema = self.expand(schema)
  144. _new_schema = self.schema.copy()
  145. _new_schema.update(schema)
  146. self.validate(_new_schema)
  147. except ValueError:
  148. raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE
  149. .format(schema))
  150. except Exception as e:
  151. raise e
  152. else:
  153. self.schema = _new_schema
  154. def regenerate_validation_schema(self):
  155. self.validation_schema = SchemaValidationSchema(self.validator)
  156. def validate(self, schema=None):
  157. if schema is None:
  158. schema = self.schema
  159. _hash = (mapping_hash(schema),
  160. mapping_hash(self.validator.types_mapping))
  161. if _hash not in self.validator._valid_schemas:
  162. self._validate(schema)
  163. self.validator._valid_schemas.add(_hash)
  164. def _validate(self, schema):
  165. """ Validates a schema that defines rules against supported rules.
  166. :param schema: The schema to be validated as a legal cerberus schema
  167. according to the rules of this Validator object.
  168. """
  169. if isinstance(schema, _str_type):
  170. schema = self.validator.schema_registry.get(schema, schema)
  171. if schema is None:
  172. raise SchemaError(errors.SCHEMA_ERROR_MISSING)
  173. schema = copy(schema)
  174. for field in schema:
  175. if isinstance(schema[field], _str_type):
  176. schema[field] = rules_set_registry.get(schema[field],
  177. schema[field])
  178. if not self.schema_validator(schema, normalize=False):
  179. raise SchemaError(self.schema_validator.errors)
  180. class UnvalidatedSchema(DefinitionSchema):
  181. def __init__(self, schema={}):
  182. if not isinstance(schema, Mapping):
  183. schema = dict(schema)
  184. self.schema = schema
  185. def validate(self, schema):
  186. pass
  187. def copy(self):
  188. # Override ancestor's copy, because
  189. # UnvalidatedSchema does not have .validator:
  190. return self.__class__(self.schema.copy())
  191. class SchemaValidationSchema(UnvalidatedSchema):
  192. def __init__(self, validator):
  193. self.schema = {'allow_unknown': False,
  194. 'schema': validator.rules,
  195. 'type': 'dict'}
  196. class SchemaValidatorMixin(object):
  197. """ This validator is extended to validate schemas passed to a Cerberus
  198. validator. """
  199. @property
  200. def known_rules_set_refs(self):
  201. """ The encountered references to rules set registry items. """
  202. return self._config.get('known_rules_set_refs', ())
  203. @known_rules_set_refs.setter
  204. def known_rules_set_refs(self, value):
  205. self._config['known_rules_set_refs'] = value
  206. @property
  207. def known_schema_refs(self):
  208. """ The encountered references to schema registry items. """
  209. return self._config.get('known_schema_refs', ())
  210. @known_schema_refs.setter
  211. def known_schema_refs(self, value):
  212. self._config['known_schema_refs'] = value
  213. @property
  214. def target_schema(self):
  215. """ The schema that is being validated. """
  216. return self._config['target_schema']
  217. @property
  218. def target_validator(self):
  219. """ The validator whose schema is being validated. """
  220. return self._config['target_validator']
  221. def _validate_logical(self, rule, field, value):
  222. """ {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """
  223. if not isinstance(value, Sequence):
  224. self._error(field, errors.BAD_TYPE)
  225. return
  226. validator = self._get_child_validator(
  227. document_crumb=rule, allow_unknown=False,
  228. schema=self.target_validator.validation_rules)
  229. for constraints in value:
  230. _hash = (mapping_hash({'turing': constraints}),
  231. mapping_hash(self.target_validator.types_mapping))
  232. if _hash in self.target_validator._valid_schemas:
  233. continue
  234. validator(constraints, normalize=False)
  235. if validator._errors:
  236. self._error(validator._errors)
  237. else:
  238. self.target_validator._valid_schemas.add(_hash)
  239. def _validator_bulk_schema(self, field, value):
  240. # resolve schema registry reference
  241. if isinstance(value, _str_type):
  242. if value in self.known_rules_set_refs:
  243. return
  244. else:
  245. self.known_rules_set_refs += (value,)
  246. definition = self.target_validator.rules_set_registry.get(value)
  247. if definition is None:
  248. self._error(field, 'Rules set definition %s not found.' % value)
  249. return
  250. else:
  251. value = definition
  252. _hash = (mapping_hash({'turing': value}),
  253. mapping_hash(self.target_validator.types_mapping))
  254. if _hash in self.target_validator._valid_schemas:
  255. return
  256. validator = self._get_child_validator(
  257. document_crumb=field, allow_unknown=False,
  258. schema=self.target_validator.rules)
  259. validator(value, normalize=False)
  260. if validator._errors:
  261. self._error(validator._errors)
  262. else:
  263. self.target_validator._valid_schemas.add(_hash)
  264. def _validator_dependencies(self, field, value):
  265. if isinstance(value, _str_type):
  266. pass
  267. elif isinstance(value, Mapping):
  268. validator = self._get_child_validator(
  269. document_crumb=field,
  270. schema={'valueschema': {'type': 'list'}},
  271. allow_unknown=True
  272. )
  273. if not validator(value, normalize=False):
  274. self._error(validator._errors)
  275. elif isinstance(value, Sequence):
  276. if not all(isinstance(x, Hashable) for x in value):
  277. path = self.document_path + (field,)
  278. self._error(path, 'All dependencies must be a hashable type.')
  279. def _validator_handler(self, field, value):
  280. if isinstance(value, Callable):
  281. return
  282. if isinstance(value, _str_type):
  283. if value not in self.target_validator.validators + \
  284. self.target_validator.coercers:
  285. self._error(field, '%s is no valid coercer' % value)
  286. elif isinstance(value, Iterable):
  287. for handler in value:
  288. self._validator_handler(field, handler)
  289. def _validator_items(self, field, value):
  290. for i, schema in enumerate(value):
  291. self._validator_bulk_schema((field, i), schema)
  292. def _validator_schema(self, field, value):
  293. try:
  294. value = self._handle_schema_reference_for_validator(field, value)
  295. except _Abort:
  296. return
  297. _hash = (mapping_hash(value),
  298. mapping_hash(self.target_validator.types_mapping))
  299. if _hash in self.target_validator._valid_schemas:
  300. return
  301. validator = self._get_child_validator(
  302. document_crumb=field,
  303. schema=None, allow_unknown=self.root_allow_unknown)
  304. validator(self._expand_rules_set_refs(value), normalize=False)
  305. if validator._errors:
  306. self._error(validator._errors)
  307. else:
  308. self.target_validator._valid_schemas.add(_hash)
  309. def _handle_schema_reference_for_validator(self, field, value):
  310. if not isinstance(value, _str_type):
  311. return value
  312. if value in self.known_schema_refs:
  313. raise _Abort
  314. self.known_schema_refs += (value,)
  315. definition = self.target_validator.schema_registry.get(value)
  316. if definition is None:
  317. path = self.document_path + (field,)
  318. self._error(path, 'Schema definition {} not found.'.format(value))
  319. raise _Abort
  320. return definition
  321. def _expand_rules_set_refs(self, schema):
  322. result = {}
  323. for k, v in schema.items():
  324. if isinstance(v, _str_type):
  325. result[k] = self.target_validator.rules_set_registry.get(v)
  326. else:
  327. result[k] = v
  328. return result
  329. def _validator_type(self, field, value):
  330. value = (value,) if isinstance(value, _str_type) else value
  331. invalid_constraints = ()
  332. for constraint in value:
  333. if constraint not in self.target_validator.types:
  334. invalid_constraints += (constraint,)
  335. if invalid_constraints:
  336. path = self.document_path + (field,)
  337. self._error(path, 'Unsupported types: %s' % invalid_constraints)
  338. ####
  339. class Registry(object):
  340. """ A registry to store and retrieve schemas and parts of it by a name
  341. that can be used in validation schemas.
  342. :param definitions: Optional, initial definitions.
  343. :type definitions: any :term:`mapping` """
  344. def __init__(self, definitions={}):
  345. self._storage = {}
  346. self.extend(definitions)
  347. def add(self, name, definition):
  348. """ Register a definition to the registry. Existing definitions are
  349. replaced silently.
  350. :param name: The name which can be used as reference in a validation
  351. schema.
  352. :type name: :class:`str`
  353. :param definition: The definition.
  354. :type definition: any :term:`mapping` """
  355. self._storage[name] = self._expand_definition(definition)
  356. def all(self):
  357. """ Returns a :class:`dict` with all registered definitions mapped to
  358. their name. """
  359. return self._storage
  360. def clear(self):
  361. """ Purge all definitions in the registry. """
  362. self._storage.clear()
  363. def extend(self, definitions):
  364. """ Add several definitions at once. Existing definitions are
  365. replaced silently.
  366. :param definitions: The names and definitions.
  367. :type definitions: a :term:`mapping` or an :term:`iterable` with
  368. two-value :class:`tuple` s """
  369. for name, definition in dict(definitions).items():
  370. self.add(name, definition)
  371. def get(self, name, default=None):
  372. """ Retrieve a definition from the registry.
  373. :param name: The reference that points to the definition.
  374. :type name: :class:`str`
  375. :param default: Return value if the reference isn't registered. """
  376. return self._storage.get(name, default)
  377. def remove(self, *names):
  378. """ Unregister definitions from the registry.
  379. :param names: The names of the definitions that are to be
  380. unregistered. """
  381. for name in names:
  382. self._storage.pop(name, None)
  383. class SchemaRegistry(Registry):
  384. @classmethod
  385. def _expand_definition(cls, definition):
  386. return DefinitionSchema.expand(definition)
  387. class RulesSetRegistry(Registry):
  388. @classmethod
  389. def _expand_definition(cls, definition):
  390. return DefinitionSchema.expand({0: definition})[0]
  391. schema_registry, rules_set_registry = SchemaRegistry(), RulesSetRegistry()