defmodule Saul do
  @moduledoc """
  Contains the core of the functionality provided by Saul.

  Saul is a data validation and conformation library. It tries to solve the
  problem of validating the shape and content of some data (most useful when
  such data come from an external source) and of conforming those data to
  arbitrary formats.

  Saul is based on the concept of **validators**: a validator is something that
  knows how to validate a term and transform it to something else if
  necessary. A good example of a validator could be something that validates
  that a term is a string representation of an integer and that converts such
  string to the represented integer.

  Validators are a powerful abstraction as they can be easily *combined*: for
  example, the `Saul.one_of/1` function takes a list of validators and returns a
  validator that passes if one of the given validators pass. Saul provides both
  "basic" validators as well as validator combinators.

  ## Validators

  A validator can be:

    * a function that takes one argument
    * a term that implements the `Saul.Validator` protocol

  The return value of function validators or implementations of
  `Saul.Validator.validate/2` has to be one of the following:

    * `{:ok, transformed}` - it means validation succeeded (the input term is
      considered valid) and `transformed` is the conformed value for the input
      term.

    * `{:error, reason}` - it means validation failed (the input term is
      invalid). `reason` can be any term: if it is not a `Saul.Error` struct,
      `validate/2` will take care of wrapping it into a `Saul.Error`.

    * `true` - it means validation succeeded. It is the same as `{:ok,
      transformed}`, but it can be used when the transformed value is the same
      as the input value. This is useful for "predicate" validators (functions
      that take one argument and return a boolean).

    * `false` - it means validation failed. It is the same as `{:error, reason}`,
      except the reason only mentions that a "predicate failed".

  Returning a boolean value is supported so that existing predicate functions
  can be used as validators without modification. Examples of such functions are
  type guards (`is_binary/1` or `is_list/1`), functions like `String.valid?/1`,
  and many others.

  ## Validating

  The only entry point for validation is `validate/2`. It hides all the
  complexity of the possible return values of validators (described in the
  "Validators" section) and always returns `{:ok, transformed}` (where
  `transformed` can be the same term as the term being validated) or `{:error,
  %Saul.Error{}}`. See the documentation for `validate/2` for more detailed
  documentation.
  """

  @typedoc """
  The type defining a validator.

  See the module documentation for more information on what are validators.
  """
  @type validator(transformed_type) ::
        (term -> {:ok, transformed_type} | {:error, term})
        | (term -> boolean)
        | Saul.Validator.t

  @doc """
  Validates the given `term` through the given `validator`.

  If the validator successfully matches `term`, then the return value of this
  function is `{:ok, transformed}` where `transformed` is the result of the
  transformation applied by the validator. If the validator returns `{:error,
  reason}`, the return value of this function is `{:error, %Saul.Error{}}`.

  Note that the given validator can return any type of `reason` when returning
  an `:error` tuple: `validate/2` will take care of wrapping it into a
  `%Saul.Error{}`. This is done so that users can work with a consistent
  interface but at the same time they can use already existing functions as
  validators (since `{:ok, term} | {:error, term}` is quite a common API in
  Erlang/Elixir).

  ## Examples

      iex> to_string = &{:ok, to_string(&1)}
      iex> Saul.validate(:foo, to_string)
      {:ok, "foo"}
      iex> Saul.validate("hello", to_string)
      {:ok, "hello"}

      iex> failer = fn(_) -> {:error, :bad} end
      iex> {:error, %Saul.Error{} = error} = Saul.validate(3.14, failer)
      iex> error.reason
      ":bad"

  """
  @spec validate(term, validator(value)) ::
        {:ok, value} | {:error, Saul.Error.t} | no_return when value: term
  def validate(term, validator) do
    result =
      case validator do
        fun when is_function(fun, 1) -> validator.(term)
        _ -> Saul.Validator.validate(validator, term)
      end

    case result do
      {:ok, _transformed} = result ->
        result
      true ->
        {:ok, term}
      {:error, %Saul.Error{}} = result ->
        result
      {:error, reason} ->
        {:error, %Saul.Error{validator: validator, reason: inspect(reason), term: {:term, term}}}
      false ->
        {:error, %Saul.Error{validator: validator, reason: "predicate failed", term: {:term, term}}}
      other ->
        raise ArgumentError, "validator should return {:ok, term}, {:error, term}, " <>
                             "or a boolean, got: #{inspect(other)}"
    end
  end

  @doc """
  Validates the given `term` through the given `validator`, raising in case of errors.

  This function works like `validate/2`, but it returns the transformed term
  directly in case validation succeeds or raises a `Saul.Error` exception in
  case validation fails.

  ## Examples

      iex> Saul.validate!("foo", &is_binary/1)
      "foo"
      iex> Saul.validate!("foo", &is_atom/1)
      ** (Saul.Error) (&:erlang.is_atom/1) predicate failed - failing term: "foo"

  """
  @spec validate!(term, validator(value)) :: value | no_return when value: any
  def validate!(term, validator) do
    case validate(term, validator) do
      {:ok, transformed} ->
        transformed
      {:error, %Saul.Error{} = error} ->
        raise(error)
    end
  end

  ## Validators

  @doc """
  Returns a validator that performs the same validation as `validator` but has
  the name `name`.

  This function is useful in order to have better errors when validation
  fails. In such cases, the name of each of the failing validators is printed
  alongside the error. If your validator is an anonymous function `f`, such name
  will be `inspect(f)`, so it won't be very useful when trying to understand
  errors. Naming a validator is also useful when your validator is an isolated
  logical unit (such as a validator that validates that a term is an integer,
  positive, and converts it to its Roman representation).

  ## Examples

      iex> failer = Saul.named_validator(fn(_) -> {:error, :oops} end, "validator that always fails")
      iex> Saul.validate!(:foo, failer)
      ** (Saul.Error) (validator that always fails) :oops - failing term: :foo

  """
  @spec named_validator(validator(value), String.t) :: validator(value) when value: any
  def named_validator(validator, name) do
    %Saul.Validator.NamedValidator{name: name, validator: validator}
  end

  @doc """
  Returns a validator that always passes and applies the given transformation
  `fun`.

  This function is useful when a validator is only applying a transformation,
  and not performing any validation. Using this function is only beneficial
  inside more complex validators, such as `all_of/1`, where `fun` needs to have
  the shape of a validator. For other cases, you can just apply `fun` directly
  to the input term.

  ## Examples

  For example, if you validated that a term is a binary in some way, but want to
  transform it to a charlist during validation, you could wrap
  `String.to_charlist/1` inside `transform/1`:

      iex> term = "this is a string"
      iex> Saul.validate!(term, Saul.transform(&String.to_charlist/1))
      'this is a string'

  """
  @spec transform((input -> output)) :: (input -> {:ok, output}) when input: var, output: var
  def transform(fun) when is_function(fun, 1) do
    &{:ok, fun.(&1)}
  end

  @doc """
  Returns a validator that checks that the input term is equal to `term`.

  This is a basic validator that allows to check for literal terms (hence its
  name, "lit"). If the input term is equal to `term`, then it is returned
  unchanged.

  ## Examples

      iex> three = Saul.lit(3)
      iex> Saul.validate(3, three)
      {:ok, 3}
      iex> {:error, error} = Saul.validate(4, three)
      iex> error.reason
      "expected exact term 3"

  """
  @spec lit(value) :: validator(value) when value: term
  def lit(term) do
    %Saul.Validator.Literal{term: term}
  end

  @doc """
  Returns a validator that matches when all the given `validators` match.

  `validators` has to be a *non-empty* list of validators.

  The validation stops and fails as soon as one of the `validators` fails, or
  succeeds and returns the value returned by the last validator if all
  validators succeed. When a validator succeeds, the transformed value it
  returns is passed as the input to the next validator in the list: this allows
  to simulate a "pipeline" of transformations that halts as soon as something
  doesn't match (similar to a small subset of what you could achieve with the
  `with` Elixir special form).

  ## Examples

      iex> validator = Saul.all_of([&{:ok, to_string(&1)}, &is_binary/1])
      iex> Saul.validate(:hello, validator)
      {:ok, "hello"}

      iex> validator = Saul.all_of([&is_binary/1, &{:ok, &1}])
      iex> Saul.validate!(:hello, validator)
      ** (Saul.Error) (&:erlang.is_binary/1) predicate failed - failing term: :hello

  """
  @spec all_of(nonempty_list(validator(term))) :: validator(term)
  def all_of([_ | _] = validators) do
    %Saul.Validator.AllOf{validators: validators}
  end

  @doc """

  Returns a validator that matches if one of the given `validators` match.

  `validators` has to be a *non-empty* list of validators.

  The validation stops and succeeds as soon as one of the `validators`
  succeeds. The value returned by the succeeding validator is the value returned
  by this validator as well. If all validators fail, an error that shows all the
  failures is returned.

  ## Examples

      iex> validator = Saul.one_of([&is_binary/1, &is_atom/1])
      iex> Saul.validate(:foo, validator)
      {:ok, :foo}

  """
  @spec one_of(nonempty_list(validator(term))) :: validator(term)
  def one_of([_ | _] = validators) do
    %Saul.Validator.OneOf{validators: validators}
  end

  @doc """
  Returns a validator that matches an enumerable where all elements match
  `validator`.

  The return value of this validator is a value constructed by collecting the
  values in the given enumerable transformed according to `validator` into the
  collectable specified by the `:into` option. This validator can be considered
  analogous to the `for` special form (with the `:into` option as well), but
  with error handling. If any of the elements in the given enumerable fails
  `validator`, this validator fails.

  ## Options

    * `:into` - (`t:Collectable.t/0`) the collectable where the transformed values
      should end up in. Defaults to `[]`.

  ## Examples

      iex> validator = Saul.enum_of(&{:ok, {inspect(&1), &1}}, into: %{})
      iex> Saul.validate(%{foo: :bar}, validator)
      {:ok, %{"{:foo, :bar}" => {:foo, :bar}}}
      iex> Saul.validate([1, 2, 3], validator)
      {:ok, %{"1" => 1, "2" => 2, "3" => 3}}

  """
  @spec enum_of(Saul.validator(term), Keyword.t) :: Saul.validator(Collectable.t)
  def enum_of(validator, options \\ []) when is_list(options) do
    Saul.Enum.enum_of(validator, options)
  end

  @doc """
  Returns a validator that validates a map with the shape specified by
  `validators_map`.

  `validators_map` must be a map with values as keys and two-element tuples
  `{required_or_optional, validator}` as values. The input map will be validated
  like this:

    * each key is checked against the validator at the corresponding key in
      `validators_map`

    * `{:required, validator}` validators mean that their corresponding key is
      required in the map; if it's not present in the input map, this
      validator fails

    * `{:optional, validator}` validators mean that their corresponding key
      can be not present in the map, and it's only validated with `validator`
      in case it's present

  The map returned by this validator has unchanged keys and values that are the
  result of the validator for each key.

  ## Options

    * `:strict` (boolean) - if this option is `true`, then this validator fails
      if the input map has keys that are not in `validators_map`. Defaults to
      `false`.

  ## Examples

      iex> validator = Saul.map([strict: false], %{
      ...>   to_string: {:required, &{:ok, to_string(&1)}},
      ...>   is_atom: {:optional, &is_atom/1},
      ...> })
      iex> Saul.validate(%{to_string: :foo, is_atom: :bar}, validator)
      {:ok, %{to_string: "foo", is_atom: :bar}}
      iex> Saul.validate(%{to_string: :foo}, validator)
      {:ok, %{to_string: "foo"}}

  """
  @spec map(Keyword.t , %{optional(term) => {:required | :optional, validator(term)}}) ::
        validator(map)
  def map(options \\ [], validators_map) when is_list(options) and is_map(validators_map) do
    Saul.Validator.Map.new(validators_map, options)
  end

  @doc """
  Returns a validator that validates a tuples with elements that match the
  validator at their corresponding position in `validators`.

  The return value of this validator is a tuple with the same number of elements
  as `validators` (and the input tuple) where elements are the result of the
  validator in their corresponding position in `validators`.

  ## Examples

      iex> atom_to_string = Saul.transform(&Atom.to_string/1)
      iex> Saul.validate({:foo, :bar}, Saul.tuple({atom_to_string, atom_to_string}))
      {:ok, {"foo", "bar"}}

  """
  @spec tuple(tuple) :: validator(tuple)
  def tuple(validators) when is_tuple(validators) do
    Saul.Tuple.tuple(validators)
  end

  @doc """
  Returns a validator that validates a map with keys that match `key_validator`
  and values that match `value_validator`.

  The return value of this validator is a map where keys are the result of
  `key_validator` for each key and values are the result of `value_validator`
  for the corresponding key. If any key or value fail, this validator fails.

  Note that if `key_validator` ends up transforming two keys into the same term,
  then they will collapse under just one key-value pair in the transformed map
  and there is no guarantee on which value will prevail.

  ## Examples

      iex> integer_to_string = Saul.all_of([&is_integer/1, &{:ok, Integer.to_string(&1)}])
      iex> validator = Saul.map_of(integer_to_string, &is_atom/1)
      iex> Saul.validate(%{1 => :ok, 2 => :not_so_ok}, validator)
      {:ok, %{"1" => :ok, "2" => :not_so_ok}}

  """
  @spec map_of(validator(key), validator(value)) :: validator(%{optional(key) => value})
        when key: any, value: any
  def map_of(key_validator, map_validator) do
    Saul.Map.map_of(key_validator, map_validator)
  end

  @doc """
  Returns a validator that validates a list where all elements match `validator`.

  The return value of this validator is a list where each element is the return
  value of `validator` for the corresponding element in the input
  list. Basically this is analogous to `Enum.map/2` but with error handling. If
  any of the elements in the list fail `validator`, this validator fails.

  ## Examples

      iex> integer_to_string = Saul.all_of([&is_integer/1, &{:ok, Integer.to_string(&1)}])
      iex> Saul.validate([1, 2, 3], Saul.list_of(integer_to_string))
      {:ok, ["1", "2", "3"]}

  """
  @spec list_of(validator(value)) :: validator([value]) when value: any
  def list_of(validator) do
    [&is_list/1, enum_of(validator, into: [])]
    |> all_of()
    |> named_validator("list_of")
  end

  @doc """
  Returns a validator that checks if the input term is a member of `enumerable`.

  The return value of this validator is the input term, unmodified.
  `Enum.member?/2` is used to check if the input term is a member of
  `enumerable`.

  ## Examples

      iex> Saul.validate(:bar, Saul.member([:foo, :bar, :baz]))
      {:ok, :bar}
      iex> Saul.validate(50, Saul.member(1..100))
      {:ok, 50}

  """
  @spec member(Enumerable.t) :: validator(term)
  def member(enumerable) do
    %Saul.Validator.Member{enumerable: enumerable}
  end
end
