# SPDX-FileCopyrightText: 2020 ash_phoenix contributors # # SPDX-License-Identifier: MIT defmodule AshPhoenix.Transformers.AddFormCodeInterfaces do @moduledoc false use Spark.Dsl.Transformer def after?(_), do: true def transform(dsl_state) do case Ash.Domain.Info.resource_references(dsl_state) do [] -> resource = Spark.Dsl.Transformer.get_persisted(dsl_state, :module) dsl_state |> Ash.Resource.Info.interfaces() |> Enum.filter(&match?(%Ash.Resource.Interface{}, &1)) |> Enum.uniq_by(& &1.name) |> Enum.reduce(dsl_state, &add_form_interface(&1, &2, resource, true)) |> then(&{:ok, &1}) references -> references |> Enum.reduce(dsl_state, fn reference, dsl_state -> reference.definitions |> Enum.filter(&match?(%Ash.Resource.Interface{}, &1)) |> Enum.uniq_by(& &1.name) |> Enum.reduce(dsl_state, &add_form_interface(&1, &2, reference.resource)) end) |> then(&{:ok, &1}) end end # sobelow_skip ["DOS.BinToAtom"] defp add_form_interface(interface, dsl_state, resource, resource? \\ false) do name = :"form_to_#{interface.name}" action = if resource? do Ash.Resource.Info.action(dsl_state, interface.action || interface.name) else Ash.Resource.Info.action(resource, interface.action || interface.name) end form = AshPhoenix.Info.form(dsl_state, interface.name) args = (form && form.args) || [] arg_vars_function = Enum.map(args, fn {:optional, key} -> default = Ash.CodeInterface.default_value(resource, action, key) {:\\, [], [{key, [], Elixir}, default]} key -> {key, [], Elixir} end) arg_names = Enum.map(args, fn {:optional, name} -> name name -> name end) delete = Enum.flat_map(arg_names, &[&1, to_string(&1)]) {private_args, params} = Enum.split_with(arg_names, fn arg -> Enum.any?(action.arguments, &(&1.name == arg && !&1.public?)) end) merge_params = {:%{}, [], Enum.map(params, &{to_string(&1), {&1, [], Elixir}})} private_args_merge = {:%{}, [], Enum.map(private_args, &{&1, {&1, [], Elixir}})} cond do !action -> dsl_state action.type in [:update, :destroy] and interface.require_reference? -> define = quote do @doc """ #{unquote(action.description) || "Creates a form for the #{unquote(action.name)} action on #{unquote(inspect(resource))}."} ## Options #{Spark.Options.docs(AshPhoenix.Form.for_opts())} Any *additional* options will be passed to the underlying call to build the source, i.e `Ash.ActionInput.for_action/4`, or `Ash.Changeset.for_*`. This means you can set things like the tenant/actor. These will be retained, and provided again when `Form.submit/3` is called. ## Nested Form Options #{Spark.Options.docs(AshPhoenix.Form.nested_form_opts())} """ |> Ash.CodeInterface.trim_double_newlines() def unquote(name)(record, unquote_splicing(arg_vars_function), form_opts \\ []) do form_opts = form_opts |> unquote(__MODULE__).merge_and_drop_params( unquote(merge_params), unquote(delete) ) |> unquote(__MODULE__).set_private_arguments(unquote(private_args_merge)) AshPhoenix.Form.for_action(record, unquote(action.name), form_opts) end end Spark.Dsl.Transformer.eval(dsl_state, [], define) true -> define = quote do @doc """ #{unquote(action.description) || "Creates a form for the #{unquote(action.name)} action on #{unquote(inspect(resource))}."} ## Options #{Spark.Options.docs(AshPhoenix.Form.for_opts())} Any *additional* options will be passed to the underlying call to build the source, i.e `Ash.ActionInput.for_action/4`, or `Ash.Changeset.for_*`. This means you can set things like the tenant/actor. These will be retained, and provided again when `Form.submit/3` is called. ## Nested Form Options #{Spark.Options.docs(AshPhoenix.Form.nested_form_opts())} """ |> Ash.CodeInterface.trim_double_newlines() def unquote(name)(unquote_splicing(arg_vars_function), form_opts \\ []) do # Only transform the argument params (merge_params), not all form params transformed_merge_params = unquote(__MODULE__).handle_custom_inputs( unquote(merge_params), unquote(Macro.escape(interface.custom_inputs)) ) form_opts = form_opts |> unquote(__MODULE__).merge_and_drop_params( transformed_merge_params, unquote(delete) ) |> unquote(__MODULE__).set_private_arguments(unquote(private_args_merge)) AshPhoenix.Form.for_action(unquote(resource), unquote(action.name), form_opts) |> Map.update!(:params, fn params -> params |> Map.drop(unquote(delete)) |> Map.merge(transformed_merge_params) end) |> Map.update!(:raw_params, fn params -> params |> Map.drop(unquote(delete)) |> Map.merge(transformed_merge_params) end) end end Spark.Dsl.Transformer.eval(dsl_state, [], define) end end @doc false def merge_and_drop_params(opts, merge_params, delete) when merge_params == %{} and delete == [] do opts end def merge_and_drop_params(opts, merge_params, delete) do opts |> Keyword.update(:params, merge_params, fn existing_params -> existing_params |> Kernel.||(%{}) |> Map.delete(delete) |> Map.merge(merge_params) end) |> Keyword.update( :transform_params, fn _form, params, _ -> params |> Map.drop(delete) |> Map.merge(merge_params) end, fn existing -> existing = existing || fn _, params, _ -> params end fn form, params, type -> params = params |> Map.drop(delete) |> Map.merge(merge_params) if is_function(existing, 2) do existing.(params, type) else existing.(form, params, type) end end end ) end @doc false def set_private_arguments(opts, empty) when empty == %{} do opts end def set_private_arguments(opts, private_args_merge) do Keyword.update( opts, :private_arguments, private_args_merge, &Map.merge(&1 || %{}, private_args_merge) ) end @doc false def handle_custom_inputs(params, []) do params end def handle_custom_inputs(params, custom_inputs) do Enum.reduce(custom_inputs, params, fn custom_input, acc_params -> case fetch_key(acc_params, custom_input.name) do {:ok, key, value} -> value = Ash.Type.Helpers.handle_indexed_maps(custom_input.type, value) case Ash.Type.cast_input(custom_input.type, value, custom_input.constraints) do {:ok, casted} -> case Ash.Type.apply_constraints(custom_input.type, casted, custom_input.constraints) do {:ok, casted} -> if is_nil(casted) && !custom_input.allow_nil? do acc_params else apply_custom_input_transform(acc_params, casted, key, custom_input) end _error -> acc_params end _error -> acc_params end :error -> acc_params end end) end defp apply_custom_input_transform(params, casted, key, %{transform: nil}) do Map.put(params, key, casted) end defp apply_custom_input_transform(params, casted, key, %{ transform: %{to: nil, using: nil} }) do Map.put(params, key, casted) end defp apply_custom_input_transform(params, casted, key, %{ transform: %{to: to, using: nil} }) do params |> Map.delete(key) |> Map.put(to_string(to), casted) end defp apply_custom_input_transform(params, casted, key, %{ transform: %{to: nil, using: using} }) do Map.put(params, key, using.(casted)) end defp apply_custom_input_transform(params, casted, key, %{ transform: %{to: to, using: using} }) do params |> Map.delete(key) |> Map.put(to_string(to), using.(casted)) end defp fetch_key(map, key) do with {_key, :error} <- {key, Map.fetch(map, key)}, string_key = to_string(key), {_key, :error} <- {string_key, Map.fetch(map, string_key)} do :error else {key, {:ok, value}} -> {:ok, key, value} end end end