lib/ex_factor.ex

defmodule ExFactor do
  @moduledoc """
  `ExFactor` is a refactoring helper.

  By identifying a Module, function name, and arity, it will identify all non-test usages
  and extract them to a new Module.

  If the Module exists, it adds the function to the end of the file and change all calls to the
  new module's name.
  """

  _docp = "results struct"
  defstruct [:module, :path, :message, :file_contents, :state]

  alias ExFactor.Changer
  alias ExFactor.Extractor
  alias ExFactor.Formatter
  alias ExFactor.Remover

  @doc """
  Call Extractor, Remover, and Formatter modules
  """
  def refactor(opts) do
    source_module = Keyword.fetch!(opts, :source_module)
    target_module = Keyword.fetch!(opts, :target_module)
    dry_run = Keyword.get(opts, :dry_run, false)

    opts =
      opts
      |> Keyword.put_new(:target_path, path(target_module))
      |> Keyword.put_new(:source_path, path(source_module))

    emplace = Extractor.emplace(opts)
    changes = Changer.change(opts)
    # remove should be last (before format)
    removals = Remover.remove(opts)

    format(%{additions: emplace, changes: changes, removals: removals}, dry_run)
  end

  def path(module) do
    Path.join(["lib", Macro.underscore(module) <> ".ex"])
  end

  defp format(%{path: nil} = struct, _dry_run), do: struct

  defp format(output, true), do: output

  defp format(%{additions: adds, changes: changes, removals: removals} = output, false) do
    %{
      additions: format(adds),
      changes: format(changes),
      removals: format(removals)
    }

    output
  end

  defp format(list) when is_list(list) do
    Enum.map(list, fn elem ->
      format(elem)
      Map.get_and_update(elem, :state, fn val -> {val, [:formatted | val]} end)
    end)
  end

  defp format(%{state: [:unchanged]} = struct), do: struct

  defp format(struct) do
    Formatter.format([struct.path])
    Map.get_and_update(struct, :state, fn val -> {val, [:formatted | val]} end)
  end
end