lib/combo_new.ex

defmodule ComboNew do
  @moduledoc """
  An opinionated project generator for Phoenix.
  """

  @app Mix.Project.config()[:app]
  @version Mix.Project.config()[:version]
  @elixir_requirement Mix.Project.config()[:elixir]

  @support_options [
    app: :string,
    module: :string
  ]

  @doc """
  Runs a generator with Mix task.
  """
  def run(_mix_task_module, _generator, [option] = _agrv)
      when option in ~w(-v --version) do
    Mix.shell().info("#{@app} v#{@version}")
  end

  def run(mix_task_module, generator, argv) do
    elixir_version_check!()

    case OptionParser.parse!(argv, strict: @support_options) do
      {opts, [base_path | _]} ->
        generate(generator, base_path, opts)

      _ ->
        task_name = to_mix_task_name(mix_task_module)
        Mix.Tasks.Help.run([task_name])
    end
  end

  defp elixir_version_check!() do
    unless Version.match?(System.version(), @elixir_requirement) do
      Mix.raise(
        "#{@app} v#{@version} requires Elixir #{@elixir_requirement}. " <>
          "But, you have Elixir #{System.version()}. \n" <> "Please update accordingly."
      )
    end
  end

  defp to_mix_task_name(mix_task_module) do
    "Mix.Tasks." <> module = inspect(mix_task_module)

    module
    |> String.split(".")
    |> Enum.map(&Macro.underscore/1)
    |> Enum.join(".")
  end

  @doc """
  Generates a project.
  """
  def generate(generator, base_path, opts) do
    base_path = Path.expand(base_path)

    app = String.to_atom(opts[:app] || Path.basename(base_path))
    module = Module.concat([opts[:module] || camelize(app)])
    env_prefix = upcase(app)

    :ok = generator.generate(base_path, app: app, module: module, env_prefix: env_prefix)

    print_next_steps(base_path)
  end

  defp camelize(term) when is_atom(term) do
    term
    |> Atom.to_string()
    |> Macro.camelize()
  end

  defp upcase(term) when is_atom(term) do
    term
    |> Atom.to_string()
    |> String.upcase()
  end

  defp print_next_steps(base_path) do
    Mix.shell().info("""

    We are almost there! The following steps are missing:

        $ cd #{relative_path(base_path)}
        $ mix setup

    Then, start the app with:

        $ mix phx.server

    Or, run the app inside IEx (Interactive Elixir) as:

        $ iex -S mix phx.server

    """)
  end

  defp relative_path(path) do
    case Path.relative_to_cwd(path) do
      ^path -> Path.basename(path)
      rel -> rel
    end
  end
end