lib/modkit/config.ex

defmodule Modkit.Config do
  def current_project do
    Mix.Project.config()
  end

  def otp_app(project) do
    project_get(project, :app)
  end

  def mount(project) do
    mount = project_get(project, [:modkit, :mount], nil) || default_mount(project)

    Modkit.Mount.from_points(mount)
  end

  defp main_module_from_current_project() do
    Mix.Project.get!()
    |> Module.split()
    |> :lists.reverse()
    |> then(fn ["MixProject" | rest] -> rest end)
    |> :lists.reverse()
    |> Module.concat()
  end

  defp default_mount(project) do
    app_mod = main_module_from_current_project()

    [first_path | _] = elixirc_paths = project_get(project, :elixirc_paths)

    base_path = Enum.find(elixirc_paths, first_path, &(&1 == "lib"))

    # here we use the OTP app instead of the to_sname/1 function because the
    # apps generated with `mix new` will create that path by default. The module
    # to snake path conversion does not mirror exactly the snake to module
    # conversion.
    app_code_path =
      Path.join(
        base_path,
        project |> otp_app() |> Atom.to_string() |> Modkit.PathTool.to_snake()
      )

    mix_tasks_path = Path.join(base_path, "mix/tasks")

    [
      {app_mod, app_code_path},
      {Mix.Tasks, {:mix_task, mix_tasks_path}}
    ]
  end

  # -- Data reader ------------------------------------------------------------

  def project_get(mod, key_or_path) do
    _project_get(mod, key_or_path)
  end

  def project_get(mod, key_or_path, default) do
    _project_get(mod, key_or_path)
  rescue
    _ in KeyError -> default
  end

  defp _project_get(project, key) when is_atom(key) do
    project_get(project, [key])
  end

  defp _project_get(project, keys) when is_list(project) do
    fetch_in!(project, keys)
  end

  defp fetch_in!(data, []) do
    data
  end

  defp fetch_in!(data, [key | keys]) when is_list(data) do
    sub_data = Keyword.fetch!(data, key)
    fetch_in!(sub_data, keys)
  end
end