lib/mix/tasks/saaskit.gen.resource.ex

defmodule Mix.Tasks.Saaskit.Gen.Resource do
  @shortdoc "Generates a resource with functions around an Ecto schema"

  use Mix.Task
  import SaasKit.MixUtils

  @name "saaskit.gen.resource"

  @doc false
  # ["Ninjas", "Ninja", "ninjas", "name", "skill:integer",
  # "user_id:references:users", "--web", "Admin", "--i"]
  def run(args) do
    refute_umbrella(@name)

    Application.ensure_all_started(:hackney)

    args
    |> account_scoped_question()
    |> build_resource_from_args(__MODULE__)
    |> additional_questions()
    |> load_templates(@name)
    |> follow_instructions()
  end

  def raise_with_help(msg) do
    Mix.raise """
    #{msg}

    mix saaskit.gen.resource
    expect a context module name, followed by singular and plural names
    of the generated resource, ending with any number of attributes.
    For example:

        mix saaskit.gen.resouerce Accounts User users name:string

    The context serves as the API boundary for the given resource.
    Multiple resources may belong to a context and a resource may be
    split over distinct contexts (such as Accounts.User and Payments.User).
    """
  end

  # %{
  #   account_scoped: false,
  #   app_name: "Racer",
  #   app_name_lower: "racer",
  #   associations: [
  #     %{
  #       foreign_key: :ninja_level_id,
  #       schema_module: "Racer.Ninjas.NinjaLevel",
  #       singular: :ninja_level,
  #       table_name: :ninja_levels
  #     }
  #   ],
  #   binary_id: true,
  #   context: true,
  #   context_name: "Ninjas",
  #   embedded: false,
  #   fields: "name:string skill:integer",
  #   indexes: ["create index(:ninjas, [:ninja_level_id])"],
  #   migration: true,
  #   route_helper: "ninja",
  #   schema: true,
  #   schema_name: "Ninja",
  #   table_name: "ninjas",
  #   types: %{name: :string, skill: :integer},
  #   web_namespace: nil
  # }
  def additional_questions(argument_map) do
    argument_map
    |> ask_about_associations()
    |> ask_about_live(true)
    |> ask_about_admin(Application.get_env(:saas_kit, :admin) == true)
  end

  def ask_about_live(argument_map, false), do: argument_map
  def ask_about_live(argument_map, true) do
    Mix.shell().info """

    ===============================================================
    === Live CRUD Interface =======================================
    ===============================================================
    Do you want to add an LiveView interface with the CRUD actions
    """

    if Mix.shell().yes?(
      """
      #{IO.ANSI.green}Do you want to add an LiveView interface?#{IO.ANSI.reset}
      """ |> String.trim_trailing()) do

      argument_map
      |> Map.put(:live_view, true)
    else
      argument_map
      |> Map.put(:live_view, false)
    end
  end

  def ask_about_admin(argument_map, false), do: argument_map
  def ask_about_admin(%{web_namespace: "Admin"} = argument_map, true), do: Map.put(argument_map, :admin, true)
  def ask_about_admin(argument_map, true) do
    Mix.shell().info """

    ===============================================================
    === Admin Interface ===========================================
    ===============================================================
    Do you want to add an admin interface with the CRUD actions
    """

    if Mix.shell().yes?(
      """
      #{IO.ANSI.green}Do you want to add an admin interface?#{IO.ANSI.reset}
      """ |> String.trim_trailing()) do

      argument_map
      |> Map.put(:admin, true)
      |> Map.put_new(:web_namespace, "Admin")
    else
      argument_map
    end
  end

  def ask_about_associations(%{associations: []} = argument_map), do: argument_map
  def ask_about_associations(%{associations: [_|_] = associations} = argument_map) do
    associations =
      Enum.map(associations, &ask_about_association(&1))

    Map.put(argument_map, :associations, associations)
  end

  defp ask_about_association(%{schema_module: schema_module, table_name: table_name} = association) do
    Mix.shell().info """

    ===============================================================
    === Associations ==============================================
    ===============================================================
    The resource seems to belong to another schema.
    """

    if Mix.shell().yes?(
      """
      Is this the correct context and schema?
      #{IO.ANSI.green}#{schema_module}#{IO.ANSI.reset}
      """ |> String.trim_trailing()) do
      association
    else
      [app_name, _, schema_name] = schema_module |> String.split(".")
      context_name = Macro.camelize "#{table_name}"

      maybe_schema_module = Enum.join([app_name, context_name, schema_name], ".")

      if Mix.shell().yes?(
        """
        Ok, is this then the correct context and schema?
        #{IO.ANSI.green}#{maybe_schema_module}#{IO.ANSI.reset}
        """ |> String.trim_trailing()) do
        Map.put(association, :schema_module, maybe_schema_module)
      else
        new_schema_module = question_with_fallback(
        """

        Add the new schema name (ex: MyApp.Todos.Todo):
        """, :schema_module)
        Map.put(association, :schema_module, new_schema_module)
      end
    end
  end

  def question_with_fallback(question, rule, valid \\ true) do
    if valid == false do
      Mix.shell().error """

      You entered an invalid value, try to enter again.
      """
    end

    response = Mix.shell().prompt(question)
    response = String.trim("#{response}")

    if response |> String.split(".") |> length() == 3 do
      String.trim(response)
    else
      question_with_fallback(question, rule, false)
    end
  end
end