text: # -*- coding: utf-8 -*-
  
  """
      eve.validation
      ~~~~~~~~~~~~~~
  
      Helper module. Allows eve submodules (methods.patch/post) to be fully
      datalayer-agnostic. Specialized Validator classes are implemented in the
      datalayer submodules.
  
      :copyright: (c) 2017 by Nicola Iarocci.
      :license: BSD, see LICENSE for more details.
  """
  
  import copy
  import cerberus
  import cerberus.errors
  from cerberus import DocumentError, SchemaError  # flake8: noqa
  
  from eve.utils import config
  
  
  class Validator(cerberus.Validator):
  
      def __init__(self, *args, **kwargs):
          if not config.VALIDATION_ERROR_AS_LIST:
              kwargs['error_handler'] = SingleErrorAsStringErrorHandler
  
          resource = kwargs.get('resource', None)
          if resource:
              resource_def = config.DOMAIN[resource]
              kwargs['allow_unknown'] = resource_def['allow_unknown']
          super(Validator, self).__init__(*args, **kwargs)
  
      def validate_update(self, document, document_id, persisted_document=None):
          """ Validate method to be invoked when performing an update, not an
          insert.
  
          :param document: the document to be validated.
          :param document_id: the unique id of the document.
          :param persisted_document: the persisted document to be updated.
          """
          self.document_id = document_id
          self.persisted_document = persisted_document
          return super(Validator, self).validate(document, update=True)
  
      def validate_replace(self, document, document_id, persisted_document=None):
          """ Validation method to be invoked when performing a document
          replacement. This differs from :func:`validation_update` since in this
          case we want to perform a full :func:`validate` (the new document is to
          be considered a new insertion and required fields needs validation).
          However, like with validate_update, we also want the current document_id
          not to be checked when validating 'unique' values.
  
          :param document: the document to be validated.
          :param document_id: the unique id of the document.
          :param persisted_document: the persisted document to be updated.
  
          .. versionadded:: 0.1.0
          """
          self.document_id = document_id
          self.persisted_document = persisted_document
          return super(Validator, self).validate(document)
  
      def _normalize_default(self, mapping, schema, field):
          """ {'nullable': True} """
          if not self.persisted_document or \
             field not in self.persisted_document:
              super(Validator, self)._normalize_default(mapping, schema, field)
  
      def _normalize_default_setter(self, mapping, schema, field):
          """ {'oneof': [
                  {'type': 'callable'},
                  {'type': 'string'}
                  ]} """
          if not self.persisted_document or \
             field not in self.persisted_document:
              super(Validator, self)._normalize_default_setter(mapping, schema,
                                                               field)
  
      def _validate_dependencies(self, dependencies, field, value):
          """ {'type': ['dict', 'hashable', 'hashables']} """
          persisted = self._filter_persisted_fields_not_in_document(dependencies)
          if persisted:
              dcopy = copy.copy(self.document)
              for field in persisted:
                  dcopy[field] = self.persisted_document[field]
              validator = self._get_child_validator()
              validator.validate(dcopy, update=self.update)
              self._error(validator._errors)
          else:
              super(Validator, self)._validate_dependencies(dependencies, field,
                                                            value)
  
      def _filter_persisted_fields_not_in_document(self, fields):
          def persisted_but_not_in_document(field):
              return field not in self.document and \
                  self.persisted_document and \
                  field in self.persisted_document
          return [field for field in fields if
                  persisted_but_not_in_document(field)]
  
      def _validate_readonly(self, read_only, field, value):
          """ {'type': 'boolean'} """
          persisted_value = self.persisted_document.get(field) \
              if self.persisted_document else None
          if value != persisted_value:
              super(Validator, self)._validate_readonly(read_only, field, value)
  
      @property
      def resource(self):
          return self._config.get('resource', None)
  
      @resource.setter
      def resource(self, value):
          self._config['resource'] = value
  
      @property
      def document_id(self):
          return self._config.get('document_id', None)
  
      @document_id.setter
      def document_id(self, value):
          self._config['document_id'] = value
  
      @property
      def persisted_document(self):
          return self._config.get('persisted_document', None)
  
      @persisted_document.setter
      def persisted_document(self, value):
          self._config['persisted_document'] = value
  
  
  class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler):
      """ Default Cerberus error handler for Eve.
  
      Since Cerberus 1.0, error messages for fields will always be returned as
      lists, even in the case of a single error. To maintain compatibility with
      clients, this error handler will unpack single-element error lists unless
      the config item VALIDATION_ERROR_AS_LIST is True.
      """
  
      @property
      def pretty_tree(self):
          pretty = super(SingleErrorAsStringErrorHandler, self).pretty_tree
          self._unpack_single_element_lists(pretty)
          return pretty
  
      def _unpack_single_element_lists(self, tree):
          for field in tree:
              error_list = tree[field]
              if len(error_list) > 0 and isinstance(tree[field][-1], dict):
                  self._unpack_single_element_lists(tree[field][-1])
              if len(tree[field]) == 1:
                  tree[field] = tree[field][0]
  