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:
diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex
index 16b98a1..079e672 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),
@@ -65,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
@@ -84,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 || %{})
@@ -339,12 +354,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 +383,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 +408,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
@@ -412,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})
diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs
index b27e5ab..7c2a1b4 100644
--- a/test/polymorphic_embed_test.exs
+++ b/test/polymorphic_embed_test.exs
@@ -23,13 +23,18 @@ defmodule PolymorphicEmbedTest do
defp get_module(name, :not_polymorphic),
do: Module.concat([PolymorphicEmbed.Regular, name])
- test "receive embed as map of values" do
+ 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}",
+ 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",
@@ -70,6 +75,8 @@ defmodule PolymorphicEmbedTest do
assert get_module(Channel.SMS, generator) == reminder.channel.__struct__
+ assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__
+
assert get_module(Channel.TwilioSMSProvider, generator) ==
reminder.channel.provider.__struct__
@@ -119,6 +126,66 @@ defmodule PolymorphicEmbedTest do
end
end
+ 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"
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(reminder_attrs)
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ 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
+
+ # 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()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ 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
+
test "validations before casting polymorphic embed still work" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -158,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"
}
}
@@ -176,6 +247,11 @@ defmodule PolymorphicEmbedTest do
action: :insert,
valid?: false,
errors: channel_errors
+ },
+ attachment: %{
+ action: :insert,
+ valid?: false,
+ errors: attachment_errors
}
}
}} = insert_result
@@ -187,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
@@ -199,6 +279,10 @@ defmodule PolymorphicEmbedTest do
channel: %{
my_type_field: "sms"
},
+ attachment: %{
+ __type__: PolymorphicEmbed.Attachment.VideoAttachment,
+ thumbnail_url: "thumbnail url"
+ },
contexts: [
%{
__type__: "location",
@@ -231,6 +315,11 @@ defmodule PolymorphicEmbedTest do
valid?: false,
errors: channel_errors
},
+ attachment: %{
+ action: :insert,
+ valid?: false,
+ errors: attachment_errors
+ },
contexts: [
%{
action: :insert,
@@ -259,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
@@ -279,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"]
} =
@@ -305,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",
@@ -414,7 +513,12 @@ defmodule PolymorphicEmbedTest do
result: struct(sms_result_module, success: true)
)
]
- )
+ ),
+ attachment:
+ struct(PolymorphicEmbed.Attachment.VideoAttachment, %{
+ url: "the url",
+ thumbnail_url: "the thumbnail url"
+ })
)
reminder
@@ -427,10 +531,11 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert sms_module == reminder.channel.__struct__
+ assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__
changeset =
reminder
- |> reminder_module.changeset(%{channel: %{provider: nil}})
+ |> reminder_module.changeset(%{channel: %{provider: nil}, attachment: %{url: nil}})
assert %Ecto.Changeset{
action: nil,
@@ -441,6 +546,11 @@ defmodule PolymorphicEmbedTest do
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
@@ -459,15 +569,54 @@ defmodule PolymorphicEmbedTest do
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
@@ -522,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)
@@ -531,6 +703,9 @@ defmodule PolymorphicEmbedTest do
text: "This is an Email reminder",
channel: %{
my_type_field: "unknown type"
+ },
+ attachment: %{
+ __type___: "UnknownType"
}
}
@@ -539,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
@@ -574,26 +750,36 @@ 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
+
+ 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
@@ -605,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()
@@ -617,6 +804,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert is_nil(reminder.channel)
+ assert is_nil(reminder.attachment)
end
end
@@ -627,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 =
@@ -643,6 +832,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert is_nil(reminder.channel)
+ assert is_nil(reminder.attachment)
end
end
@@ -683,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 = %{
@@ -695,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
}
}
@@ -714,6 +910,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert sms_module == reminder.channel.__struct__
+ assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__
end
end
@@ -732,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
}
}
@@ -742,6 +944,7 @@ defmodule PolymorphicEmbedTest do
assert {:ok, reminder} = insert_result
assert reminder.channel.custom
+ assert reminder.attachment.custom
%reminder_module{} = reminder
@@ -751,6 +954,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert sms_module == reminder.channel.__struct__
+ assert PolymorphicEmbed.Attachment.VideoAttachment == reminder.attachment.__struct__
end
end
@@ -795,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 =
@@ -804,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()
@@ -817,6 +1023,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
assert is_nil(reminder.channel)
+ assert is_nil(reminder.attachment)
end
end
@@ -835,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)
@@ -848,6 +1059,7 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
refute is_nil(reminder.channel)
+ refute is_nil(reminder.attachment)
end
end
@@ -882,7 +1094,8 @@ defmodule PolymorphicEmbedTest do
result: struct(sms_result_module, success: true)
)
]
- )
+ ),
+ attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the url")
)
reminder =
@@ -895,6 +1108,9 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(%{
channel: %{
number: "54"
+ },
+ attachment: %{
+ thumbnail_url: "the thumbnail url"
}
})
@@ -906,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
@@ -940,7 +1158,8 @@ defmodule PolymorphicEmbedTest do
result: struct(sms_result_module, success: true)
)
]
- )
+ ),
+ attachment: struct(PolymorphicEmbed.Attachment.VideoAttachment, url: "the original_url")
)
reminder =
@@ -954,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"
}
})
@@ -965,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
@@ -997,6 +1223,9 @@ defmodule PolymorphicEmbedTest do
__type__: "twilio",
api_key: "foo"
}
+ },
+ attachment: %{
+ url: "the url"
}
}
@@ -1005,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
@@ -1103,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()
@@ -1121,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)
@@ -1270,12 +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
@@ -1284,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
@@ -1297,7 +1559,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(
@@ -1311,21 +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
@@ -1342,7 +1614,8 @@ defmodule PolymorphicEmbedTest do
contexts: [
struct(location_module),
struct(location_module)
- ])
+ ]
+ )
changeset = reminder_module.changeset(struct, %{})
@@ -1372,7 +1645,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 +1656,8 @@ defmodule PolymorphicEmbedTest do
%{__type__: "location", address: "A"},
%{__type__: "location", address: "B"}
]
- })
+ }
+ )
if polymorphic?(generator) do
assert Enum.at(changeset.changes.contexts, 0).id
@@ -1584,6 +1859,10 @@ defmodule PolymorphicEmbedTest do
address: "a",
valid: true,
confirmed: true
+ },
+ attachment: %{
+ __type__: "PolymorphicEmbed.Attachment.VideoAttachment",
+ url: "the url"
}
}
@@ -1592,6 +1871,7 @@ defmodule PolymorphicEmbedTest do
|> struct()
|> reminder_module.changeset(attrs)
+ # Field channel
html =
render_component(
&liveview_form/1,
@@ -1605,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
@@ -1619,6 +1917,9 @@ defmodule PolymorphicEmbedTest do
address: "a",
valid: true,
confirmed: true
+ },
+ attachment: %{
+ url: "the url"
}
}
@@ -1643,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),
@@ -2034,6 +2358,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
@@ -2198,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
@@ -2220,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}
@@ -2227,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/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/not_polymorphic/attachment/link_attachment.ex b/test/support/models/not_polymorphic/attachment/link_attachment.ex
new file mode 100644
index 0000000..5406665
--- /dev/null
+++ b/test/support/models/not_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/not_polymorphic/attachment/video_attachment.ex b/test/support/models/not_polymorphic/attachment/video_attachment.ex
new file mode 100644
index 0000000..72f8c75
--- /dev/null
+++ b/test/support/models/not_polymorphic/attachment/video_attachment.ex
@@ -0,0 +1,28 @@
+defmodule PolymorphicEmbed.Attachment.VideoAttachment do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ embedded_schema do
+ field :url, :string
+ field :thumbnail_url, :string
+ field :custom, :boolean, default: false
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> 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 520f73b..8a3b825 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
@@ -60,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
@@ -71,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