lib/tasks/mfa.ex

defmodule GitHooks.Tasks.MFA do
  @moduledoc """
  Represents a `{module, function, arity}` (a.k.a. `mfa`) that will be evaluated
  by the Kernel module.

  An `mfa` should be configured as `{module, function, arity}`. The function of
  the module **will always receive the hook arguments** and the arity is
  expected to match the same number to avoid any unexpected behaviour.

  See [Elixir documentation](https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax) for more information.

  For example:

  ```elixir
  config :git_hooks,
    hooks: [
      pre_commit: [
        {MyModule, :my_function, 1}
      ]
    ]
  ```
  """

  @typedoc """
  Represents an `mfa` to be executed.
  """
  @type t :: %__MODULE__{
          module: atom,
          function: atom,
          args: [any],
          result: term
        }

  defstruct [:module, :function, args: [], result: nil]

  @doc """
  Creates a new `mfa` struct.

  ### Examples

      iex> #{__MODULE__}.new({MyModule, :my_function, 1}, :pre_commit, ["commit message"])
      %#{__MODULE__}{module: MyModule, function: :my_function, args: ["commit message"]}

  """
  @spec new(mfa(), GitHooks.git_hook_type(), GitHooks.git_hook_args()) :: __MODULE__.t()
  def new({module, function, arity}, git_hook_type, git_hook_args) do
    expected_arity = length(git_hook_args)

    if arity != expected_arity do
      raise """
      Invalid #{module}.#{function} arity for #{git_hook_type}, expected #{expected_arity} but got #{arity}. Check the Git hooks documentation to fix the expected parameters.
      """
    end

    %__MODULE__{
      module: module,
      function: function,
      args: git_hook_args
    }
  end
end

defimpl GitHooks.Task, for: GitHooks.Tasks.MFA do
  alias GitHooks.Tasks.MFA
  alias GitHooks.Printer

  # Kernel.apply will throw a error if something fails
  def run(
        %MFA{
          module: module,
          function: function,
          args: args
        } = mfa,
        _opts
      ) do
    result = Kernel.apply(module, function, args)

    Map.put(mfa, :result, result)
  rescue
    error ->
      IO.warn(inspect(error))
      Map.put(mfa, :result, error)
  end

  def success?(%MFA{result: :ok}), do: true
  def success?(%MFA{result: _}), do: false

  def print_result(%MFA{module: module, function: function, result: :ok} = mix_task) do
    Printer.success("`#{module}.#{function}` was successful")

    mix_task
  end

  def print_result(%MFA{module: module, function: function, result: _} = mix_task) do
    Printer.error("`#{module}.#{function}` execution failed")

    mix_task
  end
end