lib/hype_lib/use_invoker.ex

defmodule HypeLib.UseInvoker do
  @moduledoc """
  The `HypeLib.UseInvoker` module offers a use macro for utility modules.

  The macro defines a `__using__` macro in the **CALLING MODULE** where the first argument is
  expected to be an atom so we can pass it directly to the `apply/3` function as second argument.

  The macro supports the following options:

  > **Name:**
  >
  > required_utils
  >
  > **Expected data type:**
  >
  > list(atom)
  >
  > **Description:**
  >
  > A list of function names (atoms) which should always be invoked when
  > the macro is called.


  ## Examples

  ### Without any arguments

  ```elixir
  iex> defmodule MyUtils do
  ...>   use HypeLib.UseInvoker
  ...>
  ...>   def num() do
  ...>     quote do
  ...>       def add(a, b), do: a + b
  ...>     end
  ...>   end
  ...> end
  ...>
  ...> defmodule MyConsumer do
  ...>   use MyUtils, :num
  ...>
  ...>   def my_add(a, b), do: add(a, b)
  ...> end
  ...>
  ...> MyConsumer.my_add(1, 2)
  3
  ```

  ### With required_utils

  ```elixir
  iex> defmodule MyUtils do
  ...>   use HypeLib.UseInvoker, required_utils: [:core]
  ...>
  ...>   def core do
  ...>     quote do
  ...>       def core_fun(), do: "core function"
  ...>     end
  ...>   end
  ...>
  ...>   def num() do
  ...>     quote do
  ...>       def add(a, b), do: a + b
  ...>     end
  ...>   end
  ...> end
  ...>
  ...> defmodule MyConsumer do
  ...>   use MyUtils, :num
  ...> end
  ...>
  ...> MyConsumer.core_fun()
  "core function"
  ```

  """

  defmacro __using__(args) do
    required_utils = Keyword.get(args, :required_utils, [])

    quote do
      use TypeCheck

      @spec __using__(which :: atom() | list(atom())) :: Macro.t()
      defmacro __using__(which) when is_atom(which) do
        apply_using([which])
      end

      defmacro __using__(which) when is_list(which) do
        apply_using(which)
      end

      defp apply_using(which) do
        unquote(required_utils)
        |> Kernel.++(which)
        |> Enum.uniq()
        |> Enum.map(&apply(__MODULE__, &1, []))
      end
    end
  end
end