Skip to content

Commit

Permalink
Add :on_type_not_found option
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuprog committed Oct 19, 2020
1 parent b900e8f commit 47cb2fb
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 25 deletions.
57 changes: 33 additions & 24 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,40 @@ defmodule PolymorphicEmbed do
}
end)

%{metadata: metadata}
%{
metadata: metadata,
on_type_not_found: Keyword.get(opts, :on_type_not_found, :changeset_error)
}
end

def cast_polymorphic_embed(changeset, field) do
%{metadata: metadata, on_type_not_found: on_type_not_found} =
get_options(changeset.data.__struct__, field)

data_for_field =
case Map.fetch!(changeset.data, field) do
nil ->
%{}

data ->
metadata = get_metadata(changeset.data.__struct__, field)
map_from_struct(data, :polymorphic_embed, metadata)
end

params_for_field = Map.get(changeset.params, to_string(field)) || %{}

params = Map.merge(data_for_field, params_for_field)

Ecto.Changeset.cast(changeset, %{to_string(field) => params}, [field])
if do_get_polymorphic_module(params, metadata, on_type_not_found) do
Ecto.Changeset.cast(changeset, %{to_string(field) => params}, [field])
else
Ecto.Changeset.add_error(changeset, field, "is invalid")
end
end

@impl true
def cast(attrs, %{metadata: metadata}) do
# convert keys into string (in case they would be atoms)
for({key, val} <- attrs, into: %{}, do: {to_string(key), val})
def cast(attrs, %{metadata: metadata, on_type_not_found: on_type_not_found}) do
# get the right module based on the __type__ key or infer from the keys
|> do_get_polymorphic_module(metadata)
do_get_polymorphic_module(attrs, metadata, on_type_not_found)
|> cast_to_changeset(attrs)
|> case do
%{valid?: true} = changeset ->
Expand Down Expand Up @@ -92,10 +99,10 @@ defmodule PolymorphicEmbed do
end

@impl true
def load(data, _loader, %{metadata: metadata}) do
def load(data, _loader, %{metadata: metadata, on_type_not_found: on_type_not_found}) do
struct =
data
|> do_get_polymorphic_module(metadata)
|> do_get_polymorphic_module(metadata, on_type_not_found)
|> cast_to_changeset(data)
|> Ecto.Changeset.apply_changes()

Expand Down Expand Up @@ -155,19 +162,19 @@ defmodule PolymorphicEmbed do
end

def get_polymorphic_module(schema, field, type_or_data) do
metadata = get_metadata(schema, field)
do_get_polymorphic_module(type_or_data, metadata)
%{metadata: metadata, on_type_not_found: on_type_not_found} = get_options(schema, field)
do_get_polymorphic_module(type_or_data, metadata, on_type_not_found)
end

defp do_get_polymorphic_module(%{"__type__" => type}, metadata) do
type = to_string(type)
defp do_get_polymorphic_module(%{:__type__ => type}, metadata, on_type_not_found), do:
do_get_polymorphic_module(type, metadata, on_type_not_found)

metadata
|> Enum.find(&(type == &1.type))
|> Map.fetch!(:module)
end
defp do_get_polymorphic_module(%{"__type__" => type}, metadata, on_type_not_found), do:
do_get_polymorphic_module(type, metadata, on_type_not_found)

defp do_get_polymorphic_module(%{} = attrs, metadata) do
defp do_get_polymorphic_module(%{} = attrs, metadata, on_type_not_found) do
# convert keys into string (in case they would be atoms)
attrs = for({key, val} <- attrs, into: %{}, do: {to_string(key), val})
# check if one list is contained in another
# Enum.count(contained -- container) == 0
# contained -- container == []
Expand All @@ -176,14 +183,16 @@ defmodule PolymorphicEmbed do
|> Enum.find(&([] == &1.identify_by_fields -- Map.keys(attrs)))
|> case do
nil ->
raise "could not infer polymorphic embed from data #{inspect(attrs)}"
if on_type_not_found == :raise do
raise "could not infer polymorphic embed from data #{inspect(attrs)}"
end

entry ->
Map.fetch!(entry, :module)
end
end

defp do_get_polymorphic_module(type, metadata) do
defp do_get_polymorphic_module(type, metadata, _) do
type = to_string(type)

metadata
Expand All @@ -192,7 +201,7 @@ defmodule PolymorphicEmbed do
end

def get_polymorphic_type(schema, field, module_or_struct) do
metadata = get_metadata(schema, field)
%{metadata: metadata} = get_options(schema, field)
do_get_polymorphic_type(module_or_struct, metadata)
end

Expand All @@ -206,15 +215,15 @@ defmodule PolymorphicEmbed do
|> String.to_atom()
end

defp get_metadata(schema, field) do
defp get_options(schema, field) do
try do
schema.__schema__(:type, field)
rescue
_ in UndefinedFunctionError ->
raise ArgumentError, "#{inspect(schema)} is not an Ecto schema"
else
{:parameterized, PolymorphicEmbed, %{metadata: metadata}} -> metadata
{_, {:parameterized, PolymorphicEmbed, %{metadata: metadata}}} -> metadata
{:parameterized, PolymorphicEmbed, options} -> options
{_, {:parameterized, PolymorphicEmbed, options}} -> options
nil -> raise ArgumentError, "#{field} is not an Ecto.Enum field"
end
end
Expand Down
73 changes: 73 additions & 0 deletions test/polymorphic_embed_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,79 @@ defmodule PolymorphicEmbedTest do
assert reminder.channel.result.success
end

test "missing __type__ leads to changeset error" do
sms_reminder_attrs = %{
date: ~U[2020-05-28 02:57:19Z],
text: "This is an SMS reminder",
channel: %{
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 =
%Reminder{}
|> Reminder.changeset(sms_reminder_attrs)
|> Repo.insert()

assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result
end

test "missing __type__ leads to raising error" do
sms_reminder_attrs = %{
date: ~U[2020-05-28 02:57:19Z],
text: "This is an SMS reminder",
channel: %{
__type__: "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: %{
api_key: "foo"
}
}
}

assert_raise RuntimeError, ~r"could not infer polymorphic embed from data", fn ->
%Reminder{}
|> Reminder.changeset(sms_reminder_attrs)
|> Repo.insert()
end
end

test "inputs_for/4" do
attrs = %{
date: ~U[2020-05-28 02:57:19Z],
Expand Down
3 changes: 2 additions & 1 deletion test/support/models/channel/sms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule PolymorphicEmbed.Channel.SMS do
types: [
twilio: PolymorphicEmbed.Channel.TwilioSMSProvider,
test: PolymorphicEmbed.Channel.AcmeSMSProvider
]
],
on_type_not_found: :raise
)

embeds_one(:result, PolymorphicEmbed.Channel.SMSResult)
Expand Down

0 comments on commit 47cb2fb

Please sign in to comment.