lib/mix/tasks/bonny.gen.controller.ex

defmodule Mix.Tasks.Bonny.Gen.Controller do
  @moduledoc """
  Generates a new CRD controller

  An operator can have multiple controllers. Each controller handles the lifecycle of a custom resource.

  ```shell
  mix bonny.gen.controller
  ```
  Open up your controller and add functionality for your resources lifecycle:

  * Add
  * Modify
  * Delete
  * Reconcile

  If you selected to add a CRD, also edit the generated CRD version module.
  """

  # credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity

  use Mix.Task

  @switches [out: :string, help: :boolean]
  @aliases [o: :out, h: :help]

  @shortdoc "Generate a new CRD Controller for this operator"
  @spec run([binary()]) :: Keyword.t()
  def run(args) do
    Mix.Bonny.no_umbrella!()

    {opts, args, _} = Mix.Bonny.parse_args(args, [], switches: @switches, aliases: @aliases)

    if opts[:help] do
      print_usage()
      exit(:normal)
    end

    binding =
      args
      |> init_values()
      |> get_input()
      |> Keyword.put(:app_name, Mix.Bonny.app_name())

    controller_filename = Macro.underscore(binding[:controller_name])
    controller_out = opts[:out] || controller_path(controller_filename)
    test_out = opts[:out] || controller_test_path(controller_filename)

    "controller.ex"
    |> Mix.Bonny.template()
    |> EEx.eval_file(binding)
    |> Mix.Bonny.render(controller_out)

    "controller_test.ex"
    |> Mix.Bonny.template()
    |> EEx.eval_file(binding)
    |> Mix.Bonny.render(test_out)

    Owl.IO.puts([
      Owl.Data.tag(
        "Don't forget to add the controller to controllers/2 in your operator.",
        :yellow
      )
    ])

    binding
  end

  # coveralls-ignore-start trivial code and we use stdout in tests
  defp controller_path(filename) do
    Path.join(["lib", Mix.Bonny.app_dir_name(), "controllers", "#{filename}.ex"])
  end

  defp controller_test_path(filename) do
    Path.join([
      "test",
      Mix.Bonny.app_dir_name(),
      "controllers",
      "#{filename}_test.exs"
    ])
  end

  # coveralls-ignore-stop

  defp controller_name_valid?(controller_name) do
    controller_name =~ ~r/^[A-Z][^\W_]*(\.[A-Z]\[^\W_]*)*$/
  end

  def get_input(input) do
    cond do
      is_nil(input[:controller_name]) ->
        controller_name =
          Owl.IO.input(
            label:
              "What's the name of your controller? This has to be a valid Elixir module name. e.g. CronTabController"
          )

        input
        |> Keyword.put(:controller_name, controller_name)
        |> get_input()

      !controller_name_valid?(input[:controller_name]) ->
        Mix.Bonny.error(
          "The controller name you defined (#{input[:controller_name]}) is not a valid Elixir module name!"
        )

        input
        |> Keyword.delete(:controller_name)
        |> get_input()

      true ->
        input
    end
  end

  defp init_values(args) do
    args
    |> Enum.with_index()
    |> Enum.flat_map(fn
      {arg, 0} -> [{:controller_name, arg}]
    end)
  end

  defp print_usage() do
    IO.puts("""
    usage: mix bonny.gen.controller [options] [controller_name]

        controller_name:  The module name of your controller - Should be a valid Elixir module name

        options:
        -h, --help      Print this message
        -o, --out       "-" prints to stdout
    """)
  end
end