lib/patch/mock/code/generators/delegate.ex

defmodule Patch.Mock.Code.Generators.Delegate do
  @moduledoc """
  Generator for `delegate` modules.

  `delegate` modules are generated by taking the `target` module and creating a  stub function for
  each function in the module that calls the `Patch.Mock.Server`'s `delegate/3` function.

  The `delegate` module will also expose every function in the module regardless of the original
  visibility.
  """

  @generated [generated: true]

  alias Patch.Mock.Code
  alias Patch.Mock.Code.Transform
  alias Patch.Mock.Naming

  @doc """
  Generates a new delegate module based on the forms of a provided module.
  """
  @spec generate(abstract_forms :: [Code.form()], module :: module(), exports :: Code.exports()) :: [Code.form()]
  def generate(abstract_forms, module, exports) do
    delegate_name = Naming.delegate(module)

    abstract_forms
    |> Transform.clean()
    |> Transform.export(exports)
    |> Transform.filter(exports)
    |> Transform.rename(delegate_name)
    |> Enum.map(fn
      {:function, _, name, arity, _} ->
        function(module, name, arity)

      other ->
        other
    end)
  end

  ## Private

  defp arguments(0) do
    cons([])
  end

  defp arguments(arity) do
    1..arity
    |> Enum.to_list()
    |> cons()
  end

  defp cons([]), do: {nil, @generated}

  defp cons([head | tail]) do
    {:cons, @generated, {:var, @generated, :"_arg#{head}"}, cons(tail)}
  end

  defp body(module, name, arity) do
    [
      {:call, @generated,
       {:remote, @generated, {:atom, @generated, Patch.Mock.Server},
        {:atom, @generated, :delegate}},
       [
         {:atom, @generated, module},
         {:atom, @generated, name},
         arguments(arity)
       ]}
    ]
  end

  defp function(module, name, arity) do
    clause = {
      :clause,
      @generated,
      patterns(arity),
      [],
      body(module, name, arity)
    }

    {:function, @generated, name, arity, [clause]}
  end

  defp patterns(0) do
    []
  end

  defp patterns(arity) do
    Enum.map(1..arity, fn position ->
      {:var, @generated, :"_arg#{position}"}
    end)
  end
end