diff --git a/README.md b/README.md index 28c7bfc..4c5c229 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex index 47e8ad9..3d0bdb8 100644 --- a/lib/polymorphic_embed.ex +++ b/lib/polymorphic_embed.ex @@ -491,7 +491,10 @@ 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)) @@ -499,7 +502,7 @@ defmodule PolymorphicEmbed do 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 -> diff --git a/lib/polymorphic_embed/options_validator.ex b/lib/polymorphic_embed/options_validator.ex index 8ef81db..19ef824 100644 --- a/lib/polymorphic_embed/options_validator.ex +++ b/lib/polymorphic_embed/options_validator.ex @@ -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 diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs index ba50279..9de6191 100644 --- a/test/polymorphic_embed_test.exs +++ b/test/polymorphic_embed_test.exs @@ -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)