lib/craftgate/enum.ex

defmodule Craftgate.Enum do
  @moduledoc """
  Allows modules to declare enumerations of certain values, with serialization/deserialization support.

  To do this, simply `use` this module and list the individual values as a list of atoms, e.g.

  ```elixir
  defmodule MyEnum do
    use Craftgate.Enum, [
      :FOO,
      :BAR
    ]
  end
  ```

  With this, the `MyEnum` module will gain access to the following convenience methods which can be useful
  to compare incoming values with.

  ```elixir
  ...
  def foo(), do: :FOO
  def bar(), do: :BAR

  def values(), do: [:FOO, :BAR]
  ...
  ```

  The module will also adopt the `Construct.TypeC` behavior which will allow it to cast back and forth between
  values and their string representations. For more info, see `Construct.TypeC`

  ```elixir
  MyEnum.cast("FOO") # returns :FOO
  MyEnum.cast(:FOO) # returns :FOO
  ```

  Please also note that strings or atoms that are not in the actual enumaration list will still be accepted
  so as not to regressions when a remote data source decides to add more values to the list.

  When this occurs, an error message will be logged into the console with the `warn` level to let the developers
  know that a version bump might be necessary.

  ```elixir
  MyEnum.cast("BAZ") # returns :BAZ but also logs the following message:
  # An unknown enum value was received but accepted for backwards compatibility: BAZ. Known values are: BAR, BAZ
  ```
  """

  defmacro __using__(values) do
    function_defs =
      values
      |> Enum.map(fn value ->
        name = value |> Atom.to_string() |> Macro.underscore() |> String.to_atom()
        {name, value}
      end)

    typespec = quote(do: values |> Enum.map(&inspect/1) |> Enum.join(" | ") |> Code.string_to_quoted!())

    quote bind_quoted: [values: values, function_defs: function_defs, typespec: typespec] do
      @type t :: unquote(typespec)

      for {name, value} <- function_defs do
        def unquote(name)(), do: unquote(value)
      end

      def values(), do: unquote(values)

      def cast(value), do: Craftgate.Enum.castc(value, values())
    end
  end

  @behaviour Construct.TypeC

  require Logger

  def castc(value, variants) when is_atom(value) do
    unless value in variants do
      Logger.warning(
        "An unknown enum value was received but accepted for backwards compatibility: #{value}. Known values are: #{inspect(variants)}"
      )
    end

    {:ok, value}
  end

  def castc(value, variants) when is_binary(value),
    do: castc(String.to_atom(value), variants)
end