Skip to content

Commit

Permalink
improvement: ash_phoenix.gen.html generator (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
wintermeyer authored Oct 16, 2023
1 parent 8935ad2 commit 18fa992
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 0 deletions.
121 changes: 121 additions & 0 deletions lib/mix/tasks/ash_phoenix.gen.html.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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
78 changes: 78 additions & 0 deletions priv/templates/ash_phoenix.gen.html/controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule <%= @app_name %>Web.<%= @resource %>Controller do
use <%= @app_name %>Web, :controller

alias <%= @app_name %>.<%= @api %>.<%= @resource %>

def index(conn, _params) do
<%= @plural %> = <%= @resource %>.read!()
render(conn, :index, <%= @plural %>: <%= @plural %>)
end

def new(conn, _params) do
render(conn, :new, form: create_form())
end

def create(conn, %{"<%= @singular %>" => <%= @singular %>_params}) do
<%= @singular %>_params
|> create_form()
|> AshPhoenix.Form.submit()
|> case do
{:ok, <%= @singular %>} ->
conn
|> put_flash(:info, "<%= @resource %> created successfully.")
|> redirect(to: ~p"/<%= @plural %>/#{<%= @singular %>}")

{:error, form} ->
conn
|> put_flash(:error, "<%= @resource %> could not be created.")
|> render(:new, form: form)
end
end

def show(conn, %{"id" => id}) do
<%= @singular %> = <%= @resource %>.by_id!(id)
render(conn, :show, <%= @singular %>: <%= @singular %>)
end

def edit(conn, %{"id" => id}) do
<%= @singular %> = <%= @resource %>.by_id!(id)

render(conn, :edit, <%= @singular %>: <%= @singular %>, form: update_form(<%= @singular %>))
end

def update(conn, %{"<%= @singular %>" => <%= @singular %>_params, "id" => id}) do
<%= @singular %> = <%= @resource %>.by_id!(id)

<%= @singular %>
|> update_form(<%= @singular %>_params)
|> AshPhoenix.Form.submit()
|> case do
{:ok, <%= @singular %>} ->
conn
|> put_flash(:info, "<%= @resource %> updated successfully.")
|> redirect(to: ~p"/<%= @plural %>/#{<%= @singular %>}")

{:error, form} ->
conn
|> put_flash(:error, "<%= @resource %> could not be updated.")
|> render(:edit, <%= @singular %>: <%= @singular %>, form: form)
end
end

def delete(conn, %{"id" => id}) do
<%= @singular %> = <%= @resource %>.by_id!(id)
:ok = <%= @resource %>.destroy(<%= @singular %>)

conn
|> put_flash(:info, "<%= @resource %> deleted successfully.")
|> redirect(to: ~p"/<%= @plural %>")
end

defp create_form(params \\ nil) do
AshPhoenix.Form.for_create(<%= @resource %>, :create, as: "<%= @singular %>", api: <%= @app_name %>.<%= @api %>, params: params)
end

defp update_form(<%= @singular %>, params \\ nil) do
AshPhoenix.Form.for_update(<%= @singular %>, :update, as: "<%= @singular %>", api: <%= @app_name %>.<%= @api %>, params: params)
end
end
8 changes: 8 additions & 0 deletions priv/templates/ash_phoenix.gen.html/edit.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<.header>
Edit <%= @resource %> <%%= @<%= @singular %>.id %>
<:subtitle>Use this form to manage <%= @singular %> records in your database.</:subtitle>
</.header>

<.<%= @singular %>_form <%= @singular %>={@<%= @singular %>} form={@form} action={~p"/<%= @plural %>/#{@<%= @singular %>}"} />

<.back navigate={~p"/<%= @plural %>"}>Back to <%= @plural %></.back>
5 changes: 5 additions & 0 deletions priv/templates/ash_phoenix.gen.html/html.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule <%= @app_name %>Web.<%= @resource %>HTML do
use <%= @app_name %>Web, :html

embed_templates "<%= @singular %>_html/*"
end
25 changes: 25 additions & 0 deletions priv/templates/ash_phoenix.gen.html/index.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<.header>
<%= @resource %> Listing
<:actions>
<.link href={~p"/<%= @route_prefix %>/new"}>
<.button>New <%= @resource %></.button>
</.link>
</:actions>
</.header>

<.table id="<%= @plural %>" rows={@<%= @plural %>} row_click={&JS.navigate(~p"/<%= @route_prefix %>/#{&1}")}>
<%= for attribute <- @attributes do %>
<:col :let={<%= @singular %>} label="<%= attribute.name %>"><%%= <%= @singular %>.<%= attribute.name %> %></:col>
<% end %>
<:action :let={<%= @singular %>}>
<div class="sr-only">
<.link navigate={~p"/<%= @route_prefix %>/#{<%= @singular %>}"}>Show</.link>
</div>
<.link navigate={~p"/<%= @route_prefix %>/#{<%= @singular %>}/edit"}>Edit</.link>
</:action>
<:action :let={<%= @singular %>}>
<.link href={~p"/<%= @route_prefix %>/#{<%= @singular %>}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
8 changes: 8 additions & 0 deletions priv/templates/ash_phoenix.gen.html/new.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<.header>
New <%= @resource %>
<:subtitle>Use this form to manage <%= @singular %> records in your database.</:subtitle>
</.header>

<.<%= @singular %>_form form={@form} action={~p"/<%= @plural %>/"} />

<.back navigate={~p"/products"}>Back to <%= @plural %></.back>
15 changes: 15 additions & 0 deletions priv/templates/ash_phoenix.gen.html/resource_form.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<.simple_form :let={f} for={@form} action={@action}>
<.error :if={@form.submitted_once?}>
Oops, something went wrong! Please check the errors below.
</.error>
<%= for attribute <- @attributes do %>
<%= if attribute.type in [Ash.Type.Integer] do %>
<.input field={f[:<%= attribute.name %>]} type="number" label="<%= attribute.name %>" />
<% else %>
<.input field={f[:<%= attribute.name %>]} type="text" label="<%= attribute.name %>" />
<% end %>
<% end %>
<:actions>
<.button>Save Product</.button>
</:actions>
</.simple_form>
17 changes: 17 additions & 0 deletions priv/templates/ash_phoenix.gen.html/show.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<.header>
<%= @resource %> <%%= @<%= @singular %>.id %>
<:subtitle>This is a <%= @singular %> record from your database.</:subtitle>
<:actions>
<.link href={~p"/<%= @plural %>/#{@<%= @singular %>}/edit"}>
<.button>Edit <%= @singular %></.button>
</.link>
</:actions>
</.header>

<.list>
<%= for attribute <- @attributes do %>
<:item title="<%= attribute.name %>"><%%= @<%= @singular %>.<%= attribute.name %> %></:item>
<% end %>
</.list>

<.back navigate={~p"/<%= @plural %>"}>Back to <%= @plural %></.back>

0 comments on commit 18fa992

Please sign in to comment.