diff --git a/config/dev.exs b/config/dev.exs index e69de29..f047d12 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -0,0 +1 @@ +# dummy comment to suppress failing tests diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index d6ebfa4..4c78f83 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -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)) @@ -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 @@ -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 -> @@ -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?} = @@ -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) diff --git a/mix.exs b/mix.exs index 9e50af8..c41b46b 100644 --- a/mix.exs +++ b/mix.exs @@ -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},