Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions lib/ecto/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ defmodule Ecto.Changeset do
alias Ecto.Changeset.Relation
alias Ecto.Schema.Metadata

@empty_values [&Ecto.Type.empty_trimmed_string?/1]
@empty_values [&Ecto.Type.empty_trimmed_string?/2]

# If a new field is added here, def merge must be adapted
defstruct valid?: false,
Expand Down Expand Up @@ -656,7 +656,10 @@ defmodule Ecto.Changeset do

## Options

* `:empty_values` - a list of values to be considered as empty when casting.
* `:empty_values` - a list containing elements of type `t:empty_value/0`. 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-arity functions will receive the value
being casted and 2-arity functions will receive the value being casted and its field type.
Empty values are always replaced by the default value of the respective field.
If the field is an array type, any empty value inside of the array will be removed.
To set this option while keeping the current default, use `empty_values/0` and add
Expand Down Expand Up @@ -961,24 +964,31 @@ defmodule Ecto.Changeset do
end
end

defp filter_empty_values(_type, value, empty_values) do
filter_empty_value(empty_values, value)
defp filter_empty_values(type, value, empty_values) do
filter_empty_value(empty_values, value, type)
end

defp filter_empty_value([head | tail], value) when is_function(head) do
defp filter_empty_value([head | tail], value, type) when is_function(head, 1) do
case head.(value) do
true -> :empty
false -> filter_empty_value(tail, value)
false -> filter_empty_value(tail, value, type)
end
end

defp filter_empty_value([value | _tail], value),
defp filter_empty_value([head | tail], value, type) when is_function(head, 2) do
case head.(value, type) do
true -> :empty
false -> filter_empty_value(tail, value, type)
end
end

defp filter_empty_value([value | _tail], value, _type),
do: :empty

defp filter_empty_value([_head | tail], value),
do: filter_empty_value(tail, value)
defp filter_empty_value([_head | tail], value, type),
do: filter_empty_value(tail, value, type)

defp filter_empty_value([], value),
defp filter_empty_value([], value, _type),
do: {:ok, value}

# We only look at the first element because traversing the whole map
Expand Down
4 changes: 2 additions & 2 deletions lib/ecto/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1001,8 +1001,8 @@ defmodule Ecto.Type do
defp same_duration(_), do: :error

@doc false
def empty_trimmed_string?(value) do
is_binary(value) and String.trim_leading(value) == ""
def empty_trimmed_string?(value, type) do
is_binary(value) and type != :binary and String.trim_leading(value) == ""
end

## Adapter related
Expand Down
10 changes: 10 additions & 0 deletions test/ecto/changeset_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ defmodule Ecto.ChangesetTest do
assert changeset.changes == %{}
end

test "cast/4: with binary empty values" do
# <<9>> is a control character which should not be empty_trimmed_string
# for a binary field
params = %{"color" => <<9>>}
struct = %Post{}

changeset = cast(struct, params, ~w(color)a)
assert changeset.changes == %{"color": <<9>>}
end

test "cast/4: with force_changes" do
params = %{"title" => "", "body" => nil}
struct = %Post{title: "", body: nil}
Expand Down
Loading