fix: properly track form params for nested unions

fix: produce type errors for wrapped union values
This commit is contained in:
Zach Daniel 2023-08-13 05:46:42 -07:00
parent e295fb72f7
commit 3a7d436079
5 changed files with 231 additions and 67 deletions

View file

@ -127,6 +127,29 @@ defmodule AshPhoenix.Form.Auto do
constraints = unwrap_union(type, constraints) 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 -> updater = fn opts, data, params ->
{type, constraints} = determine_type(constraints, data, params) {type, constraints} = determine_type(constraints, data, params)
@ -137,31 +160,6 @@ defmodule AshPhoenix.Form.Auto do
{AshPhoenix.Form.WrappedValue, [], true} {AshPhoenix.Form.WrappedValue, [], true}
end 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 = prepare_source =
if fake_embedded? do if fake_embedded? do
fn source -> fn source ->
@ -207,7 +205,6 @@ defmodule AshPhoenix.Form.Auto do
prepare_source: prepare_source, prepare_source: prepare_source,
transform_params: transform_params, transform_params: transform_params,
embed?: true, embed?: true,
data: data,
forms: [], forms: [],
updater: fn opts -> updater: fn opts ->
Keyword.update!(opts, :forms, fn forms -> Keyword.update!(opts, :forms, fn forms ->
@ -223,6 +220,7 @@ defmodule AshPhoenix.Form.Auto do
{attr.name, {attr.name,
[ [
data: data,
type: form_type, type: form_type,
updater: updater updater: updater
]} ]}
@ -230,12 +228,6 @@ defmodule AshPhoenix.Form.Auto do
|> Keyword.new() |> Keyword.new()
end 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 defp determine_type(constraints, _data, %{"_union_type" => union_type} = params) do
constraints[:types] constraints[:types]
|> Enum.find(fn {key, _value} -> |> Enum.find(fn {key, _value} ->
@ -288,24 +280,32 @@ defmodule AshPhoenix.Form.Auto do
end end
defp tags_equal(config, key, params) do defp tags_equal(config, key, params) do
case config[:tag_value] || key do if is_map(params) do
value when is_atom(value) -> case config[:tag_value] || key do
params[to_string(config[:tag])] == to_string(value) || value when is_atom(value) ->
params[to_string(config[:tag])] == value params[to_string(config[:tag])] == to_string(value) ||
params[to_string(config[:tag])] == value
value -> value ->
params[to_string(config[:tag])] == value params[to_string(config[:tag])] == value
end
else
false
end end
end end
defp tags_equal_data(config, key, data) do defp tags_equal_data(config, key, data) do
case config[:tag_value] || key do if is_struct(data) do
value when is_atom(value) -> case config[:tag_value] || key do
data[config[:tag]] == to_string(value) || value when is_atom(value) ->
data[config[:tag]] == value data[config[:tag]] == to_string(value) ||
data[config[:tag]] == value
value -> value ->
data[config[:tag]] == value data[config[:tag]] == value
end
else
false
end end
end end

View file

@ -375,8 +375,18 @@ defmodule AshPhoenix.Form do
def for_action(resource_or_data, action, opts) do def for_action(resource_or_data, action, opts) do
{resource, data} = {resource, data} =
case resource_or_data do case resource_or_data do
module when is_atom(resource_or_data) -> {module, module.__struct__()} module when is_atom(resource_or_data) ->
%resource{} = data -> {resource, 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 end
type = type =
@ -1121,7 +1131,7 @@ defmodule AshPhoenix.Form do
|> Enum.reduce(forms, fn {params, index}, forms -> |> Enum.reduce(forms, fn {params, index}, forms ->
case Enum.find(form.forms[key] || [], &matcher.(&1, params, form, key, index)) do case Enum.find(form.forms[key] || [], &matcher.(&1, params, form, key, index)) do
nil -> nil ->
opts = update_opts(opts, nil, form_params) opts = update_opts(opts, nil, params)
new_form = new_form =
cond do cond do
@ -3201,6 +3211,7 @@ defmodule AshPhoenix.Form do
tenant: form.opts[:tenant], tenant: form.opts[:tenant],
accessing_from: config[:managed_relationship], accessing_from: config[:managed_relationship],
transform_params: config[:transform_params], transform_params: config[:transform_params],
prepare_source: config[:prepare_source],
warn_on_unhandled_errors?: form.warn_on_unhandled_errors?, warn_on_unhandled_errors?: form.warn_on_unhandled_errors?,
forms: config[:forms] || [], forms: config[:forms] || [],
data: opts[:data], data: opts[:data],

View file

@ -15,4 +15,103 @@ defmodule AshPhoenix.Form.WrappedValue do
primary? true primary? true
end end
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 end

View file

@ -56,15 +56,15 @@ defmodule AshPhoenix.AutoFormTest do
describe "single unions" do describe "single unions" do
test "a form can be added for a union" do test "a form can be added for a union" do
Post Post
|> AshPhoenix.Form.for_create(:create, |> AshPhoenix.Form.for_create(:create,
api: Api, api: Api,
forms: [ forms: [
auto?: true auto?: true
] ]
) )
|> AshPhoenix.Form.add_form(:union, params: %{"type" => "foo"}) |> AshPhoenix.Form.add_form(:union, params: %{"type" => "foo"})
|> form_for("action") |> form_for("action")
end end
test "a form can be removed from a union" do 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 test "a form can be removed from a union" do
form = form =
Post Post
|> AshPhoenix.Form.for_create(:create, |> AshPhoenix.Form.for_create(:create,
api: Api, api: Api,
forms: [ forms: [
auto?: true auto?: true
] ]
) )
|> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"}) |> AshPhoenix.Form.add_form(:union_array, params: %{"type" => "foo"})
|> form_for("action") |> form_for("action")
AshPhoenix.Form.remove_form(form, [:union_array, 0]) AshPhoenix.Form.remove_form(form, [:union_array, 0])
end 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 defp auto_forms(resource, action) do
[forms: Auto.auto(resource, action)] [forms: Auto.auto(resource, action)]

View file

@ -7,6 +7,8 @@ defmodule AshPhoenix.Test.Foo do
default "foo" default "foo"
end end
attribute :value, :string attribute :value, :string do
constraints match: ~r/abc/
end
end end
end end