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