defmodule Defconst do @moduledoc """ Define constants and enum with use in guards ## Define a contant defmodule ConstType do use Defconst defconst :one, 1 defconst :two, 2 end ## Define an enum with default values defmodule EnumType1 do use Defconst defenum [ :zero, :one, :two ] end ## Define an enum with explicit values defmodule EnumType2 do use Defconst defenum [ {:one, 1}, {:nine, 9}, :ten ] end """ @doc false defmacro __using__(_opts) do caller_module = __CALLER__.module quote do import unquote(__MODULE__) Module.register_attribute(unquote(caller_module), :constants, accumulate: true) @before_compile unquote(__MODULE__) end end @doc false defmacro __before_compile__(env) do constants = env.module |> Module.get_attribute(:constants) |> Enum.reverse() ensure_unique_names!(constants, env) constant_map = Enum.into(constants, %{}) value_map = build_value_map(constants) quote do def _constants, do: unquote(constants) @doc """ Returns all constants as list of tuples ## Examples: iex> #{__MODULE__}.constants #{unquote(constants) |> Kernel.inspect()} """ def constants, do: unquote(constants) @doc """ Returns constant for specified value ## Parameters: * value: value of a constant ## Returns: * the matching constant name when the value is unique * a list of constant names when multiple constants share the value * `nil` when no constant has the value ## Examples: iex> #{__MODULE__}.constant_of(#{unquote(constants) |> Keyword.values() |> List.first() |> Kernel.inspect()}) #{unquote(constants) |> Keyword.keys() |> List.first() |> Kernel.inspect()} """ def constant_of(value) do constants = unquote(Macro.escape(value_map))[value] case constants do [constant] -> constant _ -> constants end end @doc """ Returns value for specified constant ## Parameters: * constant: defined constant ## Examples: iex> #{__MODULE__}.value_of(#{unquote(constants) |> Keyword.keys() |> List.first() |> Kernel.inspect()}) #{unquote(constants) |> Keyword.values() |> List.first() |> Kernel.inspect()} """ def value_of(constant) do unquote(Macro.escape(constant_map))[constant] end end end @doc """ Define constant ## Examples: iex> defmodule ConstType do ...> use Defconst ...> ...> defconst :one, 1 ...> defconst :two, 2 ...> end iex> defmodule ConstUse do ...> require ConstType ...> ...> def const_value(x) do ...> case x do ...> ConstType.one -> "one" ...> ConstType.two -> "two" ...> _ -> "unknown" ...> end ...> end ...> ...> def const_guard(x) when x == ConstType.two do ...> "two" ...> end ...> end iex> ConstType.constants [{:one, 1}, {:two, 2}] iex> ConstUse.const_value(1) "one" iex> ConstUse.const_guard(2) "two" """ defmacro defconst(name, value) do caller_module = __CALLER__.module register_name!(caller_module, name, __CALLER__) var = Macro.var(name, __MODULE__) quote do Module.put_attribute(unquote(caller_module), :constants, {unquote(name), unquote(value)}) @doc """ Returns #{unquote(value)} ## Examples: iex> #{__MODULE__}.#{unquote(name)}() #{unquote(value) |> Kernel.inspect()} """ defmacro unquote(var), do: unquote(value) end end @doc """ Defines an enum with specified constant names and optional values ## Examples: iex> defmodule EnumType1 do ...> use Defconst ...> ...> defenum [ ...> :zero, ...> :one, ...> :two ...> ] ...> end iex> defmodule EnumUse1 do ...> require EnumType1 ...> ...> def enum_value(x) do ...> case x do ...> EnumType1.zero -> "zero" ...> EnumType1.one -> "one" ...> EnumType1.two -> "two" ...> _ -> "unknown" ...> end ...> end ...> ...> def enum_guard(x) when x == EnumType1.two do ...> "two" ...> end ...> end iex> EnumType1.constants [zero: 0, one: 1, two: 2] iex> EnumUse1.enum_value(1) "one" iex> EnumUse1.enum_guard(2) "two" iex> defmodule EnumType2 do ...> use Defconst ...> ...> defenum [ ...> {:zero, "zero"}, ...> {:one, 1}, ...> {:nine, 9}, ...> :ten ...> ] ...> end iex> defmodule EnumUse2 do ...> require EnumType2 ...> ...> def enum_value(x) do ...> case x do ...> EnumType2.zero -> "zero" ...> EnumType2.one -> "one" ...> EnumType2.nine -> "nine" ...> EnumType2.ten -> "ten" ...> _ -> "unknown" ...> end ...> end ...> ...> def enum_guard(x) when x == EnumType2.ten do ...> "ten" ...> end ...> end iex> EnumType2.constants [zero: "zero", one: 1, nine: 9, ten: 10] iex> EnumUse2.enum_value(1) "one" iex> EnumUse2.enum_guard(10) "ten" """ defmacro defenum(constants, quoted_generator \\ quote(do: Defconst.Enum.DefaultGenerator)) defmacro defenum(constants, quoted_generator) do # Expand quoted module generator = Macro.expand(quoted_generator, __CALLER__) constants |> normalize(generator) |> Enum.reverse() |> Enum.map(fn {name, value} -> quote do defconst(unquote(name), unquote(value)) end end) end defp normalize(constants, generator) do {result, _} = Enum.reduce(constants, {[], 0}, &normalize_constant(generator, &1, &2)) result end defp normalize_constant(generator, {constant_name, value} = constant, {accumulator, _index}) do {[constant | accumulator], generator.next_value(constant_name, value)} end defp normalize_constant(generator, constant_name, {accumulator, index}) do {[{constant_name, index} | accumulator], generator.next_value(constant_name, index)} end defp ensure_unique_names!(constants, env) do names = Keyword.keys(constants) duplicate_names = names -- Enum.uniq(names) unless duplicate_names == [] do raise CompileError, file: env.file, line: env.line, description: "Defconst: duplicate constant name(s): #{inspect(Enum.uniq(duplicate_names))}" end end defp build_value_map(constants) do Enum.reduce(constants, %{}, fn {constant, value}, map -> Map.update(map, value, [constant], &(&1 ++ [constant])) end) end # Runs at `defconst` expansion time (names are literal atoms), so a duplicate name raises # before the second macro/@doc clause is emitted — no "clause cannot match" warning noise. # Tracks seen names in a plain list attribute managed entirely at expansion time (it does not # rely on `use`-time `register_attribute`, which has not run yet when the first `defconst` # expands). defp register_name!(module, name, caller) do seen = Module.get_attribute(module, :defconst_names) || [] if name in seen do raise CompileError, file: caller.file, line: caller.line, description: "Defconst: duplicate constant name: #{inspect(name)}" end Module.put_attribute(module, :defconst_names, [name | seen]) end end