mirror of
https://github.com/ash-project/ash_phoenix.git
synced 2024-09-20 07:12:49 +12:00
fix: properly track form params for nested unions
fix: produce type errors for wrapped union values
This commit is contained in:
parent
e295fb72f7
commit
3a7d436079
5 changed files with 231 additions and 67 deletions
|
@ -127,16 +127,6 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
|
|
||||||
constraints = unwrap_union(type, constraints)
|
constraints = unwrap_union(type, constraints)
|
||||||
|
|
||||||
updater = fn opts, data, params ->
|
|
||||||
{type, constraints} = determine_type(constraints, data, params)
|
|
||||||
|
|
||||||
{embed, constraints, fake_embedded?} =
|
|
||||||
if Ash.Type.embedded_type?(type) do
|
|
||||||
{type, constraints, false}
|
|
||||||
else
|
|
||||||
{AshPhoenix.Form.WrappedValue, [], true}
|
|
||||||
end
|
|
||||||
|
|
||||||
data =
|
data =
|
||||||
case form_type do
|
case form_type do
|
||||||
:list ->
|
:list ->
|
||||||
|
@ -146,7 +136,6 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|> Enum.map(&wrap_value(&1, fake_embedded?))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
:single ->
|
:single ->
|
||||||
|
@ -158,10 +147,19 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
value -> value
|
value -> value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|> wrap_value(fake_embedded?)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
updater = fn opts, data, params ->
|
||||||
|
{type, constraints} = determine_type(constraints, data, params)
|
||||||
|
|
||||||
|
{embed, constraints, fake_embedded?} =
|
||||||
|
if Ash.Type.embedded_type?(type) do
|
||||||
|
{type, constraints, false}
|
||||||
|
else
|
||||||
|
{AshPhoenix.Form.WrappedValue, [], true}
|
||||||
|
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,6 +280,7 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp tags_equal(config, key, params) do
|
defp tags_equal(config, key, params) do
|
||||||
|
if is_map(params) do
|
||||||
case config[:tag_value] || key do
|
case config[:tag_value] || key do
|
||||||
value when is_atom(value) ->
|
value when is_atom(value) ->
|
||||||
params[to_string(config[:tag])] == to_string(value) ||
|
params[to_string(config[:tag])] == to_string(value) ||
|
||||||
|
@ -296,9 +289,13 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
value ->
|
value ->
|
||||||
params[to_string(config[:tag])] == value
|
params[to_string(config[:tag])] == value
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp tags_equal_data(config, key, data) do
|
defp tags_equal_data(config, key, data) do
|
||||||
|
if is_struct(data) do
|
||||||
case config[:tag_value] || key do
|
case config[:tag_value] || key do
|
||||||
value when is_atom(value) ->
|
value when is_atom(value) ->
|
||||||
data[config[:tag]] == to_string(value) ||
|
data[config[:tag]] == to_string(value) ||
|
||||||
|
@ -307,6 +304,9 @@ defmodule AshPhoenix.Form.Auto do
|
||||||
value ->
|
value ->
|
||||||
data[config[:tag]] == value
|
data[config[:tag]] == value
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def related(resource, action, auto_opts) do
|
def related(resource, action, auto_opts) do
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -125,8 +125,60 @@ defmodule AshPhoenix.AutoFormTest do
|
||||||
|
|
||||||
AshPhoenix.Form.remove_form(form, [:union_array, 0])
|
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
|
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)]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue