Packages

Rust like Enums for Elixir

Current section

Files

Jump to
enuma lib enuma.ex
Raw

lib/enuma.ex

defmodule Enuma do
@moduledoc """
Enuma is a library for defining and working with Rust like Enums in Elixir.
### Example
```elixir
defmodule MyEnum do
use Enuma
defenum do
item :foo
item :bar, args: [integer()]
item :baz, args: [String.t()]
end
end
```
Enuma will create macros for each item, allowing you to match on the enum values and check if a value is of a specific type.
```elixir
iex> require MyEnum
MyEnum
iex> MyEnum.foo() = :foo
true
iex> MyEnum.bar(x) = {:bar, 1}
{:bar, 1}
```
It will also generate macros for checking if a value is of a specific type. (e.g `MyEnum.is_foo(value)`).
These macros can be used in pattern matching and guards.
"""
defmacro __using__(_env) do
quote location: :keep do
import Enuma
@before_compile Enuma
end
end
@doc """
Defines an enum with the given items.
"""
defmacro defenum(do: block) do
quote location: :keep do
Enuma.assert_no_items(__MODULE__)
Module.register_attribute(__MODULE__, :enuma_defenum_items, accumulate: true)
unquote(block)
end
end
@doc """
Defines an item with the given name and options.
## Options
* `:args` - a list of arguments to be passed to the item macro
"""
defmacro item(name, opts \\ []) do
args = Keyword.get(opts, :args, []) |> List.wrap()
opts = Keyword.put(opts, :args, Macro.escape(args))
quote location: :keep do
@enuma_defenum_items {unquote(name), unquote(opts)}
end
end
def from_string(value, type_module) do
case Map.fetch(type_module.__enuma_string_mappings__(), value) do
{:ok, atom} -> {:ok, atom}
:error -> {:error, :invalid_enuma_value}
end
end
def from_string!(value, type_module) do
case from_string(value, type_module) do
{:ok, atom} -> atom
{:error, :invalid_enuma_value} -> raise "Enuma: Invalid enum value"
end
end
def to_string(key, type_module) when is_atom(key) do
case Map.fetch(type_module.__enuma_atom_mappings__(), key) do
{:ok, string} -> {:ok, string}
:error -> {:error, :invalid_enuma_value}
end
end
def to_string(value, _type_module) when is_tuple(value) do
{:error, :unsupported_enuma_ecto_type}
end
defmacro is_valid(value, type_module) do
type_module = Macro.expand(type_module, __ENV__)
items = type_module.__enuma_items__()
quote do
(is_atom(unquote(value)) and unquote(value) in unquote(items)) or
(is_tuple(unquote(value)) and elem(unquote(value), 0) in unquote(items))
end
end
def valid?(value, type_module) when is_atom(value) do
value in type_module.__enuma_items__()
end
def valid?(nil, _type_module), do: true
def valid?(value, type_module) when is_tuple(value) do
[item_type | args] = Tuple.to_list(value)
case Map.fetch(type_module.__enuma_items_map__(), item_type) do
{:ok, item} ->
arg_types = Keyword.fetch!(item, :args)
# Fast-fail: check argument count first
if Enum.count(args) != Enum.count(arg_types) do
false
else
# Check each argument matches expected type
Enum.zip(arg_types, args)
|> Enum.all?(fn {type, arg} ->
Enuma.Helpers.type_match?(arg, type)
end)
end
:error ->
false
end
end
def valid?(_, _type_module), do: false
@doc false
def assert_no_items(module) do
case Module.get_attribute(module, :enuma_defenum_items) do
[] -> :ok
nil -> :ok
_ -> raise "Enuma: Cannot redefine already defined enum"
end
end
defp generate_item_ast(item, _args = [], value, _opts) do
quote location: :keep do
defmacro unquote(item)() do
value = unquote(value)
quote location: :keep do
unquote(value)
end
end
defmacro unquote(String.to_atom("is_#{item}"))(v) do
value = unquote(value)
quote location: :keep do
unquote(v) == unquote(value)
end
end
end
end
defp generate_item_ast(item, args, value, _opts) do
quote location: :keep do
defmacro unquote(item)(unquote_splicing(args)) do
value = unquote(value)
args = unquote(args)
quote location: :keep do
{unquote(value), unquote_splicing(args)}
end
end
defmacro unquote(String.to_atom("is_#{item}"))(v) do
value = unquote(value)
quote location: :keep do
is_tuple(unquote(v)) and elem(unquote(v), 0) == unquote(value)
end
end
end
end
defmacro __before_compile__(_env) do
module = __CALLER__.module
items = Module.get_attribute(module, :enuma_defenum_items)
items_asts =
for {item, opts} <- items do
{args, opts} = Keyword.pop!(opts, :args)
args =
args
|> List.wrap()
|> Enum.count()
|> Macro.generate_arguments(__CALLER__.module)
generate_item_ast(item, args, item, opts)
end
item_keys = for {item, _opts} <- items, do: item
items_map = Enum.into(items, %{})
string_conversion_mapping =
item_keys
|> Enum.map(&{Atom.to_string(&1), &1})
|> Enum.into(%{})
|> Macro.escape()
atom_conversion_mapping =
item_keys
|> Enum.map(&{&1, Atom.to_string(&1)})
|> Enum.into(%{})
|> Macro.escape()
quote location: :keep do
unquote_splicing(items_asts)
def __enuma_string_mappings__ do
unquote(string_conversion_mapping)
end
def __enuma_atom_mappings__ do
unquote(atom_conversion_mapping)
end
def __enuma_items__ do
unquote(item_keys)
end
def __enuma_items_map__ do
unquote(Macro.escape(items_map))
end
end
end
end