Hi,
I’m validating a request with Ecto.Changeset — Ecto v3.10.3 there are quite a few parameters and I want to go a bit further with the defaults for them. Let’s only consider size for now.
Suppose I configured a Struct with
defstruct size: 10
@types %{size: :integer}
and then use this with
{%__MODULE__{}, @types}
|> Ecto.Changeset.cast(params, [:size])
|> Ecto.Changeset.apply_changes()
And specify size=not_a_number
it defaults to 10 as I want. However, when the user specifies size=-1
, this value is kept (as it is an integer). I want to silently pick the default value when size < 0.
Using :empty_values
option in cast/4
does not work as that requires a list of all numbers below 0. The best I can come up with is go through the parameters after the changes are applied (should be simple with guards then) and replacing invalid values with the defaults.
Is there a better solution?
Depending on how you are keying the errors from validations, it could be done using Ecto.reset_fields/2
defmodule Example.MyStruct do
defstruct size: 10
@types %{size: :integer}
def changeset(params) do
{%__MODULE__{}, @types}
|> Ecto.Changeset.cast(params, [:size])
|> Ecto.Changeset.validate_number(:size, greater_than: 0)
|> apply_changes_with_defaults()
end
defp apply_changes_with_defaults(%Ecto.Changeset{errors: errors} = changeset) do
changeset
|> Ecto.Changeset.apply_changes()
|> Ecto.reset_fields(Keyword.keys(errors))
end
end
1 Like
The docs on empty values specifically states the following:
Those are either values, which will be considered empty if they match, or a function that must return a boolean if the value is empty or not.
1 Like
For anyone looking for this:
{%__MODULE__{}, @types}
|> Ecto.Changeset.cast(params, [:size],
empty_values: Ecto.Changeset.empty_values() ++ [&non_positive_integer?/1]
)
defp non_positive_integer?(value) do
case Integer.parse(value) do
{integer, ""} -> integer < 0
_ -> true
end
end
2 Likes