Skip to content

Commit

Permalink
Support map with multiple types
Browse files Browse the repository at this point in the history
Targeting schemaless
  • Loading branch information
v0idpwn committed Jun 13, 2024
1 parent e0bb729 commit 08477d6
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 10 deletions.
47 changes: 37 additions & 10 deletions lib/ecto/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,11 @@ defmodule Ecto.Type do
&map(&1, fun, false, %{})
end

defp cast_fun({:map, %{} = map_types}) do
funs = Map.new(map_types, fn {key, type} -> {key, cast_fun(type)} end)
&map(&1, funs, true, %{})
end

defp cast_fun({:map, type}) do
fun = cast_fun(type)
&map(&1, fun, true, %{})
Expand Down Expand Up @@ -1272,6 +1277,11 @@ defmodule Ecto.Type do
end
end

defp equal_fun({:map, %{} = map_types}) do
funs = Map.new(map_types, fn {key, type} -> {key, cast_fun(type)} end)
&equal_map?(funs, &1, &2)
end

defp equal_fun({:map, type}) do
if fun = equal_fun(type) do
&equal_map?(fun, &1, &2)
Expand Down Expand Up @@ -1319,7 +1329,9 @@ defmodule Ecto.Type do
end
end

defp equal_map?(fun, [{key, val} | tail], other_map) do
defp equal_map?(fun_or_funs, [{key, val} | tail], other_map) do
fun = if is_function(fun_or_funs), do: fun_or_funs, else: fun_or_funs[key]

case other_map do
%{^key => other_val} -> fun.(val, other_val) and equal_map?(fun, tail, other_map)
_ -> false
Expand Down Expand Up @@ -1391,10 +1403,14 @@ defmodule Ecto.Type do
:error
end

defp map(map, fun, skip_nil?, acc) when is_map(map) do
defp map(map, fun, skip_nil?, acc) when is_map(map) and is_function(fun) do
map_each(Map.to_list(map), fun, skip_nil?, acc)
end

defp map(map, funs, skip_nil?, acc) when is_map(map) and is_map(funs) do
map_each(Map.to_list(map), funs, skip_nil?, acc)
end

defp map(_, _, _, _) do
:error
end
Expand All @@ -1403,16 +1419,27 @@ defmodule Ecto.Type do
map_each(t, fun, true, Map.put(acc, key, nil))
end

defp map_each([{key, value} | t], fun, skip_nil?, acc) do
case fun.(value) do
{:ok, value} ->
map_each(t, fun, skip_nil?, Map.put(acc, key, value))
defp map_each([{key, value} | t], fun_or_funs, skip_nil?, acc) do
fun = if is_function(fun_or_funs), do: fun_or_funs, else: fun_or_funs[key]

:error ->
:error
if fun do
case fun.(value) do
{:ok, value} ->
map_each(t, fun_or_funs, skip_nil?, Map.put(acc, key, value))

{:error, custom_errors} ->
{:error, Keyword.update(custom_errors, :source, [key], &[key | &1])}
:error ->
# Else branch needed for backwards compatibility
if is_map(fun_or_funs) do
{:error, [source: [key]]}
else
:error
end

{:error, custom_errors} ->
{:error, Keyword.update(custom_errors, :source, [key], &[key | &1])}
end
else
map_each(t, fun_or_funs, skip_nil?, acc)
end
end

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

test "nested schemaless changeset" do
{data, types} =
{%{},
%{
name: :string,
age: :integer,
favorite_fruits: {:array, :string},
metadata:
{:map,
%{
favorite_color: :string,
favorite_fruits: {:array, :string},
addresses: {:array, {:map, %{city: :string}}}
}}
}}

success_params = %{
name: "Jim",
age: 92,
metadata: %{
favorite_color: "green",
favorite_fruits: ["passionfruit", "tomato"],
foo: "bar",
addresses: [
%{city: "Sao Paulo"},
%{city: "Grozny"}
]
}
}

changeset = cast({data, types}, success_params, [:name, :age, :metadata])
expected_result = Map.update!(success_params, :metadata, &Map.delete(&1, :foo))
assert changeset.changes == expected_result

# Invalid address
failure_params = %{
name: "Jim",
age: 92,
metadata: %{
favorite_color: "green",
favorite_fruits: ["passionfruit", "tomato"],
foo: "bar",
addresses: [
%{city: 1}
]
}
}

changeset = cast({data, types}, failure_params, [:name, :age, :metadata])
refute changeset.valid?
assert {"is invalid", error_meta} = changeset.errors[:metadata]
assert error_meta[:source] == [:addresses, 0, :city]
end

## Changeset functions

test "merge/2: merges changes" do
Expand Down

0 comments on commit 08477d6

Please sign in to comment.