Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# dummy comment to suppress failing tests
67 changes: 64 additions & 3 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule PolymorphicEmbed do
use Ecto.ParameterizedType

@relations [:embed, :assoc]

defmacro polymorphic_embeds_one(field_name, opts) do
quote do
field(unquote(field_name), PolymorphicEmbed, unquote(opts))
Expand Down Expand Up @@ -418,10 +420,14 @@ defmodule PolymorphicEmbed do
defp raise_cannot_infer_type_from_data(data),
do: raise("could not infer polymorphic embed from data #{inspect(data)}")

def traverse_errors(%Ecto.Changeset{changes: changes, types: types} = changeset, msg_func)
def traverse_errors(
%Ecto.Changeset{changes: changes, types: types, errors: errors} = changeset,
msg_func
)
when is_function(msg_func, 1) or is_function(msg_func, 3) do
changeset
|> Ecto.Changeset.traverse_errors(msg_func)
errors
|> Enum.reverse()
|> merge_keyword_keys(msg_func, changeset)
|> merge_polymorphic_keys(changes, types, msg_func)
end

Expand All @@ -433,6 +439,20 @@ defmodule PolymorphicEmbed do
%{}
end

defp merge_keyword_keys(keyword_list, msg_func, _) when is_function(msg_func, 1) do
Enum.reduce(keyword_list, %{}, fn {key, val}, acc ->
val = msg_func.(val)
Map.update(acc, key, [val], &[val | &1])
end)
end

defp merge_keyword_keys(keyword_list, msg_func, changeset) when is_function(msg_func, 3) do
Enum.reduce(keyword_list, %{}, fn {key, val}, acc ->
val = msg_func.(changeset, key, val)
Map.update(acc, key, [val], &[val | &1])
end)
end

defp merge_polymorphic_keys(map, changes, types, msg_func) do
Enum.reduce(types, map, fn
{field, {:parameterized, PolymorphicEmbed, _opts}}, acc ->
Expand All @@ -445,6 +465,11 @@ defmodule PolymorphicEmbed do
acc
end

{field, {:parameterized, parameterized_type, _opts}}, acc ->
if function_exported?(parameterized_type, :traverse_errors, 4),
do: parameterized_type.traverse_errors(field, changes, msg_func, acc),
else: acc

{field, {:array, {:parameterized, PolymorphicEmbed, _opts}}}, acc ->
if changesets = Map.get(changes, field) do
{errors, all_empty?} =
Expand All @@ -461,6 +486,42 @@ defmodule PolymorphicEmbed do
acc
end

# `Ecto.Schema.embeds_one()` field(s) inside of ecto-based schemas gets translated
# to the changeset type {:embed, %Ecto.Embedded{cardinality: :one, field: field}}
# and `Ecto.Schema.belongs_to()` get translated to
# {:assoc, %Ecto.Association.Has{cardinality: :one, field: field}}
# where the :cardinality atom can be one of: `:one` or `:many`, hence can be
# pattern-matched in order to distinguish between `one` and `many` cardinality
{field, {tag, %{cardinality: :one, field: field}}}, acc when tag in @relations ->
if changeset = Map.get(changes, field) do
case traverse_errors(changeset, msg_func) do
errors when errors == %{} -> acc
errors -> Map.put(acc, field, errors)
end
else
acc
end

# `Ecto.Schema.embeds_many()` field(s) inside of ecto-based schemas gets translated
# to the changeset type {:embed, %Ecto.Embedded{cardinality: :many, field: field}}
# and `Ecto.Schema.has_many()` get translated to
# {:assoc, %Ecto.Association.Has{cardinality: :many, field: field}}
# where the :cardinality atom can be one of: `:one` or `:many`, hence can be
# pattern-matched in order to distinguish between `one` and `many` cardinality
{field, {tag, %{cardinality: :many, field: field}}}, acc when tag in @relations ->
multiple_changesets = Map.get(changes, field)

if not is_nil(multiple_changesets) and is_list(multiple_changesets) do
Enum.reduce(multiple_changesets, acc, fn changeset, acc ->
case traverse_errors(changeset, msg_func) do
errors when errors == %{} -> acc
errors -> Map.update(acc, field, [errors], &[errors | &1])
end
end)
else
acc
end

{_, _}, acc ->
acc
end)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule PolymorphicEmbed.MixProject do
defp deps do
[
{:ecto, "~> 3.9"},
{:jason, "~> 1.4"},
{:jason, "~> 1.2"},
{:phoenix_html, "~> 2.14 or ~> 3.2", optional: true},
{:ex_doc, "~> 0.28", only: :dev},
{:ecto_sql, "~> 3.9", only: :test},
Expand Down