diff --git a/lib/ash_phoenix/form/auto.ex b/lib/ash_phoenix/form/auto.ex index 62c3705..a51ec38 100644 --- a/lib/ash_phoenix/form/auto.ex +++ b/lib/ash_phoenix/form/auto.ex @@ -127,6 +127,29 @@ defmodule AshPhoenix.Form.Auto do constraints = unwrap_union(type, constraints) + data = + case form_type do + :list -> + fn parent -> + if parent do + Map.get(parent, attr.name) || [] + else + [] + end + end + + :single -> + fn parent -> + if parent do + case Map.get(parent, attr.name) do + [value | _] -> value + [] -> nil + value -> value + end + end + end + end + updater = fn opts, data, params -> {type, constraints} = determine_type(constraints, data, params) @@ -137,31 +160,6 @@ defmodule AshPhoenix.Form.Auto do {AshPhoenix.Form.WrappedValue, [], true} end - data = - case form_type do - :list -> - fn parent -> - if parent do - Map.get(parent, attr.name) || [] - else - [] - end - |> Enum.map(&wrap_value(&1, fake_embedded?)) - end - - :single -> - fn parent -> - if parent do - case Map.get(parent, attr.name) do - [value | _] -> value - [] -> nil - value -> value - end - end - |> wrap_value(fake_embedded?) - end - end - prepare_source = if fake_embedded? do fn source -> @@ -207,7 +205,6 @@ defmodule AshPhoenix.Form.Auto do prepare_source: prepare_source, transform_params: transform_params, embed?: true, - data: data, forms: [], updater: fn opts -> Keyword.update!(opts, :forms, fn forms -> @@ -223,6 +220,7 @@ defmodule AshPhoenix.Form.Auto do {attr.name, [ + data: data, type: form_type, updater: updater ]} @@ -230,12 +228,6 @@ defmodule AshPhoenix.Form.Auto do |> Keyword.new() end - defp wrap_value(value, true) do - %AshPhoenix.Form.WrappedValue{value: value} - end - - defp wrap_value(value, _), do: value - defp determine_type(constraints, _data, %{"_union_type" => union_type} = params) do constraints[:types] |> Enum.find(fn {key, _value} -> @@ -288,24 +280,32 @@ defmodule AshPhoenix.Form.Auto do end defp tags_equal(config, key, params) do - case config[:tag_value] || key do - value when is_atom(value) -> - params[to_string(config[:tag])] == to_string(value) || - params[to_string(config[:tag])] == value + if is_map(params) do + case config[:tag_value] || key do + value when is_atom(value) -> + params[to_string(config[:tag])] == to_string(value) || + params[to_string(config[:tag])] == value - value -> - params[to_string(config[:tag])] == value + value -> + params[to_string(config[:tag])] == value + end + else + false end end defp tags_equal_data(config, key, data) do - case config[:tag_value] || key do - value when is_atom(value) -> - data[config[:tag]] == to_string(value) || - data[config[:tag]] == value + if is_struct(data) do + case config[:tag_value] || key do + value when is_atom(value) -> + data[config[:tag]] == to_string(value) || + data[config[:tag]] == value - value -> - data[config[:tag]] == value + value -> + data[config[:tag]] == value + end + else + false end end diff --git a/lib/ash_phoenix/form/form.ex b/lib/ash_phoenix/form/form.ex index 1ce260d..eaf4950 100644 --- a/lib/ash_phoenix/form/form.ex +++ b/lib/ash_phoenix/form/form.ex @@ -375,8 +375,18 @@ defmodule AshPhoenix.Form do def for_action(resource_or_data, action, opts) do {resource, data} = case resource_or_data do - module when is_atom(resource_or_data) -> {module, module.__struct__()} - %resource{} = data -> {resource, data} + module when is_atom(resource_or_data) -> + {module, module.__struct__()} + + %resource{} = data -> + if Ash.Resource.Info.resource?(resource) do + {resource, data} + else + {AshPhoenix.Form.WrappedValue, %AshPhoenix.Form.WrappedValue{value: data}} + end + + value -> + {AshPhoenix.Form.WrappedValue, %AshPhoenix.Form.WrappedValue{value: value}} end type = @@ -1121,7 +1131,7 @@ defmodule AshPhoenix.Form do |> Enum.reduce(forms, fn {params, index}, forms -> case Enum.find(form.forms[key] || [], &matcher.(&1, params, form, key, index)) do nil -> - opts = update_opts(opts, nil, form_params) + opts = update_opts(opts, nil, params) new_form = cond do @@ -3201,6 +3211,7 @@ defmodule AshPhoenix.Form do tenant: form.opts[:tenant], accessing_from: config[:managed_relationship], transform_params: config[:transform_params], + prepare_source: config[:prepare_source], warn_on_unhandled_errors?: form.warn_on_unhandled_errors?, forms: config[:forms] || [], data: opts[:data], diff --git a/lib/ash_phoenix/form/wrapped_value.ex b/lib/ash_phoenix/form/wrapped_value.ex index e1899eb..2ba7a95 100644 --- a/lib/ash_phoenix/form/wrapped_value.ex +++ b/lib/ash_phoenix/form/wrapped_value.ex @@ -15,4 +15,103 @@ defmodule AshPhoenix.Form.WrappedValue do primary? true end end + + changes do + change fn changeset, _ -> + if Ash.Changeset.changing_attribute?(changeset, :value) do + value = Ash.Changeset.get_attribute(changeset, :value) + + with {:ok, casted} <- + Ash.Type.Helpers.cast_input( + changeset.context.type, + value, + changeset.context.constraints, + changeset + ), + {:constrained, {:ok, casted}} when not is_nil(casted) <- + {:constrained, + Ash.Type.apply_constraints( + changeset.context.type, + casted, + changeset.context.constraints + )} do + Ash.Changeset.force_change_attribute(changeset, :value, casted) + else + {:constrained, {:ok, nil}} -> + Ash.Changeset.force_change_attribute(changeset, :value, nil) + + {:constrained, {:error, error}, argument} -> + add_invalid_errors(value, :attribute, changeset, :value, error) + + {:error, error} -> + add_invalid_errors(value, :attributes, changeset, :value, error) + end + else + changeset + end + end + end + + defp add_invalid_errors(value, type, changeset, attribute, message) do + messages = + if Keyword.keyword?(message) do + [message] + else + List.wrap(message) + end + + Enum.reduce(messages, changeset, fn message, changeset -> + if is_exception(message) do + error = + message + |> Ash.Error.to_ash_error() + + errors = + case error do + %class{errors: errors} + when class in [ + Ash.Error.Invalid, + Ash.Error.Unknown, + Ash.Error.Forbidden, + Ash.Error.Framework + ] -> + errors + + error -> + [error] + end + + Enum.reduce(errors, changeset, fn error, changeset -> + Ash.Changeset.add_error(changeset, Ash.Error.set_path(error, attribute)) + end) + else + opts = Ash.Type.Helpers.error_to_exception_opts(message, %{name: attribute}) + + exception = + case type do + :attribute -> InvalidAttribute + :argument -> InvalidArgument + end + + Enum.reduce(opts, changeset, fn opts, changeset -> + error = + exception.exception( + value: value, + field: Keyword.get(opts, :field), + message: Keyword.get(opts, :message), + vars: opts + ) + + error = + if opts[:path] do + Ash.Error.set_path(error, opts[:path]) + else + error + end + + Ash.Changeset.add_error(changeset, error) + end) + end + end) + end end diff --git a/test/auto_form_test.exs b/test/auto_form_test.exs index 80fb8fc..a30505a 100644 --- a/test/auto_form_test.exs +++ b/test/auto_form_test.exs @@ -56,15 +56,15 @@ defmodule AshPhoenix.AutoFormTest do describe "single unions" do test "a form can be added for a union" do - Post - |> AshPhoenix.Form.for_create(:create, - api: Api, - forms: [ - auto?: true - ] - ) - |> AshPhoenix.Form.add_form(:union, params: %{"type" => "foo"}) - |> form_for("action") + Post + |> AshPhoenix.Form.for_create(:create, + api: Api, + forms: [ + auto?: true + ] + ) + |> AshPhoenix.Form.add_form(:union, params: %{"type" => "foo"}) + |> form_for("action") end test "a form can be removed from a union" do @@ -113,20 +113,72 @@ defmodule AshPhoenix.AutoFormTest do test "a form can be removed from a union" do form = - Post - |> AshPhoenix.Form.for_create(:create, - api: Api, - forms: [ - auto?: true - ] - ) - |> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"}) - |> form_for("action") + Post + |> AshPhoenix.Form.for_create(:create, + api: Api, + forms: [ + auto?: true + ] + ) + |> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"}) + |> form_for("action") AshPhoenix.Form.remove_form(form, [:union_array, 0]) end - end + test "validating a form with valid values works" do + form = + Post + |> AshPhoenix.Form.for_create(:create, + api: Api, + forms: [ + auto?: true + ] + ) + |> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"}) + |> form_for("action") + + assert %{union_array: [%Ash.Union{value: %{value: "abc"}}]} = + form + |> AshPhoenix.Form.validate(%{ + "text" => "text", + "union_array" => %{ + "0" => %{ + "type" => "foo", + "value" => "abc" + } + } + }) + |> AshPhoenix.Form.submit!() + end + + test "validating a form with an invalid value works" do + form = + Post + |> AshPhoenix.Form.for_create(:create, + api: Api, + forms: [ + auto?: true + ] + ) + |> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"}) + |> form_for("action") + + assert_raise Ash.Error.Invalid, ~r/must match the pattern/, fn -> + form + |> AshPhoenix.Form.validate(%{ + "text" => "text", + "union_array" => %{ + "0" => %{ + "type" => "foo", + "value" => "def" + } + } + }) + |> AshPhoenix.Form.submit!() + end + end + end defp auto_forms(resource, action) do [forms: Auto.auto(resource, action)] diff --git a/test/support/resources/foo.ex b/test/support/resources/foo.ex index 14e47eb..25fa287 100644 --- a/test/support/resources/foo.ex +++ b/test/support/resources/foo.ex @@ -7,6 +7,8 @@ defmodule AshPhoenix.Test.Foo do default "foo" end - attribute :value, :string + attribute :value, :string do + constraints match: ~r/abc/ + end end end