mirror of
https://github.com/ash-project/ash_phoenix.git
synced 2024-09-19 06:42:47 +12:00
improvement: make generators more consistent
This commit is contained in:
parent
6b80d0fe50
commit
459dca3dfe
11 changed files with 201 additions and 113 deletions
|
@ -2385,9 +2385,8 @@ defmodule AshPhoenix.Form do
|
|||
defp get_non_attribute_non_argument_param(changeset, form, field) do
|
||||
if Ash.Resource.Info.attribute(changeset.resource, field) ||
|
||||
Enum.any?(changeset.action.arguments, &(&1.name == field)) do
|
||||
with :error <- Map.fetch(changeset.params, field),
|
||||
:error <- Map.fetch(changeset.params, to_string(field)) do
|
||||
:error
|
||||
with :error <- Map.fetch(changeset.params, field) do
|
||||
Map.fetch(changeset.params, to_string(field))
|
||||
end
|
||||
else
|
||||
Map.fetch(AshPhoenix.Form.params(form), Atom.to_string(field))
|
||||
|
|
79
lib/ash_phoenix/gen/gen.ex
Normal file
79
lib/ash_phoenix/gen/gen.ex
Normal file
|
@ -0,0 +1,79 @@
|
|||
defmodule AshPhoenix.Gen do
|
||||
@moduledoc false
|
||||
|
||||
def docs do
|
||||
"""
|
||||
## Positional Arguments
|
||||
|
||||
- `api` - The API (e.g. "Shop").
|
||||
- `resource` - The resource (e.g. "Product").
|
||||
|
||||
## Options
|
||||
|
||||
- `--resource-plural` - The plural resource name (e.g. "products")
|
||||
"""
|
||||
end
|
||||
|
||||
def parse_opts(argv) do
|
||||
{api, resource, rest} =
|
||||
case argv do
|
||||
[api, resource | rest] ->
|
||||
{api, resource, rest}
|
||||
|
||||
argv ->
|
||||
raise "Not enough arguments. Expected 2, got #{Enum.count(argv)}"
|
||||
end
|
||||
|
||||
if String.starts_with?(api, "-") do
|
||||
raise "Expected first argument to be an api module, not an option"
|
||||
end
|
||||
|
||||
if String.starts_with?(resource, "-") do
|
||||
raise "Expected second argument to be a resource module, not an option"
|
||||
end
|
||||
|
||||
{parsed, _, _} =
|
||||
OptionParser.parse(rest,
|
||||
strict: [resource_plural: :string, actor: :string, no_actor: :boolean]
|
||||
)
|
||||
|
||||
api = Module.concat([api])
|
||||
resource = Module.concat([resource])
|
||||
|
||||
parsed =
|
||||
Keyword.put_new_lazy(rest, :resource_plural, fn ->
|
||||
plural_name!(resource, parsed)
|
||||
end)
|
||||
|
||||
{api, resource, parsed, rest}
|
||||
end
|
||||
|
||||
defp plural_name!(resource, opts) do
|
||||
plural_name =
|
||||
opts[:resource_plural] ||
|
||||
Ash.Resource.Info.plural_name(resource) ||
|
||||
Mix.shell().prompt(
|
||||
"""
|
||||
Please provide a plural_name for #{inspect(resource)}. For example the plural of tweet is tweets.
|
||||
|
||||
This can also be configured on the resource. To do so, press enter to abort,
|
||||
and add the following configuration to your resource (using the proper plural name)
|
||||
|
||||
resource do
|
||||
plural_name :tweets
|
||||
end
|
||||
>
|
||||
"""
|
||||
|> String.trim()
|
||||
)
|
||||
|> String.trim()
|
||||
|
||||
case plural_name do
|
||||
empty when empty in ["", nil] ->
|
||||
raise("Must configure `plural_name` on resource or provide --resource-plural")
|
||||
|
||||
plural_name ->
|
||||
to_string(plural_name)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,38 +2,12 @@ defmodule AshPhoenix.Gen.Live do
|
|||
@moduledoc false
|
||||
|
||||
def generate_from_cli(argv) do
|
||||
if Mix.Project.umbrella?() do
|
||||
Mix.raise(
|
||||
"mix phx.gen.live must be invoked from within your *_web application root directory"
|
||||
)
|
||||
end
|
||||
|
||||
{api, resource, rest} =
|
||||
case argv do
|
||||
[api, resource | rest] ->
|
||||
{api, resource, rest}
|
||||
|
||||
argv ->
|
||||
raise "Not enough arguments. Expected 2, got #{Enum.count(argv)}"
|
||||
end
|
||||
|
||||
if String.starts_with?(api, "-") do
|
||||
raise "Expected first argument to be an api module, not an option"
|
||||
end
|
||||
|
||||
if String.starts_with?(resource, "-") do
|
||||
raise "Expected second argument to be a resource module, not an option"
|
||||
end
|
||||
|
||||
{parsed, _, _} =
|
||||
OptionParser.parse(rest,
|
||||
strict: [resource_plural: :string, actor: :string, no_actor: :boolean]
|
||||
)
|
||||
{api, resource, opts, _rest} = AshPhoenix.Gen.parse_opts(argv)
|
||||
|
||||
generate(
|
||||
Module.concat([api]),
|
||||
Module.concat([resource]),
|
||||
Keyword.put(parsed, :interactive?, true)
|
||||
api,
|
||||
resource,
|
||||
Keyword.put(opts, :interactive?, true)
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -47,11 +21,11 @@ defmodule AshPhoenix.Gen.Live do
|
|||
"Would you like to name your actor? For example: `current_user`. If you choose no, we will not add any actor logic."
|
||||
) do
|
||||
actor =
|
||||
Mix.shell().prompt("What would you like to name it? For example: `current_user`")
|
||||
Mix.shell().prompt("What would you like to name it? Default: `current_user`")
|
||||
|> String.trim()
|
||||
|
||||
if actor == "" do
|
||||
opts
|
||||
Keyword.put(opts, :actor, "current_user")
|
||||
else
|
||||
Keyword.put(opts, :actor, actor)
|
||||
end
|
||||
|
@ -172,7 +146,7 @@ defmodule AshPhoenix.Gen.Live do
|
|||
|> Ash.Resource.Info.short_name()
|
||||
|> to_string()
|
||||
|
||||
plural_name = plural_name!(resource, opts)
|
||||
plural_name = opts[:resource_plural]
|
||||
|
||||
pkey =
|
||||
case Ash.Resource.Info.primary_key(resource) do
|
||||
|
@ -331,33 +305,6 @@ defmodule AshPhoenix.Gen.Live do
|
|||
end
|
||||
end
|
||||
|
||||
defp plural_name!(resource, opts) do
|
||||
plural_name =
|
||||
opts[:resource_plural] ||
|
||||
Ash.Resource.Info.plural_name(resource) ||
|
||||
Mix.shell().prompt(
|
||||
"""
|
||||
Please provide a plural_name. For example the plural of tweet is tweets.
|
||||
You can press enter to abort, and then configure one on the resource, for example:
|
||||
|
||||
resource do
|
||||
plural_name :tweets
|
||||
end
|
||||
>
|
||||
"""
|
||||
|> String.trim()
|
||||
)
|
||||
|> String.trim()
|
||||
|
||||
case plural_name do
|
||||
empty when empty in ["", nil] ->
|
||||
raise("Must configure `plural_name` on resource or provide --resource-plural")
|
||||
|
||||
plural_name ->
|
||||
to_string(plural_name)
|
||||
end
|
||||
end
|
||||
|
||||
defp web_path do
|
||||
web_module().module_info[:compile][:source]
|
||||
|> Path.relative_to(root_path())
|
||||
|
|
|
@ -6,18 +6,14 @@ defmodule Mix.Tasks.AshPhoenix.Gen.Html do
|
|||
@moduledoc """
|
||||
This task renders .ex and .heex templates and copies them to specified directories.
|
||||
|
||||
## Arguments
|
||||
#{AshPhoenix.Gen.docs()}
|
||||
|
||||
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
|
||||
mix ash_phoenix.gen.html MyApp.Shop MyApp.Shop.Product --plural-name products
|
||||
"""
|
||||
|
||||
def run([]) do
|
||||
not_umbrella!()
|
||||
|
||||
Mix.shell().info("""
|
||||
#{Mix.Task.shortdoc(__MODULE__)}
|
||||
|
||||
|
@ -25,55 +21,70 @@ defmodule Mix.Tasks.AshPhoenix.Gen.Html do
|
|||
""")
|
||||
end
|
||||
|
||||
def run(args) when length(args) == 3 do
|
||||
def run(args) do
|
||||
not_umbrella!()
|
||||
Mix.Task.run("compile")
|
||||
|
||||
[api, resource, plural] = args
|
||||
singular = String.downcase(resource)
|
||||
{api, resource, opts, _} = AshPhoenix.Gen.parse_opts(args)
|
||||
|
||||
singular = to_string(Ash.Resource.Info.short_name(resource))
|
||||
|
||||
opts = %{
|
||||
api: api,
|
||||
resource: resource,
|
||||
resource: List.last(Module.split(resource)),
|
||||
full_resource: resource,
|
||||
full_api: api,
|
||||
singular: singular,
|
||||
plural: plural
|
||||
plural: opts[:resource_plural]
|
||||
}
|
||||
|
||||
if Code.ensure_loaded?(resource_module(opts)) do
|
||||
if Code.ensure_loaded?(resource) do
|
||||
source_path = Application.app_dir(:ash_phoenix, "priv/templates/ash_phoenix.gen.html")
|
||||
resource_html_dir = Macro.underscore(opts[:resource]) <> "_html"
|
||||
resource_html_dir = to_string(opts[:singular]) <> "_html"
|
||||
|
||||
template_files(resource_html_dir, opts)
|
||||
|> generate_files(assigns([:api, :resource, :singular, :plural], opts), source_path)
|
||||
|> generate_files(
|
||||
assigns([:api, :full_resource, :full_api, :resource, :singular, :plural], resource, opts),
|
||||
source_path
|
||||
)
|
||||
|
||||
print_shell_instructions(opts[:resource], opts[:plural])
|
||||
print_shell_instructions(opts)
|
||||
else
|
||||
Mix.shell().info(
|
||||
"The resource #{app_name()}.#{opts[:api]}.#{opts[:resource]} does not exist."
|
||||
"The resource #{inspect(opts[:api])}.#{inspect(opts[:resource])} does not exist."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp assigns(keys, opts) do
|
||||
defp not_umbrella! do
|
||||
if Mix.Project.umbrella?() do
|
||||
Mix.raise(
|
||||
"mix phx.gen.html must be invoked from within your *_web application root directory"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp assigns(keys, resource, opts) do
|
||||
binding = Enum.map(keys, fn key -> {key, opts[key]} end)
|
||||
binding = [{:route_prefix, Macro.underscore(opts[:plural])} | binding]
|
||||
binding = [{:route_prefix, to_string(opts[:plural])} | binding]
|
||||
binding = [{:app_name, app_name()} | binding]
|
||||
binding = [{:attributes, attributes(opts)} | binding]
|
||||
binding = [{:attributes, attributes(resource)} | binding]
|
||||
binding = [{:update_attributes, update_attributes(resource)} | binding]
|
||||
binding = [{:create_attributes, create_attributes(resource)} | binding]
|
||||
Enum.into(binding, %{})
|
||||
end
|
||||
|
||||
defp template_files(resource_html_dir, opts) do
|
||||
app_web_path = "lib/#{Macro.underscore(app_name())}_web"
|
||||
app_web_path = "lib/#{app_name_underscore()}_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",
|
||||
"#{app_web_path}/controllers/#{resource_html_dir}/#{opts[:singular]}_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"
|
||||
"controller.ex" => "#{app_web_path}/controllers/#{opts[:singular]}_controller.ex",
|
||||
"html.ex" => "#{app_web_path}/controllers/#{opts[:singular]}_html.ex"
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -86,36 +97,63 @@ defmodule Mix.Tasks.AshPhoenix.Gen.Html do
|
|||
end)
|
||||
end
|
||||
|
||||
defp app_name_underscore do
|
||||
Mix.Project.config()[:app]
|
||||
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
|
||||
defp print_shell_instructions(opts) do
|
||||
Mix.shell().info("""
|
||||
|
||||
Add the resource to your browser scope in lib/#{Macro.underscore(resource)}_web/router.ex:
|
||||
Add the resource to your browser scope in lib/#{opts[:singular]}_web/router.ex:
|
||||
|
||||
resources "/#{plural}", #{resource}Controller
|
||||
resources "/#{opts[:plural]}", #{opts[:resource]}Controller
|
||||
""")
|
||||
end
|
||||
|
||||
defp resource_module(opts) do
|
||||
Module.concat(["#{app_name()}.#{opts[:api]}.#{opts[:resource]}"])
|
||||
defp attributes(resource) do
|
||||
resource
|
||||
|> Ash.Resource.Info.public_attributes()
|
||||
|> Enum.reject(&(&1.type == Ash.Type.UUID))
|
||||
|> Enum.map(&attribute_map/1)
|
||||
end
|
||||
|
||||
defp attributes(opts) do
|
||||
resource_module(opts)
|
||||
|> Ash.Resource.Info.attributes()
|
||||
defp create_attributes(resource) do
|
||||
create_action = Ash.Resource.Info.primary_action!(resource, :create)
|
||||
|
||||
attrs =
|
||||
create_action.accept
|
||||
|> Enum.map(&Ash.Resource.Info.attribute(resource, &1))
|
||||
|> Enum.filter(& &1.writable?)
|
||||
|
||||
create_action.arguments
|
||||
|> Enum.concat(attrs)
|
||||
|> Enum.map(&attribute_map/1)
|
||||
end
|
||||
|
||||
defp update_attributes(resource) do
|
||||
update_action = Ash.Resource.Info.primary_action!(resource, :update)
|
||||
|
||||
attrs =
|
||||
update_action.accept
|
||||
|> Enum.map(&Ash.Resource.Info.attribute(resource, &1))
|
||||
|> Enum.filter(& &1.writable?)
|
||||
|
||||
update_action.arguments
|
||||
|> Enum.concat(attrs)
|
||||
|> 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?}
|
||||
%{
|
||||
name: attr.name,
|
||||
type: attr.type,
|
||||
writable?: Map.get(attr, :writable?, true),
|
||||
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
|
||||
|
|
|
@ -4,6 +4,8 @@ defmodule Mix.Tasks.AshPhoenix.Gen.Live do
|
|||
|
||||
The api and resource must already exist, this task does not define them.
|
||||
|
||||
#{AshPhoenix.Gen.docs()}
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
|
@ -15,6 +17,13 @@ defmodule Mix.Tasks.AshPhoenix.Gen.Live do
|
|||
@shortdoc "Generates liveviews for a resource"
|
||||
def run(argv) do
|
||||
Mix.Task.run("compile")
|
||||
|
||||
if Mix.Project.umbrella?() do
|
||||
Mix.raise(
|
||||
"mix phx.gen.live must be invoked from within your *_web application root directory"
|
||||
)
|
||||
end
|
||||
|
||||
AshPhoenix.Gen.Live.generate_from_cli(argv)
|
||||
end
|
||||
end
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -124,6 +124,10 @@ defmodule AshPhoenix.MixProject do
|
|||
AshPhoenix.LiveView,
|
||||
AshPhoenix.SubdomainPlug
|
||||
],
|
||||
Generators: [
|
||||
Mix.Tasks.AshPhoenix.Gen.Html,
|
||||
Mix.Tasks.AshPhoenix.Gen.Live
|
||||
],
|
||||
Forms: [
|
||||
AshPhoenix.Form,
|
||||
AshPhoenix.Form.Auto,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule <%= @app_name %>Web.<%= @resource %>Controller do
|
||||
use <%= @app_name %>Web, :controller
|
||||
|
||||
alias <%= @app_name %>.<%= @api %>.<%= @resource %>
|
||||
alias <%= inspect @full_resource %>
|
||||
|
||||
def index(conn, _params) do
|
||||
<%= @plural %> = <%= @resource %>.read!()
|
||||
|
@ -69,10 +69,10 @@ defmodule <%= @app_name %>Web.<%= @resource %>Controller do
|
|||
end
|
||||
|
||||
defp create_form(params \\ nil) do
|
||||
AshPhoenix.Form.for_create(<%= @resource %>, :create, as: "<%= @singular %>", api: <%= @app_name %>.<%= @api %>, params: params)
|
||||
AshPhoenix.Form.for_create(<%= @resource %>, :create, as: "<%= @singular %>", api: <%= @full_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)
|
||||
AshPhoenix.Form.for_update(<%= @singular %>, :update, as: "<%= @singular %>", api: <%= @full_api %>, params: params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<.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>
|
||||
<:col :let={<%= @singular %>} label="<%= Phoenix.Naming.humanize(attribute.name) %>"><%%= <%= @singular %>.<%= attribute.name %> %></:col>
|
||||
<% end %>
|
||||
<:action :let={<%= @singular %>}>
|
||||
<div class="sr-only">
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
|
||||
<.<%= @singular %>_form form={@form} action={~p"/<%= @plural %>/"} />
|
||||
|
||||
<.back navigate={~p"/products"}>Back to <%= @plural %></.back>
|
||||
<.back navigate={~p"/<%= @plural %>"}>Back to <%= @plural %></.back>
|
||||
|
|
|
@ -2,13 +2,25 @@
|
|||
<.error :if={@form.submitted_once?}>
|
||||
Oops, something went wrong! Please check the errors below.
|
||||
</.error>
|
||||
<%= for attribute <- @attributes do %>
|
||||
<%%= if @form.type == :update do %>
|
||||
<%= for attribute <- @update_attributes do %>
|
||||
<%= if attribute.type in [Ash.Type.Integer] do %>
|
||||
<.input field={f[:<%= attribute.name %>]} type="number" label="<%= attribute.name %>" />
|
||||
<.input field={f[:<%= attribute.name %>]} type="number" label="<%= Phoenix.Naming.humanize(attribute.name) %>" />
|
||||
<% else %>
|
||||
<.input field={f[:<%= attribute.name %>]} type="text" label="<%= attribute.name %>" />
|
||||
<.input field={f[:<%= attribute.name %>]} type="text" label="<%= Phoenix.Naming.hunamize(attribute.name) %>" />
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%% end %>
|
||||
|
||||
<%%= if @form.type == :create do %>
|
||||
<%= for attribute <- @update_attributes do %>
|
||||
<%= if attribute.type in [Ash.Type.Integer] do %>
|
||||
<.input field={f[:<%= attribute.name %>]} type="number" label="<%= Phoenix.Naming.humanize(attribute.name) %>" />
|
||||
<% else %>
|
||||
<.input field={f[:<%= attribute.name %>]} type="text" label="<%= Phoenix.Naming.humanize(attribute.name) %>" />
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%% end %>
|
||||
<:actions>
|
||||
<.button>Save Product</.button>
|
||||
</:actions>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<.list>
|
||||
<%= for attribute <- @attributes do %>
|
||||
<:item title="<%= attribute.name %>"><%%= @<%= @singular %>.<%= attribute.name %> %></:item>
|
||||
<:item title="<%= Phoenix.Naming.hunamize(attribute.name) %>"><%%= @<%= @singular %>.<%= attribute.name %> %></:item>
|
||||
<% end %>
|
||||
</.list>
|
||||
|
||||
|
|
Loading…
Reference in a new issue