Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ polymorphic_embeds_many :contexts,
By default, a changeset error "is invalid" is added.
* `:on_replace` – mandatory option that can only be set to `:update` for a single embed and `:delete` for a list of
embeds (we force a value as the default value of this option for `embeds_one` and `embeds_many` is `:raise`).
* `:retain_unlisted_types_on_load`: allow unconfigured types to be loaded without raising an error. Useful for handling deprecated structs still present in the database.
* `:retain_unlisted_types_on_load`: allow unconfigured types to be loaded without raising an error. Useful for handling deprecated structs still present in the database. Pass `:all` to retain every type the field does not know, e.g. when several application versions share a database during a rolling deploy, or a type was removed without migrating its rows; the data then loads as the raw map. A raw map cannot be dumped back, so treat such records as read-only.
* `:nilify_unlisted_types_on_load`: same as `:retain_unlisted_types_on_load`, but nilify the struct on load.

### Displaying form inputs and errors in Phoenix templates
Expand Down
7 changes: 5 additions & 2 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -491,15 +491,18 @@ defmodule PolymorphicEmbed do
case get_polymorphic_module_from_map(data, type_field_name, types_metadata) do
nil ->
retain_type_list =
Map.fetch!(field_opts, :retain_unlisted_types_on_load) |> Enum.map(&to_string(&1))
case Map.fetch!(field_opts, :retain_unlisted_types_on_load) do
:all -> :all
types -> Enum.map(types, &to_string(&1))
end

nilify_type_list =
Map.fetch!(field_opts, :nilify_unlisted_types_on_load) |> Enum.map(&to_string(&1))

type = Map.get(data, type_field_name |> to_string)

cond do
type in retain_type_list ->
retain_type_list == :all or type in retain_type_list ->
{:ok, data}

type in nilify_type_list ->
Expand Down
5 changes: 3 additions & 2 deletions lib/polymorphic_embed/options_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ defmodule PolymorphicEmbed.OptionsValidator do
retain_unlisted_types = Keyword.fetch!(options, :retain_unlisted_types_on_load)
nilify_unlisted_types = Keyword.fetch!(options, :nilify_unlisted_types_on_load)

unless is_list(retain_unlisted_types) and Enum.all?(retain_unlisted_types, &is_atom/1) do
raise "`:retain_unlisted_types_on_load` option must be a list of types as atoms."
unless retain_unlisted_types == :all or
(is_list(retain_unlisted_types) and Enum.all?(retain_unlisted_types, &is_atom/1)) do
raise "`:retain_unlisted_types_on_load` option must be :all or a list of types as atoms."
end

unless is_list(nilify_unlisted_types) and Enum.all?(nilify_unlisted_types, &is_atom/1) do
Expand Down
53 changes: 53 additions & 0 deletions test/polymorphic_embed_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,59 @@ defmodule PolymorphicEmbedTest do
|> Repo.one()
end

describe "retain_unlisted_types_on_load: :all" do
defp retain_all_params(retain) do
PolymorphicEmbed.init(
field: :channel,
schema: PolymorphicEmbed.Reminder,
array?: false,
default: nil,
types: [sms: PolymorphicEmbed.Channel.SMS],
on_replace: :update,
type_field_name: :my_type_field,
retain_unlisted_types_on_load: retain
)
end

test "loads any type the field does not know as the raw map" do
params = retain_all_params(:all)

for type <- ["some_deprecated_type", "a_type_from_a_newer_release"] do
data = %{"my_type_field" => type, "number" => "02/807.05.53"}
assert {:ok, ^data} = PolymorphicEmbed.load(data, &Ecto.Type.load/2, params)
end
end

test "still loads a known type as its struct" do
params = retain_all_params(:all)
data = %{"my_type_field" => "sms", "number" => "02/807.05.53", "country_code" => 1}

assert {:ok, %PolymorphicEmbed.Channel.SMS{number: "02/807.05.53"}} =
PolymorphicEmbed.load(data, &Ecto.Type.load/2, params)
end

test "a list keeps retaining only the listed types" do
params = retain_all_params([:some_deprecated_type])

assert {:ok, _} =
PolymorphicEmbed.load(
%{"my_type_field" => "some_deprecated_type"},
&Ecto.Type.load/2,
params
)

assert_raise RuntimeError, fn ->
PolymorphicEmbed.load(%{"my_type_field" => "unlisted"}, &Ecto.Type.load/2, params)
end
end

test "rejects anything other than :all or a list of atoms" do
assert_raise RuntimeError, ~r/must be :all or a list of types as atoms/, fn ->
retain_all_params(:some)
end
end
end

test "changing type" do
generator = :polymorphic
reminder_module = get_module(Reminder, generator)
Expand Down