Packages
ash_phoenix
2.1.7
2.3.24
2.3.23
2.3.22
2.3.21
2.3.20
2.3.19
2.3.18
2.3.17
2.3.16
2.3.15
2.3.14
2.3.13
2.3.12
2.3.11
2.3.10
2.3.9
2.3.8
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.0
2.1.26
2.1.25
2.1.24
2.1.23
2.1.22
2.1.21
2.1.20
2.1.19
2.1.18
2.1.17
2.1.16
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.3.7
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.26
1.2.25
1.2.24
1.2.23
1.2.22
1.2.21
1.2.20
1.2.19
1.2.18
1.2.17
1.2.16
1.2.15
1.2.14
1.2.13
1.2.12
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.0-rc.1
1.0.0-rc.0
1.0.0-pre.2
1.0.0-pre.1
1.0.0-pre.0
0.7.7
0.7.6-rc.0
0.7.5
0.7.4
0.7.3
0.7.2-rc.2
0.7.2-rc.1
0.7.2-rc.0
0.7.1
0.7.0
0.6.0-rc.7
0.6.0-rc.6
0.6.0-rc.5
0.6.0-rc.4
0.6.0-rc.3
0.6.0-rc.2
0.6.0-rc.1
0.6.0-rc.0
0.5.19-rc.2
0.5.19-rc.1
0.5.19-rc.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.24
0.4.23
0.4.23-rc.1
0.4.23-rc.0
0.4.22-rc2
0.4.22-rc1
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Utilities for integrating Ash and Phoenix
Current section
Files
Jump to
Current section
Files
lib/ash_phoenix/gen/live.ex
defmodule AshPhoenix.Gen.Live do
@moduledoc false
def generate_from_cli(argv) do
{domain, resource, opts, _rest} = AshPhoenix.Gen.parse_opts(argv)
generate(
domain,
resource,
Keyword.put(opts, :interactive?, true)
)
end
def generate(domain, resource, opts \\ []) do
Code.ensure_compiled!(domain)
Code.ensure_compiled!(resource)
opts =
if !opts[:actor] && opts[:interactive?] && !opts[:no_actor] do
if Mix.shell().yes?(
"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? Default: `current_user`")
|> String.trim()
if actor == "" do
Keyword.put(opts, :actor, "current_user")
else
Keyword.put(opts, :actor, actor)
end
else
opts
end
else
opts
end
opts =
if opts[:no_actor] do
Keyword.put(opts, :actor, nil)
else
opts
end
if !Spark.Dsl.is?(domain, Ash.Domain) do
raise "#{inspect(domain)} is not a valid Ash Domain module"
end
if !Ash.Resource.Info.resource?(resource) do
raise "#{inspect(resource)} is not a valid Ash Resource module"
end
assigns =
[
domain: inspect(domain),
resource: inspect(resource),
web_module: inspect(web_module()),
actor: opts[:actor],
actor_opt: actor_opt(opts)
]
|> add_resource_assigns(resource, opts)
web_live = Path.join([web_path(), "live", "#{assigns[:resource_singular]}_live"])
generate_opts =
if opts[:interactive?] do
[]
else
[force: true, quiet: true]
end
write_formatted_template(
"ash_phoenix.gen.live/index.ex.eex",
"index.ex",
web_live,
assigns,
generate_opts
)
if assigns[:update_action] || assigns[:create_action] do
write_formatted_template(
"ash_phoenix.gen.live/form_component.ex.eex",
"form_component.ex",
web_live,
assigns,
generate_opts
)
end
write_formatted_template(
"ash_phoenix.gen.live/show.ex.eex",
"show.ex",
web_live,
assigns,
generate_opts
)
if opts[:interactive?] do
Mix.shell().info("""
Add the live routes to your browser scope in #{web_path()}/router.ex:
#{for line <- live_route_instructions(assigns), do: " #{line}"}
""")
end
end
defp live_route_instructions(assigns) do
[
~s|live "/#{assigns[:resource_plural]}", #{assigns[:resource_alias]}Live.Index, :index\n|,
if assigns[:create_action] do
~s|live "/#{assigns[:resource_plural]}/new", #{assigns[:resource_alias]}Live.Index, :new\n|
end,
if assigns[:update_action] do
~s|live "/#{assigns[:resource_plural]}/:id/edit", #{assigns[:resource_alias]}Live.Index, :edit\n\n|
end,
~s|live "/#{assigns[:resource_plural]}/:id", #{assigns[:resource_alias]}Live.Show, :show\n|,
if assigns[:update_action] do
~s|live "/#{assigns[:resource_plural]}/:id/show/edit", #{assigns[:resource_alias]}Live.Show, :edit|
end
]
|> Enum.reject(&is_nil/1)
end
defp write_formatted_template(path, destination, web_live, assigns, generate_opts) do
destination_path =
web_live
|> Path.join(destination)
{formatter_function, _options} =
Mix.Tasks.Format.formatter_for_file(destination_path)
contents =
path
|> template()
|> EEx.eval_file(assigns: assigns)
|> formatter_function.()
Mix.Generator.create_file(destination_path, contents, generate_opts)
end
defp add_resource_assigns(assigns, resource, opts) do
short_name =
resource
|> Ash.Resource.Info.short_name()
|> to_string()
plural_name = opts[:resource_plural]
pkey =
case Ash.Resource.Info.primary_key(resource) do
[pkey] ->
pkey
_ ->
raise "Resources without a primary key or with a composite primary key are not currently supported."
end
get_by_pkey = get_by_pkey(resource, pkey, opts)
create_action = action(resource, opts, :create)
update_action = action(resource, opts, :update)
Keyword.merge(assigns,
resource_singular: short_name,
resource_alias: Macro.camelize(short_name),
resource_human_singular: Phoenix.Naming.humanize(short_name),
resource_human_plural: Phoenix.Naming.humanize(plural_name),
resource_plural: plural_name,
create_action: create_action,
update_action: update_action,
create_inputs: inputs(resource, create_action),
update_inputs: inputs(resource, update_action),
destroy: destroy(short_name, get_by_pkey, resource, opts),
pkey: pkey,
get_by_pkey: get_by_pkey,
attrs: attrs(resource),
route_prefix: "/#{plural_name}"
)
end
defp attrs(resource) do
resource
|> Ash.Resource.Info.public_attributes()
end
# sobelow_skip ["DOS.BinToAtom"]
defp action(resource, opts, type) do
action =
case opts[:"#{type}_action"] do
nil ->
Ash.Resource.Info.primary_action(resource, type)
action ->
case Ash.Resource.Info.action(resource, action, type) do
nil ->
raise "No such #{type} action #{inspect(action)}"
action ->
action
end
end
if opts[:interactive?] && !action do
actions =
resource
|> Ash.Resource.Info.actions()
|> Enum.filter(&(&1.type == type))
if Enum.empty?(actions) do
if Mix.shell().yes?(
"Primary #{type} action not found, and a #{type} action not supplied. Would you like to create one?"
) do
if Mix.shell().yes?("""
This is a manual step currently. Please add a primary #{type} action or designate one as primary, and then select Y.
Press anything else to cancel and proceed with no update action.
""") do
action(resource, opts, type)
end
end
else
if Mix.shell().yes?(
"Primary #{type} action not found. Would you like to use one of the following?:\n#{Enum.map_join(actions, "\n", &"- #{&1.name}")}"
) do
action =
Mix.shell().prompt(
"""
Please enter the name of the action you would like to use.
Press enter to cancel and proceed with no #{type} action.
>
"""
|> String.trim()
)
|> String.trim()
case action do
"" ->
nil
action ->
action(resource, Keyword.put(opts, :"#{type}_action", String.to_atom(action)), type)
end
else
if Mix.shell().yes?("Would you like to create one?") do
if Mix.shell().yes?("""
This is a manual step currently. Please add a primary #{type} action or designate one as primary, and then select Y.
Press anything else to cancel and proceed with no #{type} action.
""") do
action(resource, opts, type)
end
end
end
end
else
action
end
end
defp destroy(short_name, get_by_pkey, resource, opts) do
action = action(resource, opts, :destroy)
if action do
resource
|> Ash.Resource.Info.interfaces()
|> Enum.find(fn interface ->
interface.action == action.name && interface.args == []
end)
|> case do
nil ->
"""
#{short_name} = #{get_by_pkey}
Ash.destroy!(#{short_name}#{actor_opt(opts)})
"""
interface ->
"""
#{short_name} = #{get_by_pkey}
#{inspect(resource)}.#{interface.name}!(#{short_name}#{actor_opt(opts)})
"""
end
end
end
defp get_by_pkey(resource, pkey, opts) do
resource
|> Ash.Resource.Info.interfaces()
|> Enum.find(fn interface ->
to_string(interface.name) == "by_#{pkey}" and List.wrap(interface.get_by) == [pkey]
end)
|> case do
nil ->
"Ash.get!(#{inspect(resource)}, #{pkey}#{actor_opt(opts)})"
interface ->
"#{inspect(resource)}.#{interface.name}!(#{pkey}#{actor_opt(opts)})"
end
end
defp actor_opt(opts) do
if opts[:actor] do
", actor: socket.assigns.#{opts[:actor]}"
else
""
end
end
defp web_path do
web_module().module_info[:compile][:source]
|> Path.relative_to(root_path())
|> Path.rootname()
end
defp root_path do
Mix.Project.get().module_info[:compile][:source]
|> Path.dirname()
end
defp web_module do
base = Mix.Phoenix.base()
cond do
Mix.Phoenix.context_app() != Mix.Phoenix.otp_app() ->
Module.concat([base])
String.ends_with?(base, "Web") ->
Module.concat([base])
true ->
Module.concat(["#{base}Web"])
end
end
defp template(path) do
:code.priv_dir(:ash_phoenix) |> Path.join("templates") |> Path.join(path)
end
def inputs(resource, action) do
Enum.map(
action.arguments ++ Enum.map(action.accept, &Ash.Resource.Info.attribute(resource, &1)),
fn field ->
case field.type do
Ash.Type.Integer ->
~s(<.input field={@form[#{inspect(field.name)}]} type="number" label="#{label(field.name)}" />)
Ash.Type.Float ->
~s(<.input field={@form[#{inspect(field.name)}]} type="number" label="#{label(field.name)}" step="any" />)
Ash.Type.Decimal ->
~s(<.input field={@form[#{inspect(field.name)}]} type="number" label="#{label(field.name)}" step="any" />)
Ash.Type.Boolean ->
~s(<.input field={@form[#{inspect(field.name)}]} type="checkbox" label="#{label(field.name)}" />)
Ash.Type.String ->
~s(<.input field={@form[#{inspect(field.name)}]} type="text" label="#{label(field.name)}" />)
Ash.Type.Date ->
~s(<.input field={@form[#{inspect(field.name)}]} type="date" label="#{label(field.name)}" />)
Ash.Type.Time ->
~s(<.input field={@form[#{inspect(field.name)}]} type="time" label="#{label(field.name)}" />)
datetime when datetime in [Ash.Type.UtcDatetime, Ash.Type.UtcDatetimeUsec] ->
~s(<.input field={@form[#{inspect(field.name)}]} type="datetime-local" label="#{label(field.name)}" />)
Ash.Type.NaiveDatetime ->
~s(<.input field={@form[#{inspect(field.name)}]} type="datetime-local" label="#{label(field.name)}" />)
Ash.Type.Atom ->
case field.constraints[:one_of] do
nil ->
~s(<.input field={@form[#{inspect(field.name)}]} type="text" label="#{label(field.name)}" />)
_ ->
~s"""
<.input
field={@form[#{inspect(field.name)}]}
type="select"
label="#{label(field.name)}"
options={Ash.Resource.Info.attribute(#{inspect(resource)}, #{inspect(field.name)}).constraints[:one_of]}
/>
"""
end
{:array, type} ->
~s"""
<.input
field={@form[#{inspect(field.name)}]}
type="select"
multiple
label="#{label(field.name)}"
options={#{inspect(default_options(type))}}
/>
"""
type when is_atom(type) ->
if function_exported?(type, :values, 0) do
~s"""
<.input
field={@form[#{inspect(field.name)}]}
type="select"
multiple
label="#{label(field.name)}"
options={#{inspect(type)}.values()}
/>
"""
else
~s(<.input field={@form[#{inspect(field.name)}]} type="text" label="#{label(field.name)}" />)
end
_ ->
~s(<.input field={@form[#{inspect(field.name)}]} type="text" label="#{label(field.name)}" />)
end
end
)
end
defp default_options(Ash.Type.String),
do: Enum.map([1, 2], &{"Option #{&1}", "option#{&1}"})
defp default_options(Ash.Type.Integer),
do: Enum.map([1, 2], &{"#{&1}", &1})
defp default_options(_), do: []
defp label(key), do: Phoenix.Naming.humanize(to_string(key))
end