lib/parameter/enum.ex

defmodule Parameter.Enum do
  @moduledoc """
  Enum type that represents a group of constants that have a value with a associated key.

  ## Example

      defmodule MyApp.UserParam do
        use Parameter.Schema

        enum Status do
          value "userOnline", as: :user_online
          value "userOffline", as: :user_offline
        end

        param do
          field :first_name, :string, key: "firstName"
          field :status, MyApp.UserParam.Status
        end
      end

    The `Status` enum should automatically translate the `userOnline` and `userOffline` values when loading
    to the respective atom values.
      iex> Parameter.load(MyApp.UserParam, %{"firstName" => "John", "status" => "userOnline"})
      {:ok, %{first_name: "John", status: :user_online}}
      ...> # It also uses the key for dumping:
      ...> Parameter.dump(MyApp.UserParam, %{first_name: "John", status: :user_online})
      {:ok, %{"firstName" => "John", "status" => "userOnline"}}

    Enum also supports a shorter version if the key and value are already the same:

      defmodule MyApp.UserParam do
        ...
        enum Status, values: [:user_online,  :user_offline]
        ...
      end

      iex> Parameter.load(MyApp.UserParam, %{"firstName" => "John", "status" => "user_online"})
      {:ok, %{first_name: "John", status: :user_online}}

    It's also possible to create enums in different modules by using the
    `enum/1` macro:

      defmodule MyApp.Status do
        import Parameter.Enum

        enum do
          value "userOnline", as: :user_online
          value "userOffline", as: :user_offline
        end
      end

      defmodule MyApp.UserParam do
        use Parameter.Schema
        alias MyApp.Status

        param do
          field :first_name, :string, key: "firstName"
          field :status, Status
        end
      end

    And it's short version:

      enum values: [:user_online,  :user_offline]
  """

  defmacro enum(do: block) do
    module_block = create_module_block(block)

    quote do
      unquote(module_block)
    end
  end

  defmacro enum(values: values) do
    block =
      quote do
        Enum.map(unquote(values), fn val ->
          value(to_string(val), as: val)
        end)
      end

    quote do
      enum(do: unquote(block))
    end
  end

  defmacro enum(module_name, do: block) do
    module_block = create_module_block(block) |> Macro.escape()

    quote bind_quoted: [module_name: module_name, module_block: module_block] do
      module_name = Module.concat(__ENV__.module, module_name)
      Module.create(module_name, module_block, __ENV__)
    end
  end

  defmacro enum(module_name, values: values) when is_list(values) do
    block =
      quote do
        Enum.map(unquote(values), fn val ->
          value(to_string(val), as: val)
        end)
      end

    quote do
      enum(unquote(module_name), do: unquote(block))
    end
  end

  defmacro value(key, as: value) do
    quote bind_quoted: [key: key, value: value] do
      Module.put_attribute(__MODULE__, :enum_values, {key, value})
    end
  end

  defp create_module_block(block) do
    quote do
      @moduledoc """
      Enum parameter type
      """
      use Parameter.Parametrizable

      Module.register_attribute(__MODULE__, :enum_values, accumulate: true)

      unquote(block)

      @impl true
      def load(value) do
        @enum_values
        |> Enum.find(fn {key, enum_value} ->
          key == value
        end)
        |> case do
          nil -> error_tuple()
          {_key, enum_value} -> {:ok, enum_value}
        end
      end

      @impl true
      def dump(value) do
        @enum_values
        |> Enum.find(fn {_key, enum_value} ->
          value == enum_value
        end)
        |> case do
          nil -> error_tuple()
          {key, _value} -> {:ok, key}
        end
      end

      @impl true
      def validate(value) do
        case dump(value) do
          {:error, reason} -> {:error, reason}
          {:ok, _key} -> :ok
        end
      end

      defp error_tuple, do: {:error, "invalid enum type"}
    end
  end
end