Packages
doma_algae
1.3.1-doma
Bootstrapped algebraic data types for Elixir, forked by doma for maintenance and testing packagesets
Current section
Files
Jump to
Current section
Files
lib/algae/internal.ex
defmodule Algae.Internal do
@moduledoc false
@type ast() :: {atom(), any(), any()}
@doc """
Construct a data type AST
"""
@spec data_ast(module(), Macro.Env.t() | [module()], ast()) :: ast()
def data_ast(lines, %{aliases: _} = caller) when is_list(lines) do
{field_values, field_types, specs, args, defaults} = module_elements(lines, caller)
arg_count = Enum.count(args)
overridables =
Enum.map(0..arg_count, &({:new_partial, &1}))
++ [new: arg_count]
# More verbose, but clearer.
# for arity <- 0..Enum.count(args) do
# {:new, arity}
# end
args_without_defaults =
Enum.map(args, fn({:\\, [], [stripped, _]}) -> stripped end)
quote do
use Quark
@type t :: %__MODULE__{unquote_splicing(field_types)}
defstruct unquote(field_values)
defpartial new_partial(unquote_splicing(args_without_defaults)) do
struct(__MODULE__, unquote(defaults))
end
@doc "Positional constructor, with args in the same order as they were defined in"
@spec new(unquote_splicing(specs)) :: t()
def new(unquote_splicing(args)) do
struct(__MODULE__, unquote(defaults))
end
defoverridable unquote(overridables)
end
end
def data_ast(modules, {:none, _, _}) do
full_module = modules |> List.wrap() |> Module.concat()
quote do
defmodule unquote(full_module) do
@type t :: %__MODULE__{}
defstruct []
@doc "Default #{__MODULE__} struct"
@spec new() :: t()
def new, do: struct(__MODULE__)
defoverridable [new: 0]
end
end
end
def data_ast(caller_module, type) do
default = default_value(type)
field = module_to_field(caller_module)
quote do
@type t :: %unquote(caller_module){
unquote(field) => unquote(type)
}
defstruct [{unquote(field), unquote(default)}]
@doc "Default #{__MODULE__} struct"
@spec new() :: t()
def new, do: struct(__MODULE__)
@doc "Constructor helper for piping"
@spec new(unquote(type)) :: t()
def new(field), do: struct(__MODULE__, [unquote(field), field])
defoverridable [new: 0, new: 1]
end
end
@spec data_ast([module()], any(), ast()) :: ast()
def data_ast(name, default, type_ctx) do
full_module = Module.concat(name)
field = module_to_field(name)
quote do
defmodule unquote(full_module) do
@type t :: %unquote(full_module){
unquote(field) => unquote(type_ctx)
}
defstruct [{unquote(field), unquote(default)}]
@doc "Default #{__MODULE__} struct. Value defaults to #{inspect unquote(default)}."
@spec new() :: t()
def new, do: struct(__MODULE__)
@doc "Helper for initializing struct with a specific value"
@spec new(unquote(type_ctx)) :: t()
def new(value), do: struct(__MODULE__, [{unquote(field), value}])
end
end
end
@spec embedded_data_ast() :: ast()
def embedded_data_ast do
quote do
@type t :: %__MODULE__{}
defstruct []
@doc "Default #{__MODULE__} struct"
@spec new() :: t()
def new, do: struct(__MODULE__)
end
end
def embedded_data_ast(module_ctx, default, type_ctx) do
field = module_to_field(module_ctx)
quote do
@type t :: %__MODULE__{
unquote(field) => unquote(type_ctx)
}
defstruct [{unquote(field), unquote(default)}]
@doc "Default #{__MODULE__} struct"
@spec new(unquote(type_ctx)) :: t()
def new(field \\ unquote(default)), do: struct(__MODULE__, [field])
defoverridable [new: 1]
end
end
@type field :: {atom(), [any()], [any()]}
@type type :: {atom(), [any()], [any()]}
@spec module_elements([ast()], Macro.Env.t())
:: {
[{field(), any()}],
[{field(), type()}],
[type],
[{:\\, [], any()}],
[{field(), any()}]
}
def module_elements(lines, caller) do
List.foldr(lines, {[], [], [], [], []},
fn(line, {value_acc, type_acc, typespec_acc, acc_arg, acc_mapping}) ->
{field, type, default_value} = normalize_elements(line, caller)
arg = {field, [], Elixir}
{
[{field, default_value} | value_acc],
[{field, type} | type_acc],
[type | typespec_acc],
[{:\\, [], [arg, default_value]} | acc_arg],
[{field, arg} | acc_mapping]
}
end)
end
@spec normalize_elements(ast(), Macro.Env.t()) :: {atom(), type(), any()}
def normalize_elements({:::, _, [{field, _, _}, type]}, caller) do
expanded_type = resolve_alias(type, caller)
{field, expanded_type, default_value(expanded_type)}
end
def normalize_elements({:\\, _, [{:::, _, [{field, _, _}, type]}, default]}, _) do
{field, type, default}
end
@spec resolve_alias(ast(), Macro.Env.t()) :: ast()
def resolve_alias({{_, _, _} = a, b, c}, caller) do
{resolve_alias(a, caller), b, c}
end
def resolve_alias({:. = a, b, [{:__aliases__, _, _} = the_alias | rest]}, caller) do
resolved_alias = Macro.expand(the_alias, caller)
{a, b, [resolved_alias | rest]}
end
def resolve_alias(a, _), do: a
@spec or_types([ast()], module()) :: [ast()]
def or_types({:\\, _, [{:::, _, [_, types]}, _]}, module_ctx) do
or_types(types, module_ctx)
end
def or_types([head | tail], module_ctx) do
Enum.reduce(tail, call_type(head, module_ctx), fn(module, acc) ->
{:|, [], [call_type(module, module_ctx), acc]}
end)
end
@spec modules(module(), [module()]) :: [module()]
def modules(top, module_ctx), do: [top | extract_name(module_ctx)]
@spec call_type(module(), [module()]) :: ast()
def call_type(new_module, module_ctx) do
full_module = List.wrap(module_ctx) ++ submodule_name(new_module)
{{:., [], [{:__aliases__, [alias: false], full_module}, :t]}, [], []}
end
@spec submodule_name({:defdata, any(), [{:::, any(), [any()]}]})
:: [module()]
def submodule_name({:defdata, _, [{:::, _, [body, _]}]}) do
body
|> case do
{:\\, _, [inner_module_ctx, _]} -> inner_module_ctx
{:__aliases__, _, module} -> module
outer_module_ctx -> outer_module_ctx
end
|> List.wrap()
end
def submodule_name({:defdata, _, [{:\\, _, [{:::, _, [{:__aliases__, _, module}, _]}, _]}]}) do
List.wrap(module)
end
def submodule_name({:defdata, _, [{:__aliases__, _, module}, _]}) do
List.wrap(module)
end
@spec extract_name({any(), any(), atom()} | [module()]) :: [module()]
def extract_name({_, _, inner_name}), do: List.wrap(inner_name)
def extract_name(module_chain) when is_list(module_chain), do: module_chain
def module_to_field(modules) when is_list(modules) do
modules
|> List.last()
|> module_to_field()
end
def module_to_field(module) do
module
|> Atom.to_string()
|> String.split(".")
|> List.last()
|> String.downcase()
|> String.trim_leading("elixir.")
|> String.to_atom()
end
# credo:disable-for-lines:21 Credo.Check.Refactor.CyclomaticComplexity
def default_value({{:., _, [{_, _, [:String]}, :t]}, _, _}), do: ""
def default_value({{:., _, [String, :t]}, _, _}), do: ""
def default_value({{:., _, [{_, _, adt}, :t]}, _, []}) do
quote do: unquote(Module.concat(adt)).new()
end
def default_value({{:., _, [module, :t]}, _, []}) do
quote do: unquote(module).new()
end
def default_value([_]), do: []
def default_value({type, _, _}) do
type
|> case do
:boolean -> false
:number -> 0
:integer -> 0
:float -> 0.0
:pos_integer -> 1
:non_neg_integer -> 0
:bitstring -> ""
:charlist -> []
[] -> []
:list -> []
:map -> %{}
:fun -> &Quark.id/1
:-> -> &Quark.id/1
:any -> nil
:t -> raise %Algae.Internal.NeedsExplicitDefaultError{message: "Type is lone `t`"}
atom -> atom
end
|> Macro.escape()
end
end