From 04efb905c8d24e3ccf4a09ada36fa20c7489cc69 Mon Sep 17 00:00:00 2001 From: Ivor Paul Date: Tue, 4 Oct 2022 18:15:52 +0200 Subject: [PATCH 1/5] First pass adding :by_module option. --- lib/polymorphic_embed.ex | 68 ++- test/polymorphic_embed_test.exs | 401 +++++++++++------- .../20000101000001_add_attachments.exs | 9 + .../polymorphic/attachment/link_attachment.ex | 14 + .../attachment/video_attachment.ex | 15 + test/support/models/polymorphic/reminder.ex | 3 + 6 files changed, 346 insertions(+), 164 deletions(-) create mode 100644 test/support/migrations/20000101000001_add_attachments.exs create mode 100644 test/support/models/polymorphic/attachment/link_attachment.ex create mode 100644 test/support/models/polymorphic/attachment/video_attachment.ex diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 16b98a1..363b924 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -29,22 +29,29 @@ defmodule PolymorphicEmbed do types_metadata = opts |> Keyword.fetch!(:types) - |> Enum.map(fn - {type_name, type_opts} when is_list(type_opts) -> - {type_name, type_opts} - - {type_name, module} -> - {type_name, module: module} - end) - |> Enum.map(fn - {type_name, type_opts} -> - %{ - type: type_name, - module: Keyword.fetch!(type_opts, :module), - identify_by_fields: - type_opts |> Keyword.get(:identify_by_fields, []) |> Enum.map(&to_string/1) - } - end) + |> case do + :by_module -> + %{lookup_type_fun: :by_module} + + types when is_list(types) -> + types + |> Enum.map(fn + {type_name, type_opts} when is_list(type_opts) -> + {type_name, type_opts} + + {type_name, module} -> + {type_name, module: module} + end) + |> Enum.map(fn + {type_name, type_opts} -> + %{ + type: type_name, + module: Keyword.fetch!(type_opts, :module), + identify_by_fields: + type_opts |> Keyword.get(:identify_by_fields, []) |> Enum.map(&to_string/1) + } + end) + end %{ default: Keyword.get(opts, :default, nil), @@ -339,12 +346,25 @@ defmodule PolymorphicEmbed do # Enum.count(contained -- container) == 0 # contained -- container == [] types_metadata - |> Enum.filter(&([] != &1.identify_by_fields)) - |> Enum.find(&([] == &1.identify_by_fields -- Map.keys(attrs))) - |> (&(&1 && Map.fetch!(&1, :module))).() + |> case do + %{lookup_type_fun: :by_module} -> + nil + + _ -> + types_metadata + |> Enum.filter(&([] != &1.identify_by_fields)) + |> Enum.find(&([] == &1.identify_by_fields -- Map.keys(attrs))) + |> (&(&1 && Map.fetch!(&1, :module))).() + end end end + defp do_get_polymorphic_module_for_type(type, %{lookup_type_fun: :by_module}) do + type = to_string(type) + + Module.safe_concat([type]) + end + defp do_get_polymorphic_module_for_type(type, types_metadata) do get_metadata_for_type(type, types_metadata) |> (&(&1 && Map.fetch!(&1, :module))).() @@ -355,6 +375,14 @@ defmodule PolymorphicEmbed do do_get_polymorphic_type(module_or_struct, types_metadata) end + defp do_get_polymorphic_type(%module{}, %{lookup_type_fun: :by_module}), + do: do_get_polymorphic_type(module, %{lookup_type_fun: :by_module}) + + defp do_get_polymorphic_type(module, %{lookup_type_fun: :by_module}) do + module = to_string(module) + Module.safe_concat([module]) + end + defp do_get_polymorphic_type(%module{}, types_metadata), do: do_get_polymorphic_type(module, types_metadata) @@ -372,7 +400,7 @@ defmodule PolymorphicEmbed do """ def types(schema, field) do %{types_metadata: types_metadata} = get_field_options(schema, field) - Enum.map(types_metadata, &(&1.type)) + Enum.map(types_metadata, & &1.type) end defp get_metadata_for_module(module, types_metadata) do diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs index b27e5ab..c1fe7f8 100644 --- a/test/polymorphic_embed_test.exs +++ b/test/polymorphic_embed_test.exs @@ -23,99 +23,161 @@ defmodule PolymorphicEmbedTest do defp get_module(name, :not_polymorphic), do: Module.concat([PolymorphicEmbed.Regular, name]) - test "receive embed as map of values" do - for generator <- @generators do - reminder_module = get_module(Reminder, generator) + describe "receive embedded as a map of values" do + test "when passing tyeps as keyword" do + for generator <- @generators do + reminder_module = get_module(Reminder, generator) - sms_reminder_attrs = %{ - date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}", - channel: %{ - my_type_field: "sms", - number: "02/807.05.53", - country_code: 1, - result: %{success: true}, - attempts: [ - %{ - date: ~U[2020-05-28 07:27:05Z], - result: %{success: true} - }, - %{ - date: ~U[2020-05-29 07:27:05Z], - result: %{success: false} - }, - %{ - date: ~U[2020-05-30 07:27:05Z], - result: %{success: true} + sms_reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder #{generator}", + channel: %{ + my_type_field: "sms", + number: "02/807.05.53", + country_code: 1, + result: %{success: true}, + attempts: [ + %{ + date: ~U[2020-05-28 07:27:05Z], + result: %{success: true} + }, + %{ + date: ~U[2020-05-29 07:27:05Z], + result: %{success: false} + }, + %{ + date: ~U[2020-05-30 07:27:05Z], + result: %{success: true} + } + ], + provider: %{ + __type__: "twilio", + api_key: "foo" } - ], - provider: %{ - __type__: "twilio", - api_key: "foo" } } + + insert_result = + struct(reminder_module) + |> reminder_module.changeset(sms_reminder_attrs) + |> Repo.insert() + + assert {:ok, %reminder_module{}} = insert_result + + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") + |> Repo.one() + + assert get_module(Channel.SMS, generator) == reminder.channel.__struct__ + + assert get_module(Channel.TwilioSMSProvider, generator) == + reminder.channel.provider.__struct__ + + assert get_module(Channel.SMSResult, generator) == reminder.channel.result.__struct__ + assert true == reminder.channel.result.success + assert ~U[2020-05-28 07:27:05Z] == hd(reminder.channel.attempts).date + + assert Map.has_key?(reminder.channel, :id) + assert reminder.channel.id + + sms_module = get_module(Channel.SMS, generator) + + sms_reminder_attrs = %{ + channel: %{ + country_code: 2 + } + } + + {:ok, update_result} = + reminder + |> reminder_module.changeset(sms_reminder_attrs) + |> Repo.update() + + assert reminder.channel.id == update_result.channel.id + + # test changing entity + + sms_module = + struct(sms_module, + id: 10, + country_code: 10 + ) + + sms_changeset = Ecto.Changeset.change(sms_module) + + {:ok, update_result} = + reminder + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_change(:channel, nil) + |> Repo.update() + + {:ok, _} = + update_result + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_change(:channel, sms_changeset) + |> Repo.update() + end + end + + test "with :by_module" do + reminder_module = get_module(Reminder, :polymorphic) + + # With Link + reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder with link #{:polymorphic}", + attachment: %{ + __type__: PolymorphicEmbed.Attachment.LinkAttachment, + url: "some_link", + title: "title_for_link" + } } insert_result = struct(reminder_module) - |> reminder_module.changeset(sms_reminder_attrs) + |> reminder_module.changeset(reminder_attrs) |> Repo.insert() assert {:ok, %reminder_module{}} = insert_result reminder = reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") + |> QueryBuilder.where(text: "This is an SMS reminder with link #{:polymorphic}") |> Repo.one() - assert get_module(Channel.SMS, generator) == reminder.channel.__struct__ - - assert get_module(Channel.TwilioSMSProvider, generator) == - reminder.channel.provider.__struct__ + assert %PolymorphicEmbed.Attachment.LinkAttachment{ + url: "some_link", + title: "title_for_link" + } = reminder.attachment - assert get_module(Channel.SMSResult, generator) == reminder.channel.result.__struct__ - assert true == reminder.channel.result.success - assert ~U[2020-05-28 07:27:05Z] == hd(reminder.channel.attempts).date - - assert Map.has_key?(reminder.channel, :id) - assert reminder.channel.id - - sms_module = get_module(Channel.SMS, generator) - - sms_reminder_attrs = %{ - channel: %{ - country_code: 2 + # With Video + reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder with video #{:polymorphic}", + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + url: "some_video_url", + thumbnail_url: "some_video_thumbnail_url" } } - {:ok, update_result} = - reminder - |> reminder_module.changeset(sms_reminder_attrs) - |> Repo.update() - - assert reminder.channel.id == update_result.channel.id - - # test changing entity + insert_result = + struct(reminder_module) + |> reminder_module.changeset(reminder_attrs) + |> Repo.insert() - sms_module = - struct(sms_module, - id: 10, - country_code: 10 - ) + assert {:ok, %reminder_module{}} = insert_result - sms_changeset = Ecto.Changeset.change(sms_module) + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder with video #{:polymorphic}") + |> Repo.one() - {:ok, update_result} = - reminder - |> Ecto.Changeset.change() - |> Ecto.Changeset.put_change(:channel, nil) - |> Repo.update() - - {:ok, _} = - update_result - |> Ecto.Changeset.change() - |> Ecto.Changeset.put_change(:channel, sms_changeset) - |> Repo.update() + assert %PolymorphicEmbed.Attachment.VideoAttachment{ + url: "some_video_url", + thumbnail_url: "some_video_thumbnail_url" + } = reminder.attachment end end @@ -383,38 +445,104 @@ defmodule PolymorphicEmbedTest do end end - test "receive embed as struct" do - for generator <- @generators do - reminder_module = get_module(Reminder, generator) - sms_module = get_module(Channel.SMS, generator) - sms_provider_module = get_module(Channel.TwilioSMSProvider, generator) - sms_result_module = get_module(Channel.SMSResult, generator) - sms_attempts_module = get_module(Channel.SMSAttempts, generator) + describe "receive embed as struct" do + test "when types is a keyword list" do + for generator <- @generators do + reminder_module = get_module(Reminder, generator) + sms_module = get_module(Channel.SMS, generator) + sms_provider_module = get_module(Channel.TwilioSMSProvider, generator) + sms_result_module = get_module(Channel.SMSResult, generator) + sms_attempts_module = get_module(Channel.SMSAttempts, generator) + + reminder = + struct(reminder_module, + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder #{generator}", + channel: + struct(sms_module, + provider: + struct(sms_provider_module, + api_key: "foo" + ), + country_code: 1, + number: "02/807.05.53", + result: struct(sms_result_module, success: true), + attempts: [ + struct(sms_attempts_module, + date: ~U[2020-05-28 07:27:05Z], + result: struct(sms_result_module, success: true) + ), + struct(sms_attempts_module, + date: ~U[2020-05-28 07:27:05Z], + result: struct(sms_result_module, success: true) + ) + ] + ) + ) + + reminder + |> reminder_module.changeset(%{}) + |> Repo.insert() + + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") + |> Repo.one() + + assert sms_module == reminder.channel.__struct__ + + changeset = + reminder + |> reminder_module.changeset(%{channel: %{provider: nil}}) + + assert %Ecto.Changeset{ + action: nil, + valid?: false, + errors: [], + changes: %{ + channel: %{ + action: :update, + valid?: false, + errors: [provider: {"can't be blank", [validation: :required]}] + } + } + } = changeset + + insert_result = + changeset + |> Repo.insert() + + assert {:error, + %Ecto.Changeset{ + action: :insert, + valid?: false, + errors: errors, + changes: %{ + channel: %{ + action: :update, + valid?: false, + errors: channel_errors + } + } + }} = insert_result + + assert [] = errors + assert %{provider: {"can't be blank", [validation: :required]}} = Map.new(channel_errors) + end + end + + test "when types is :by_module" do + reminder_module = get_module(Reminder, :polymorphic) reminder = struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}", - channel: - struct(sms_module, - provider: - struct(sms_provider_module, - api_key: "foo" - ), - country_code: 1, - number: "02/807.05.53", - result: struct(sms_result_module, success: true), - attempts: [ - struct(sms_attempts_module, - date: ~U[2020-05-28 07:27:05Z], - result: struct(sms_result_module, success: true) - ), - struct(sms_attempts_module, - date: ~U[2020-05-28 07:27:05Z], - result: struct(sms_result_module, success: true) - ) - ] - ) + text: "This is an SMS reminder with video.", + attachment: + struct(PolymorphicEmbed.Attachment.VideoAttachment, %{ + url: "some_video_url", + thumbnail_url: "some_thumbnail_url" + }) ) reminder @@ -423,48 +551,17 @@ defmodule PolymorphicEmbedTest do reminder = reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") + |> QueryBuilder.where(text: "This is an SMS reminder with video.") |> Repo.one() - assert sms_module == reminder.channel.__struct__ + assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ - changeset = - reminder - |> reminder_module.changeset(%{channel: %{provider: nil}}) - - assert %Ecto.Changeset{ - action: nil, - valid?: false, - errors: [], - changes: %{ - channel: %{ - action: :update, - valid?: false, - errors: [provider: {"can't be blank", [validation: :required]}] - } - } - } = changeset + changeset = reminder_module.changeset(reminder, %{attachment: %{url: nil}}) - insert_result = - changeset - |> Repo.insert() - - assert {:error, - %Ecto.Changeset{ - action: :insert, - valid?: false, - errors: errors, - changes: %{ - channel: %{ - action: :update, - valid?: false, - errors: channel_errors - } - } - }} = insert_result + refute changeset.valid? - assert [] = errors - assert %{provider: {"can't be blank", [validation: :required]}} = Map.new(channel_errors) + assert changeset.changes.attachment.errors[:url] == + {"can't be blank", [validation: :required]} end end @@ -574,9 +671,11 @@ defmodule PolymorphicEmbedTest do reminder_module = get_module(Reminder, generator) - assert_raise RuntimeError, ~r"cast_polymorphic_embed/3 only accepts a changeset as first argument", fn -> - PolymorphicEmbed.cast_polymorphic_embed(struct(reminder_module), :channel) - end + assert_raise RuntimeError, + ~r"cast_polymorphic_embed/3 only accepts a changeset as first argument", + fn -> + PolymorphicEmbed.cast_polymorphic_embed(struct(reminder_module), :channel) + end end test "cast embed after change/2 call should succeed" do @@ -1270,7 +1369,8 @@ defmodule PolymorphicEmbedTest do struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], text: "This is an SMS reminder #{generator}", - channel: struct(sms_module)) + channel: struct(sms_module) + ) changeset = reminder_module.changeset(struct, %{}) @@ -1297,7 +1397,8 @@ defmodule PolymorphicEmbedTest do struct = struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}") + text: "This is an SMS reminder #{generator}" + ) changeset = reminder_module.changeset( @@ -1312,7 +1413,8 @@ defmodule PolymorphicEmbedTest do api_key: "foo" } } - }) + } + ) if polymorphic?(generator) do assert changeset.changes.channel.id @@ -1342,7 +1444,8 @@ defmodule PolymorphicEmbedTest do contexts: [ struct(location_module), struct(location_module) - ]) + ] + ) changeset = reminder_module.changeset(struct, %{}) @@ -1372,7 +1475,8 @@ defmodule PolymorphicEmbedTest do struct = struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}") + text: "This is an SMS reminder #{generator}" + ) changeset = reminder_module.changeset( @@ -1382,7 +1486,8 @@ defmodule PolymorphicEmbedTest do %{__type__: "location", address: "A"}, %{__type__: "location", address: "B"} ] - }) + } + ) if polymorphic?(generator) do assert Enum.at(changeset.changes.contexts, 0).id @@ -2034,6 +2139,14 @@ defmodule PolymorphicEmbedTest do } ) == :email end + + test "returns the module for a :by_module embed" do + assert PolymorphicEmbed.get_polymorphic_type( + PolymorphicEmbed.Reminder, + :attachment, + %PolymorphicEmbed.Attachment.VideoAttachment{} + ) == PolymorphicEmbed.Attachment.VideoAttachment + end end describe "Form.get_polymorphic_type/3" do diff --git a/test/support/migrations/20000101000001_add_attachments.exs b/test/support/migrations/20000101000001_add_attachments.exs new file mode 100644 index 0000000..cd4e9e7 --- /dev/null +++ b/test/support/migrations/20000101000001_add_attachments.exs @@ -0,0 +1,9 @@ +defmodule PolymorphicEmbed.AddAttachments do + use Ecto.Migration + + def change do + alter table(:reminders) do + add(:attachment, :map) + end + end +end diff --git a/test/support/models/polymorphic/attachment/link_attachment.ex b/test/support/models/polymorphic/attachment/link_attachment.ex new file mode 100644 index 0000000..5406665 --- /dev/null +++ b/test/support/models/polymorphic/attachment/link_attachment.ex @@ -0,0 +1,14 @@ +defmodule PolymorphicEmbed.Attachment.LinkAttachment do + use Ecto.Schema + import Ecto.Changeset + + embedded_schema do + field :url, :string + field :title, :string + end + + def changeset(struct, params) do + struct + |> cast(params, [:url, :title]) + end +end diff --git a/test/support/models/polymorphic/attachment/video_attachment.ex b/test/support/models/polymorphic/attachment/video_attachment.ex new file mode 100644 index 0000000..067c101 --- /dev/null +++ b/test/support/models/polymorphic/attachment/video_attachment.ex @@ -0,0 +1,15 @@ +defmodule PolymorphicEmbed.Attachment.VideoAttachment do + use Ecto.Schema + import Ecto.Changeset + + embedded_schema do + field :url, :string + field :thumbnail_url, :string + end + + def changeset(struct, params) do + struct + |> cast(params, [:url, :thumbnail_url]) + |> validate_required([:url]) + end +end diff --git a/test/support/models/polymorphic/reminder.ex b/test/support/models/polymorphic/reminder.ex index 520f73b..e6ea706 100644 --- a/test/support/models/polymorphic/reminder.ex +++ b/test/support/models/polymorphic/reminder.ex @@ -39,6 +39,8 @@ defmodule PolymorphicEmbed.Reminder do on_replace: :delete ) + polymorphic_embeds_one(:attachment, types: :by_module, on_replace: :update) + timestamps() end @@ -47,6 +49,7 @@ defmodule PolymorphicEmbed.Reminder do |> cast(values, [:date, :text]) |> validate_required(:date) |> cast_polymorphic_embed(:channel) + |> cast_polymorphic_embed(:attachment) |> cast_polymorphic_embed(:contexts) |> cast_polymorphic_embed(:contexts2) end From bb3914dfaeea9ea6fcb13939dcb5d4444cfed07a Mon Sep 17 00:00:00 2001 From: Ivor Paul Date: Wed, 5 Oct 2022 09:24:00 +0200 Subject: [PATCH 2/5] Fix typo. --- test/polymorphic_embed_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs index c1fe7f8..c71de69 100644 --- a/test/polymorphic_embed_test.exs +++ b/test/polymorphic_embed_test.exs @@ -24,7 +24,7 @@ defmodule PolymorphicEmbedTest do do: Module.concat([PolymorphicEmbed.Regular, name]) describe "receive embedded as a map of values" do - test "when passing tyeps as keyword" do + test "when passing types as keyword" do for generator <- @generators do reminder_module = get_module(Reminder, generator) From c4aeee345e0aa548585138147b2c7ce6734504d5 Mon Sep 17 00:00:00 2001 From: Ivor Paul Date: Wed, 5 Oct 2022 09:24:08 +0200 Subject: [PATCH 3/5] Update readme. --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef3c9b7..57f4811 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,9 @@ changeset ### PolymorphicEmbed Ecto type -The `:types` option for the `PolymorphicEmbed` custom type contains a keyword list mapping an atom representing the type -(in this example `:email` and `:sms`) with the corresponding embedded schema module. +The `:types` option for the `PolymorphicEmbed` custom type is used to determine the type of the polymorphic embed. The value is either a keyword list mapping an atom representing the type (in this example `:email` and `:sms`) with the corresponding embedded schema module, or the value :by_module which indicates that the struct will be passed in at runtime. -There are two strategies to detect the right embedded schema to use: +There are three strategies to detect the right embedded schema to use: 1. ```elixir @@ -137,6 +136,16 @@ parameter is then no longer required. Note that you may still include a `__type__` parameter that will take precedence over this strategy (this could still be useful if you need to store incomplete data, which might not allow identifying the type). +3. +```elixir +polymorphic_embeds_many :attachment, + types: :by_module, + on_replace: :update +``` + +The `:by_module` value indicates that this field will receive an embedded_struct in the `type_field` (by default `__type__`) which is used to cash the embed. +If you specify `types: :by_module`, the `"__type__"` (or `:__type__`) parameter should contain the fully qualified and reachable module name of the embedded schema, such as "MyApp.Channel.Email". This is equivalent to specifying + #### List of polymorphic embeds Lists of polymorphic embeds are also supported: From 5b56a921e6196d073b3956250db9639182e9a6c0 Mon Sep 17 00:00:00 2001 From: Ivor Paul Date: Thu, 6 Oct 2022 18:11:33 +0200 Subject: [PATCH 4/5] Handle passing with option with :by_module. --- lib/polymorphic_embed.ex | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 363b924..079e672 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -72,6 +72,7 @@ defmodule PolymorphicEmbed do %{array?: array?, types_metadata: types_metadata} = field_options required = Keyword.get(cast_options, :required, false) + with = Keyword.get(cast_options, :with, nil) changeset_fun = fn @@ -91,6 +92,13 @@ defmodule PolymorphicEmbed do fun -> apply(fun, [struct, params]) end + + struct, params when is_tuple(with) and tuple_size(with) == 3 -> + {module, function_name, args} = with + apply(module, function_name, [struct, params | args]) + + struct, params when is_function(with) -> + apply(with, [struct, params]) end (changeset.params || %{}) @@ -440,6 +448,11 @@ defmodule PolymorphicEmbed do end end + defp convert_map_keys_to_string(%_{} = struct) do + Map.from_struct(struct) + |> convert_map_keys_to_string() + end + defp convert_map_keys_to_string(%{} = map), do: for({key, val} <- map, into: %{}, do: {to_string(key), val}) From 9c4fef4e5d8d7e61bdef3d9aabe505de33fe6a43 Mon Sep 17 00:00:00 2001 From: Ivor Paul Date: Thu, 6 Oct 2022 18:11:41 +0200 Subject: [PATCH 5/5] More tests. --- test/polymorphic_embed_test.exs | 710 ++++++++++++------ .../attachment/link_attachment.ex | 0 .../attachment/video_attachment.ex | 13 + .../models/not_polymorphic/reminder.ex | 8 + .../models/polymorphic/channel/email.ex | 5 +- test/support/models/polymorphic/reminder.ex | 26 +- 6 files changed, 510 insertions(+), 252 deletions(-) rename test/support/models/{polymorphic => not_polymorphic}/attachment/link_attachment.ex (100%) rename test/support/models/{polymorphic => not_polymorphic}/attachment/video_attachment.ex (52%) diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs index c71de69..7c2a1b4 100644 --- a/test/polymorphic_embed_test.exs +++ b/test/polymorphic_embed_test.exs @@ -23,162 +23,167 @@ defmodule PolymorphicEmbedTest do defp get_module(name, :not_polymorphic), do: Module.concat([PolymorphicEmbed.Regular, name]) - describe "receive embedded as a map of values" do - test "when passing types as keyword" do - for generator <- @generators do - reminder_module = get_module(Reminder, generator) + test "receive embedded as a map of values" do + for generator <- @generators do + reminder_module = get_module(Reminder, generator) - sms_reminder_attrs = %{ - date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}", - channel: %{ - my_type_field: "sms", - number: "02/807.05.53", - country_code: 1, - result: %{success: true}, - attempts: [ - %{ - date: ~U[2020-05-28 07:27:05Z], - result: %{success: true} - }, - %{ - date: ~U[2020-05-29 07:27:05Z], - result: %{success: false} - }, - %{ - date: ~U[2020-05-30 07:27:05Z], - result: %{success: true} - } - ], - provider: %{ - __type__: "twilio", - api_key: "foo" + sms_reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder #{generator}", + attachment: %{ + __type__: "PolymorphicEmbed.Attachment.VideoAttachment", + url: "some_video_url", + thumbnail_url: "some_video_thumbnail_url" + }, + channel: %{ + my_type_field: "sms", + number: "02/807.05.53", + country_code: 1, + result: %{success: true}, + attempts: [ + %{ + date: ~U[2020-05-28 07:27:05Z], + result: %{success: true} + }, + %{ + date: ~U[2020-05-29 07:27:05Z], + result: %{success: false} + }, + %{ + date: ~U[2020-05-30 07:27:05Z], + result: %{success: true} } + ], + provider: %{ + __type__: "twilio", + api_key: "foo" } } + } - insert_result = - struct(reminder_module) - |> reminder_module.changeset(sms_reminder_attrs) - |> Repo.insert() + insert_result = + struct(reminder_module) + |> reminder_module.changeset(sms_reminder_attrs) + |> Repo.insert() - assert {:ok, %reminder_module{}} = insert_result + assert {:ok, %reminder_module{}} = insert_result - reminder = - reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") - |> Repo.one() + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") + |> Repo.one() - assert get_module(Channel.SMS, generator) == reminder.channel.__struct__ + assert get_module(Channel.SMS, generator) == reminder.channel.__struct__ - assert get_module(Channel.TwilioSMSProvider, generator) == - reminder.channel.provider.__struct__ + assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ - assert get_module(Channel.SMSResult, generator) == reminder.channel.result.__struct__ - assert true == reminder.channel.result.success - assert ~U[2020-05-28 07:27:05Z] == hd(reminder.channel.attempts).date + assert get_module(Channel.TwilioSMSProvider, generator) == + reminder.channel.provider.__struct__ - assert Map.has_key?(reminder.channel, :id) - assert reminder.channel.id + assert get_module(Channel.SMSResult, generator) == reminder.channel.result.__struct__ + assert true == reminder.channel.result.success + assert ~U[2020-05-28 07:27:05Z] == hd(reminder.channel.attempts).date - sms_module = get_module(Channel.SMS, generator) + assert Map.has_key?(reminder.channel, :id) + assert reminder.channel.id - sms_reminder_attrs = %{ - channel: %{ - country_code: 2 - } + sms_module = get_module(Channel.SMS, generator) + + sms_reminder_attrs = %{ + channel: %{ + country_code: 2 } + } - {:ok, update_result} = - reminder - |> reminder_module.changeset(sms_reminder_attrs) - |> Repo.update() + {:ok, update_result} = + reminder + |> reminder_module.changeset(sms_reminder_attrs) + |> Repo.update() - assert reminder.channel.id == update_result.channel.id + assert reminder.channel.id == update_result.channel.id - # test changing entity + # test changing entity - sms_module = - struct(sms_module, - id: 10, - country_code: 10 - ) + sms_module = + struct(sms_module, + id: 10, + country_code: 10 + ) - sms_changeset = Ecto.Changeset.change(sms_module) + sms_changeset = Ecto.Changeset.change(sms_module) - {:ok, update_result} = - reminder - |> Ecto.Changeset.change() - |> Ecto.Changeset.put_change(:channel, nil) - |> Repo.update() - - {:ok, _} = - update_result - |> Ecto.Changeset.change() - |> Ecto.Changeset.put_change(:channel, sms_changeset) - |> Repo.update() - end + {:ok, update_result} = + reminder + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_change(:channel, nil) + |> Repo.update() + + {:ok, _} = + update_result + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_change(:channel, sms_changeset) + |> Repo.update() end + end - test "with :by_module" do - reminder_module = get_module(Reminder, :polymorphic) + test "receive embedded as a map of values with different dynamic types" do + reminder_module = get_module(Reminder, :polymorphic) - # With Link - reminder_attrs = %{ - date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder with link #{:polymorphic}", - attachment: %{ - __type__: PolymorphicEmbed.Attachment.LinkAttachment, - url: "some_link", - title: "title_for_link" - } + # With Link + reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder with link #{:polymorphic}", + attachment: %{ + __type__: PolymorphicEmbed.Attachment.LinkAttachment, + url: "some_link", + title: "title_for_link" } + } - insert_result = - struct(reminder_module) - |> reminder_module.changeset(reminder_attrs) - |> Repo.insert() + insert_result = + struct(reminder_module) + |> reminder_module.changeset(reminder_attrs) + |> Repo.insert() - assert {:ok, %reminder_module{}} = insert_result + assert {:ok, %reminder_module{}} = insert_result - reminder = - reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder with link #{:polymorphic}") - |> Repo.one() + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder with link #{:polymorphic}") + |> Repo.one() - assert %PolymorphicEmbed.Attachment.LinkAttachment{ - url: "some_link", - title: "title_for_link" - } = reminder.attachment + assert %PolymorphicEmbed.Attachment.LinkAttachment{ + url: "some_link", + title: "title_for_link" + } = reminder.attachment - # With Video - reminder_attrs = %{ - date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder with video #{:polymorphic}", - attachment: %{ - __type__: PolymorphicEmbed.Attachment.VideoAttachment, - url: "some_video_url", - thumbnail_url: "some_video_thumbnail_url" - } + # With Video + reminder_attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder with video #{:polymorphic}", + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + url: "some_video_url", + thumbnail_url: "some_video_thumbnail_url" } + } - insert_result = - struct(reminder_module) - |> reminder_module.changeset(reminder_attrs) - |> Repo.insert() + insert_result = + struct(reminder_module) + |> reminder_module.changeset(reminder_attrs) + |> Repo.insert() - assert {:ok, %reminder_module{}} = insert_result + assert {:ok, %reminder_module{}} = insert_result - reminder = - reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder with video #{:polymorphic}") - |> Repo.one() + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder with video #{:polymorphic}") + |> Repo.one() - assert %PolymorphicEmbed.Attachment.VideoAttachment{ - url: "some_video_url", - thumbnail_url: "some_video_thumbnail_url" - } = reminder.attachment - end + assert %PolymorphicEmbed.Attachment.VideoAttachment{ + url: "some_video_url", + thumbnail_url: "some_video_thumbnail_url" + } = reminder.attachment end test "validations before casting polymorphic embed still work" do @@ -220,6 +225,10 @@ defmodule PolymorphicEmbedTest do text: "This is an SMS reminder", channel: %{ my_type_field: "sms" + }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + thumbnail_url: "the thumbnail url" } } @@ -238,6 +247,11 @@ defmodule PolymorphicEmbedTest do action: :insert, valid?: false, errors: channel_errors + }, + attachment: %{ + action: :insert, + valid?: false, + errors: attachment_errors } } }} = insert_result @@ -249,6 +263,10 @@ defmodule PolymorphicEmbedTest do country_code: {"can't be blank", [validation: :required]}, provider: {"can't be blank", [validation: :required]} } = Map.new(channel_errors) + + assert %{ + url: {"can't be blank", [validation: :required]} + } = Map.new(attachment_errors) end end @@ -261,6 +279,10 @@ defmodule PolymorphicEmbedTest do channel: %{ my_type_field: "sms" }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + thumbnail_url: "thumbnail url" + }, contexts: [ %{ __type__: "location", @@ -293,6 +315,11 @@ defmodule PolymorphicEmbedTest do valid?: false, errors: channel_errors }, + attachment: %{ + action: :insert, + valid?: false, + errors: attachment_errors + }, contexts: [ %{ action: :insert, @@ -321,6 +348,8 @@ defmodule PolymorphicEmbedTest do provider: {"can't be blank", [validation: :required]} } = Map.new(channel_errors) + assert %{url: {"can't be blank", [validation: :required]}} = Map.new(attachment_errors) + assert [date: {"can't be blank", [validation: :required]}] = changeset.errors assert [date: {"can't be blank", [validation: :required]}] = errors @@ -341,6 +370,9 @@ defmodule PolymorphicEmbedTest do number: ["can't be blank"], provider: ["can't be blank"] }, + attachment: %{ + url: ["can't be blank"] + }, contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}], date: ["can't be blank"] } = @@ -367,6 +399,11 @@ defmodule PolymorphicEmbedTest do country_code: 1, provider: %{__type__: "twilio", api_key: "somekey"} }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + thumbnail_url: "thumbnail url", + url: "the url" + }, contexts: [ %{ __type__: "location", @@ -445,103 +482,42 @@ defmodule PolymorphicEmbedTest do end end - describe "receive embed as struct" do - test "when types is a keyword list" do - for generator <- @generators do - reminder_module = get_module(Reminder, generator) - sms_module = get_module(Channel.SMS, generator) - sms_provider_module = get_module(Channel.TwilioSMSProvider, generator) - sms_result_module = get_module(Channel.SMSResult, generator) - sms_attempts_module = get_module(Channel.SMSAttempts, generator) - - reminder = - struct(reminder_module, - date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder #{generator}", - channel: - struct(sms_module, - provider: - struct(sms_provider_module, - api_key: "foo" - ), - country_code: 1, - number: "02/807.05.53", - result: struct(sms_result_module, success: true), - attempts: [ - struct(sms_attempts_module, - date: ~U[2020-05-28 07:27:05Z], - result: struct(sms_result_module, success: true) - ), - struct(sms_attempts_module, - date: ~U[2020-05-28 07:27:05Z], - result: struct(sms_result_module, success: true) - ) - ] - ) - ) - - reminder - |> reminder_module.changeset(%{}) - |> Repo.insert() - - reminder = - reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") - |> Repo.one() - - assert sms_module == reminder.channel.__struct__ - - changeset = - reminder - |> reminder_module.changeset(%{channel: %{provider: nil}}) - - assert %Ecto.Changeset{ - action: nil, - valid?: false, - errors: [], - changes: %{ - channel: %{ - action: :update, - valid?: false, - errors: [provider: {"can't be blank", [validation: :required]}] - } - } - } = changeset - - insert_result = - changeset - |> Repo.insert() - - assert {:error, - %Ecto.Changeset{ - action: :insert, - valid?: false, - errors: errors, - changes: %{ - channel: %{ - action: :update, - valid?: false, - errors: channel_errors - } - } - }} = insert_result - - assert [] = errors - assert %{provider: {"can't be blank", [validation: :required]}} = Map.new(channel_errors) - end - end - - test "when types is :by_module" do - reminder_module = get_module(Reminder, :polymorphic) + test "receive embed as struct" do + for generator <- @generators do + reminder_module = get_module(Reminder, generator) + sms_module = get_module(Channel.SMS, generator) + sms_provider_module = get_module(Channel.TwilioSMSProvider, generator) + sms_result_module = get_module(Channel.SMSResult, generator) + sms_attempts_module = get_module(Channel.SMSAttempts, generator) reminder = struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], - text: "This is an SMS reminder with video.", + text: "This is an SMS reminder #{generator}", + channel: + struct(sms_module, + provider: + struct(sms_provider_module, + api_key: "foo" + ), + country_code: 1, + number: "02/807.05.53", + result: struct(sms_result_module, success: true), + attempts: [ + struct(sms_attempts_module, + date: ~U[2020-05-28 07:27:05Z], + result: struct(sms_result_module, success: true) + ), + struct(sms_attempts_module, + date: ~U[2020-05-28 07:27:05Z], + result: struct(sms_result_module, success: true) + ) + ] + ), attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, %{ - url: "some_video_url", - thumbnail_url: "some_thumbnail_url" + url: "the url", + thumbnail_url: "the thumbnail url" }) ) @@ -551,20 +527,96 @@ defmodule PolymorphicEmbedTest do reminder = reminder_module - |> QueryBuilder.where(text: "This is an SMS reminder with video.") + |> QueryBuilder.where(text: "This is an SMS reminder #{generator}") |> Repo.one() + assert sms_module == reminder.channel.__struct__ assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ - changeset = reminder_module.changeset(reminder, %{attachment: %{url: nil}}) + changeset = + reminder + |> reminder_module.changeset(%{channel: %{provider: nil}, attachment: %{url: nil}}) + + assert %Ecto.Changeset{ + action: nil, + valid?: false, + errors: [], + changes: %{ + channel: %{ + action: :update, + valid?: false, + errors: [provider: {"can't be blank", [validation: :required]}] + }, + attachment: %{ + action: :update, + valid?: false, + errors: [url: {"can't be blank", [validation: :required]}] + } + } + } = changeset - refute changeset.valid? + insert_result = + changeset + |> Repo.insert() - assert changeset.changes.attachment.errors[:url] == - {"can't be blank", [validation: :required]} + assert {:error, + %Ecto.Changeset{ + action: :insert, + valid?: false, + errors: errors, + changes: %{ + channel: %{ + action: :update, + valid?: false, + errors: channel_errors + }, + attachment: %{ + action: :update, + valid?: false, + errors: attachment_errors + } + } + }} = insert_result + + assert [] = errors + assert %{provider: {"can't be blank", [validation: :required]}} = Map.new(channel_errors) + assert %{url: {"can't be blank", [validation: :required]}} = Map.new(attachment_errors) end end + test "receive embed as struct when types is :by_module" do + reminder_module = get_module(Reminder, :polymorphic) + + reminder = + struct(reminder_module, + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder with video.", + attachment: + struct(PolymorphicEmbed.Attachment.VideoAttachment, %{ + url: "some_video_url", + thumbnail_url: "some_thumbnail_url" + }) + ) + + reminder + |> reminder_module.changeset(%{}) + |> Repo.insert() + + reminder = + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder with video.") + |> Repo.one() + + assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ + + changeset = reminder_module.changeset(reminder, %{attachment: %{url: nil}}) + + refute changeset.valid? + + assert changeset.changes.attachment.errors[:url] == + {"can't be blank", [validation: :required]} + end + test "cannot generate IDs if struct didn't go through cast_polymorphic_embed/3" do generator = :polymorphic @@ -619,6 +671,29 @@ defmodule PolymorphicEmbedTest do assert email_module == reminder.channel.__struct__ end + test "without __type__ when types: :by_module" do + generator = :polymorphic + reminder_module = get_module(Reminder, generator) + + attrs = %{ + date: ~U[2020-05-28 02:57:19Z], + text: "This is a reminder with an attachment", + attachment: %{ + url: "the url", + thumbnail_url: "the thumbnail_url" + } + } + + insert_result = + struct(reminder_module) + |> reminder_module.changeset(attrs) + |> Repo.insert() + + assert {:error, changeset} = insert_result + + assert %{attachment: {"is invalid", []}} = Map.new(changeset.errors) + end + test "wrong type as string adds error in changeset" do generator = :polymorphic reminder_module = get_module(Reminder, generator) @@ -628,6 +703,9 @@ defmodule PolymorphicEmbedTest do text: "This is an Email reminder", channel: %{ my_type_field: "unknown type" + }, + attachment: %{ + __type___: "UnknownType" } } @@ -636,7 +714,8 @@ defmodule PolymorphicEmbedTest do |> reminder_module.changeset(attrs) |> Repo.insert() - assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result + assert {:error, %Ecto.Changeset{errors: errors}} = insert_result + assert %{channel: {"is invalid", []}, attachment: {"is invalid", []}} = Map.new(errors) end test "wrong type as string raises" do @@ -676,23 +755,31 @@ defmodule PolymorphicEmbedTest do fn -> PolymorphicEmbed.cast_polymorphic_embed(struct(reminder_module), :channel) end + + assert_raise RuntimeError, + ~r"cast_polymorphic_embed/3 only accepts a changeset as first argument", + fn -> + PolymorphicEmbed.cast_polymorphic_embed(struct(reminder_module), :attachment) + end end test "cast embed after change/2 call should succeed" do for generator <- @generators do - reminder_module = get_module(Reminder, generator) + for field <- [:channel, :attachment] do + reminder_module = get_module(Reminder, generator) - changeset = Ecto.Changeset.change(struct(reminder_module)) + changeset = Ecto.Changeset.change(struct(reminder_module)) - changeset = - if polymorphic?(generator) do - PolymorphicEmbed.cast_polymorphic_embed(changeset, :channel) - else - Ecto.Changeset.cast_embed(changeset, :channel) - end + changeset = + if polymorphic?(generator) do + PolymorphicEmbed.cast_polymorphic_embed(changeset, field) + else + Ecto.Changeset.cast_embed(changeset, field) + end - assert changeset.valid? - assert map_size(changeset.changes) == 0 + assert changeset.valid? + assert map_size(changeset.changes) == 0 + end end end @@ -704,7 +791,8 @@ defmodule PolymorphicEmbedTest do struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], text: "This is an Email reminder #{generator}", - channel: nil + channel: nil, + attachment: nil ) |> Repo.insert() @@ -716,6 +804,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert is_nil(reminder.channel) + assert is_nil(reminder.attachment) end end @@ -726,7 +815,8 @@ defmodule PolymorphicEmbedTest do attrs = %{ date: ~U[2020-05-28 02:57:19Z], text: "This is an Email reminder #{generator}", - channel: nil + channel: nil, + attachment: nil } insert_result = @@ -742,6 +832,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert is_nil(reminder.channel) + assert is_nil(reminder.attachment) end end @@ -782,6 +873,7 @@ defmodule PolymorphicEmbedTest do test "custom changeset by passing MFA" do for generator <- @generators do reminder_module = get_module(Reminder, generator) + sms_module = get_module(Channel.SMS, generator) sms_reminder_attrs = %{ @@ -794,6 +886,11 @@ defmodule PolymorphicEmbedTest do attempts: [], provider: %{__type__: "twilio", api_key: "somekey"}, custom: true + }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + url: "the url", + custom: true } } @@ -813,6 +910,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert sms_module == reminder.channel.__struct__ + assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ end end @@ -831,6 +929,11 @@ defmodule PolymorphicEmbedTest do attempts: [], provider: %{__type__: "twilio", api_key: "somekey"}, custom: true + }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + url: "the url", + custom: true } } @@ -841,6 +944,7 @@ defmodule PolymorphicEmbedTest do assert {:ok, reminder} = insert_result assert reminder.channel.custom + assert reminder.attachment.custom %reminder_module{} = reminder @@ -850,6 +954,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert sms_module == reminder.channel.__struct__ + assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__ end end @@ -894,7 +999,8 @@ defmodule PolymorphicEmbedTest do attrs = %{ date: ~U[2020-05-28 02:57:19Z], text: "This is an SMS reminder #{generator}", - channel: nil + channel: nil, + attachment: nil } insert_result = @@ -903,7 +1009,8 @@ defmodule PolymorphicEmbedTest do struct(sms_module, number: "02/807.05.53", country_code: 32 - ) + ), + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, %{url: "the url"}) ) |> reminder_module.changeset(attrs) |> Repo.insert() @@ -916,6 +1023,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert is_nil(reminder.channel) + assert is_nil(reminder.attachment) end end @@ -934,6 +1042,10 @@ defmodule PolymorphicEmbedTest do channel: struct(sms_module, number: "02/807.05.53" + ), + attachment: + struct(PolymorphicEmbed.Attachment.VideoAttachment, + url: "the url" ) ) |> reminder_module.changeset(attrs) @@ -947,6 +1059,7 @@ defmodule PolymorphicEmbedTest do |> Repo.one() refute is_nil(reminder.channel) + refute is_nil(reminder.attachment) end end @@ -981,7 +1094,8 @@ defmodule PolymorphicEmbedTest do result: struct(sms_result_module, success: true) ) ] - ) + ), + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the url") ) reminder = @@ -994,6 +1108,9 @@ defmodule PolymorphicEmbedTest do |> reminder_module.changeset(%{ channel: %{ number: "54" + }, + attachment: %{ + thumbnail_url: "the thumbnail url" } }) @@ -1005,6 +1122,8 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert reminder.channel.result.success + assert reminder.attachment.url == "the url" + assert reminder.attachment.thumbnail_url == "the thumbnail url" end end @@ -1039,7 +1158,8 @@ defmodule PolymorphicEmbedTest do result: struct(sms_result_module, success: true) ) ] - ) + ), + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the original_url") ) reminder = @@ -1053,6 +1173,11 @@ defmodule PolymorphicEmbedTest do "channel" => %{ "my_type_field" => "sms", "number" => "54" + }, + "attachment" => %{ + "__type__" => "PolymorphicEmbed.Attachment.VideoAttachment", + "url" => "the new url", + "thumbnail_url" => "thumbnail url" } }) @@ -1064,6 +1189,8 @@ defmodule PolymorphicEmbedTest do |> Repo.one() assert reminder.channel.result.success + assert reminder.attachment.url == "the new url" + assert reminder.attachment.thumbnail_url == "thumbnail url" end end @@ -1096,6 +1223,9 @@ defmodule PolymorphicEmbedTest do __type__: "twilio", api_key: "foo" } + }, + attachment: %{ + url: "the url" } } @@ -1104,7 +1234,9 @@ defmodule PolymorphicEmbedTest do |> reminder_module.changeset(sms_reminder_attrs) |> Repo.insert() - assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result + assert {:error, %Ecto.Changeset{errors: errors}} = insert_result + + assert %{channel: {"is invalid", []}, attachment: {"is invalid", []}} = Map.new(errors) end test "missing __type__ nilifies" do @@ -1202,7 +1334,8 @@ defmodule PolymorphicEmbedTest do struct(sms_module, country_code: 1, number: "02/807.05.53" - ) + ), + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the url") ) |> reminder_module.changeset(%{}) |> Repo.insert() @@ -1220,6 +1353,31 @@ defmodule PolymorphicEmbedTest do end end + test "cannot load the right struct with :by_module" do + generator = :polymorphic + reminder_module = get_module(Reminder, generator) + + struct(reminder_module, + date: ~U[2020-05-28 02:57:19Z], + text: "This is an SMS reminder", + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the url") + ) + |> reminder_module.changeset(%{}) + |> Repo.insert() + + Ecto.Adapters.SQL.query!( + Repo, + "UPDATE reminders SET channel = jsonb_set(attachment, '{__type__}', '\"bar\"')", + [] + ) + + assert_raise RuntimeError, ~r"could not infer polymorphic embed from data .* \"bar\"", fn -> + reminder_module + |> QueryBuilder.where(text: "This is an SMS reminder") + |> Repo.one() + end + end + test "changing type" do generator = :polymorphic reminder_module = get_module(Reminder, generator) @@ -1369,13 +1527,15 @@ defmodule PolymorphicEmbedTest do struct(reminder_module, date: ~U[2020-05-28 02:57:19Z], text: "This is an SMS reminder #{generator}", - channel: struct(sms_module) + channel: struct(sms_module), + attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the url") ) changeset = reminder_module.changeset(struct, %{}) if polymorphic?(generator) do assert changeset.changes.channel.id + assert changeset.changes.attachment.id else assert map_size(changeset.changes) == 0 end @@ -1384,8 +1544,10 @@ defmodule PolymorphicEmbedTest do if polymorphic?(generator) do assert changeset.changes.channel.id == struct.channel.id + assert changeset.changes.attachment.id == struct.attachment.id else assert struct.channel.id + assert struct.attachment.id end end end @@ -1412,22 +1574,30 @@ defmodule PolymorphicEmbedTest do __type__: "twilio", api_key: "foo" } + }, + attachment: %{ + __type__: PolymorphicEmbed.Attachment.VideoAttachment, + url: "the url" } } ) if polymorphic?(generator) do assert changeset.changes.channel.id + assert changeset.changes.attachment.id else refute Map.has_key?(changeset.changes.channel, :id) + refute Map.has_key?(changeset.changes.attachment, :id) end struct = Repo.insert!(changeset) if polymorphic?(generator) do assert changeset.changes.channel.id == struct.channel.id + assert changeset.changes.attachment.id == struct.attachment.id else assert struct.channel.id + assert struct.attachment.id end end end @@ -1689,6 +1859,10 @@ defmodule PolymorphicEmbedTest do address: "a", valid: true, confirmed: true + }, + attachment: %{ + __type__: "PolymorphicEmbed.Attachment.VideoAttachment", + url: "the url" } } @@ -1697,6 +1871,7 @@ defmodule PolymorphicEmbedTest do |> struct() |> reminder_module.changeset(attrs) + # Field channel html = render_component( &liveview_form/1, @@ -1710,6 +1885,24 @@ defmodule PolymorphicEmbedTest do assert [input] = Floki.find(html, "#reminder_channel_number") assert Floki.attribute(input, "type") == ["text"] + + # Field attachment + html = + render_component( + &liveview_form/1, + %{changeset: changeset, field: :attachment, embedded_fields: :url} + ) + |> Floki.parse_fragment!() + + assert [input] = Floki.find(html, "#reminder_attachment___type__") + assert Floki.attribute(input, "type") == ["hidden"] + + assert Floki.attribute(input, "value") == [ + "Elixir.PolymorphicEmbed.Attachment.VideoAttachment" + ] + + assert [input] = Floki.find(html, "#reminder_attachment_url") + assert Floki.attribute(input, "type") == ["text"] end end @@ -1724,6 +1917,9 @@ defmodule PolymorphicEmbedTest do address: "a", valid: true, confirmed: true + }, + attachment: %{ + url: "the url" } } @@ -1748,6 +1944,29 @@ defmodule PolymorphicEmbedTest do assert contents == expected_contents + attachment_contents = + safe_inputs_for( + changeset, + :attachment, + PolymorphicEmbed.Attachment.VideoAttachment, + generator, + fn f -> + assert f.impl == Phoenix.HTML.FormData.Ecto.Changeset + assert f.errors == [] + text_input(f, :url) + end + ) + + expected_attachment_contents = + if(polymorphic?(generator), + do: + ~s(), + else: + ~s() + ) + + assert attachment_contents == expected_attachment_contents + contents = safe_inputs_for( Map.put(changeset, :action, :insert), @@ -2311,7 +2530,13 @@ defmodule PolymorphicEmbedTest do fn f -> inputs_for(f, field, fun) end :polymorphic -> - fn f -> polymorphic_embed_inputs_for(f, field, fun) end + fn f -> + if embed_type do + polymorphic_embed_inputs_for(f, field, embed_type, fun) + else + polymorphic_embed_inputs_for(f, field, fun) + end + end :polymorphic_with_type -> fn f -> polymorphic_embed_inputs_for(f, field, embed_type, fun) end @@ -2333,6 +2558,9 @@ defmodule PolymorphicEmbedTest do end defp liveview_form(assigns) do + assigns = Map.put_new(assigns, :embedded_fields, [:number]) + # embedded_fields = (assigns[:embedded_fields] || :number) |> List.wrap() + ~H""" <.form let={f} @@ -2340,7 +2568,9 @@ defmodule PolymorphicEmbedTest do > <%= for sms_form <- polymorphic_embed_inputs_for f, @field do %> <%= hidden_inputs_for(sms_form) %> - <%= text_input sms_form, :number %> + <%= for field <- List.wrap(@embedded_fields) do %> + <%= text_input sms_form, field %> + <% end %> <% end %> """ diff --git a/test/support/models/polymorphic/attachment/link_attachment.ex b/test/support/models/not_polymorphic/attachment/link_attachment.ex similarity index 100% rename from test/support/models/polymorphic/attachment/link_attachment.ex rename to test/support/models/not_polymorphic/attachment/link_attachment.ex diff --git a/test/support/models/polymorphic/attachment/video_attachment.ex b/test/support/models/not_polymorphic/attachment/video_attachment.ex similarity index 52% rename from test/support/models/polymorphic/attachment/video_attachment.ex rename to test/support/models/not_polymorphic/attachment/video_attachment.ex index 067c101..72f8c75 100644 --- a/test/support/models/polymorphic/attachment/video_attachment.ex +++ b/test/support/models/not_polymorphic/attachment/video_attachment.ex @@ -5,6 +5,7 @@ defmodule PolymorphicEmbed.Attachment.VideoAttachment do embedded_schema do field :url, :string field :thumbnail_url, :string + field :custom, :boolean, default: false end def changeset(struct, params) do @@ -12,4 +13,16 @@ defmodule PolymorphicEmbed.Attachment.VideoAttachment do |> cast(params, [:url, :thumbnail_url]) |> validate_required([:url]) end + + def custom_changeset(struct, attrs, _foo, _bar) do + struct + |> changeset(attrs) + |> cast(attrs, [:custom]) + end + + def custom_changeset2(struct, attrs) do + struct + |> changeset(attrs) + |> cast(attrs, [:custom]) + end end diff --git a/test/support/models/not_polymorphic/reminder.ex b/test/support/models/not_polymorphic/reminder.ex index 026b6da..adb4643 100644 --- a/test/support/models/not_polymorphic/reminder.ex +++ b/test/support/models/not_polymorphic/reminder.ex @@ -8,6 +8,7 @@ defmodule PolymorphicEmbed.Regular.Reminder do field(:text, :string) embeds_one(:channel, PolymorphicEmbed.Regular.Channel.SMS, on_replace: :update) + embeds_one(:attachment, PolymorphicEmbed.Attachment.VideoAttachment, on_replace: :update) embeds_many(:contexts, PolymorphicEmbed.Regular.Reminder.Context.Location, on_replace: :delete) @@ -19,6 +20,7 @@ defmodule PolymorphicEmbed.Regular.Reminder do |> cast(values, [:date, :text]) |> validate_required(:date) |> cast_embed(:channel) + |> cast_embed(:attachment) |> cast_embed(:contexts) end @@ -28,6 +30,9 @@ defmodule PolymorphicEmbed.Regular.Reminder do |> cast_embed(:channel, with: {PolymorphicEmbed.Regular.Channel.SMS, :custom_changeset, ["foo", "bar"]} ) + |> cast_embed(:attachment, + with: {PolymorphicEmbed.Attachment.VideoAttachment, :custom_changeset, ["foo", "bar"]} + ) |> validate_required(:date) end @@ -35,6 +40,9 @@ defmodule PolymorphicEmbed.Regular.Reminder do struct |> cast(values, [:date, :text]) |> cast_embed(:channel, with: &PolymorphicEmbed.Regular.Channel.SMS.custom_changeset2/2) + |> cast_embed(:attachment, + with: &PolymorphicEmbed.Attachment.VideoAttachment.custom_changeset2/2 + ) |> validate_required(:date) end end diff --git a/test/support/models/polymorphic/channel/email.ex b/test/support/models/polymorphic/channel/email.ex index b1d7ebc..e79d25f 100644 --- a/test/support/models/polymorphic/channel/email.ex +++ b/test/support/models/polymorphic/channel/email.ex @@ -13,7 +13,8 @@ defmodule PolymorphicEmbed.Channel.Email do def changeset(email, params) do email |> cast(params, ~w(address confirmed valid)a) - |> validate_required(:address) - |> validate_length(:address, min: 3) + + # |> validate_required(:address) + # |> validate_length(:address, min: 3) end end diff --git a/test/support/models/polymorphic/reminder.ex b/test/support/models/polymorphic/reminder.ex index e6ea706..8a3b825 100644 --- a/test/support/models/polymorphic/reminder.ex +++ b/test/support/models/polymorphic/reminder.ex @@ -63,6 +63,9 @@ defmodule PolymorphicEmbed.Reminder do email: {PolymorphicEmbed.Channel.Email, :custom_changeset, ["foo", "bar"]} ] ) + |> cast_polymorphic_embed(:attachment, + with: {PolymorphicEmbed.Attachment.VideoAttachment, :custom_changeset, ["foo", "bar"]} + ) |> validate_required(:date) end @@ -74,17 +77,20 @@ defmodule PolymorphicEmbed.Reminder do sms: &PolymorphicEmbed.Channel.SMS.custom_changeset2/2 ] ) - |> validate_required(:date) - end - - def custom_changeset3(struct, values) do - struct - |> cast(values, [:date, :text]) - |> cast_polymorphic_embed(:channel, - with: [ - sms: &PolymorphicEmbed.Channel.SMS.custom_changeset2/2 - ] + |> cast_polymorphic_embed(:attachment, + with: &PolymorphicEmbed.Attachment.VideoAttachment.custom_changeset2/2 ) |> validate_required(:date) end + + # def custom_changeset3(struct, values) do + # struct + # |> cast(values, [:date, :text]) + # |> cast_polymorphic_embed(:channel, + # with: [ + # sms: &PolymorphicEmbed.Channel.SMS.custom_changeset2/2 + # ] + # ) + # |> validate_required(:date) + # end end