if Code.ensure_loaded?(Igniter) do defmodule AshPhoenix.Gen.Live do @moduledoc false def generate_from_cli(%Igniter{} = igniter, options) do domain = Keyword.fetch!(options, :domain) |> Igniter.Project.Module.parse() resource = Keyword.fetch!(options, :resource) |> Igniter.Project.Module.parse() resource_plural = Keyword.fetch!(options, :resource_plural) || resource |> Module.split() |> List.last() |> Macro.underscore() |> Igniter.Inflex.pluralize() opts = [ interactive?: true, resource_plural: resource_plural, phx_version: options[:phx_version] ] generate(igniter, domain, resource, opts) end def generate(igniter, 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(igniter)), actor: opts[:actor], actor_opt: actor_opt(opts) ] |> add_resource_assigns(resource, opts) web_live = Path.join([web_path(igniter), "live", "#{assigns[:resource_singular]}_live"]) generate_opts = if opts[:interactive?] do [] else [force: true, quiet: true] end igniter = write_formatted_templates(igniter, web_live, assigns, generate_opts, opts[:phx_version]) igniter = if opts[:interactive?] do Igniter.add_notice(igniter, """ Add the live routes to your browser scope in #{web_path(igniter)}/router.ex: #{for line <- live_route_instructions(assigns), do: " #{line}"} """) else igniter end igniter 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.Form, :new\n| end, if assigns[:update_action] do ~s|live "/#{assigns[:resource_plural]}/:id/edit", #{assigns[:resource_alias]}Live.Form, :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_templates(igniter, web_live, assigns, generate_opts, phx_version) do {path, igniter} = if String.starts_with?(phx_version, "1.8") do message = """ if Layouts.app causes a problem, you may be on an older version of the generators, try deleting the files and running the command again with --phx-version 1.7 """ {"new", Igniter.add_notice(igniter, message)} else {"old", igniter} end template_folder = template_folder(path) action? = assigns[:update_action] || assigns[:create_action] Enum.reduce(File.ls!(template_folder), igniter, fn "form.ex.eex", igniter when is_nil(action?) -> igniter file, igniter -> destination = String.replace_trailing(file, ".eex", "") destination_path = Path.join(web_live, destination) {formatter_function, _options} = Mix.Tasks.Format.formatter_for_file(destination_path) path = Path.join(template_folder, file) contents = path |> EEx.eval_file(assigns: assigns) |> formatter_function.() Igniter.create_new_file(igniter, destination_path, contents, generate_opts) end) 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: create_action && inputs(resource, create_action), update_inputs: update_action && 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(igniter) do web_module_path = Igniter.Project.Module.proper_location(igniter, web_module(igniter)) lib_dir = Path.dirname(web_module_path) Path.join([lib_dir, Path.basename(web_module_path, ".ex")]) end defp web_module(igniter) do Igniter.Libs.Phoenix.web_module(igniter) end defp template_folder(path) do :code.priv_dir(:ash_phoenix) |> Path.join("templates/ash_phoenix.gen.live") |> 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 end