# Copyright 2019 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.

"""Helpers for using the `attr` library."""

from future.utils import iteritems


def attr_type(type_, subname=''):
  """An `attr.s` validator for asserting the type of a value.

  Essentially the same as `attr.validators.instance_of`, except that it allows
  the provision of a 'subname' to provide a better error message when checking
  a sub-field of the value.

  Args:
    * type_ (object) - The python type object to validate `value` against.
    * subname (str) - Some sub-element of attrib.name; e.g. if checking
      a dictionary key, this might be ' keys'.

  Returns a validator function which raises TypeError if the value doesn't match
  the value.
  """

  def inner(_self, attrib, value):
    if not isinstance(value, type_):
      raise TypeError(
        "'{name}' must be {type!r} (got {value!r} that is a "
        "{actual!r}).".format(
          name=attrib.name+subname,
          type=type_,
          actual=value.__class__,
          value=value,
        ),
        attrib,
        value,
        type_,
        subname,
      )
  return inner


def attr_superclass(type_, subname=''):
  """An `attr.s` validator for asserting the superclass of a value.

  Args:
    * type_ (object) - The python type object to validate `value` is a subclass
      of.
    * subname (str) - Some sub-element of attrib.name; e.g. if checking
      a dictionary key, this might be ' keys'.

  Returns a validator function which raises TypeError if the value doesn't match
  the value.
  """

  def inner(_self, attrib, value):
    if not issubclass(type(value), type_):
      raise TypeError(
        "'{name}' must be a subclass of {type!r} (got {value!r} that is a "
        "{actual!r}).".format(
          name=attrib.name+subname,
          type=type_,
          actual=value.__class__,
          value=value,
        ),
        attrib,
        value,
        type_,
        subname,
      )
  return inner


def attr_dict_type(key_type, val_type, value_seq=False):
  """Helper function for writing attr.s validators for dictionary types.

  Args:
    * key_type (object) - The python type object to validate the dict's keys.
    * val_type (object) - The python type object to validate the dict's
      values.
    * value_seq (bool) - If the dictionary maps to a sequence of val_type.

  Returns a validator function which raises TypeError if:
    * The value is not a dictionary
    * All of it's keys don't match `key_type`
    * All of it's values don't match `val_type`
  """

  def inner(self, attrib, value):
    # late import to avoid import cycle
    from ..engine_types import FrozenDict

    attr_type((dict, FrozenDict))(self, attrib, value)
    for k, subval in iteritems(value):
      attr_type(key_type, ' keys')(self, attrib, k)
      subname = '[%r]' % k
      if value_seq:
        attr_seq_type(val_type, subname)(self, attrib, subval)
      else:
        attr_type(val_type, subname)(self, attrib, subval)

  return inner


def attr_seq_type(val_type, subname=''):
  """Helper function for writing attr.s validators for list types.

  Args:
    * val_type (object) - The python type object to validate the list's values.

  Returns a validator function which raises TypeError if:
    * The value is not a list, tuple, set or frozenset
    * All of it's values don't match `val_type`
  """

  def inner(self, attrib, value):
    attr_type((list, tuple, set, frozenset), subname)(self, attrib, value)
    for subval in value:
      attr_type(val_type, subname + ' values')(self, attrib, subval)

  return inner


def attr_list_type(val_type):
  """Helper function for writing attr.s validators for list types.

  Args:
    * val_type (object) - The python type object to validate the list's values.

  Returns a validator function which raises TypeError if:
    * The value is not a list
    * All of it's values don't match `val_type`
  """

  def inner(self, attrib, value):
    attr_type((list, tuple))(self, attrib, value)
    for subval in value:
      attr_type(val_type, ' values')(self, attrib, subval)

  return inner


def attr_value_is(msg, check_fn, subname=''):
  """Helper function for writing attr.s validators.

  Args:
    * msg (str) - The message to include in the ValueError. Should be
      the quoted part of: name is not '<msg>'.
    * check_fn (callable) - Called with 'value'; should return True iff the
      value is valid.
    * subname (str) - Some sub-element of attrib.name; e.g. if checking
      a dictionary key, this might be ' keys'.

  Returns a validator function which raises TypeError if the value doesn't match
  the value.
  """
  def inner(_self, attrib, value):
    if not check_fn(value):
      raise ValueError(
        "'{name}' is not {msg} (got {value!r})".format(
          name=attrib.name+subname,
          msg=msg,
          value=value,
        ),
        attrib,
        value,
        msg,
        subname,
      )
  return inner
