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 001/129] 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 002/129] 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 003/129] 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 004/129] 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 005/129] 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 006/129] 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 0d22e90ae03480dd61d4855670eac29157db9678 Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit Date: Sun, 9 Oct 2022 18:12:52 +0900 Subject: [PATCH 007/129] fix type determination for default and custom type fields --- lib/polymorphic_embed/html/form.ex | 14 +++- test/polymorphic_embed_test.exs | 65 +++++++++++++++++-- .../20000101000000_create_tables.exs | 2 + test/support/models/polymorphic/reminder.ex | 17 +++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/lib/polymorphic_embed/html/form.ex b/lib/polymorphic_embed/html/form.ex index c6ac550..767626b 100644 --- a/lib/polymorphic_embed/html/form.ex +++ b/lib/polymorphic_embed/html/form.ex @@ -14,7 +14,19 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d %_{} = value -> PolymorphicEmbed.get_polymorphic_type(schema, field, value) - _ -> + %{} = map -> + case PolymorphicEmbed.get_polymorphic_module(schema, field, map) do + nil -> + nil + + module -> + PolymorphicEmbed.get_polymorphic_type(schema, field, module) + end + + list when is_list(list) -> + nil + + nil -> nil end end diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs index dd98de6..87377e6 100644 --- a/test/polymorphic_embed_test.exs +++ b/test/polymorphic_embed_test.exs @@ -2045,7 +2045,7 @@ defmodule PolymorphicEmbedTest do end describe "Form.get_polymorphic_type/3" do - test "returns type from changeset" do + test "returns type from changeset via identify_by_fields" do reminder_module = get_module(Reminder, :polymorphic) attrs = %{ @@ -2094,9 +2094,9 @@ defmodule PolymorphicEmbedTest do end) end - test "returns type from string parameters" do + test "returns type from changeset via custom type field" do reminder_module = get_module(Reminder, :polymorphic) - attrs = %{"channel" => %{"my_type_field" => "email"}} + attrs = %{"channel" => %{"my_type_field" => "sms"}} changeset = reminder_module @@ -2105,15 +2105,32 @@ defmodule PolymorphicEmbedTest do safe_form_for(changeset, fn f -> assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) == + :sms + + text_input(f, :text) + end) + end + + test "returns type from map with default type field (string)" do + reminder_module = get_module(Reminder, :polymorphic) + attrs = %{"channel2" => %{"__type__" => "email"}} + + changeset = + reminder_module + |> struct() + |> reminder_module.changeset(attrs) + + safe_form_for(changeset, fn f -> + assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel2) == :email text_input(f, :text) end) end - test "returns type from atom parameters" do + test "returns type from map with default type field (atom)" do reminder_module = get_module(Reminder, :polymorphic) - attrs = %{channel: %{my_type_field: :email}} + attrs = %{"channel2" => %{__type__: :email}} changeset = reminder_module @@ -2121,14 +2138,48 @@ defmodule PolymorphicEmbedTest do |> reminder_module.changeset(attrs) safe_form_for(changeset, fn f -> - assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) == + assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel2) == + :email + + text_input(f, :text) + end) + end + + test "returns type from map with custom type field (string)" do + reminder_module = get_module(Reminder, :polymorphic) + attrs = %{"channel3" => %{"my_type_field" => "email"}} + + changeset = + reminder_module + |> struct() + |> reminder_module.changeset(attrs) + + safe_form_for(changeset, fn f -> + assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel3) == + :email + + text_input(f, :text) + end) + end + + test "returns type from map with custom type field (atom)" do + reminder_module = get_module(Reminder, :polymorphic) + attrs = %{"channel3" => %{my_type_field: "email"}} + + changeset = + reminder_module + |> struct() + |> reminder_module.changeset(attrs) + + safe_form_for(changeset, fn f -> + assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel3) == :email text_input(f, :text) end) end - test "returns type from parameters while type field is custom" do + test "returns nil with map when custom type field is configured and default type field is set" do reminder_module = get_module(Reminder, :polymorphic) attrs = %{channel: %{__type__: :email}} diff --git a/test/support/migrations/20000101000000_create_tables.exs b/test/support/migrations/20000101000000_create_tables.exs index 53be4e2..e1167f9 100644 --- a/test/support/migrations/20000101000000_create_tables.exs +++ b/test/support/migrations/20000101000000_create_tables.exs @@ -7,6 +7,8 @@ defmodule PolymorphicEmbed.CreateTables do add(:text, :text, null: false) add(:channel, :map) + add(:channel2, :map) + add(:channel3, :map) add(:contexts, :map) add(:contexts2, :map) diff --git a/test/support/models/polymorphic/reminder.ex b/test/support/models/polymorphic/reminder.ex index 520f73b..0bc7386 100644 --- a/test/support/models/polymorphic/reminder.ex +++ b/test/support/models/polymorphic/reminder.ex @@ -20,6 +20,23 @@ defmodule PolymorphicEmbed.Reminder do type_field: :my_type_field ) + polymorphic_embeds_one(:channel2, + types: [ + sms: PolymorphicEmbed.Channel.SMS, + email: PolymorphicEmbed.Channel.Email + ], + on_replace: :update + ) + + polymorphic_embeds_one(:channel3, + types: [ + sms: PolymorphicEmbed.Channel.SMS, + email: PolymorphicEmbed.Channel.Email + ], + on_replace: :update, + type_field: :my_type_field + ) + polymorphic_embeds_many(:contexts, types: [ location: PolymorphicEmbed.Reminder.Context.Location, From 7c8871b3190b204d9828630b852c9b4a2710904d Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit Date: Sun, 13 Nov 2022 15:38:44 +0900 Subject: [PATCH 008/129] ignore .elixir_ls folder --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 03d70f1..0993e91 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ polymorphic_embed-*.tar # Ignore Dialyzer plts file .plts + +.elixir_ls From 27c2668238b29eb10f1148691d260d0497933613 Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit Date: Sun, 13 Nov 2022 15:46:38 +0900 Subject: [PATCH 009/129] split CI workflow --- .github/workflows/ci.yml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5931fef..3147b33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI +env: + ELIXIR_VERSION: "1.14" + OTP_VERSION: "25.1" + on: push: branches: @@ -11,7 +15,7 @@ on: jobs: test: runs-on: ubuntu-latest - name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + name: Test OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: otp: ["24.3", "25.1"] @@ -39,6 +43,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 + id: beam with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} @@ -50,6 +55,35 @@ jobs: deps key: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('mix.lock') }} restore-keys: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}- + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Tests + run: mix coveralls.github + + lint: + runs-on: ubuntu-latest + name: Lint + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + id: beam + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION}} + - name: Restore dependencies and build cache + uses: actions/cache@v3 + with: + path: | + _build + deps + key: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-otp-${{ steps.beam.outputs.otp-version }}-elixir-${{ steps.beam.outputs.elixir-version }}- - name: Restore PLT cache uses: actions/cache@v3 id: plt_cache @@ -75,5 +109,3 @@ jobs: run: mix hex.audit - name: Generate docs run: mix docs - - name: Run Tests - run: mix coveralls.github 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 010/129] 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 011/129] 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 012/129] 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 813e0f6959373d7e46e3d1f59fddd7e7b99543d8 Mon Sep 17 00:00:00 2001 From: Emily Elmseld Hansson Date: Fri, 31 Mar 2023 16:28:55 +0200 Subject: [PATCH 013/129] Remove error from example code --- README.md | 20 ++++++++++++++------ lib/polymorphic_embed/html/form.ex | 11 ++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ef3c9b7..dd133f5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ defmodule MyApp.Channel.Email do def changeset(email, params) do email - |> cast(params, ~w(address confirmed)a) + |> cast(params, [:address, :confirmed]) |> validate_required(:address) |> validate_length(:address, min: 4) end @@ -72,12 +72,19 @@ end ```elixir defmodule MyApp.Channel.SMS do use Ecto.Schema + import Ecto.Changeset @primary_key false embedded_schema do field :number, :string end + + def changeset(sms, params) do + sms + |> cast(params, [:number]) + |> validate_required(:number) + end end ``` @@ -171,7 +178,7 @@ polymorphic_embeds_many :contexts, The library comes with a form helper in order to build form inputs for polymorphic embeds and display changeset errors. -In the entrypoint defining your web interface (`lib/your_app_web.ex` file), add the following import: +In the entrypoint defining your web interface (`lib/your_app_web.ex` file), add the following import, change `view` to the one your component use, ex. `liew_view`: ```elixir def view do @@ -190,7 +197,7 @@ Here is an example form using the imported function: <%= inputs_for f, :reminders, fn reminder_form -> %> <%= polymorphic_embed_inputs_for reminder_form, :channel, :sms, fn sms_form -> %>
-