lib/mix/new.ex

defmodule Mix.Tasks.Gringotts.New do
  @shortdoc """
  Generates a barebones implementation for a gateway.
  """

  @moduledoc """
  Generates a barebones implementation for a gateway.

  It expects the (brand) name of the gateway as argument and we recommend that
  it be capitalized. *This will not necessarily be the module name*.

  ```
  mix gringotts.new NAME [-m, --module MODULE] [-f, --file FILENAME] [--url URL]
  ```

  A barebones implementation of the gateway will be created along with skeleton
  mock and integration tests in `lib/gringotts/gateways/`. The command will
  prompt for the module name, and other metadata.

  ## Options

  > ***Tip!***
  > You can supply the extra arguments to `gringotts.new` to skip (some of) the
  > prompts.

  * `-m` `--module` - The module name for the Gateway.
  * `-f` `--file` - The filename.
  * `--url` - The homepage of the gateway.

  ## Examples

      mix gringotts.new FooBar

  The prompts for this will be:
  ```
  MODULE = "Foobar"
  URL = "https://www.foobar.com"
  FILENAME = "foo_bar.ex"
  ```
  """

  use Mix.Task
  import Mix.Generator

  @long_msg ~s{
Comma separated list of required configuration keys:
(This can be skipped by hitting `Enter`)
> }

  def run(args) do
    {key_list, [name], []} =
      OptionParser.parse(
        args,
        switches: [module: :string, url: :string, file: :string],
        aliases: [m: :module, f: :file]
      )

    Mix.Shell.IO.info("Generating barebones implementation for #{name}.")
    Mix.Shell.IO.info("Hit enter to select the suggestion.")

    module_suggestion =
      name |> String.split() |> Enum.map(&String.capitalize(&1)) |> Enum.join("")

    module_name =
      case Keyword.fetch(key_list, :module) do
        :error ->
          prompt_with_suggestion("\nModule name", module_suggestion)

        {:ok, mod_name} ->
          mod_name
      end

    url =
      case Keyword.fetch(key_list, :url) do
        :error ->
          prompt_with_suggestion(
            "\nHomepage URL",
            "https://www.#{String.downcase(module_suggestion)}.com"
          )

        {:ok, url} ->
          url
      end

    file_name =
      case Keyword.fetch(key_list, :file) do
        :error ->
          prompt_with_suggestion("\nFilename", Macro.underscore(module_name) <> ".ex")

        {:ok, filename} ->
          filename
      end

    file_base_name = String.slice(file_name, 0..-4)

    required_keys =
      @long_msg
      |> Mix.Shell.IO.prompt()
      |> String.trim()
      |> keys_to_atom(",")

    bindings = [
      gateway: name,
      gateway_module: module_name,
      gateway_underscore: file_name,
      # The key :gateway_filename is not used in any template as of now.
      gateway_filename: "#{file_name}",
      required_config_keys: required_keys,
      gateway_url: url,
      mock_test_filename: "#{file_base_name}_test.exs",
      mock_response_filename: "#{file_base_name}_mock.exs"
    ]

    if Mix.Shell.IO.yes?(
         "\nDoes this look good?\n#{inspect(bindings, pretty: true, width: 40)}\n>"
       ) do
      gateway = EEx.eval_file("templates/gateway.eex", bindings)
      mock = EEx.eval_file("templates/test.eex", bindings)
      mock_response = EEx.eval_file("templates/mock_response.eex", bindings)
      integration = EEx.eval_file("templates/integration.eex", bindings)

      create_file("lib/gringotts/gateways/#{bindings[:gateway_filename]}", gateway)
      create_file("test/integration/gateways/#{bindings[:mock_test_filename]}", integration)

      if Mix.Shell.IO.yes?("\nAlso create empty mock test suite?\n>") do
        create_file("test/gateways/#{bindings[:mock_test_filename]}", mock)
        create_file("test/mocks/#{bindings[:mock_response_filename]}", mock_response)
      end
    else
      Mix.Shell.IO.info("Doing nothing, bye!")
    end
  end

  defp prompt_with_suggestion(message, suggestion) do
    response =
      "#{message} [#{suggestion}]"
      |> Mix.Shell.IO.prompt()
      |> String.trim()

    if response == "", do: suggestion, else: response
  end

  defp keys_to_atom("", _splitter) do
    []
  end

  defp keys_to_atom(key_list, splitter) when is_binary(key_list) do
    key_list
    |> String.split(splitter)
    |> Enum.map(&String.trim/1)
    |> Enum.map(&String.to_atom/1)
  end
end