lib/dredd.ex

defmodule Dredd do
  @moduledoc """
  Dredd judges data for you. It's a validator for a wide range of
  datastructures. 

  See the [README][1] for a detailed guide.

  [1]: readme.html
  """

  @type number_t ::
          :float
          | :integer
          | :non_neg_integer
          | :pos_integer

  @doc """
  Validates the given values is of type boolean. Optionally also validates
  against a specific boolean value.

  ## Options
  * `:is` - the expected value (`true`|`false`)
  * `:wrong_type_message` - error message, defaults to "is not a boolean"
  * `:wrong_value_message` - error message, defaults to "expected value: %{expected}"

  ## Examples
  Simple case with data of invalid type:
  ```elixir
  iex> Dredd.validate_boolean('foo')
  %Dredd.Dataset{
    data: 'foo',
    error: %Dredd.SingleError{
      validator: :boolean,
      message: "is not a boolean",
      metadata: %{kind: :type}
    },
    valid?: false
  }
  ```

  Simple case with valid boolean type:
  ```elixir
  iex> Dredd.validate_boolean(true)
  %Dredd.Dataset{data: true, error: nil, valid?: true}
  ```

  Special invalid case with optional expected value:
  ```elixir
  iex> Dredd.validate_boolean(false, is: true)
  %Dredd.Dataset{
    data: false,
    error: %Dredd.SingleError{
      validator: :boolean,
      message: "expected value: %{expected}",
      metadata: %{expected: true, kind: :value}
    },
    valid?: false
  }
  ```
  """
  @spec validate_boolean(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_boolean(dataset, opts \\ []),
    to: Dredd.Validators.Boolean,
    as: :call

  @doc """
  Validates the given values is of type string. Optionally also validates
  the length of the string either as codepoints or graphemes.

  ## Options
  * `:is` - exact required length of a string
  * `:min` - minimal required length of a string
    (should not be used together with `is`)
  * `:max` - maximal allowed length of a string
    (should not be used together with `is`)
  * `:count` - can be `:codepoints` or `:graphemes`.
     Defaults to `:graphemes`
  * `:type_message` - error message in case the type is wrong; 
    defaults to "is not a string"
  * `:is_message` - error message in case the exact length is wrong
     defaults to "should be %{count} character(s)"
  * `:min_message` - error message in case the length is too short 
    defaults to "should be at least %{count} character(s)"
  * `:max_message` - error message in case the length is too long
    defaults to "should be at "should be at most %{count} character(s)"

  ## Examples
  Here's a simple data with invalid data:
  ```elixir
  iex> Dredd.validate_string(10)
  %Dredd.Dataset{
    data: 10,
    error: %Dredd.SingleError{
      validator: :string,
      message: "is not a string",
      metadata: %{kind: :type}
    },
    valid?: false
  }
  ```

  Here's an example with a string that is too short but has a length
  requirement:
  ```elixir
  iex> Dredd.validate_string("", min_length: 5)
  %Dredd.Dataset{
    data: "",
    error: %Dredd.SingleError{
      validator: :string,
      message: "should be at least %{count} character(s)",
      metadata: %{count: 5, kind: :min_length}
    },
    valid?: false
  }
  ```
  """
  @spec validate_string(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_string(dataset, opts \\ []),
    to: Dredd.Validators.String,
    as: :call

  @doc """
  Validates the given values binaries. Optionally also validates the length of the binary either .

  ## Options
  * `:is` - exact required length of a binary
  * `:min` - minimal required length of a binary
    (should not be used together with `is`)
  * `:max` - maximal allowed length of a binary
    (should not be used together with `is`)
  * `:type_message` - error message in case the type is wrong;
    defaults to "is not a binary"
  * `:is_message` - error message in case the exact length is wrong
     defaults to "should be %{count} bytes(s)"
  * `:min_message` - error message in case the length is too short
    defaults to "should be at least %{count} bytes(s)"
  * `:max_message` - error message in case the length is too long
    defaults to "should be at "should be at most %{count} bytes(s)"
  """
  @spec validate_binary(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_binary(dataset, opts \\ []),
    to: Dredd.Validators.Binary,
    as: :call

  @type single_validator_fun :: (any() -> Dredd.Dataset.t())

  @doc """
  Applies a validator function to a each element of a list contained in a field.

  ## Options
  * `:is` - exact required length of a list
  * `:min` - minimal required length of a list 
    (should not be used together with `is`)
  * `:max` - maximal allowed length of a list
    (should not be used together with `is`)
  * `:type_message` - error message in case the type is wrong;
    defaults to "is not a list"
  * `:is_message` - error message in case the exact length is wrong
     defaults to "should be %{count} item(s)"
  * `:min_message` - error message in case the length is too short
    defaults to "should be at least %{count} item(s)"
  * `:max_message` - error message in case the length is too long
    defaults to "should be at "should be at most %{count} item(s)"

  ## Example
  ```elixir
  ```
  """
  @spec validate_list(any(), single_validator_fun(), Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_list(dataset, validator, opts \\ []),
    to: Dredd.Validators.List,
    as: :call

  @type field_spec :: single_validator_fun() | {:optional, single_validator_fun()}

  @type validator_map :: %{any() => field_spec()}

  @doc """
  Validates the structure of a Map, Keyword List, Struct or anything else
  that supports the Access behaviour and whose structure can be represented
  by a map.

  ## Options
  * `message` - error message in case the type-check fails
     defaults to: "is not a map"

  ## Example
  ```elixir
  iex> value = %{ field_a: 10, field_b: "foo" }
  %{field_a: 10, field_b: "foo"}
  iex> validator_map = %{
  ...>   field_a: &Dredd.validate_string/1,
  ...>   field_b: fn data -> Dredd.validate_number(data, :integer) end
  ...>}
  iex> Dredd.validate_map(value, validator_map)
  %Dredd.Dataset{
    data: %{field_a: 10, field_b: "foo"},
    error: %Dredd.MapErrors{
      validator: :map,
      errors: %{
        field_a: %Dredd.SingleError{
          validator: :string,
          message: "is not a string",
          metadata: %{kind: :type}
        },
        field_b: %Dredd.SingleError{
          validator: :number,
          message: "has incorrect numerical type",
          metadata: %{kind: :integer}
        }
      }
    },
    valid?: false
  }
  ```
  """
  @spec validate_map(any(), validator_map(), Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_map(dataset, validator_map, opts \\ []),
    to: Dredd.Validators.Map,
    as: :call

  @doc """
  Validates the value for the given field is not contained within the provided
  enumerable.

  ## Options

  * `:message` - error message, defaults to "is reserved"
  """
  @spec validate_exclusion(any, Enum.t(), Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_exclusion(dataset, enum, opts \\ []),
    to: Dredd.Validators.Exclusion,
    as: :call

  @doc """
  Validates the value of the given field matches the provided format.

  ## Options

  * `:message` - error message, defaults to "has invalid format"
  """
  @spec validate_format(any, Regex.t(), Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_format(dataset, format, opts \\ []),
    to: Dredd.Validators.Format,
    as: :call

  @doc """
  Validates the value for the given field is contained within the provided
  enumerable.

  ## Options

  * `:message` - error message, defaults to "is invalid"
  """
  @spec validate_inclusion(any, Enum.t(), Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_inclusion(dataset, enum, opts \\ []),
    to: Dredd.Validators.Inclusion,
    as: :call

  @doc """
  Validates that the value of a field is a number.

  Supported types:

  * `:float`
  * `:integer`
  * `:non_neg_integer`
  * `:pos_integer`

  ## Options

  * `:message` - error message, defaults to "has invalid type"
  """
  @spec validate_number(any, number_t, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_number(dataset, type, opts \\ []),
    to: Dredd.Validators.Number,
    as: :call

  @doc """
  Validates if the value of a given field is an email.

  NOTE: this validator is not RFC822 compliant. If you really need to be sure,
  send an email to that address.

  ## Options
  * `:message` - error message, default to "is not a valid email address"
  """
  @spec validate_email(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_email(dataset, opts \\ []),
    to: Dredd.Validators.Email,
    as: :call

  @doc """
  Validates if the value of a given field is a UUID.binary_to_string!

  ## Options

  * `:message` - error message, defaults to "is not a valid UUID"
  """
  @spec validate_uuid(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_uuid(dataset, opts \\ []),
    to: Dredd.Validators.UUID,
    as: :call

  @doc """
  Validates if the value of a given field is a UUID.binary_to_string!

  ## Options

  * `:message` - error message, defaults to "is not a valid UUID"
  """
  @spec validate_nanoid(any, Keyword.t()) :: Dredd.Dataset.t()
  defdelegate validate_nanoid(dataset, opts \\ []),
    to: Dredd.Validators.NanoID,
    as: :call

  @doc """
  This is a convenience function in case you want to write your own
  validators. It will set the `valid?` flag of the given `Dredd.Dataset` to false.
  It will also create a `Dredd.SingleError` structure with the given values
  and assing it to the `error` field of the `Dredd.Dataset`.
  """
  @spec set_single_error(Dredd.Dataset.t(), String.t(), atom(), map()) :: Dredd.Dataset.t()
  def set_single_error(dataset, message, validator, metadata \\ %{}) do
    %Dredd.Dataset{
      dataset
      | valid?: false,
        error: %Dredd.SingleError{
          validator: validator,
          message: message,
          metadata: metadata
        }
    }
  end
end