improvement: add match_other_defaults? to attribute

This commit is contained in:
Zach Daniel 2022-07-05 10:18:50 -04:00
parent b71aa3ddeb
commit 394e2d089a
4 changed files with 154 additions and 35 deletions

View file

@ -103,6 +103,7 @@ locals_without_parens = [
many_to_many: 3,
map: 2,
map: 3,
match_other_defaults?: 1,
message: 1,
metadata: 2,
metadata: 3,

View file

@ -816,43 +816,58 @@ defmodule Ash.Changeset do
def set_defaults(changeset, action_type, lazy? \\ false)
def set_defaults(changeset, :create, lazy?) do
changeset.resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(fn attribute ->
not is_nil(attribute.default) &&
(lazy? or
not (is_function(attribute.default) or match?({_, _, _}, attribute.default)))
end)
|> Enum.reduce(changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute_lazy(attribute.name, fn ->
default(:create, attribute)
attributes = Ash.Resource.Info.attributes(changeset.resource)
with_static_defaults =
attributes
|> Enum.filter(fn attribute ->
not is_nil(attribute.default) &&
not (is_function(attribute.default) or match?({_, _, _}, attribute.default))
end)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
|> Enum.reduce(changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute_lazy(attribute.name, fn ->
default(:create, attribute)
end)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
end)
end)
end)
|> Map.update!(:defaults, &Enum.uniq/1)
if lazy? do
set_lazy_defaults(with_static_defaults, attributes, :create)
else
with_static_defaults
end
|> Map.update!(:defaults, &Enum.uniq/1)
end
def set_defaults(changeset, :update, lazy?) do
changeset.resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(fn attribute ->
not is_nil(attribute.update_default) &&
(lazy? or
not (is_function(attribute.update_default) or
match?({_, _, _}, attribute.update_default)))
end)
|> Enum.reduce(changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute_lazy(attribute.name, fn ->
default(:update, attribute)
attributes = Ash.Resource.Info.attributes(changeset.resource)
with_static_defaults =
attributes
|> Enum.filter(fn attribute ->
not is_nil(attribute.update_default) &&
not (is_function(attribute.update_default) or
match?({_, _, _}, attribute.update_default))
end)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
|> Enum.reduce(changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute_lazy(attribute.name, fn ->
default(:update, attribute)
end)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
end)
end)
end)
if lazy? do
set_lazy_defaults(with_static_defaults, attributes, :update)
else
with_static_defaults
end
|> Map.update!(:defaults, &Enum.uniq/1)
end
@ -860,6 +875,74 @@ defmodule Ash.Changeset do
changeset
end
defp set_lazy_defaults(changeset, attributes, type) do
changeset
|> set_lazy_non_matching_defaults(attributes, type)
|> set_lazy_matching_defaults(attributes, type)
end
defp set_lazy_non_matching_defaults(changeset, attributes, type) do
attributes
|> Enum.filter(fn attribute ->
!attribute.match_other_defaults? && get_default_fun(attribute, type)
end)
|> Enum.reduce(changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute_lazy(attribute.name, fn ->
default(type, attribute)
end)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
end)
end)
end
defp get_default_fun(attribute, :create) do
if is_function(attribute.default) or match?({_, _, _}, attribute.default) do
attribute.default
end
end
defp get_default_fun(attribute, :update) do
if is_function(attribute.update_default) or match?({_, _, _}, attribute.update_default) do
attribute.update_default
end
end
defp set_lazy_matching_defaults(changeset, attributes, type) do
attributes
|> Enum.filter(fn attribute ->
attribute.match_other_defaults? && get_default_fun(attribute, type)
end)
|> Enum.group_by(fn attribute ->
case type do
:create ->
attribute.default
:update ->
attribute.update_default
end
end)
|> Enum.reduce(changeset, fn {default_fun, attributes}, changeset ->
default_value =
case default_fun do
function when is_function(function) ->
function.()
{m, f, a} when is_atom(m) and is_atom(f) and is_list(a) ->
apply(m, f, a)
end
Enum.reduce(attributes, changeset, fn attribute, changeset ->
changeset
|> force_change_new_attribute(attribute.name, default_value)
|> Map.update!(:defaults, fn defaults ->
[attribute.name | defaults]
end)
end)
end)
end
defp default(:create, %{default: {mod, func, args}}), do: apply(mod, func, args)
defp default(:create, %{default: function}) when is_function(function, 0), do: function.()
defp default(:create, %{default: value}), do: value
@ -2228,7 +2311,21 @@ defmodule Ash.Changeset do
end
@doc """
Force change an attribute if is not currently being changed, by calling the provided function
Force change an attribute if it is not currently being changed
See `change_new_attribute/3` for more.
"""
@spec force_change_new_attribute(t(), atom, term) :: t()
def force_change_new_attribute(changeset, attribute, value) do
if changing_attribute?(changeset, attribute) do
changeset
else
force_change_attribute(changeset, attribute, value)
end
end
@doc """
Force change an attribute if it is not currently being changed, by calling the provided function
See `change_new_attribute_lazy/3` for more.
"""

View file

@ -14,6 +14,7 @@ defmodule Ash.Resource.Attribute do
:update_default,
:description,
:source,
match_other_defaults?: false,
sensitive?: false,
filterable?: true,
constraints: []
@ -136,6 +137,18 @@ defmodule Ash.Resource.Attribute do
description: [
type: :string,
doc: "An optional description for the attribute."
],
match_other_defaults?: [
type: :boolean,
default: false,
doc: """
Ensures that other attributes that use a matching "lazy" default (a default that is a zero argument function), use the same default value.
Has no effect unless `default` is a zero argument function.
This is used for timestamps, for example, when their default is `&DateTime.utc_now/0`, we want `created_at` and `updated_at` to be equal to eachoter,
instead of separated by the small amount of time between calling both functions.
"""
]
]
@ -143,12 +156,14 @@ defmodule Ash.Resource.Attribute do
|> OptionsHelpers.set_default!(:writable?, false)
|> OptionsHelpers.set_default!(:private?, true)
|> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
|> OptionsHelpers.set_default!(:match_other_defaults?, true)
|> OptionsHelpers.set_default!(:type, Ash.Type.UtcDatetimeUsec)
|> OptionsHelpers.set_default!(:allow_nil?, false)
@update_timestamp_schema @schema
|> OptionsHelpers.set_default!(:writable?, false)
|> OptionsHelpers.set_default!(:private?, true)
|> OptionsHelpers.set_default!(:match_other_defaults?, true)
|> OptionsHelpers.set_default!(:default, &DateTime.utc_now/0)
|> OptionsHelpers.set_default!(
:update_default,

View file

@ -206,6 +206,8 @@ defmodule Ash.Test.Actions.CreateTest do
items: [min: -10, max: 10]
]
)
timestamps()
end
relationships do
@ -261,11 +263,6 @@ defmodule Ash.Test.Actions.CreateTest do
end
end
describe "upserts" do
test "allows upserting a record using an identity" do
end
end
describe "simple creates" do
test "allows creating a record with valid attributes" do
assert %Post{title: "foo", contents: "bar"} =
@ -280,6 +277,15 @@ defmodule Ash.Test.Actions.CreateTest do
|> Api.create!()
end
test "timestamps will match each other" do
post =
Post
|> for_create(:create, %{title: "foobar"})
|> Api.create!()
assert post.inserted_at == post.updated_at
end
test "allow_nil validation" do
{:error, error} =
Post