text: defmodule EctoEnum do
    @moduledoc """
    Provides `defenum/2` and `defenum/3` macro for defining an Enum Ecto type.
  
    This module can also be `use`d to create an Ecto Enum like:
  
        defmodule CustomEnum do
          use EctoEnum, ready: 0, set: 1, go: 2
        end
  
    Or in place of using `EctoEnum.Postgres` like:
  
        defmodule PostgresType do
          use EctoEnum, type: :new_type, enums: [:ready, :set, :go]
        end
  
    The difference between the above two examples is that the previous one would use an
    integer column in the database while the latter one would use a custom type in PostgreSQL.
  
    Note that only PostgreSQL is supported for custom data types at the moment.
    """
  
    @doc """
    Defines an enum custom `Ecto.Type`.
  
    For second argument, it accepts either a list of strings or a keyword list with keyword
    values that are either strings or integers. Below are examples of a valid argument:
        [registered: 0, active: 1, inactive: 2, archived: 3]
        [registered: "registered", active: "active", inactive: "inactive", archived: "archived"]
        ["registered", "active", "inactive", "archived"]
  
    It can be used like any other `Ecto.Type` by passing it to a field in your model's
    schema block. For example:
  
        import EctoEnum
        defenum StatusEnum, registered: 0, active: 1, inactive: 2, archived: 3
  
        defmodule User do
          use Ecto.Schema
  
          schema "users" do
            field :status, StatusEnum
          end
        end
  
    In the above example, the `:status` will behave like an enum and will allow you to
    pass an `integer`, `atom` or `string` to it. This applies to saving the model,
    invoking `Ecto.Changeset.cast/4`, or performing a query on the status field. Let's
    do a few examples:
  
        iex> user = Repo.insert!(%User{status: 0})
        iex> Repo.get(User, user.id).status
        :registered
  
        iex> %{changes: changes} = cast(%User{}, %{"status" => "Active"}, ~w(status), [])
        iex> changes.status
        :active
  
        iex> from(u in User, where: u.status == :registered) |> Repo.all() |> length
        1
  
    Passing an invalid value to a `Ecto.Changeset.cast/3` will add an error to `changeset.errors`
    field.
  
        iex> changeset = cast(%User{}, %{"status" => "retroactive"}, ~w(status), [])
        iex> changeset.errors
        [status: "is invalid"]
  
    Passing an invalid value directly into a model struct will in an error when calling
    `Repo` functions.
  
        iex> Repo.insert!(%User{status: :none})
        ** (Ecto.ChangeError) `"none"` is not a valid enum value for `EctoEnumTest.StatusEnum`.
        Valid enum values are `[0, 1, 2, 3, :registered, :active, :inactive, :archived, "active",
        "archived", "inactive", "registered"]`
  
    The enum type `StatusEnum` will also have a reflection function for inspecting the
    enum map in runtime.
  
        iex> StatusEnum.__enum_map__()
        [registered: 0, active: 1, inactive: 2, archived: 3]
  
    Enums also generate a typespec for use with dialyzer, available as the `t()` type
  
        iex> t(StatusEnum)
        @type t() :: :registered | :active | :inactive | :archived
    """
  
    defmacro __using__(opts) do
      quote do
        opts = unquote(opts)
  
        if opts[:type] && opts[:enums] do
          use EctoEnum.Postgres.Use, unquote(opts)
        else
          use EctoEnum.Use, unquote(opts)
        end
      end
    end
  
    defmacro defenum(module, type, enum, options \\ []) do
      EctoEnum.Postgres.defenum(module, type, enum, options)
    end
  
    defmacro defenum(module, enum) do
      quote do
        enum = Macro.escape(unquote(enum))
        [h | _t] = enum
  
        enum =
          cond do
            Keyword.keyword?(enum) ->
              enum
  
            is_binary(h) ->
              Enum.map(enum, fn value -> {String.to_atom(value), value} end)
  
            true ->
              raise "Enum must be a keyword list or a list of strings"
          end
  
        defmodule unquote(module) do
          use EctoEnum.Use, enum
        end
      end
    end
  
    alias Ecto.Changeset
  
    @spec validate_enum(
            Ecto.Changeset.t(),
            atom,
            (atom, String.t(), list(String.t() | integer | atom) -> String.t())
          ) :: Ecto.Changeset.t()
    def validate_enum(changeset, field, error_msg \\ &default_error_msg/3) do
      Changeset.validate_change(changeset, field, :validate_enum, fn field, value ->
        type = changeset.types[field]
        error_msg = error_msg.(field, value, type.__valid_values__())
  
        if type.valid_value?(value) do
          []
        else
          Keyword.put([], field, error_msg)
        end
      end)
    end
  
    defp default_error_msg(field, value, valid_values) do
      "Value `#{inspect(value)}` is not a valid enum for `#{inspect(field)}` field. " <>
        "Valid enums are `#{inspect(valid_values)}`"
    end
  end
  