lib/mix/tasks/ash_phoenix.gen.html.ex

defmodule Mix.Tasks.AshPhoenix.Gen.Html do
  use Mix.Task

  @shortdoc "Generates a controller and HTML views for an existing Ash resource."

  @moduledoc """
  This task renders .ex and .heex templates and copies them to specified directories.

  ## Arguments

  api         The API (e.g. "Shop").
  resource    The resource (e.g. "Product").
  plural      The plural schema name (e.g. "products").

  ## Example

  mix ash_phoenix.gen.html Shop Product products
  """

  def run([]) do
    Mix.shell().info("""
    #{Mix.Task.shortdoc(__MODULE__)}

    #{Mix.Task.moduledoc(__MODULE__)}
    """)
  end

  def run(args) when length(args) == 3 do
    Mix.Task.run("compile")

    [api, resource, plural] = args
    singular = String.downcase(resource)

    opts = %{
      api: api,
      resource: resource,
      singular: singular,
      plural: plural
    }

    if Code.ensure_loaded?(resource_module(opts)) do
      source_path = Application.app_dir(:ash_phoenix, "priv/templates/ash_phoenix.gen.html")
      resource_html_dir = Macro.underscore(opts[:resource]) <> "_html"

      template_files(resource_html_dir, opts)
      |> generate_files(assigns([:api, :resource, :singular, :plural], opts), source_path)

      print_shell_instructions(opts[:resource], opts[:plural])
    else
      Mix.shell().info(
        "The resource #{app_name()}.#{opts[:api]}.#{opts[:resource]} does not exist."
      )
    end
  end

  defp assigns(keys, opts) do
    binding = Enum.map(keys, fn key -> {key, opts[key]} end)
    binding = [{:route_prefix, Macro.underscore(opts[:plural])} | binding]
    binding = [{:app_name, app_name()} | binding]
    binding = [{:attributes, attributes(opts)} | binding]
    Enum.into(binding, %{})
  end

  defp template_files(resource_html_dir, opts) do
    app_web_path = "lib/#{Macro.underscore(app_name())}_web"

    %{
      "index.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/index.html.heex",
      "show.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/show.html.heex",
      "resource_form.html.heex" =>
        "#{app_web_path}/controllers/#{resource_html_dir}/#{Macro.underscore(opts[:resource])}_form.html.heex",
      "new.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/new.html.heex",
      "edit.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/edit.html.heex",
      "controller.ex" =>
        "#{app_web_path}/controllers/#{Macro.underscore(opts[:resource])}_controller.ex",
      "html.ex" => "#{app_web_path}/controllers/#{Macro.underscore(opts[:resource])}_html.ex"
    }
  end

  defp generate_files(template_files, assigns, source_path) do
    Enum.each(template_files, fn {source_file, dest_file} ->
      Mix.Generator.create_file(
        dest_file,
        EEx.eval_file("#{source_path}/#{source_file}", assigns: assigns)
      )
    end)
  end

  defp app_name do
    app_name_atom = Mix.Project.config()[:app]
    Macro.camelize(Atom.to_string(app_name_atom))
  end

  defp print_shell_instructions(resource, plural) do
    Mix.shell().info("""

      Add the resource to your browser scope in lib/#{Macro.underscore(resource)}_web/router.ex:

        resources "/#{plural}", #{resource}Controller
    """)
  end

  defp resource_module(opts) do
    Module.concat(["#{app_name()}.#{opts[:api]}.#{opts[:resource]}"])
  end

  defp attributes(opts) do
    resource_module(opts)
    |> Ash.Resource.Info.attributes()
    |> Enum.map(&attribute_map/1)
    |> Enum.reject(&reject_attribute?/1)
  end

  defp attribute_map(attr) do
    %{name: attr.name, type: attr.type, writable?: attr.writable?, private?: attr.private?}
  end

  defp reject_attribute?(%{name: :id, type: Ash.Type.UUID}), do: true
  defp reject_attribute?(%{private?: true}), do: true
  defp reject_attribute?(_), do: false
end