Current section
Files
Jump to
Current section
Files
lib/union_type.ex
defmodule UnionType do
@type t :: %__MODULE__{
__module__: module(),
__name__: atom(),
__values__: tuple()
}
@enforce_keys [:__module__, :__name__, :__values__]
defstruct @enforce_keys
defmacro __using__(_options) do
quote do
import UnionType, only: [union_type: 1]
end
end
@doc """
Provides a mechanism for defining a union type.
## Example
iex> defmodule AuthEnum do
...> use UnionType
...>
...> union_type do
...> admin(level)
...> none()
...> end
...> end
...>
...> defmodule App do
...> require AuthEnum
...>
...> def grant_access?(AuthEnum.admin(level)) do
...> level == :top
...> end
...>
...> def grant_access?(AuthEnum.none()) do
...> false
...> end
...>
...> def check, do: grant_access?(AuthEnum.admin(:top))
...> end
...>
...> App.check()
true
"""
defmacro union_type(do: {:__block__, _, variants}) do
Enum.map(variants, &generate_variant(&1, __CALLER__.module))
end
@doc """
Serializes the union type value into a plain Elixir tuple.
## Examples
iex> UserEnum.customer(\"John\") |> UnionType.to_tuple()
{UserEnum, :customer, {\"John\"}}
iex> UserEnum.admin(\"Linda\") |> UnionType.to_tuple()
{UserEnum, :admin, {\"Linda\"}}
iex> UserEnum.contractor(\"Erin\", \"TechCo\") |> UnionType.to_tuple()
{UserEnum, :contractor, {\"Erin\", \"TechCo\"}}
iex> UserEnum.guest() |> UnionType.to_tuple()
{UserEnum, :guest, {}}
"""
@spec to_tuple(t()) :: {module(), variant_name :: atom(), tuple()}
def to_tuple(%__MODULE__{__module__: module, __name__: name, __values__: values}) do
{module, name, values}
end
@doc """
Deserializes the Elixir tuple into a union type.
## Examples
iex> UnionType.from_tuple({UserEnum, :customer, {\"John\"}})
UserEnum.customer(\"John\")
iex> UnionType.from_tuple({UserEnum, :admin, {\"Linda\"}})
UserEnum.admin(\"Linda\")
iex> UnionType.from_tuple({UserEnum, :contractor, {\"Erin\", \"TechCo\"}})
UserEnum.contractor(\"Erin\", \"TechCo\")
iex> UnionType.from_tuple({UserEnum, :guest, {}})
UserEnum.guest()
"""
@spec from_tuple({module(), variant_name :: atom(), tuple()}) :: t()
def from_tuple({module, name, values}) do
%__MODULE__{__module__: module, __name__: name, __values__: values}
end
@doc """
Like Kernel.elem/2, but operates on the values of the union type.
## Examples
iex> UserEnum.customer(\"John\") |> UnionType.at(0)
\"John\"
iex> UserEnum.contractor(\"Erin\", \"TechCo\") |> UnionType.at(1)
\"TechCo\"
"""
@spec at(t(), non_neg_integer()) :: term()
def at(%__MODULE__{__values__: values}, index) do
elem(values, index)
end
defp generate_variant(variant, caller) do
quote do
unquote(generate_doc())
unquote(generate_function(variant, caller))
end
end
defp generate_doc do
quote do
@doc """
TODO: Make this useful
"""
end
end
defp generate_function({name, _, args}, caller) do
quote do
defmacro unquote(name)(unquote_splicing(args)) do
is_match_expression? =
Enum.all?(binding(), fn
{name, {value, _, nil}} when is_atom(name) and is_atom(value) ->
true
_other ->
false
end)
match_vars = binding() |> Enum.map(&elem(&1, 1)) |> Enum.reverse()
if is_match_expression? do
name = unquote(name)
caller = unquote(caller)
quote do
%UnionType{
__module__: unquote(caller),
__name__: unquote(name),
__values__: {unquote_splicing(match_vars)}
}
end
else
Macro.escape(%UnionType{
__module__: unquote(caller),
__name__: unquote(name),
__values__: List.to_tuple(match_vars)
})
end
end
end
end
defimpl String.Chars do
def to_string(union_type) do
module =
union_type.__module__
|> Module.split()
|> Enum.join(".")
args =
union_type.__values__
|> Tuple.to_list()
|> Enum.map_join(", ", &inspect/1)
"#{module}.#{union_type.__name__}(#{args})"
end
end
defimpl Inspect do
def inspect(union_type, _opts) do
to_string(union_type)
end
end
end