defmodule Defnamed do @moduledoc """ Compile-time named arguments for Elixir functions and macro """ require Defnamed.Check, as: Check @compilertime_caller :caller @compilertime_params [ @compilertime_caller ] @keys [ :args_struct_list_alias, :args_struct_module_name, :args_struct_ast, :caller_module_name, :original_name, :original_args_kv, :default_args_kv, :required_args, :do_name, @compilertime_caller ] @enforce_keys @keys defstruct @keys @type t :: %__MODULE__{ args_struct_list_alias: list(atom), args_struct_module_name: module, args_struct_ast: tuple, caller_module_name: module, original_name: atom, original_args_kv: Keyword.t(), default_args_kv: Keyword.t(), required_args: list(atom), do_name: atom, caller: tuple | nil } @doc """ Helper that imports `defn/2`, `defn/3`, `defpn/2`, `defpn/3`, `defmacron/2`, `defmacron/3`, `defmacropn/2`, `defmacropn/3` ## Examples ``` iex> use Defnamed Defnamed ``` """ defmacro __using__(opts) do case opts do [] -> quote do import Defnamed, only: [ defn: 2, defn: 3, defpn: 2, defpn: 3, defmacron: 2, defmacron: 3, defmacropn: 2, defmacropn: 3 ] end [replace_kernel: true] -> quote do use Defnamed import Kernel, except: [ def: 1, def: 2, defp: 1, defp: 2, defmacro: 1, defmacro: 2, defmacrop: 1, defmacrop: 2 ] import Defnamed.Kernel end end end [ {:def, [is_public?: true, is_macro?: false]}, {:defp, [is_public?: false, is_macro?: false]}, {:defmacro, [is_public?: true, is_macro?: true]}, {:defmacrop, [is_public?: false, is_macro?: true]} ] |> Enum.each(fn {raw_expression, [is_public?: is_public?, is_macro?: is_macro?]} -> named_expression = "#{raw_expression}n" |> String.to_atom() low_level_expression = is_macro? |> case do true -> :defp false -> raw_expression end defmacro unquote(named_expression)( {:when, original_when_meta, [{original_name, original_meta, [original_args_kv]} | original_guards]}, compiletime_params, do: original_body ) do low_level_expression = unquote(low_level_expression) %Macro.Env{module: caller_module_name} = __CALLER__ %__MODULE__{ do_name: do_name, args_struct_ast: args_struct_ast, caller: compiletime_caller } = params = generate_params(original_name, original_args_kv, caller_module_name, compiletime_params) :ok = validate_original_args_kv!(params, false) caller_code = compiletime_caller |> case do nil -> [] _ -> [ quote do unquote(compiletime_caller) = unquote(__CALLER__ |> Macro.escape()) end ] end code = params |> maybe_define_named_interface(unquote(is_public?), unquote(is_macro?)) |> Enum.concat([ quote do Kernel.unquote(low_level_expression)( unquote({:when, original_when_meta, [{do_name, original_meta, [args_struct_ast]} | original_guards]}), do: (unquote_splicing([caller_code, original_body])) ) end ]) quote do (unquote_splicing(code)) end end defmacro unquote(named_expression)( {original_name, original_meta, [original_args_kv]}, compiletime_params, do: original_body ) do low_level_expression = unquote(low_level_expression) %Macro.Env{module: caller_module_name} = __CALLER__ %__MODULE__{ do_name: do_name, args_struct_ast: args_struct_ast, caller: compiletime_caller } = params = generate_params(original_name, original_args_kv, caller_module_name, compiletime_params) :ok = validate_original_args_kv!(params, false) caller_code = compiletime_caller |> case do nil -> [] _ -> [ quote do unquote(compiletime_caller) = unquote(__CALLER__ |> Macro.escape()) end ] end code = params |> maybe_define_named_interface(unquote(is_public?), unquote(is_macro?)) |> Enum.concat([ quote do Kernel.unquote(low_level_expression)( unquote({do_name, original_meta, [args_struct_ast]}), do: (unquote_splicing([caller_code, original_body])) ) end ]) quote do (unquote_splicing(code)) end end defmacro unquote(named_expression)(header, body) do named_expression = unquote(named_expression) quote do unquote(named_expression)(unquote(header), [], unquote(body)) end end end) @spec validate_original_args_kv!(t, bool) :: :ok | no_return def validate_original_args_kv!( %__MODULE__{ caller_module_name: caller_module_name, original_name: original_name, original_args_kv: original_args_kv, required_args: required_args, args_struct_module_name: args_struct_module_name }, validate_keys? ) when is_boolean(validate_keys?) do message = "#{caller_module_name}.#{original_name} argument" validate_keys? |> case do true -> acceptable_arg_names = args_struct_module_name.__struct__() |> Map.from_struct() |> Map.keys() |> MapSet.new() original_args_kv |> Check.validate_kv!(acceptable_arg_names, required_args, message) false -> original_args_kv |> Check.validate_kv!(message) end end defp generate_params(original_name, raw_original_args_kv, caller_module_name, compiletime_params) do :ok = validate_compiletime_params!(compiletime_params) :ok = pre_validate_original_args_kv!(raw_original_args_kv) args_struct_subname = "__#{ original_name |> Atom.to_string() |> Macro.camelize() }__" |> String.to_atom() args_struct_list_alias = caller_module_name |> Module.split() |> Enum.map(&String.to_atom/1) |> Enum.concat([args_struct_subname]) args_struct_module_name = args_struct_list_alias |> Module.concat() original_args_kv = raw_original_args_kv |> Enum.map(fn {key, {:\\, _, [arg, _]}} -> {key, arg} {_, _} = pair -> pair end) default_args_kv = raw_original_args_kv |> Enum.map(fn {key, {:\\, _, [_, default]}} -> {key, default} {key, _} -> {key, nil} end) required_args = raw_original_args_kv |> Enum.flat_map(fn {_, {:\\, _, [_, _]}} -> [] {key, _} -> [key] end) args_struct_ast = { :%, [], [ {:__aliases__, [alias: false], args_struct_list_alias}, {:%{}, [], original_args_kv} ] } %__MODULE__{ args_struct_list_alias: args_struct_list_alias, args_struct_module_name: args_struct_module_name, args_struct_ast: args_struct_ast, caller_module_name: caller_module_name, original_name: original_name, original_args_kv: original_args_kv, default_args_kv: default_args_kv, required_args: required_args, do_name: String.to_atom("__#{original_name}__"), caller: compiletime_params[@compilertime_caller] } end defp validate_compiletime_params!(compiletime_params) do message = "Defnamed compiletime parameters argument" compiletime_params |> Check.validate_kv!(MapSet.new(@compilertime_params), [], message) end defp pre_validate_original_args_kv!(original_args_kv) do message = "Defnamed argument" original_args_kv |> Check.validate_kv!(message) end defp maybe_define_named_interface( %__MODULE__{ original_name: original_name, caller_module_name: caller_module_name, args_struct_list_alias: args_struct_list_alias, args_struct_module_name: args_struct_module_name, default_args_kv: default_args_kv, do_name: do_name } = params, is_public?, is_macro? ) when is_boolean(is_public?) do args_struct_module_name |> named_module_registered? |> case do true -> [] false -> :ok = register_named_module(args_struct_module_name) additional_macro_layer_expression = is_public? |> case do true -> :defmacro false -> :defmacrop end zero_arity_shortcut = is_public? |> case do true -> quote do caller_module_name = unquote(caller_module_name) original_name = unquote(original_name) quote do unquote(caller_module_name).unquote(original_name)([]) end end false -> quote do original_name = unquote(original_name) quote do unquote(original_name)([]) end end end macro_layer_do_bloack = is_macro? |> case do true -> quote do {struct_ast, _} = { :%, [], [ {:__aliases__, [alias: false], unquote(args_struct_list_alias)}, {:%{}, [], Enum.map(kv, fn {k, v} -> {k, Macro.escape(v)} end)} ] } |> Code.eval_quoted() unquote(do_name)(struct_ast) end false -> is_public? |> case do true -> quote do caller_module_name = unquote(caller_module_name) do_name = unquote(do_name) struct_ast = { :%, [], [ {:__aliases__, [alias: false], unquote(args_struct_list_alias)}, {:%{}, [], kv} ] } quote do unquote(caller_module_name).unquote(do_name)(unquote(struct_ast)) end end false -> quote do do_name = unquote(do_name) struct_ast = { :%, [], [ {:__aliases__, [alias: false], unquote(args_struct_list_alias)}, {:%{}, [], kv} ] } quote do unquote(do_name)(unquote(struct_ast)) end end end end additional_macro_layer = [ quote do Kernel.unquote(additional_macro_layer_expression)( unquote(original_name)(), do: unquote(zero_arity_shortcut) ) Kernel.unquote(additional_macro_layer_expression)( unquote(original_name)(kv), do: ( :ok = %unquote(__MODULE__){ unquote(params |> Macro.escape()) | original_args_kv: kv } |> unquote(__MODULE__).validate_original_args_kv!(true) unquote(macro_layer_do_bloack) ) ) end ] [ quote do defmodule unquote(args_struct_module_name) do defstruct unquote(default_args_kv) end end ] |> Enum.concat(additional_macro_layer) end end defp register_named_module(module) do _ = Agent.start(fn -> MapSet.new() end, name: __MODULE__) :ok = Agent.update(__MODULE__, &MapSet.put(&1, module)) end defp named_module_registered?(module) do if Process.whereis(__MODULE__) do Agent.get(__MODULE__, &MapSet.member?(&1, module)) else false end end end