text: # -*- coding: utf-8 -*-
  """
  validator handlers string module.
  """
  
  import re
  
  from sqlalchemy import String, Text
  
  import pyrin.utils.string as string_utils
  import pyrin.utils.misc as misc_utils
  
  from pyrin.core.globals import _
  from pyrin.database.orm.sql.schema.columns import StringColumn
  from pyrin.validator.handlers.base import ValidatorBase
  from pyrin.admin.enumerations import FormFieldTypeEnum
  from pyrin.validator.handlers.exceptions import LongStringLengthError, ShortStringLengthError, \
      ValueCouldNotBeBlankError, ValueCouldNotBeWhitespaceError, ValueDoesNotMatchPatternError, \
      InvalidRegularExpressionError, RegularExpressionMustBeProvidedError, ValueIsNotStringError, \
      MinimumLengthHigherThanMaximumLengthError, InvalidEmailError, InvalidIPv4Error, \
      InvalidURLError, InvalidHTTPURLError, InvalidHTTPSURLError, InvalidIPv6Error
  
  
  class StringValidator(ValidatorBase):
      """
      string validator class.
      """
  
      invalid_type_error = ValueIsNotStringError
      invalid_type_message = _('The provided value for [{param_name}] '
                               'must be a string.')
      long_length_error = LongStringLengthError
      long_length_message = _('The provided value for [{param_name}] '
                              'should have at most [{count}] characters.')
      short_length_error = ShortStringLengthError
      short_length_message = _('The provided value for [{param_name}] '
                               'should have at least [{count}] characters.')
      blank_value_error = ValueCouldNotBeBlankError
      blank_value_message = _('The provided value for [{param_name}] '
                              'could not be blank.')
      whitespace_value_error = ValueCouldNotBeWhitespaceError
      whitespace_value_message = _('The provided value for [{param_name}] '
                                   'could not be whitespace.')
  
      default_allow_blank = None
      default_allow_whitespace = None
      default_minimum_length = None
      default_maximum_length = None
      default_fixer = string_utils.coerce_to_string
  
      def __init__(self, domain, field, **options):
          """
          initializes an instance of StringValidator.
  
          :param type[BaseEntity] | str domain: the domain in which this validator
                                                must be registered. it could be a
                                                type of a BaseEntity subclass.
                                                if a validator must be registered
                                                independent from any BaseEntity subclass,
                                                the domain could be a unique string name.
                                                note that the provided string name must be
                                                unique at application level.
  
          :param InstrumentedAttribute | str field: validator field name. it could be a
                                                    string or a column. each validator will
                                                    be registered with its field name in
                                                    corresponding domain. to enable automatic
                                                    validations, the provided field name must
                                                    be the exact name of the parameter which
                                                    this validator will validate. if you pass
                                                    a column attribute, some constraints
                                                    such as `nullable`, `min_length`, `max_length`,
                                                    `min_value`, `max_value`, `allow_blank`,
                                                    `allow_whitespace`, `check_in` and
                                                    `check_not_in` could be extracted
                                                    automatically from that column if not provided
                                                    in inputs.
  
          :keyword bool nullable: specifies that null values should be accepted as valid.
                                  defaults to True if not provided.
  
          :keyword str localized_name: localized name of the parameter
                                       which this validator will validate.
                                       it must be passed using `_` method
                                       from `pyrin.core.globals`.
                                       defaults to `name` if not provided.
  
          :keyword bool is_list: specifies that the value must be a list of items.
                                 defaults to False if not provided.
  
          :keyword bool null_items: specifies that list items could be None.
                                    it is only used if `is_list=True` is provided.
                                    defaults to False if not provided.
  
          :keyword bool allow_single: specifies that list validator should also
                                      accept single, non list values.
                                      it is only used if `is_list=True` is provided.
                                      defaults to False if not provided.
  
          :keyword bool allow_empty_list: specifies that list validators should also
                                          accept empty lists.
                                          it is only used if `is_list=True` is provided.
                                          defaults to False if not provided.
  
          :keyword str name: a custom name for this validator.
                             if provided, the name of `field` will be ignored.
  
          :keyword bool for_find: specifies that this validator must only
                                  be used on validation for find.
                                  defaults to False if not provided.
  
          :keyword bool allow_blank: specifies that empty strings should be accepted
                                     as valid. defaults to False if not provided.
  
          :keyword bool allow_whitespace: specifies that whitespace strings should be accepted
                                          as valid. defaults to False if not provided.
  
          :keyword int minimum_length: specifies the minimum valid length for string value.
                                       no min length checking will be done if not provided.
  
          :keyword int maximum_length: specifies the maximum valid length for string value.
                                       no max length checking will be done if not provided.
  
          :raises ValidatorFieldIsRequiredError: validator field is required error.
          :raises ValidatorNameIsRequiredError: validator name is required error.
          :raises InvalidValidatorDomainError: invalid validator domain error.
          :raises InvalidNotAcceptedTypeError: invalid not accepted type error.
          :raises ValidatorFixerMustBeCallable: validator fixer must be callable.
          :raises InvalidValidationExceptionTypeError: invalid validation exception type error.
          :raises MinimumLengthHigherThanMaximumLengthError: minimum length higher than
                                                             maximum length error.
          """
  
          options.update(accepted_type=str)
          super().__init__(domain, field, **options)
  
          allow_blank = options.get('allow_blank')
          if allow_blank is None:
              if self.default_allow_blank is not None:
                  allow_blank = self.default_allow_blank
              elif self.is_string_column and self.field.allow_blank is not None:
                  allow_blank = self.field.allow_blank
              else:
                  allow_blank = False
  
          allow_whitespace = options.get('allow_whitespace')
          if allow_whitespace is None:
              if self.default_allow_whitespace is not None:
                  allow_whitespace = self.default_allow_whitespace
              elif self.is_string_column and self.field.allow_whitespace is not None:
                  allow_whitespace = self.field.allow_whitespace
              else:
                  allow_whitespace = False
  
          self._minimum_length = options.get('minimum_length')
          if self._minimum_length is None:
              if self.default_minimum_length is not None:
                  self._minimum_length = self.default_minimum_length
              elif self.is_string_column and self.field.min_length is not None:
                  self._minimum_length = self.field.min_length
              elif allow_blank is False:
                  self._minimum_length = 1
  
          self._maximum_length = options.get('maximum_length')
          if self._maximum_length is None:
              if self.default_maximum_length is not None:
                  self._maximum_length = self.default_maximum_length
              elif self.is_string_type and self.field.type.length is not None:
                  self._maximum_length = self.field.type.length
              elif self.is_string_column and self.field.max_length is not None:
                  self._maximum_length = self.field.max_length
  
          if self._minimum_length is not None and self._maximum_length is not None \
                  and self._minimum_length > self._maximum_length:
              raise MinimumLengthHigherThanMaximumLengthError('Minimum length of string for '
                                                              'validator [{name}] could not be '
                                                              'higher than maximum length.'
                                                              .format(name=self))
  
          self._allow_blank = allow_blank
          self._allow_whitespace = allow_whitespace
  
          self._validate_exception_type(self.long_length_error)
          self._validate_exception_type(self.short_length_error)
          self._validate_exception_type(self.blank_value_error)
          self._validate_exception_type(self.whitespace_value_error)
  
      def _validate(self, value, **options):
          """
          validates the given value.
  
          it raises an error if validation fails.
          the raised error must be an instance of ValidationError.
          each overridden method must call `super()._validate()`
          preferably at the beginning.
  
          :param str value: value to be validated.
  
          :keyword bool allow_blank: determines that empty strings should be
                                     considered valid. this value has precedence
                                     over `allow_blank` instance attribute
                                     if provided.
  
          :keyword bool allow_whitespace: determines that whitespace strings should be
                                          considered valid. this value has precedence
                                          over `allow_whitespace` instance attribute
                                          if provided.
  
          :keyword str field_name: a custom field name to be used in validation errors.
                                   if not provided, the `localized_name` value of this
                                   validator will be used.
  
          :raises LongStringLengthError: long string length error.
          :raises ShortStringLengthError: short string length error.
          :raises ValueCouldNotBeBlankError: value could not be blank error.
          :raises ValueCouldNotBeWhitespaceError: value could not be whitespace error.
          """
  
          super()._validate(value, **options)
  
          allow_blank = options.get('allow_blank')
          if allow_blank is None:
              allow_blank = self.allow_blank
  
          allow_whitespace = options.get('allow_whitespace')
          if allow_whitespace is None:
              allow_whitespace = self.allow_whitespace
  
          length = len(value)
          if self.maximum_length is not None and length > self.maximum_length:
              raise self.long_length_error(self.long_length_message.format(
                  param_name=self._get_field_name(**options), count=self.maximum_length))
  
          if self.minimum_length is not None and length < self.minimum_length:
              raise self.short_length_error(self.short_length_message.format(
                  param_name=self._get_field_name(**options), count=self.minimum_length))
  
          if allow_blank is not True and length == 0:
              raise self.blank_value_error(self.blank_value_message.format(
                  param_name=self._get_field_name(**options)))
  
          if allow_whitespace is not True and value.isspace():
              raise self.whitespace_value_error(self.whitespace_value_message.format(
                  param_name=self._get_field_name(**options)))
  
          if length > 0 and not value.isspace():
              self._validate_extra(value, **options)
  
      def _validate_extra(self, value, **options):
          """
          validates the given value.
  
          this method is intended to be overridden by subclasses.
          it raises an error if validation fails.
          the raised error must be an instance of ValidationError.
          each overridden method must call `super()._validate_extra()`
          preferably at the beginning.
  
          :param str value: value to be validated.
  
          :keyword str field_name: a custom field name to be used in validation errors.
                                   if not provided, the `localized_name` value of this
                                   validator will be used.
  
          :raises ValidationError: validation error.
          """
          pass
  
      def _get_info(self):
          """
          gets the info of this validator.
  
          :rtype: dict
          """
  
          info = dict()
          if self.minimum_length is not None:
              info.update(min_length=self.minimum_length)
  
          if self.maximum_length is not None:
              info.update(max_length=self.maximum_length)
  
          if self.allow_blank is not None:
              info.update(allow_blank=self.allow_blank)
  
          if self.allow_whitespace is not None:
              info.update(allow_whitespace=self.allow_whitespace)
  
          if self.field is not None and \
                  misc_utils.is_subclass_or_instance(self.field.type, Text):
              info.update(form_field_type=FormFieldTypeEnum.TEXT)
  
          base_info = super()._get_info()
          if base_info:
              info.update(base_info)
  
          return info
  
      @property
      def minimum_length(self):
          """
          gets the minimum accepted length of this validator.
  
          returns None if no minimum has been set.
  
          :rtype: int
          """
  
          return self._minimum_length
  
      @property
      def maximum_length(self):
          """
          gets the maximum accepted length of this validator.
  
          returns None if no maximum has been set.
  
          :rtype: int
          """
  
          return self._maximum_length
  
      @property
      def allow_blank(self):
          """
          gets a value indicating that empty strings are considered valid.
  
          :rtype: bool
          """
  
          return self._allow_blank
  
      @property
      def allow_whitespace(self):
          """
          gets a value indicating that whitespace strings are considered valid.
  
          :rtype: bool
          """
  
          return self._allow_whitespace
  
      @property
      def is_string_type(self):
          """
          gets a value indicating that the field type of this validator is sqlalchemy `String`.
  
          :rtype: bool
          """
  
          return self.field is not None and isinstance(self.field.type, String)
  
      @property
      def is_string_column(self):
          """
          gets a value indicating that the field column type of this validator is `StringColumn`.
  
          :rtype: bool
          """
  
          return isinstance(self.column, StringColumn)
  
  
  class RegexValidator(StringValidator):
      """
      regex validator class.
      """
  
      # the regular expression to be used for validation.
      # it could be set with a string containing a regex
      # or with a `Pattern` object from re.compile() method.
      regex = None
      pattern_not_match_error = ValueDoesNotMatchPatternError
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'does not match the required pattern.')
  
      default_flags = re.IGNORECASE | re.DOTALL
  
      def __init__(self, domain, field, **options):
          """
          initializes an instance of RegexValidator.
  
          :param type[BaseEntity] | str domain: the domain in which this validator
                                                must be registered. it could be a
                                                type of a BaseEntity subclass.
                                                if a validator must be registered
                                                independent from any BaseEntity subclass,
                                                the domain could be a unique string name.
                                                note that the provided string name must be
                                                unique at application level.
  
          :param InstrumentedAttribute | str field: validator field name. it could be a
                                                    string or a column. each validator will
                                                    be registered with its field name in
                                                    corresponding domain. to enable automatic
                                                    validations, the provided field name must
                                                    be the exact name of the parameter which
                                                    this validator will validate. if you pass
                                                    a column attribute, some constraints
                                                    such as `nullable`, `min_length`, `max_length`,
                                                    `min_value`, `max_value`, `allow_blank`,
                                                    `allow_whitespace`, `check_in` and
                                                    `check_not_in` could be extracted
                                                    automatically from that column if not provided
                                                    in inputs.
  
          :keyword bool nullable: specifies that null values should be accepted as valid.
                                  defaults to True if not provided.
  
          :keyword str localized_name: localized name of the parameter
                                       which this validator will validate.
                                       it must be passed using `_` method
                                       from `pyrin.core.globals`.
                                       defaults to `name` if not provided.
  
          :keyword bool is_list: specifies that the value must be a list of items.
                                 defaults to False if not provided.
  
          :keyword bool null_items: specifies that list items could be None.
                                    it is only used if `is_list=True` is provided.
                                    defaults to False if not provided.
  
          :keyword bool allow_single: specifies that list validator should also
                                      accept single, non list values.
                                      it is only used if `is_list=True` is provided.
                                      defaults to False if not provided.
  
          :keyword bool allow_empty_list: specifies that list validators should also
                                          accept empty lists.
                                          it is only used if `is_list=True` is provided.
                                          defaults to False if not provided.
  
          :keyword str name: a custom name for this validator.
                             if provided, the name of `field` will be ignored.
  
          :keyword bool for_find: specifies that this validator must only
                                  be used on validation for find.
                                  defaults to False if not provided.
  
          :keyword bool allow_blank: specifies that empty strings should be accepted
                                     as valid. defaults to False if not provided.
  
          :keyword bool allow_whitespace: specifies that whitespace strings should be accepted
                                          as valid. defaults to False if not provided.
  
          :keyword int minimum_length: specifies the minimum valid length for string value.
                                       no min length checking will be done if not provided.
  
          :keyword int maximum_length: specifies the maximum valid length for string value.
                                       no max length checking will be done if not provided.
  
          :keyword int flags: flags to be used for regular expression
                              compilation using `re.compile` method.
                              this will only be used if a string regex is provided.
                              if no flags are provided, `default_flags` will be applied.
  
          :raises ValidatorFieldIsRequiredError: validator field is required error.
          :raises ValidatorNameIsRequiredError: validator name is required error.
          :raises InvalidValidatorDomainError: invalid validator domain error.
          :raises InvalidNotAcceptedTypeError: invalid not accepted type error.
          :raises ValidatorFixerMustBeCallable: validator fixer must be callable.
          :raises InvalidValidationExceptionTypeError: invalid validation exception type error.
          :raises InvalidRegularExpressionError: invalid regular expression error.
          :raises RegularExpressionMustBeProvidedError: regular expression must be provided error.
          """
  
          super().__init__(domain, field, **options)
  
          is_string = isinstance(self.regex, str)
          if not isinstance(self.regex, re.Pattern) and not is_string:
              raise InvalidRegularExpressionError('The provided regular expression for validator '
                                                  '[{name}] is invalid. it must be a string '
                                                  'containing a regular expression or a Pattern '
                                                  'object returned from re.compile() method.'
                                                  .format(name=self))
  
          if is_string and (len(self.regex) <= 0 or self.regex.isspace()):
              raise RegularExpressionMustBeProvidedError('The provided string for regular '
                                                         'expression on validator [{name}] '
                                                         'could not be blank.'
                                                         .format(name=self))
  
          if is_string:
              flags = options.get('flags')
              if flags is None:
                  flags = self.default_flags
              self._pattern = re.compile(self.regex, flags=flags)
          else:
              self._pattern = self.regex
  
          self._validate_exception_type(self.pattern_not_match_error)
  
      def _validate_extra(self, value, **options):
          """
          validates the given value.
  
          this method is intended to be overridden by subclasses.
          it raises an error if validation fails.
          the raised error must be an instance of ValidationError.
          each overridden method must call `super()._validate_extra()`
          preferably at the beginning.
  
          :param str value: value to be validated.
  
          :keyword str field_name: a custom field name to be used in validation errors.
                                   if not provided, the `localized_name` value of this
                                   validator will be used.
  
          :raises ValueDoesNotMatchPatternError: value does not match pattern error.
          """
  
          super()._validate_extra(value, **options)
  
          if not self.pattern.match(value):
              raise self.pattern_not_match_error(
                  self.pattern_not_match_message.format(
                      param_name=self._get_field_name(**options)))
  
      def _get_info(self):
          """
          gets the info of this validator.
  
          :rtype: dict
          """
  
          info = dict(regex=self.pattern.pattern)
          base_info = super()._get_info()
          if base_info:
              info.update(base_info)
  
          return info
  
      @property
      def pattern(self):
          """
          gets the pattern of this validator.
  
          :rtype: re.Pattern
          """
  
          return self._pattern
  
  
  class EmailValidator(RegexValidator):
      """
      email validator class.
      """
  
      regex = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"
      pattern_not_match_error = InvalidEmailError
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid email address.')
  
      default_minimum_length = 6
      default_form_field_type = FormFieldTypeEnum.EMAIL
  
  
  class IPv4Validator(RegexValidator):
      """
      ipv4 validator class.
      """
  
      regex = r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}' \
              r'([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
  
      pattern_not_match_error = InvalidIPv4Error
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid IPv4 address.')
  
      default_maximum_length = 15
      default_minimum_length = 7
      default_form_field_type = FormFieldTypeEnum.IPV4
  
  
  class IPv6Validator(RegexValidator):
      """
      ipv6 validator class.
      """
  
      regex = r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|' \
              r'([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}' \
              r'(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|' \
              r'([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}' \
              r'(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|' \
              r':((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|' \
              r'::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.)' \
              r'{3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:' \
              r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}' \
              r'[0-9]){0,1}[0-9]))$'
  
      pattern_not_match_error = InvalidIPv6Error
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid IPv6 address.')
  
      default_maximum_length = 39
      default_minimum_length = 2
      default_form_field_type = FormFieldTypeEnum.IPV6
  
  
  class URLValidator(RegexValidator):
      """
      url validator class.
  
      this matches urls starting with `www`.
      """
  
      regex = r'^www\..+\.\w+$'
      pattern_not_match_error = InvalidURLError
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid url.')
  
      default_minimum_length = 7
      default_form_field_type = FormFieldTypeEnum.URL
  
      def _validate_extra(self, value, **options):
          """
          validates the given value.
  
          this method is intended to be overridden by subclasses.
          it raises an error if validation fails.
          the raised error must be an instance of ValidationError.
          each overridden method must call `super()._validate_extra()`
          preferably at the beginning.
  
          :param str value: value to be validated.
  
          :keyword str field_name: a custom field name to be used in validation errors.
                                   if not provided, the `localized_name` value of this
                                   validator will be used.
  
          :raises InvalidURLError: invalid url error.
          """
  
          super()._validate_extra(value, **options)
  
          if ' ' in value or '..' in value:
              raise self.pattern_not_match_error(
                  self.pattern_not_match_message.format(
                      param_name=self._get_field_name(**options)))
  
  
  class HTTPValidator(URLValidator):
      """
      http validator class.
  
      this matches urls starting with `http://www`.
      """
  
      regex = r'^http://www\..+\.\w+$'
      pattern_not_match_error = InvalidHTTPURLError
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid http url.')
  
      default_minimum_length = 14
  
  
  class HTTPSValidator(URLValidator):
      """
      https validator class.
  
      this matches urls starting with `https://www`.
      """
  
      regex = r'^https://www\..+\.\w+$'
      pattern_not_match_error = InvalidHTTPSURLError
      pattern_not_match_message = _('The provided value for [{param_name}] '
                                    'is not a valid https url.')
  
      default_minimum_length = 15
  