lib/types/enum_type.ex

defmodule Talos.Types.EnumType do
  alias Talos.Types.MapType
  alias Talos.Types.ListType

  @moduledoc ~S"""
  Enum type is used to check value to be one of enumerable

  For example:
  ```elixir
    iex> import Talos
    iex> genders = enum(members: ["male", "female"])
    iex> Talos.valid?(genders, "male")
    true
    iex> Talos.valid?(genders, "female")
    true
    iex> Talos.valid?(genders, "heli")
    false

  ```


  Also there can be another Talos types:
  ```elixir

    iex> digit_string = %Talos.Types.StringType{regexp: ~r/^\d+$/}
    iex> numbers = %Talos.Types.EnumType{members: [digit_string, %Talos.Types.IntegerType{}]}
    iex> Talos.valid?(numbers, "1")
    true
    iex> Talos.valid?(numbers, 1)
    true
    iex> Talos.valid?(numbers, "One")
    false

  ```


  Additional parameters:

  `allow_nil` - allows value to be nil

  `members` - list of possible values or TalosTypes

  """
  defstruct [:members, allow_nil: false, example_value: nil]

  @type t :: %{
          __struct__: atom,
          allow_nil: boolean,
          members: maybe_improper_list,
          example_value: any
        }

  @behaviour Talos.Types

  @spec valid?(Talos.Types.EnumType.t(), any) :: boolean
  def valid?(type, value) do
    errors(type, value) == []
  end

  @spec errors(Talos.Types.EnumType.t(), any) :: list
  def errors(%__MODULE__{allow_nil: true}, nil) do
    []
  end

  def errors(%__MODULE__{members: members}, value) when is_list(members) do
    case value in members do
      true ->
        []

      false ->
        errors =
          members
          |> Enum.map(fn something ->
            errors_for_members(something, value)
          end)

        case Enum.any?(errors, fn error -> error in [%{}, []] end) do
          true -> []
          false -> errors
        end
    end
  end

  @spec permit(Talos.Types.EnumType.t(), any) :: any
  def permit(%__MODULE__{members: members}, value) do
    member =
      Enum.find(members || [], fn member ->
        is_map(member) && Map.get(member, :__struct__) in [MapType, ListType] &&
          Talos.valid?(member, value)
      end)

    case is_nil(member) do
      true -> value
      false -> Talos.permit(member, value)
    end
  end

  defp errors_for_members(%module{} = maybe_type, value) do
    # preload for function_exported?
    Code.ensure_loaded(module)

    case function_exported?(module, :errors, 2) do
      true -> Talos.errors(maybe_type, value)
      false -> [inspect(value), "should be #{module}"]
    end
  end

  defp errors_for_members(something, _value) do
    "allowed value is #{inspect(something)}"
  end
end