From 2420a2aab1a283d8464c35691eb848ecec47bfca Mon Sep 17 00:00:00 2001 From: Bashar Qassis <23612682+bashar-qassis@users.noreply.github.com> Date: Wed, 20 Oct 2021 14:16:55 +0300 Subject: [PATCH 01/12] handling cases where only a non-polymorphic changeset error exists --- lib/polymorphic_embed.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 244ffbb..10a1912 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -360,6 +360,8 @@ defmodule PolymorphicEmbed do |> merge_polymorphic_keys(changes, types, msg_func) end + def traverse_errors(%{}, msg_func) when is_function(msg_func, 1) or is_function(msg_func, 3), do: %{} + defp merge_polymorphic_keys(map, changes, types, msg_func) do Enum.reduce types, map, fn {field, {:parameterized, PolymorphicEmbed, _opts}}, acc -> From 0e7aaf5defb199fc46286aa7d16ac64d252cb353 Mon Sep 17 00:00:00 2001 From: Hamza Hejja Date: Thu, 4 Nov 2021 07:15:42 +0200 Subject: [PATCH 02/12] extend merge_polymorphic_keys to look for PolymorphicEmbed inside ecto embedded schemas --- lib/polymorphic_embed.ex | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 10a1912..a29e0e9 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -246,7 +246,8 @@ defmodule PolymorphicEmbed do def load(data, loader, params) when is_map(data), do: do_load(data, loader, params) - def load(data, loader, params) when is_binary(data), do: do_load(Jason.decode!(data), loader, params) + def load(data, loader, params) when is_binary(data), + do: do_load(Jason.decode!(data), loader, params) def do_load(data, _loader, %{types_metadata: types_metadata, type_field: type_field}) do case do_get_polymorphic_module_from_map(data, type_field, types_metadata) do @@ -355,15 +356,15 @@ defmodule PolymorphicEmbed do def traverse_errors(%Ecto.Changeset{changes: changes, types: types} = changeset, msg_func) when is_function(msg_func, 1) or is_function(msg_func, 3) do - Ecto.Changeset.traverse_errors(changeset, msg_func) |> merge_polymorphic_keys(changes, types, msg_func) end - def traverse_errors(%{}, msg_func) when is_function(msg_func, 1) or is_function(msg_func, 3), do: %{} + def traverse_errors(%{}, msg_func) when is_function(msg_func, 1) or is_function(msg_func, 3), + do: %{} defp merge_polymorphic_keys(map, changes, types, msg_func) do - Enum.reduce types, map, fn + Enum.reduce(types, map, fn {field, {:parameterized, PolymorphicEmbed, _opts}}, acc -> if changeset = Map.get(changes, field) do case traverse_errors(changeset, msg_func) do @@ -383,15 +384,25 @@ defmodule PolymorphicEmbed do end) case all_empty? do - true -> acc + true -> acc false -> Map.put(acc, field, errors) end else acc end - {_, _}, acc -> - acc - end + {field, {:embed, %Ecto.Embedded{field: field}}}, acc -> + 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 + + {_, _}, acc -> + acc + end) end end From e85b2b03316c89ab46f31313dae99c52ded3f7de Mon Sep 17 00:00:00 2001 From: Hamza Hejja Date: Sun, 7 Nov 2021 17:32:17 +0200 Subject: [PATCH 03/12] handle Ecto Embedded with cardinality: :many in traverse_errors --- lib/polymorphic_embed.ex | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index a29e0e9..bbf1d1e 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -391,7 +391,11 @@ defmodule PolymorphicEmbed do acc end - {field, {:embed, %Ecto.Embedded{field: field}}}, acc -> + # `Ecto.Schema.embeds_one()` field(s) inside of ecto-based schemas gets translated + # to the changeset type {:embed, %Ecto.Embedded{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 `embeds_one` and `embeds_many` + {field, {:embed, %Ecto.Embedded{cardinality: :one, field: field}}}, acc -> if changeset = Map.get(changes, field) do case traverse_errors(changeset, msg_func) do errors when errors == %{} -> acc @@ -401,6 +405,24 @@ defmodule PolymorphicEmbed do 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}} + # where the :cardinality atom can be one of: `:one` or `:many`, hence can be + # pattern-matched in order to distinguish between `embeds_one` and `embeds_many` + {field, {:embed, %Ecto.Embedded{cardinality: :many, field: field}}}, acc -> + with multiple_changesets when not is_nil(multiple_changesets) <- Map.get(changes, field) do + Enum.each(multiple_changesets, fn changeset -> + case traverse_errors(changeset, msg_func) do + errors when errors == %{} -> acc + errors -> Map.put(acc, field, errors) + end + end) + + acc + else + nil -> acc + end + {_, _}, acc -> acc end) From f03d3ee1ba61432c5eba193912440afaf6a1dfb5 Mon Sep 17 00:00:00 2001 From: Hamza Hejja Date: Mon, 8 Nov 2021 11:46:00 +0200 Subject: [PATCH 04/12] use if instead of with for compact form --- lib/polymorphic_embed.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index bbf1d1e..d8ee2da 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -410,7 +410,7 @@ defmodule PolymorphicEmbed do # where the :cardinality atom can be one of: `:one` or `:many`, hence can be # pattern-matched in order to distinguish between `embeds_one` and `embeds_many` {field, {:embed, %Ecto.Embedded{cardinality: :many, field: field}}}, acc -> - with multiple_changesets when not is_nil(multiple_changesets) <- Map.get(changes, field) do + if multiple_changesets = Map.get(changes, field) do Enum.each(multiple_changesets, fn changeset -> case traverse_errors(changeset, msg_func) do errors when errors == %{} -> acc @@ -420,7 +420,7 @@ defmodule PolymorphicEmbed do acc else - nil -> acc + acc end {_, _}, acc -> From 06fb45e0114a40526e710b54bc2045e8645daa53 Mon Sep 17 00:00:00 2001 From: Hamza Hejja Date: Mon, 8 Nov 2021 11:52:00 +0200 Subject: [PATCH 05/12] ensure that change is indeed a list and not nil --- lib/polymorphic_embed.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index d8ee2da..fafb3d3 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -410,7 +410,9 @@ defmodule PolymorphicEmbed do # where the :cardinality atom can be one of: `:one` or `:many`, hence can be # pattern-matched in order to distinguish between `embeds_one` and `embeds_many` {field, {:embed, %Ecto.Embedded{cardinality: :many, field: field}}}, acc -> - if multiple_changesets = Map.get(changes, field) do + multiple_changesets = Map.get(changes, field) + + if not is_nil(multiple_changesets) and is_list(multiple_changesets) do Enum.each(multiple_changesets, fn changeset -> case traverse_errors(changeset, msg_func) do errors when errors == %{} -> acc From fddf4f57559ea980ef73f2f1f9dc3f1e6300fb24 Mon Sep 17 00:00:00 2001 From: Bashar Qassis <23612682+bashar-qassis@users.noreply.github.com> Date: Mon, 31 Jan 2022 11:11:01 +0200 Subject: [PATCH 06/12] Adding capability to customize traverse_errors per module for parameterized types --- lib/polymorphic_embed.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index fafb3d3..34e20c9 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -375,6 +375,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?} = From 8e5007c7e0fa6297be8d6cf58bc52a28b3223892 Mon Sep 17 00:00:00 2001 From: Bashar Qassis <23612682+bashar-qassis@users.noreply.github.com> Date: Tue, 31 Jan 2023 16:54:52 +0200 Subject: [PATCH 07/12] remove extra new line --- config/dev.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/config/dev.exs b/config/dev.exs index 8b13789..e69de29 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1 +0,0 @@ - From 7c0fb107cada82a1c320bdf671a4573879cd21c3 Mon Sep 17 00:00:00 2001 From: Bashar Qassis <23612682+bashar-qassis@users.noreply.github.com> Date: Tue, 31 Jan 2023 16:57:49 +0200 Subject: [PATCH 08/12] suppress failing tests errors --- config/dev.exs | 1 + 1 file changed, 1 insertion(+) 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 From fdec5fccb9de254814657232a4f4dde606acda0f Mon Sep 17 00:00:00 2001 From: Bashar Qassis <23612682+bashar-qassis@users.noreply.github.com> Date: Tue, 31 Jan 2023 17:07:18 +0200 Subject: [PATCH 09/12] downgrade json to 1.2 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}, From eccb0ef75e76125d41241080100b2669fda85aeb Mon Sep 17 00:00:00 2001 From: Tamara Adeeb Date: Thu, 13 Apr 2023 11:20:53 +0200 Subject: [PATCH 10/12] fix error traversing from :embed type fields with many cardinality and limit traverse error loop --- lib/polymorphic_embed.ex | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index a5402ea..f68aa2d 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 -> @@ -470,7 +490,7 @@ defmodule PolymorphicEmbed do # to the changeset type {:embed, %Ecto.Embedded{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 `embeds_one` and `embeds_many` - {field, {:embed, %Ecto.Embedded{cardinality: :one, field: field}}}, acc -> + {field, {tag, %Ecto.Embedded{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 @@ -484,18 +504,16 @@ defmodule PolymorphicEmbed do # to the changeset type {:embed, %Ecto.Embedded{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 `embeds_one` and `embeds_many` - {field, {:embed, %Ecto.Embedded{cardinality: :many, field: field}}}, acc -> + {field, {tag, %Ecto.Embedded{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.each(multiple_changesets, fn changeset -> + Enum.reduce(multiple_changesets, acc, fn changeset, acc -> case traverse_errors(changeset, msg_func) do errors when errors == %{} -> acc - errors -> Map.put(acc, field, errors) + errors -> Map.update(acc, field, [errors], &[errors | &1]) end end) - - acc else acc end From ade58191ba28df2adcea3aad447ac21057b9e970 Mon Sep 17 00:00:00 2001 From: Tamara Adeeb Date: Thu, 13 Apr 2023 12:07:41 +0200 Subject: [PATCH 11/12] handle :assoc field type with :one amd :many cardinality --- lib/polymorphic_embed.ex | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index f68aa2d..48deebf 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -488,9 +488,11 @@ defmodule PolymorphicEmbed do # `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 `embeds_one` and `embeds_many` - {field, {tag, %Ecto.Embedded{cardinality: :one, field: field}}}, acc when tag in @relations -> + # 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 @@ -502,9 +504,11 @@ defmodule PolymorphicEmbed do # `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 `embeds_one` and `embeds_many` - {field, {tag, %Ecto.Embedded{cardinality: :many, field: field}}}, acc when tag in @relations -> + # 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 @@ -518,7 +522,7 @@ defmodule PolymorphicEmbed do acc end - {_, _}, acc -> + data, acc -> acc end) end From 043bfe26e9223d29970530c9f423474ca9d6456f Mon Sep 17 00:00:00 2001 From: Tamara Adeeb Date: Thu, 13 Apr 2023 12:49:01 +0200 Subject: [PATCH 12/12] fix failed Compilation due to warning: variable data is unused --- lib/polymorphic_embed.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 48deebf..4c78f83 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -522,7 +522,7 @@ defmodule PolymorphicEmbed do acc end - data, acc -> + {_, _}, acc -> acc end) end