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