+ Number
<%= text_input sms_form, :number %>
<%= error_tag sms_form, :number %>
@@ -208,54 +225,46 @@ They both render a hidden input for the `"__type__"` field.
### Displaying form inputs and errors in LiveView
-You may use `polymorphic_embed_inputs_for/2` when working with LiveView.
+You may use `PolymorphicEmbed.HTML.Component.polymorphic_embed_inputs_for/1` when working with LiveView, which functions similarly to [`Phoenix.Component.inputs_for/1`](Phoenix.Component.inputs_for).
```elixir
<.form
- let={f}
+ :let={f}
for={@changeset}
id="reminder-form"
phx-change="validate"
phx-submit="save"
>
- <%= for channel_form <- polymorphic_embed_inputs_for f, :channel do %>
- <%= hidden_inputs_for(channel_form) %>
-
- <%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
- <% :sms -> %>
- <%= label channel_form, :number %>
- <%= text_input channel_form, :number %>
-
- <% :email -> %>
- <%= label channel_form, :email %>
- <%= text_input channel_form, :email %>
- <% end %>
+ <.polymorphic_embed_inputs_for field={f[:channel]} :let={channel_form}>
+ <%= case source_module(channel_form) do %>
+ <% SMS -> %>
+ <.input field={channel_form[:number]} label="Number" />
+
+ <% Email -> %>
+ <.input field={channel_form[:address]} label="Email Address" />
+ <% end %>
+
```
-Using this function, you have to render the necessary hidden inputs manually as shown above.
-
-### Get the type of a polymorphic embed
+### Get the type and module of a polymorphic embed
Sometimes you need to serialize the polymorphic embed and, once in the front-end, need to distinguish them.
-`get_polymorphic_type/3` returns the type of the polymorphic embed:
+`PolymorphicEmbed.get_polymorphic_type/3` returns the type of the polymorphic embed:
```elixir
PolymorphicEmbed.get_polymorphic_type(Reminder, :channel, SMS) == :sms
```
-### `traverse_errors/2`
+To get the module for a specific type, use:
-The function `Ecto.changeset.traverse_errors/2` won't include the errors of polymorphic embeds. You may instead use `PolymorphicEmbed.traverse_errors/2` when working with polymorphic embeds.
+```elixir
+PolymorphicEmbed.get_polymorphic_module(Reminder, :channel, :sms) == SMS
+```
-## Features
+### `traverse_errors/2`
-* Detect which types to use for the data being `cast`-ed, based on fields present in the data (no need for a *type* field in the data)
-* Run changeset validations when a `changeset/2` function is present (when absent, the library will introspect the fields to cast)
-* Support for nested polymorphic embeds
-* Support for nested `embeds_one`/`embeds_many` embeds
-* Display form inputs for polymorphic embeds in Phoenix templates
-* Tests to ensure code quality
+The function `Ecto.changeset.traverse_errors/2` won't include the errors of polymorphic embeds. You may instead use `PolymorphicEmbed.traverse_errors/2` when working with polymorphic embeds.
## Installation
@@ -264,7 +273,7 @@ Add `polymorphic_embed` for Elixir as a dependency in your `mix.exs` file:
```elixir
def deps do
[
- {:polymorphic_embed, "~> 3.0.5"}
+ {:polymorphic_embed, "~> 5.0"}
]
end
```
diff --git a/config/test.exs b/config/test.exs
index 8489bba..a4d6fd7 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,8 +1,6 @@
import Config
-config :logger, level: :warn
-
-config :phoenix, :json_library, Jason
+config :logger, level: :warning
config :polymorphic_embed,
ecto_repos: [PolymorphicEmbed.Repo]
diff --git a/lib/polymorphic_embed.ex b/lib/polymorphic_embed.ex
index d6ebfa4..9952a27 100644
--- a/lib/polymorphic_embed.ex
+++ b/lib/polymorphic_embed.ex
@@ -1,25 +1,83 @@
defmodule PolymorphicEmbed do
use Ecto.ParameterizedType
+ @type t() :: any()
+
+ require PolymorphicEmbed.OptionsValidator
+
+ alias Ecto.Changeset
+ alias PolymorphicEmbed.OptionsValidator
+
defmacro polymorphic_embeds_one(field_name, opts) do
+ opts =
+ opts
+ |> Keyword.put_new(:array?, false)
+ |> Keyword.put_new(:default, nil)
+ |> Keyword.update!(:types, &expand_alias(&1, __CALLER__))
+
quote do
field(unquote(field_name), PolymorphicEmbed, unquote(opts))
end
end
defmacro polymorphic_embeds_many(field_name, opts) do
- opts = Keyword.merge(opts, default: [])
+ opts =
+ opts
+ |> Keyword.put_new(:array?, true)
+ |> Keyword.put_new(:default, [])
+ |> Keyword.update!(:types, &expand_alias(&1, __CALLER__))
quote do
field(unquote(field_name), {:array, PolymorphicEmbed}, unquote(opts))
end
end
+ # Expand module aliases to avoid creating compile-time dependencies between the
+ # parent schema that uses `polymorphic_embeds_one` or `polymorphic_embeds_many`
+ # and the embedded schemas.
+ defp expand_alias(types, env) when is_list(types) do
+ Enum.map(types, fn
+ {type_name, type_opts} when is_list(type_opts) ->
+ {type_name, Keyword.update!(type_opts, :module, &do_expand_alias(&1, env))}
+
+ {type_name, module} ->
+ {type_name, do_expand_alias(module, env)}
+ end)
+ end
+
+ # If it's not a list or a map, it means it's being defined by a reference of some kind,
+ # possibly via module attribute like:
+ # @types [twilio: PolymorphicEmbed.Channel.TwilioSMSProvider]
+ # # ...
+ # polymorphic_embeds_one(:fallback_provider, types: @types)
+ # which means we can't expand aliases
+ defp expand_alias(types, _env) do
+ types
+ end
+
+ defp do_expand_alias({:__aliases__, _, _} = ast, env) do
+ Macro.expand(ast, %{env | function: {:__schema__, 2}})
+ end
+
+ defp do_expand_alias(ast, _env) do
+ ast
+ end
+
@impl true
def type(_params), do: :map
@impl true
def init(opts) do
+ opts = Keyword.put_new(opts, :on_replace, nil)
+ # opts = Keyword.put_new(opts, :type_field_name, :__type__)
+ # TODO remove in v6
+ opts = Keyword.put_new(opts, :type_field_name, Keyword.get(opts, :type_field, :__type__))
+ opts = Keyword.put_new(opts, :on_type_not_found, :changeset_error)
+ opts = Keyword.put_new(opts, :nilify_unlisted_types_on_load, [])
+ opts = Keyword.put_new(opts, :retain_unlisted_types_on_load, [])
+
+ OptionsValidator.validate!(opts)
+
if Keyword.get(opts, :on_replace) not in [:update, :delete] do
raise(
"`:on_replace` option for polymorphic embed must be set to `:update` (single embed) or `:delete` (list of embeds)"
@@ -42,53 +100,56 @@ defmodule PolymorphicEmbed do
type: type_name,
module: Keyword.fetch!(type_opts, :module),
identify_by_fields:
- type_opts |> Keyword.get(:identify_by_fields, []) |> Enum.map(&to_string/1)
+ Keyword.get(type_opts, :identify_by_fields, []) |> Enum.map(&to_string/1)
}
end)
%{
- default: Keyword.get(opts, :default, nil),
+ array?: Keyword.fetch!(opts, :array?),
+ default: Keyword.fetch!(opts, :default),
+ use_parent_field_for_type: Keyword.get(opts, :use_parent_field_for_type),
on_replace: Keyword.fetch!(opts, :on_replace),
- on_type_not_found: Keyword.get(opts, :on_type_not_found, :changeset_error),
- type_field: Keyword.get(opts, :type_field, :__type__) |> to_string(),
+ on_type_not_found: Keyword.fetch!(opts, :on_type_not_found),
+ nilify_unlisted_types_on_load: Keyword.fetch!(opts, :nilify_unlisted_types_on_load),
+ retain_unlisted_types_on_load: Keyword.fetch!(opts, :retain_unlisted_types_on_load),
+ type_field_name: Keyword.fetch!(opts, :type_field_name),
types_metadata: types_metadata
}
end
- def cast_polymorphic_embed(changeset, field, cast_options \\ [])
+ def cast_polymorphic_embed(changeset, field, cast_opts \\ [])
- def cast_polymorphic_embed(%Ecto.Changeset{} = changeset, field, cast_options) do
- field_options = get_field_options(changeset.data.__struct__, field)
+ # credo:disable-for-next-line
+ def cast_polymorphic_embed(%Ecto.Changeset{} = changeset, field, cast_opts) do
+ field_opts = get_field_opts(changeset.data.__struct__, field)
- raise_if_invalid_options(field, field_options)
+ raise_if_invalid_options(field, field_opts)
- %{array?: array?, types_metadata: types_metadata} = field_options
+ %{array?: array?, types_metadata: types_metadata} = field_opts
- required = Keyword.get(cast_options, :required, false)
- with = Keyword.get(cast_options, :with, nil)
+ required = Keyword.get(cast_opts, :required, false)
+ with = Keyword.get(cast_opts, :with, nil)
- changeset_fun = fn
- struct, params when is_nil(with) ->
- struct.__struct__.changeset(struct, params)
+ changeset_fun = &changeset_fun(&1, &2, with, types_metadata)
- struct, params when is_list(with) ->
- type = do_get_polymorphic_type(struct, types_metadata)
+ # used for sort_param and drop_param support for many embeds
+ sort = param_value_for_cast_opt(:sort_param, cast_opts, changeset.params)
+ drop = param_value_for_cast_opt(:drop_param, cast_opts, changeset.params)
- case Keyword.get(with, type) do
- {module, function_name, args} ->
- apply(module, function_name, [struct, params | args])
+ case Map.fetch(changeset.params || %{}, to_string(field)) do
+ # consider sort and drop params even if the assoc param was not given, as in Ecto
+ :error when (array? and is_list(sort)) or is_list(drop) ->
+ create_sort_default = fn -> sort_create(Enum.into(cast_opts, %{}), field_opts) end
+ params_for_field = apply_sort_drop(%{}, sort, drop, create_sort_default)
- nil ->
- struct.__struct__.changeset(struct, params)
+ cast_polymorphic_embeds_many(
+ changeset,
+ field,
+ changeset_fun,
+ params_for_field,
+ field_opts
+ )
- fun ->
- apply(fun, [struct, params])
- end
- end
-
- (changeset.params || %{})
- |> Map.fetch(to_string(field))
- |> case do
:error when required ->
if data_for_field = Map.fetch!(changeset.data, field) do
data_for_field = autogenerate_id(data_for_field, changeset.action)
@@ -114,26 +175,32 @@ defmodule PolymorphicEmbed do
{:ok, map} when map == %{} and not array? ->
changeset
- {:ok, params_for_field} ->
- cond do
- array? and is_list(params_for_field) ->
- cast_polymorphic_embeds_many(
- changeset,
- field,
- changeset_fun,
- params_for_field,
- field_options
- )
-
- not array? and is_map(params_for_field) ->
- cast_polymorphic_embeds_one(
- changeset,
- field,
- changeset_fun,
- params_for_field,
- field_options
- )
- end
+ {:ok, params_for_field}
+ when array? and (is_map(params_for_field) or is_list(params_for_field)) ->
+ create_sort_default = fn -> sort_create(Enum.into(cast_opts, %{}), field_opts) end
+ params_for_field = apply_sort_drop(params_for_field, sort, drop, create_sort_default)
+
+ cast_polymorphic_embeds_many(
+ changeset,
+ field,
+ changeset_fun,
+ params_for_field,
+ field_opts
+ )
+
+ {:ok, params_for_field} when is_map(params_for_field) and not array? ->
+ cast_polymorphic_embeds_one(
+ changeset,
+ field,
+ changeset_fun,
+ params_for_field,
+ field_opts
+ )
+
+ # Params of the wrong shape (a scalar, a list for a single embed, a
+ # scalar for a list of embeds) are a client error, never a crash.
+ {:ok, _params_for_field} ->
+ Ecto.Changeset.add_error(changeset, field, "is invalid")
end
end
@@ -141,38 +208,111 @@ defmodule PolymorphicEmbed do
raise "cast_polymorphic_embed/3 only accepts a changeset as first argument"
end
- defp cast_polymorphic_embeds_one(changeset, field, changeset_fun, params, field_options) do
- %{
- types_metadata: types_metadata,
- on_type_not_found: on_type_not_found,
- type_field: type_field
- } = field_options
+ defp sort_create(%{sort_param: _} = cast_opts, field_opts) do
+ default_type = Map.get(cast_opts, :default_type_on_sort_create)
+ type_field_name = Map.fetch!(field_opts, :type_field_name)
+ types_metadata = Map.fetch!(field_opts, :types_metadata)
- data_for_field = Map.fetch!(changeset.data, field)
+ case default_type do
+ nil ->
+ # If type is not provided, use the first type from types_metadata
+ [first_type_metadata | _] = types_metadata
+ first_type = first_type_metadata.type
+ %{type_field_name => first_type}
- # We support partial update of the embed. If the type cannot be inferred from the parameters, or if the found type
- # hasn't changed, pass the data to the changeset.
- action_and_struct =
- case do_get_polymorphic_module_from_map(params, type_field, types_metadata) do
- nil ->
- if data_for_field do
- {:update, data_for_field}
- else
- :type_not_found
+ _ ->
+ default_type =
+ case default_type do
+ fun when is_function(fun, 0) -> fun.()
+ _ -> default_type
end
- module when is_nil(data_for_field) ->
- {:insert, struct(module)}
+ # If type is provided, ensure it exists in types_metadata
+ unless Enum.find(types_metadata, &(&1.type === default_type)) do
+ raise "incorrect type atom #{inspect(default_type)}"
+ end
- module ->
- if data_for_field.__struct__ != module do
- {:insert, struct(module)}
- else
- {:update, data_for_field}
- end
+ %{type_field_name => default_type}
+ end
+ end
+
+ defp sort_create(_cast_opts, _field_opts), do: nil
+
+ defp apply_sort_drop(value, sort, drop, create_sort_default) when is_map(value) do
+ drop = if is_list(drop), do: drop, else: []
+
+ {sorted, pending} =
+ if is_list(sort) do
+ Enum.map_reduce(sort -- drop, value, &Map.pop(&2, &1, create_sort_default.()))
+ else
+ {[], value}
end
- case action_and_struct do
+ sorted ++
+ (pending
+ |> Map.drop(drop)
+ |> Enum.map(&key_as_int/1)
+ |> Enum.sort()
+ |> Enum.map(&elem(&1, 1)))
+ end
+
+ defp apply_sort_drop(value, _sort, _drop, _default) do
+ value
+ end
+
+ defp param_value_for_cast_opt(opt, opts, params) do
+ if key = opts[opt] do
+ Map.get(params, Atom.to_string(key), nil)
+ end
+ end
+
+ defp key_as_int({key, val}) when is_binary(key) do
+ case Integer.parse(key) do
+ {key, ""} -> {key, val}
+ _ -> {key, val}
+ end
+ end
+
+ # from Ecto
+ # We check for the byte size to avoid creating unnecessary large integers
+ # which would never map to a database key (u64 is 20 digits only).
+ defp key_as_int({key, val}) when is_binary(key) and byte_size(key) < 32 do
+ case Integer.parse(key) do
+ {key, ""} -> {key, val}
+ _ -> {key, val}
+ end
+ end
+
+ defp key_as_int(key_val), do: key_val
+
+ defp changeset_fun(struct, params, with, types_metadata) when is_list(with) do
+ type = do_get_polymorphic_type(struct, types_metadata)
+
+ case Keyword.get(with, type) do
+ {module, function_name, args} ->
+ apply(module, function_name, [struct, params | args])
+
+ nil ->
+ struct.__struct__.changeset(struct, params)
+
+ fun ->
+ apply(fun, [struct, params])
+ end
+ end
+
+ defp changeset_fun(struct, params, nil, _) do
+ struct.__struct__.changeset(struct, params)
+ end
+
+ defp cast_polymorphic_embeds_one(changeset, field, changeset_fun, params, field_opts) do
+ %{on_type_not_found: on_type_not_found} = field_opts
+
+ data_for_field = Map.fetch!(changeset.data, field)
+
+ # We support partial update of the embed. If the type cannot be inferred from the parameters, or if the found type
+ # hasn't changed, pass the data to the changeset.
+
+ case action_and_struct(changeset, params, field_opts, data_for_field) do
:type_not_found when on_type_not_found == :raise ->
raise_cannot_infer_type_from_data(params)
@@ -200,39 +340,110 @@ defmodule PolymorphicEmbed do
end
end
- defp cast_polymorphic_embeds_many(changeset, field, changeset_fun, list_params, field_options) do
+ defp action_and_struct(changeset, params, field_opts, data_for_field) do
+ %{
+ types_metadata: types_metadata,
+ type_field_name: type_field_name,
+ use_parent_field_for_type: parent_field_for_type
+ } = field_opts
+
+ if parent_field_for_type != nil do
+ type_from_map = Attrs.get(params, type_field_name)
+ type_from_parent_field = Ecto.Changeset.fetch_field!(changeset, parent_field_for_type)
+
+ cond do
+ is_nil(type_from_parent_field) ->
+ :type_not_found
+
+ is_nil(type_from_map) ->
+ module = get_polymorphic_module_for_type(type_from_parent_field, types_metadata)
+
+ if is_nil(data_for_field) or data_for_field.__struct__ != module do
+ {:insert, struct(module)}
+ else
+ {:update, data_for_field}
+ end
+
+ to_string(type_from_parent_field) != to_string(type_from_map) ->
+ raise "type specified in the parent field \"#{type_from_parent_field}\" does not match the type in the embedded map \"#{type_from_map}\""
+
+ true ->
+ # type_from_parent_field and type_from_map match
+ module = get_polymorphic_module_for_type(type_from_parent_field, types_metadata)
+
+ if is_nil(data_for_field) or data_for_field.__struct__ != module do
+ {:insert, struct(module)}
+ else
+ {:update, data_for_field}
+ end
+ end
+ else
+ case get_polymorphic_module_from_map(params, type_field_name, types_metadata) do
+ nil ->
+ if data_for_field do
+ {:update, data_for_field}
+ else
+ :type_not_found
+ end
+
+ module when is_nil(data_for_field) ->
+ {:insert, struct(module)}
+
+ module ->
+ if data_for_field.__struct__ != module do
+ {:insert, struct(module)}
+ else
+ {:update, data_for_field}
+ end
+ end
+ end
+ end
+
+ defp cast_polymorphic_embeds_many(changeset, field, changeset_fun, list_params, field_opts) do
%{
types_metadata: types_metadata,
on_type_not_found: on_type_not_found,
- type_field: type_field
- } = field_options
+ type_field_name: type_field_name
+ } = field_opts
+
+ list_data_for_field = Map.fetch!(changeset.data, field) || []
embeds =
- Enum.map(list_params, fn params ->
- case do_get_polymorphic_module_from_map(params, type_field, types_metadata) do
- nil when on_type_not_found == :raise ->
- raise_cannot_infer_type_from_data(params)
-
- nil when on_type_not_found == :changeset_error ->
- :error
-
- nil when on_type_not_found == :ignore ->
- :ignore
-
- module ->
- embed_changeset = changeset_fun.(struct(module), params)
- embed_changeset = %{embed_changeset | action: :insert}
-
- case embed_changeset do
- %{valid?: true} = embed_changeset ->
- embed_changeset
- |> Ecto.Changeset.apply_changes()
- |> autogenerate_id(embed_changeset.action)
-
- %{valid?: false} = embed_changeset ->
- embed_changeset
- end
- end
+ Enum.map(list_params, fn
+ # A non-map element cannot carry a type or fields; reject the list.
+ params when not is_map(params) ->
+ :error
+
+ params ->
+ case get_polymorphic_module_from_map(params, type_field_name, types_metadata) do
+ nil when on_type_not_found == :raise ->
+ raise_cannot_infer_type_from_data(params)
+
+ nil when on_type_not_found == :changeset_error ->
+ :error
+
+ nil when on_type_not_found == :ignore ->
+ :ignore
+
+ module ->
+ data_for_field =
+ Enum.find(list_data_for_field, fn
+ %{id: id} = datum when not is_nil(id) ->
+ id == params[:id] and datum.__struct__ == module
+
+ _ ->
+ nil
+ end)
+
+ embed_changeset =
+ if data_for_field do
+ %{changeset_fun.(data_for_field, params) | action: :update}
+ else
+ %{changeset_fun.(struct(module), params) | action: :insert}
+ end
+
+ maybe_apply_changes(embed_changeset)
+ end
end)
if Enum.any?(embeds, &(&1 == :error)) do
@@ -256,6 +467,14 @@ defmodule PolymorphicEmbed do
end
end
+ defp maybe_apply_changes(%{valid?: true} = embed_changeset) do
+ embed_changeset
+ |> Ecto.Changeset.apply_changes()
+ |> autogenerate_id(embed_changeset.action)
+ end
+
+ defp maybe_apply_changes(%Changeset{valid?: false} = changeset), do: changeset
+
@impl true
def cast(_data, _params),
do:
@@ -274,10 +493,35 @@ defmodule PolymorphicEmbed do
def load(data, loader, params) when is_binary(data),
do: do_load(Jason.decode!(data), loader, params)
- def do_load(data, _loader, %{types_metadata: types_metadata, type_field: type_field}) do
- case do_get_polymorphic_module_from_map(data, type_field, types_metadata) do
- nil -> raise_cannot_infer_type_from_data(data)
- module when is_atom(module) -> {:ok, Ecto.embedded_load(module, data, :json)}
+ def do_load(data, _loader, field_opts) do
+ %{
+ types_metadata: types_metadata,
+ type_field_name: type_field_name
+ } = field_opts
+
+ 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))
+
+ 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 ->
+ {:ok, data}
+
+ type in nilify_type_list ->
+ {:ok, nil}
+
+ true ->
+ raise_cannot_infer_type_from_data(data)
+ end
+
+ module when is_atom(module) ->
+ {:ok, Ecto.embedded_load(module, data, :json)}
end
end
@@ -290,7 +534,10 @@ defmodule PolymorphicEmbed do
dump(Ecto.Changeset.apply_changes(changeset), dumper, params)
end
- def dump(%module{} = struct, dumper, %{types_metadata: types_metadata, type_field: type_field}) do
+ def dump(%module{} = struct, dumper, %{
+ types_metadata: types_metadata,
+ type_field_name: type_field_name
+ }) do
case module.__schema__(:autogenerate_id) do
{key, _source, :binary_id} ->
unless Map.get(struct, key) do
@@ -304,7 +551,8 @@ defmodule PolymorphicEmbed do
map =
struct
|> map_from_struct()
- |> Map.put(type_field, do_get_polymorphic_type(module, types_metadata))
+ # use the atom instead of string form for mongodb
+ |> Map.put(type_field_name, do_get_polymorphic_type(module, types_metadata))
dumper.(:map, map)
end
@@ -316,42 +564,49 @@ defmodule PolymorphicEmbed do
end
def get_polymorphic_module(schema, field, type_or_data) do
- %{types_metadata: types_metadata, type_field: type_field} = get_field_options(schema, field)
+ %{types_metadata: types_metadata, type_field_name: type_field_name} =
+ get_field_opts(schema, field)
case type_or_data do
map when is_map(map) ->
- do_get_polymorphic_module_from_map(map, type_field, types_metadata)
+ get_polymorphic_module_from_map(map, type_field_name, types_metadata)
type when is_atom(type) or is_binary(type) ->
- do_get_polymorphic_module_for_type(type, types_metadata)
+ get_polymorphic_module_for_type(type, types_metadata)
end
end
- defp do_get_polymorphic_module_from_map(%{} = attrs, type_field, types_metadata) do
- attrs = attrs |> convert_map_keys_to_string()
-
- type = Enum.find_value(attrs, fn {key, value} -> key == type_field && value end)
-
- if type do
- do_get_polymorphic_module_for_type(type, types_metadata)
+ defp get_polymorphic_module_from_map(%{} = attrs, type_field_name, types_metadata) do
+ if type = Attrs.get(attrs, type_field_name) do
+ get_polymorphic_module_for_type(type, types_metadata)
else
# check if one list is contained in another
# 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))).()
+
+ types_metadata =
+ types_metadata
+ |> Enum.filter(&([] != &1.identify_by_fields))
+
+ if types_metadata != [] do
+ keys = Map.keys(attrs) |> Enum.map(&to_string/1)
+
+ types_metadata
+ |> Enum.find(&([] == &1.identify_by_fields -- keys))
+ |> (&(&1 && Map.fetch!(&1, :module))).()
+ else
+ nil
+ end
end
end
- defp do_get_polymorphic_module_for_type(type, types_metadata) do
+ defp get_polymorphic_module_for_type(type, types_metadata) do
get_metadata_for_type(type, types_metadata)
|> (&(&1 && Map.fetch!(&1, :module))).()
end
def get_polymorphic_type(schema, field, module_or_struct) do
- %{types_metadata: types_metadata} = get_field_options(schema, field)
+ %{types_metadata: types_metadata} = get_field_opts(schema, field)
do_get_polymorphic_type(module_or_struct, types_metadata)
end
@@ -371,7 +626,7 @@ defmodule PolymorphicEmbed do
#=> [:location, :age, :device]
"""
def types(schema, field) do
- %{types_metadata: types_metadata} = get_field_options(schema, field)
+ %{types_metadata: types_metadata} = get_field_opts(schema, field)
Enum.map(types_metadata, & &1.type)
end
@@ -384,16 +639,17 @@ defmodule PolymorphicEmbed do
Enum.find(types_metadata, &(type == to_string(&1.type)))
end
- defp get_field_options(schema, field) do
+ @doc false
+ def get_field_opts(schema, field) do
try do
schema.__schema__(:type, field)
rescue
_ in UndefinedFunctionError ->
- raise ArgumentError, "#{inspect(schema)} is not an Ecto schema"
+ reraise ArgumentError, "#{inspect(schema)} is not an Ecto schema", __STACKTRACE__
else
- {:parameterized, PolymorphicEmbed, options} -> Map.put(options, :array?, false)
- {:array, {:parameterized, PolymorphicEmbed, options}} -> Map.put(options, :array?, true)
- {_, {:parameterized, PolymorphicEmbed, options}} -> Map.put(options, :array?, false)
+ {:parameterized, {PolymorphicEmbed, options}} -> Map.put(options, :array?, false)
+ {:array, {:parameterized, {PolymorphicEmbed, options}}} -> Map.put(options, :array?, true)
+ {_, {:parameterized, {PolymorphicEmbed, options}}} -> Map.put(options, :array?, false)
nil -> raise ArgumentError, "#{field} is not a polymorphic embed"
end
end
@@ -412,9 +668,6 @@ defmodule PolymorphicEmbed do
end
end
- defp convert_map_keys_to_string(%{} = map),
- do: for({key, val} <- map, into: %{}, do: {to_string(key), val})
-
defp raise_cannot_infer_type_from_data(data),
do: raise("could not infer polymorphic embed from data #{inspect(data)}")
@@ -434,38 +687,105 @@ defmodule PolymorphicEmbed do
end
defp merge_polymorphic_keys(map, changes, types, msg_func) do
- Enum.reduce(types, map, fn
- {field, {:parameterized, PolymorphicEmbed, _opts}}, acc ->
- if changeset = Map.get(changes, field) do
- case traverse_errors(changeset, msg_func) do
- errors when errors == %{} -> acc
- errors -> Map.put(acc, field, errors)
- end
- else
- acc
- end
+ Enum.reduce(types, map, &polymorphic_key_reducer(&1, &2, changes, msg_func))
+ end
- {field, {:array, {:parameterized, PolymorphicEmbed, _opts}}}, acc ->
- if changesets = Map.get(changes, field) do
- {errors, all_empty?} =
- Enum.map_reduce(changesets, true, fn changeset, all_empty? ->
- errors = traverse_errors(changeset, msg_func)
- {errors, all_empty? and errors == %{}}
- end)
-
- case all_empty? do
- true -> acc
- false -> Map.put(acc, field, errors)
- end
- else
- acc
- end
+ defp polymorphic_key_reducer(
+ {field, {rel, %{cardinality: :one}}},
+ acc,
+ changes,
+ msg_func
+ )
+ when rel in [:assoc, :embed] do
+ if changeset = Map.get(changes, field) do
+ case traverse_errors(changeset, msg_func) do
+ errors when errors == %{} -> acc
+ errors -> Map.put(acc, field, errors)
+ end
+ else
+ acc
+ end
+ end
- {_, _}, acc ->
- acc
- end)
+ defp polymorphic_key_reducer(
+ {field, {:parameterized, {PolymorphicEmbed, _opts}}},
+ acc,
+ changes,
+ msg_func
+ ) do
+ if changeset = Map.get(changes, field) do
+ case traverse_errors(changeset, msg_func) do
+ errors when errors == %{} -> acc
+ errors -> Map.put(acc, field, errors)
+ end
+ else
+ acc
+ end
+ end
+
+ # Userpilot fork extension: any other parameterized type may take part in
+ # error traversal by exporting traverse_errors/4 (field, changes, msg_func, acc).
+ defp polymorphic_key_reducer(
+ {field, {:parameterized, {module, _opts}}},
+ acc,
+ changes,
+ msg_func
+ )
+ when module != PolymorphicEmbed do
+ if function_exported?(module, :traverse_errors, 4) do
+ module.traverse_errors(field, changes, msg_func, acc)
+ else
+ acc
+ end
+ end
+
+ defp polymorphic_key_reducer(
+ {field, {rel, %{cardinality: :many}}},
+ acc,
+ changes,
+ msg_func
+ )
+ when rel in [:assoc, :embed] do
+ if changesets = Map.get(changes, field) do
+ {errors, all_empty?} =
+ Enum.map_reduce(changesets, true, fn changeset, all_empty? ->
+ errors = traverse_errors(changeset, msg_func)
+ {errors, all_empty? and errors == %{}}
+ end)
+
+ case all_empty? do
+ true -> acc
+ false -> Map.put(acc, field, errors)
+ end
+ else
+ acc
+ end
end
+ defp polymorphic_key_reducer(
+ {field, {:array, {:parameterized, {PolymorphicEmbed, _opts}}}},
+ acc,
+ changes,
+ msg_func
+ ) do
+ if changesets = Map.get(changes, field) do
+ {errors, all_empty?} =
+ Enum.map_reduce(changesets, true, fn changeset, all_empty? ->
+ errors = traverse_errors(changeset, msg_func)
+ {errors, all_empty? and errors == %{}}
+ end)
+
+ case all_empty? do
+ true -> acc
+ false -> Map.put(acc, field, errors)
+ end
+ else
+ acc
+ end
+ end
+
+ defp polymorphic_key_reducer({_, _}, acc, _, _), do: acc
+
defp autogenerate_id([], _action), do: []
defp autogenerate_id([schema | rest], action) do
diff --git a/lib/polymorphic_embed/html/component.ex b/lib/polymorphic_embed/html/component.ex
new file mode 100644
index 0000000..1d3f708
--- /dev/null
+++ b/lib/polymorphic_embed/html/component.ex
@@ -0,0 +1,136 @@
+if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &&
+ Code.ensure_loaded?(Phoenix.Component) do
+ defmodule PolymorphicEmbed.HTML.Component do
+ use Phoenix.Component
+
+ import PolymorphicEmbed.HTML.Helpers
+
+ @doc """
+ Renders nested form inputs for polymorphic embeds.
+
+ See `Phoenix.Component.inputs_for/1`.
+ """
+ @doc type: :component
+ attr(:field, Phoenix.HTML.FormField,
+ required: true,
+ doc: "A %Phoenix.HTML.Form{}/field name tuple, for example: {@form[:email]}."
+ )
+
+ attr(:id, :string,
+ doc: """
+ The id to be used in the form, defaults to the concatenation of the given
+ field to the parent form id.
+ """
+ )
+
+ attr(:as, :atom,
+ doc: """
+ The name to be used in the form, defaults to the concatenation of the given
+ field to the parent form name.
+ """
+ )
+
+ attr(:default, :any, doc: "The value to use if none is available.")
+
+ attr(:prepend, :list,
+ doc: """
+ The values to prepend when rendering. This only applies if the field value
+ is a list and no parameters were sent through the form.
+ """
+ )
+
+ attr(:append, :list,
+ doc: """
+ The values to append when rendering. This only applies if the field value
+ is a list and no parameters were sent through the form.
+ """
+ )
+
+ attr(:skip_hidden, :boolean,
+ default: false,
+ doc: """
+ Skip the automatic rendering of hidden fields to allow for more tight control
+ over the generated markup.
+ """
+ )
+
+ slot(:inner_block, required: true, doc: "The content rendered for each nested form.")
+
+ @persistent_id "_persistent_id"
+ def polymorphic_embed_inputs_for(assigns) do
+ %Phoenix.HTML.FormField{field: field_name, form: parent_form} = assigns.field
+ options = assigns |> Map.take([:id, :as, :default, :append, :prepend]) |> Keyword.new()
+
+ options =
+ parent_form.options
+ |> Keyword.take([:multipart])
+ |> Keyword.merge(options)
+
+ forms =
+ to_form(
+ parent_form.source,
+ parent_form,
+ field_name,
+ options
+ )
+
+ seen_ids = for f <- forms, vid = f.params[@persistent_id], into: %{}, do: {vid, true}
+
+ {forms, _} =
+ Enum.map_reduce(forms, seen_ids, fn %Phoenix.HTML.Form{params: params} = form, seen_ids ->
+ id =
+ case params do
+ %{@persistent_id => id} -> id
+ %{} -> next_id(map_size(seen_ids), seen_ids)
+ end
+
+ form_id = "#{parent_form.id}_#{field_name}_#{id}"
+ new_params = Map.put(params, @persistent_id, id)
+ new_hidden = [{@persistent_id, id} | form.hidden]
+
+ new_form = %Phoenix.HTML.Form{
+ form
+ | id: form_id,
+ params: new_params,
+ hidden: new_hidden
+ }
+
+ {new_form, Map.put(seen_ids, id, true)}
+ end)
+
+ assigns = assign(assigns, :forms, forms)
+
+ ~H"""
+ <%= for finner <- @forms do %>
+ <%= unless @skip_hidden do %>
+ <%= for {name, value_or_values} <- finner.hidden,
+ id = Phoenix.HTML.Form.input_id(finner, name),
+ name = name_for_value_or_values(finner, name, value_or_values),
+ value <- List.wrap(value_or_values) do %>
+
+ <% end %>
+ <% end %>
+ <%= render_slot(@inner_block, finner) %>
+ <% end %>
+ """
+ end
+
+ defp next_id(idx, %{} = seen_ids) do
+ id_str = to_string(idx)
+
+ if Map.has_key?(seen_ids, id_str) do
+ next_id(idx + 1, seen_ids)
+ else
+ id_str
+ end
+ end
+
+ defp name_for_value_or_values(form, field, values) when is_list(values) do
+ Phoenix.HTML.Form.input_name(form, field) <> "[]"
+ end
+
+ defp name_for_value_or_values(form, field, _value) do
+ Phoenix.HTML.Form.input_name(form, field)
+ end
+ end
+end
diff --git a/lib/polymorphic_embed/html/form.ex b/lib/polymorphic_embed/html/form.ex
index c6ac550..e2c1697 100644
--- a/lib/polymorphic_embed/html/form.ex
+++ b/lib/polymorphic_embed/html/form.ex
@@ -1,23 +1,19 @@
-if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) do
+if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) &&
+ Code.ensure_loaded?(PhoenixHTMLHelpers.Form) do
defmodule PolymorphicEmbed.HTML.Form do
import Phoenix.HTML, only: [html_escape: 1]
- import Phoenix.HTML.Form, only: [hidden_inputs_for: 1, input_value: 2]
+ import PhoenixHTMLHelpers.Form, only: [hidden_inputs_for: 1]
- @doc """
- Returns the polymorphic type of the given field in the given form data.
- """
- def get_polymorphic_type(%Phoenix.HTML.Form{} = form, schema, field) do
- case input_value(form, field) do
- %Ecto.Changeset{data: value} ->
- PolymorphicEmbed.get_polymorphic_type(schema, field, value)
+ defdelegate get_polymorphic_type(form, field), to: PolymorphicEmbed.HTML.Helpers
- %_{} = value ->
- PolymorphicEmbed.get_polymorphic_type(schema, field, value)
+ defdelegate get_polymorphic_type(form_field), to: PolymorphicEmbed.HTML.Helpers
- _ ->
- nil
- end
- end
+ defdelegate source_data(form), to: PolymorphicEmbed.HTML.Helpers
+
+ defdelegate source_module(form), to: PolymorphicEmbed.HTML.Helpers
+
+ defdelegate to_form(source_changeset, form, field, options),
+ to: PolymorphicEmbed.HTML.Helpers
@doc """
Generates a new form builder without an anonymous function.
@@ -31,7 +27,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
## Example
<.form
- let={f}
+ :let={f}
for={@changeset}
id="reminder-form"
phx-change="validate"
@@ -40,7 +36,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
<%= for channel_form <- polymorphic_embed_inputs_for f, :channel do %>
<%= hidden_inputs_for(channel_form) %>
- <%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
+ <%= case get_polymorphic_type(reminder_form, Reminder, :channel) do %>
<% :sms -> %>
<%= label channel_form, :number %>
<%= text_input channel_form, :number %>
@@ -54,9 +50,8 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
def polymorphic_embed_inputs_for(form, field)
when is_atom(field) or is_binary(field) do
options = Keyword.take(form.options, [:multipart])
- %schema{} = form.source.data
- type = get_polymorphic_type(form, schema, field)
- to_form(form.source, form, field, type, options)
+
+ to_form(form.source, form, field, options)
end
@doc """
@@ -67,7 +62,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
<%= inputs_for f, :reminders, fn reminder_form -> %>
<%= polymorphic_embed_inputs_for reminder_form, :channel, fn channel_form -> %>
- <%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
+ <%= case get_polymorphic_type(reminder_form, Reminder, :channel) do %>
<% :sms -> %>
<%= label poly_form, :number %>
<%= text_input poly_form, :number %>
@@ -91,9 +86,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
def polymorphic_embed_inputs_for(form, field, fun)
when is_atom(field) or is_binary(field) do
options = Keyword.take(form.options, [:multipart])
- %schema{} = form.source.data
- type = get_polymorphic_type(form, schema, field)
- forms = to_form(form.source, form, field, type, options)
+ forms = to_form(form.source, form, field, options)
html_escape(
Enum.map(forms, fn form ->
@@ -102,10 +95,10 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
)
end
- def polymorphic_embed_inputs_for(form, field, type, fun)
+ def polymorphic_embed_inputs_for(form, field, type \\ nil, fun)
when is_atom(field) or is_binary(field) do
options = Keyword.take(form.options, [:multipart])
- forms = to_form(form.source, form, field, type, options)
+ forms = to_form(form.source, form, field, [{:polymorphic_type, type} | options])
html_escape(
Enum.map(forms, fn form ->
@@ -113,69 +106,5 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
end)
)
end
-
- def to_form(%{action: parent_action} = source_changeset, form, field, type, options) do
- id = to_string(form.id <> "_#{field}")
- name = to_string(form.name <> "[#{field}]")
-
- params = Map.get(source_changeset.params || %{}, to_string(field), %{}) |> List.wrap()
- list_data = get_data(source_changeset, field, type) |> List.wrap()
-
- list_data
- |> Enum.with_index()
- |> Enum.map(fn {data, i} ->
- params = Enum.at(params, i) || %{}
-
- changeset =
- data
- |> Ecto.Changeset.change()
- |> apply_action(parent_action)
-
- errors = get_errors(changeset)
-
- changeset = %Ecto.Changeset{
- changeset
- | action: parent_action,
- params: params,
- errors: errors,
- valid?: errors == []
- }
-
- %Phoenix.HTML.Form{
- source: changeset,
- impl: Phoenix.HTML.FormData.Ecto.Changeset,
- id: id,
- index: if(length(list_data) > 1, do: i),
- name: name,
- errors: errors,
- data: data,
- params: params,
- hidden: [__type__: to_string(type)],
- options: options
- }
- end)
- end
-
- defp get_data(changeset, field, type) do
- struct = Ecto.Changeset.apply_changes(changeset)
-
- case Map.get(struct, field) do
- nil ->
- module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
- if module, do: struct(module), else: []
-
- data ->
- data
- end
- end
-
- # If the parent changeset had no action, we need to remove the action
- # from children changeset so we ignore all errors accordingly.
- defp apply_action(changeset, nil), do: %{changeset | action: nil}
- defp apply_action(changeset, _action), do: changeset
-
- defp get_errors(%{action: nil}), do: []
- defp get_errors(%{action: :ignore}), do: []
- defp get_errors(%{errors: errors}), do: errors
end
end
diff --git a/lib/polymorphic_embed/html/helpers.ex b/lib/polymorphic_embed/html/helpers.ex
new file mode 100644
index 0000000..bcd4b76
--- /dev/null
+++ b/lib/polymorphic_embed/html/helpers.ex
@@ -0,0 +1,128 @@
+if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) do
+ defmodule PolymorphicEmbed.HTML.Helpers do
+ @doc """
+ Returns the polymorphic type of the given field in the given form data.
+ """
+ def get_polymorphic_type(%Phoenix.HTML.Form{} = form, field) do
+ %schema{} = form.source.data
+
+ case form[field] && form[field].value do
+ %Ecto.Changeset{data: value} ->
+ PolymorphicEmbed.get_polymorphic_type(schema, field, value)
+
+ %_{} = value ->
+ PolymorphicEmbed.get_polymorphic_type(schema, field, value)
+
+ %{} = map ->
+ case PolymorphicEmbed.get_polymorphic_module(schema, field, map) do
+ nil ->
+ nil
+
+ module ->
+ PolymorphicEmbed.get_polymorphic_type(schema, field, module)
+ end
+
+ list when is_list(list) ->
+ raise "Cannot infer the polymorphic type as the list of embeds may contain multiple types"
+
+ nil ->
+ nil
+ end
+ end
+
+ def get_polymorphic_type(%Phoenix.HTML.FormField{} = form_field) do
+ %{field: field_name, form: parent_form} = form_field
+ get_polymorphic_type(parent_form, field_name)
+ end
+
+ @doc """
+ Returns the source data structure
+ """
+ def source_data(%Phoenix.HTML.Form{} = form) do
+ form.source.data
+ end
+
+ @doc """
+ Returns the source data structure
+ """
+ def source_module(%Phoenix.HTML.Form{} = form) do
+ form.source.data.__struct__
+ end
+
+ def to_form(%{action: parent_action} = source_changeset, form, field, options) do
+ id = to_string(form.id <> "_#{field}")
+ name = to_string(form.name <> "[#{field}]")
+
+ params = Map.get(source_changeset.params || %{}, to_string(field), %{}) |> List.wrap()
+
+ struct = Ecto.Changeset.apply_changes(source_changeset)
+
+ list_data =
+ case Map.get(struct, field) do
+ nil ->
+ type = Keyword.get(options, :polymorphic_type, get_polymorphic_type(form, field))
+ module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
+ if module, do: [struct(module)], else: []
+
+ data ->
+ List.wrap(data)
+ end
+
+ list_data
+ |> Enum.with_index()
+ |> Enum.map(fn {data, i} ->
+ params = Enum.at(params, i) || %{}
+
+ %Ecto.Changeset{} =
+ changeset =
+ data
+ |> Ecto.Changeset.change()
+ |> apply_action(parent_action)
+
+ errors = get_errors(changeset)
+
+ changeset = %Ecto.Changeset{
+ changeset
+ | action: parent_action,
+ params: params,
+ errors: errors,
+ valid?: errors == []
+ }
+
+ %schema{} = source_changeset.data
+
+ field_opts = PolymorphicEmbed.get_field_opts(schema, field)
+ type_field_name = Map.fetch!(field_opts, :type_field_name)
+ # correctly set id and name for embeds_many inputs
+ array? = Map.get(field_opts, :array?, false)
+
+ index_string = Integer.to_string(i)
+
+ type = PolymorphicEmbed.get_polymorphic_type(schema, field, changeset.data)
+
+ %Phoenix.HTML.Form{
+ source: changeset,
+ impl: Phoenix.HTML.FormData.Ecto.Changeset,
+ id: if(array?, do: id <> "_" <> index_string, else: id),
+ name: if(array?, do: name <> "[" <> index_string <> "]", else: name),
+ index: if(array?, do: i),
+ errors: errors,
+ data: data,
+ action: parent_action,
+ params: params,
+ hidden: [{type_field_name, to_string(type)}],
+ options: options
+ }
+ end)
+ end
+
+ # If the parent changeset had no action, we need to remove the action
+ # from children changeset so we ignore all errors accordingly.
+ defp apply_action(changeset, nil), do: %{changeset | action: nil}
+ defp apply_action(changeset, _action), do: changeset
+
+ defp get_errors(%{action: nil}), do: []
+ defp get_errors(%{action: :ignore}), do: []
+ defp get_errors(%{errors: errors}), do: errors
+ end
+end
diff --git a/lib/polymorphic_embed/options_validator.ex b/lib/polymorphic_embed/options_validator.ex
new file mode 100644
index 0000000..8ef81db
--- /dev/null
+++ b/lib/polymorphic_embed/options_validator.ex
@@ -0,0 +1,86 @@
+defmodule PolymorphicEmbed.OptionsValidator do
+ require Logger
+
+ @known_options_names [
+ :types,
+ :on_replace,
+ :type_field,
+ :type_field_name,
+ :on_type_not_found,
+ :use_parent_field_for_type,
+ :retain_unlisted_types_on_load,
+ :nilify_unlisted_types_on_load,
+ :array?,
+ # Ecto
+ :field,
+ :schema,
+ :default
+ ]
+ @valid_on_type_not_found_options [:raise, :changeset_error, :nilify, :ignore]
+
+ def validate!(options) do
+ unless is_nil(options[:default]) or options[:default] == [] do
+ raise "`:default` expected to be `nil` or `[]`."
+ end
+
+ if is_nil(options[:default]) and options[:on_replace] != :update do
+ raise "`:on_replace` must be set to `:update` for a single polymorphic embed."
+ end
+
+ if is_list(options[:default]) and options[:on_replace] != :delete do
+ raise "`:on_replace` must be set to `:delete` for a list of polymorphic embeds."
+ end
+
+ unless Keyword.fetch!(options, :on_type_not_found) in @valid_on_type_not_found_options do
+ raise(
+ "Invalid `:on_type_not_found` option. Valid options: #{@valid_on_type_not_found_options |> Enum.join(", ")}."
+ )
+ end
+
+ if use_parent_field_for_type = options[:use_parent_field_for_type] do
+ unless is_atom(use_parent_field_for_type) do
+ raise "`:use_parent_field_for_type` must be an atom."
+ end
+
+ if options[:default] == [] do
+ raise "`:use_parent_field_for_type` option cannot be used for a list of polymorphic embeds."
+ end
+ end
+
+ # TODO remove in v6
+ if Keyword.has_key?(options, :type_field) do
+ Logger.warning(
+ "`:type_field` option is deprecated and must be replaced with `:type_field_name`."
+ )
+ end
+
+ unless is_atom(Keyword.fetch!(options, :type_field_name)) do
+ raise "`:type_field_name` option must be an atom."
+ end
+
+ 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."
+ end
+
+ unless is_list(nilify_unlisted_types) and Enum.all?(nilify_unlisted_types, &is_atom/1) do
+ raise "`:retain_unlisted_types_on_load` option must be a list of types as atoms."
+ end
+
+ keys = Keyword.keys(options)
+ key_count = keys |> Enum.count()
+ unique_key_count = Enum.uniq(keys) |> Enum.count()
+
+ if key_count != unique_key_count do
+ raise "Duplicate keys found in options for polymorphic embed."
+ end
+
+ unknown_options = Keyword.drop(options, @known_options_names)
+
+ if length(unknown_options) > 0 do
+ raise "Unknown options: #{unknown_options |> Keyword.keys() |> Enum.join(", ")}"
+ end
+ end
+end
diff --git a/mix.exs b/mix.exs
index 9e50af8..a177994 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,12 +1,12 @@
defmodule PolymorphicEmbed.MixProject do
use Mix.Project
- @version "3.0.5"
+ @version "5.0.6"
def project do
[
app: :polymorphic_embed,
- elixir: "~> 1.9",
+ elixir: "~> 1.13",
deps: deps(),
aliases: aliases(),
elixirc_paths: elixirc_paths(Mix.env()),
@@ -15,6 +15,11 @@ defmodule PolymorphicEmbed.MixProject do
version: @version,
package: package(),
description: "Polymorphic embeds in Ecto",
+ hex: [
+ # The fix for this decimal advisory is only released in decimal 3.x,
+ # and ecto 3.12 requires decimal ~> 2.0. Test-only exposure here.
+ ignore_advisories: ["CVE-2026-32686"]
+ ],
# ExDoc
name: "Polymorphic Embed",
@@ -28,14 +33,7 @@ defmodule PolymorphicEmbed.MixProject do
],
# ExCoveralls
- test_coverage: [tool: ExCoveralls],
- preferred_cli_env: [
- coveralls: :test,
- "coveralls.detail": :test,
- "coveralls.post": :test,
- "coveralls.html": :test,
- "coveralls.github": :test
- ]
+ test_coverage: [tool: ExCoveralls]
]
end
@@ -45,21 +43,35 @@ defmodule PolymorphicEmbed.MixProject do
]
end
+ def cli do
+ [
+ preferred_envs: [
+ coveralls: :test,
+ "coveralls.detail": :test,
+ "coveralls.post": :test,
+ "coveralls.html": :test,
+ "coveralls.github": :test
+ ]
+ ]
+ end
+
defp deps do
[
- {:ecto, "~> 3.9"},
+ {:ecto, "~> 3.12"},
{:jason, "~> 1.4"},
- {:phoenix_html, "~> 2.14 or ~> 3.2", optional: true},
- {:ex_doc, "~> 0.28", only: :dev},
- {:ecto_sql, "~> 3.9", only: :test},
- {:postgrex, "~> 0.16", only: :test},
- {:query_builder, "~> 1.0", only: :test},
- {:phoenix_ecto, "~> 4.4", only: :test},
- {:phoenix_live_view, "~> 0.18", only: :test},
- {:floki, "~> 0.33", only: :test},
- {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
- {:excoveralls, "~> 0.15", only: :test},
- {:credo, "~> 1.6", only: [:dev, :test], runtime: false}
+ {:attrs, "~> 0.6"},
+ {:phoenix_html, "~> 4.1", optional: true},
+ {:phoenix_html_helpers, "~> 1.0", optional: true},
+ {:phoenix_live_view, "~> 0.20 or ~> 1.0", optional: true},
+ {:ex_doc, "~> 0.34", only: :dev},
+ {:ecto_sql, "~> 3.12", only: :test},
+ {:postgrex, "~> 0.18 or ~> 0.19", only: :test},
+ {:query_builder, "~> 1.4", only: :test},
+ {:phoenix_ecto, "~> 4.6", only: :test},
+ {:floki, "~> 0.36", only: :test},
+ {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
+ {:excoveralls, "~> 0.18", only: :test},
+ {:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
end
@@ -67,8 +79,12 @@ defmodule PolymorphicEmbed.MixProject do
[
test: [
"ecto.create --quiet",
- "ecto.rollback --all",
- "ecto.migrate",
+ "ecto.rollback --all --quiet",
+ fn _args ->
+ :code.delete(PolymorphicEmbed.CreateTables)
+ :code.purge(PolymorphicEmbed.CreateTables)
+ end,
+ "ecto.migrate --quiet",
"test"
]
]
@@ -79,7 +95,7 @@ defmodule PolymorphicEmbed.MixProject do
defp package do
[
- licenses: ["Apache 2.0"],
+ licenses: ["Apache-2.0"],
maintainers: ["Mathieu Decaffmeyer"],
links: %{
"GitHub" => "https://github.com/mathieuprog/polymorphic_embed",
diff --git a/mix.lock b/mix.lock
index 7a1ba79..25d36cb 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,43 +1,37 @@
%{
- "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
- "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"},
- "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
- "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
- "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
- "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
- "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
- "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.27", "755da957e2b980618ba3397d3f923004d85bac244818cf92544eaa38585cb3a8", [:mix], [], "hexpm", "8d02465c243ee96bdd655e7c9a91817a2a80223d63743545b2861023c4ff39ac"},
- "ecto": {:hex, :ecto, "3.9.0", "7c74fc0d950a700eb7019057ff32d047ed7f19b57c1b2ca260cf0e565829101d", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fed5ebc5831378b916afd0b5852a0c5bb3e7390665cc2b0ec8ab0c712495b73d"},
- "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"},
- "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
- "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"},
- "excoveralls": {:hex, :excoveralls, "0.15.0", "ac941bf85f9f201a9626cc42b2232b251ad8738da993cf406a4290cacf562ea4", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9631912006b27eca30a2f3c93562bc7ae15980afb014ceb8147dc5cdd8f376f1"},
- "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
- "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"},
- "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
- "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
- "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
- "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
- "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
- "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
- "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
- "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
- "phoenix": {:hex, :phoenix, "1.6.13", "5b3152907afdb8d3a6cdafb4b149e8aa7aabbf1422fd9f7ef4c2a67ead57d24a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13d8806c31176e2066da4df2d7443c144211305c506ed110ad4044335b90171d"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
- "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.1", "1e1703e26d0580dbd84e9b668e6da164a368f125dfa3d813c9a098da508e2a72", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6 or ~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b785e1cc13b05e6d9482d3ca1eedfb24e8113fd6421b47996ba16b9d20869706"},
- "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
- "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
- "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
- "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
- "query_builder": {:hex, :query_builder, "1.0.1", "88fac8ec002825a54a5ef2739bbef12a870098efc5e609e3baf02a2bdda6f31e", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "c6afbc439a5d825e95b938cef23bf0632d546a1507ee4b07d20098be44b98176"},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
- "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
- "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "attrs": {:hex, :attrs, "0.6.0", "25d738b47829f964a786ef73897d2550b66f3e7d1d7c49a83bc8fd81c71bed93", [:mix], [], "hexpm", "9c30ac15255c2ba8399263db55ba32c2f4e5ec267b654ce23df99168b405c82e"},
+ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
+ "castore": {:hex, :castore, "1.0.20", "455e48f7115eca98c9f2b0e7a152b5a2e8f2a8a4f964c96e95bd31645ee5fa59", [:mix], [], "hexpm", "940eafbfd8b14bee649f083bc11b3b54ec555b54c3e4ea8213351ff6fee39c10"},
+ "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"},
+ "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
+ "decimal": {:hex, :decimal, "2.4.1", "6c0fbede12fb122ba685e9ab41c6a40c129e322b3aa192f9e072e61f3a6ffaf2", [:mix], [], "hexpm", "7e618897933a8455f19a727d7c5e50a2c071a544b700e5e724298ecb4340187f"},
+ "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
+ "ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"},
+ "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
+ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
+ "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
+ "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"},
+ "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
+ "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
+ "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
+ "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
+ "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
+ "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
+ "phoenix": {:hex, :phoenix, "1.7.24", "4cb76aed6d3f03878893769020e97c4394ee95b62b2b2d6313c20f66d7d37baa", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "a283a9d91517116166244fdd8ed8b405c142774dc39b4a4047d179cecca9c09f"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
+ "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
+ "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.1", "5389a30658176c0de816636ce276567478bffd063c082515a6e8368b8fc9a0db", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0f517e6f290f10dbb94343ac22e0109437fb1fa6f0696e7c73967b789c1c285"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
+ "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
+ "postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"},
+ "query_builder": {:hex, :query_builder, "1.4.2", "5a61c63e5ea7093d110589aacf081362e2fe8ae0188644732edd02b1230a7be5", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "abffaf756a5fdfd37bd594455e00083649a4f01e436279865592ccb8115dd739"},
+ "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
}
diff --git a/test/polymorphic_embed_test.exs b/test/polymorphic_embed_test.exs
index dd98de6..488eb9a 100644
--- a/test/polymorphic_embed_test.exs
+++ b/test/polymorphic_embed_test.exs
@@ -1,3 +1,46 @@
+defmodule PolymorphicEmbedTest.CustomParameterizedType do
+ use Ecto.ParameterizedType
+
+ @impl true
+ def type(_params), do: :map
+
+ @impl true
+ def init(opts), do: Enum.into(opts, %{})
+
+ @impl true
+ def cast(data, _params), do: {:ok, data}
+
+ @impl true
+ def load(data, _loader, _params), do: {:ok, data}
+
+ @impl true
+ def dump(data, _dumper, _params), do: {:ok, data}
+
+ def traverse_errors(field, changes, _msg_func, acc) do
+ if Map.has_key?(changes, field) do
+ Map.put(acc, field, ["custom traversal"])
+ else
+ acc
+ end
+ end
+end
+
+defmodule PolymorphicEmbedTest.SchemaWithCustomParameterizedType do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ embedded_schema do
+ field(:name, :string)
+ field(:settings, PolymorphicEmbedTest.CustomParameterizedType)
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:name, :settings])
+ |> validate_required(:name)
+ end
+end
+
defmodule PolymorphicEmbedTest do
use ExUnit.Case
@@ -5,9 +48,10 @@ defmodule PolymorphicEmbedTest do
import Phoenix.Component
import Phoenix.HTML
- import Phoenix.HTML.Form
+ import PhoenixHTMLHelpers.Form
import Phoenix.LiveViewTest
import PolymorphicEmbed.HTML.Form
+ import PolymorphicEmbed.HTML.Component
alias PolymorphicEmbed.Repo
@@ -119,6 +163,114 @@ defmodule PolymorphicEmbedTest do
end
end
+ test "infer type from parent field via :use_parent_field_for_type option" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ type: "sms",
+ channel4: %{
+ number: "02/807.05.53",
+ country_code: 1,
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert {:ok, %{}} = insert_result
+ end
+
+ test "infer type from parent field but type is also present in embed map and it is different" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ type: "sms",
+ channel4: %{
+ __type__: "email",
+ number: "02/807.05.53",
+ country_code: 1,
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ }
+ }
+
+ assert_raise RuntimeError,
+ ~r"does not match",
+ fn ->
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ end
+ end
+
+ test "infer type from parent field but type is nil" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ channel4: %{
+ __type__: "sms",
+ number: "02/807.05.53",
+ country_code: 1,
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert {:error, %Ecto.Changeset{}} = insert_result
+ end
+
+ test "infer type from parent field when type in embed map matches parent field type" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ # Both parent field type and embed __type__ are "sms" - they match
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ type: "sms",
+ channel4: %{
+ __type__: "sms",
+ number: "02/807.05.53",
+ country_code: 1,
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert {:ok, %{channel4: %PolymorphicEmbed.Channel.SMS{number: "02/807.05.53"}}} =
+ insert_result
+ end
+
test "validations before casting polymorphic embed still work" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -190,6 +342,49 @@ defmodule PolymorphicEmbedTest do
end
end
+ test "wrong-shaped embed params produce a changeset error" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ base_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder"
+ }
+
+ wrong_shapes = [
+ channel: "sms",
+ channel: 5,
+ channel: ["sms"],
+ contexts: "device",
+ contexts: %{"my_type_field" => "device"},
+ contexts: ["device"]
+ ]
+
+ for {field, params} <- wrong_shapes do
+ changeset =
+ struct(reminder_module)
+ |> reminder_module.changeset(Map.put(base_attrs, field, params))
+
+ refute changeset.valid?
+ assert {"is invalid", _} = changeset.errors[field]
+ end
+ end
+ end
+
+ test "traverse_errors delegates to traverse_errors/4 of other parameterized types" do
+ changeset =
+ PolymorphicEmbedTest.SchemaWithCustomParameterizedType.changeset(
+ %PolymorphicEmbedTest.SchemaWithCustomParameterizedType{},
+ %{"settings" => %{"foo" => "bar"}}
+ )
+
+ refute changeset.valid?
+
+ errors = PolymorphicEmbed.traverse_errors(changeset, fn {msg, _opts} -> msg end)
+
+ assert %{name: ["can't be blank"], settings: ["custom traversal"]} == errors
+ end
+
test "traverse_errors" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -293,36 +488,37 @@ defmodule PolymorphicEmbedTest do
end
end
- test "traverse_errors on changesets with valid polymorphic structs" do
+ test "traverse_errors on nested *-to-many relations" do
for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ event_module = get_module(Event, generator)
- sms_reminder_attrs = %{
- text: "This is an SMS reminder",
- channel: %{
- my_type_field: "sms",
- number: "02/807.05.53",
- country_code: 1,
- provider: %{__type__: "twilio", api_key: "somekey"}
- },
- contexts: [
- %{
- __type__: "location",
- address: "hello",
- country: %{
- name: ""
- }
- },
+ event_attrs = %{
+ reminders: [
%{
- __type__: "location",
- address: ""
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms"
+ },
+ contexts: [
+ %{
+ __type__: "location",
+ address: "hello",
+ country: %{
+ name: ""
+ }
+ },
+ %{
+ __type__: "location",
+ address: ""
+ }
+ ]
}
]
}
changeset =
- struct(reminder_module)
- |> reminder_module.changeset(sms_reminder_attrs)
+ struct(event_module)
+ |> event_module.changeset(event_attrs)
insert_result = Repo.insert(changeset)
@@ -332,30 +528,51 @@ defmodule PolymorphicEmbedTest do
valid?: false,
errors: errors,
changes: %{
- contexts: [
+ reminders: [
%{
action: :insert,
valid?: false,
- errors: context1_errors,
+ errors: reminder_errors,
changes: %{
- country: %{
+ channel: %{
action: :insert,
valid?: false,
- errors: country_errors
- }
+ errors: channel_errors
+ },
+ contexts: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context1_errors,
+ changes: %{
+ country: %{
+ action: :insert,
+ valid?: false,
+ errors: country_errors
+ }
+ }
+ },
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context2_errors
+ }
+ ]
}
- },
- %{
- action: :insert,
- valid?: false,
- errors: context2_errors
}
]
}
}} = insert_result
- assert [date: {"can't be blank", [validation: :required]}] = changeset.errors
- assert [date: {"can't be blank", [validation: :required]}] = errors
+ assert [] = errors
+
+ assert [date: {"can't be blank", [validation: :required]}] = reminder_errors
+
+ assert %{
+ number: {"can't be blank", [validation: :required]},
+ country_code: {"can't be blank", [validation: :required]},
+ provider: {"can't be blank", [validation: :required]}
+ } = Map.new(channel_errors)
assert [] = context1_errors
assert %{address: {"can't be blank", [validation: :required]}} = Map.new(context2_errors)
@@ -369,8 +586,17 @@ defmodule PolymorphicEmbedTest do
end
%{
- contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
- date: ["can't be blank"]
+ reminders: [
+ %{
+ channel: %{
+ country_code: ["can't be blank"],
+ number: ["can't be blank"],
+ provider: ["can't be blank"]
+ },
+ contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
+ date: ["can't be blank"]
+ }
+ ]
} =
traverse_errors_fun.(
changeset,
@@ -383,71 +609,39 @@ defmodule PolymorphicEmbedTest do
end
end
- test "receive embed as struct" do
+ test "traverse_errors on nested embeds_many relations" 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)
- )
- ]
- )
- )
+ event_module = get_module(Event, generator)
- 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__
+ event_attrs = %{
+ embedded_reminders: [
+ %{
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms"
+ },
+ contexts: [
+ %{
+ __type__: "location",
+ address: "hello",
+ country: %{
+ name: ""
+ }
+ },
+ %{
+ __type__: "location",
+ address: ""
+ }
+ ]
+ }
+ ]
+ }
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
+ struct(event_module)
+ |> event_module.changeset(event_attrs)
- insert_result =
- changeset
- |> Repo.insert()
+ insert_result = Repo.insert(changeset)
assert {:error,
%Ecto.Changeset{
@@ -455,13 +649,486 @@ defmodule PolymorphicEmbedTest do
valid?: false,
errors: errors,
changes: %{
- channel: %{
- action: :update,
- valid?: false,
- errors: channel_errors
- }
- }
- }} = insert_result
+ embedded_reminders: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: reminder_errors,
+ changes: %{
+ channel: %{
+ action: :insert,
+ valid?: false,
+ errors: channel_errors
+ },
+ contexts: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context1_errors,
+ changes: %{
+ country: %{
+ action: :insert,
+ valid?: false,
+ errors: country_errors
+ }
+ }
+ },
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context2_errors
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }} = insert_result
+
+ assert [] = errors
+
+ assert [date: {"can't be blank", [validation: :required]}] = reminder_errors
+
+ assert %{
+ number: {"can't be blank", [validation: :required]},
+ country_code: {"can't be blank", [validation: :required]},
+ provider: {"can't be blank", [validation: :required]}
+ } = Map.new(channel_errors)
+
+ assert [] = context1_errors
+ assert %{address: {"can't be blank", [validation: :required]}} = Map.new(context2_errors)
+ assert %{name: {"can't be blank", [validation: :required]}} = Map.new(country_errors)
+
+ traverse_errors_fun =
+ if polymorphic?(generator) do
+ &PolymorphicEmbed.traverse_errors/2
+ else
+ &Ecto.Changeset.traverse_errors/2
+ end
+
+ %{
+ embedded_reminders: [
+ %{
+ channel: %{
+ country_code: ["can't be blank"],
+ number: ["can't be blank"],
+ provider: ["can't be blank"]
+ },
+ contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
+ date: ["can't be blank"]
+ }
+ ]
+ } =
+ traverse_errors_fun.(
+ changeset,
+ fn {msg, opts} ->
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", to_string(value))
+ end)
+ end
+ )
+ end
+ end
+
+ test "traverse_errors on nested *-to-one relations" do
+ for generator <- @generators do
+ todo_module = get_module(Todo, generator)
+
+ todo_attrs = %{
+ reminder: %{
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms"
+ },
+ contexts: [
+ %{
+ __type__: "location",
+ address: "hello",
+ country: %{
+ name: ""
+ }
+ },
+ %{
+ __type__: "location",
+ address: ""
+ }
+ ]
+ }
+ }
+
+ changeset =
+ struct(todo_module)
+ |> todo_module.changeset(todo_attrs)
+
+ insert_result = Repo.insert(changeset)
+
+ assert {:error,
+ %Ecto.Changeset{
+ action: :insert,
+ valid?: false,
+ errors: errors,
+ changes: %{
+ reminder: %{
+ action: :insert,
+ valid?: false,
+ errors: reminder_errors,
+ changes: %{
+ channel: %{
+ action: :insert,
+ valid?: false,
+ errors: channel_errors
+ },
+ contexts: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context1_errors,
+ changes: %{
+ country: %{
+ action: :insert,
+ valid?: false,
+ errors: country_errors
+ }
+ }
+ },
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context2_errors
+ }
+ ]
+ }
+ }
+ }
+ }} = insert_result
+
+ assert [] = errors
+
+ assert [date: {"can't be blank", [validation: :required]}] = reminder_errors
+
+ assert %{
+ number: {"can't be blank", [validation: :required]},
+ country_code: {"can't be blank", [validation: :required]},
+ provider: {"can't be blank", [validation: :required]}
+ } = Map.new(channel_errors)
+
+ assert [] = context1_errors
+ assert %{address: {"can't be blank", [validation: :required]}} = Map.new(context2_errors)
+ assert %{name: {"can't be blank", [validation: :required]}} = Map.new(country_errors)
+
+ traverse_errors_fun =
+ if polymorphic?(generator) do
+ &PolymorphicEmbed.traverse_errors/2
+ else
+ &Ecto.Changeset.traverse_errors/2
+ end
+
+ %{
+ reminder: %{
+ channel: %{
+ country_code: ["can't be blank"],
+ number: ["can't be blank"],
+ provider: ["can't be blank"]
+ },
+ contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
+ date: ["can't be blank"]
+ }
+ } =
+ traverse_errors_fun.(
+ changeset,
+ fn {msg, opts} ->
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", to_string(value))
+ end)
+ end
+ )
+ end
+ end
+
+ test "traverse_errors on nested embeds_one relations" do
+ for generator <- @generators do
+ todo_module = get_module(Todo, generator)
+
+ todo_attrs = %{
+ embedded_reminder: %{
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms"
+ },
+ contexts: [
+ %{
+ __type__: "location",
+ address: "hello",
+ country: %{
+ name: ""
+ }
+ },
+ %{
+ __type__: "location",
+ address: ""
+ }
+ ]
+ }
+ }
+
+ changeset =
+ struct(todo_module)
+ |> todo_module.changeset(todo_attrs)
+
+ insert_result = Repo.insert(changeset)
+
+ assert {:error,
+ %Ecto.Changeset{
+ action: :insert,
+ valid?: false,
+ errors: errors,
+ changes: %{
+ embedded_reminder: %{
+ action: :insert,
+ valid?: false,
+ errors: reminder_errors,
+ changes: %{
+ channel: %{
+ action: :insert,
+ valid?: false,
+ errors: channel_errors
+ },
+ contexts: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context1_errors,
+ changes: %{
+ country: %{
+ action: :insert,
+ valid?: false,
+ errors: country_errors
+ }
+ }
+ },
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context2_errors
+ }
+ ]
+ }
+ }
+ }
+ }} = insert_result
+
+ assert [] = errors
+
+ assert [date: {"can't be blank", [validation: :required]}] = reminder_errors
+
+ assert %{
+ number: {"can't be blank", [validation: :required]},
+ country_code: {"can't be blank", [validation: :required]},
+ provider: {"can't be blank", [validation: :required]}
+ } = Map.new(channel_errors)
+
+ assert [] = context1_errors
+ assert %{address: {"can't be blank", [validation: :required]}} = Map.new(context2_errors)
+ assert %{name: {"can't be blank", [validation: :required]}} = Map.new(country_errors)
+
+ traverse_errors_fun =
+ if polymorphic?(generator) do
+ &PolymorphicEmbed.traverse_errors/2
+ else
+ &Ecto.Changeset.traverse_errors/2
+ end
+
+ %{
+ embedded_reminder: %{
+ channel: %{
+ country_code: ["can't be blank"],
+ number: ["can't be blank"],
+ provider: ["can't be blank"]
+ },
+ contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
+ date: ["can't be blank"]
+ }
+ } =
+ traverse_errors_fun.(
+ changeset,
+ fn {msg, opts} ->
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", to_string(value))
+ end)
+ end
+ )
+ end
+ end
+
+ test "traverse_errors on changesets with valid polymorphic structs" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms",
+ number: "02/807.05.53",
+ country_code: 1,
+ provider: %{__type__: "twilio", api_key: "somekey"}
+ },
+ contexts: [
+ %{
+ __type__: "location",
+ address: "hello",
+ country: %{
+ name: ""
+ }
+ },
+ %{
+ __type__: "location",
+ address: ""
+ }
+ ]
+ }
+
+ changeset =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+
+ insert_result = Repo.insert(changeset)
+
+ assert {:error,
+ %Ecto.Changeset{
+ action: :insert,
+ valid?: false,
+ errors: errors,
+ changes: %{
+ contexts: [
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context1_errors,
+ changes: %{
+ country: %{
+ action: :insert,
+ valid?: false,
+ errors: country_errors
+ }
+ }
+ },
+ %{
+ action: :insert,
+ valid?: false,
+ errors: context2_errors
+ }
+ ]
+ }
+ }} = insert_result
+
+ assert [date: {"can't be blank", [validation: :required]}] = changeset.errors
+ assert [date: {"can't be blank", [validation: :required]}] = errors
+
+ assert [] = context1_errors
+ assert %{address: {"can't be blank", [validation: :required]}} = Map.new(context2_errors)
+ assert %{name: {"can't be blank", [validation: :required]}} = Map.new(country_errors)
+
+ traverse_errors_fun =
+ if polymorphic?(generator) do
+ &PolymorphicEmbed.traverse_errors/2
+ else
+ &Ecto.Changeset.traverse_errors/2
+ end
+
+ %{
+ contexts: [%{country: %{name: ["can't be blank"]}}, %{address: ["can't be blank"]}],
+ date: ["can't be blank"]
+ } =
+ traverse_errors_fun.(
+ changeset,
+ fn {msg, opts} ->
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", to_string(value))
+ end)
+ end
+ )
+ 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)
+
+ 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)
@@ -497,10 +1164,238 @@ defmodule PolymorphicEmbedTest do
reminder_module = get_module(Reminder, generator)
email_module = get_module(Channel.Email, generator)
- attrs = %{
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel: %{
+ address: "john@example.com",
+ valid: true,
+ confirmed: false
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an Email reminder")
+ |> Repo.one()
+
+ assert email_module == reminder.channel.__struct__
+ end
+
+ test "wrong type as string adds error in changeset" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel: %{
+ my_type_field: "unknown type"
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
+
+ assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result
+ end
+
+ test "wrong type as string raises" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms",
+ number: "02/807.05.53",
+ country_code: 1,
+ result: %{success: true},
+ attempts: [],
+ provider: %{
+ __type__: "unknown type",
+ api_key: "foo"
+ }
+ }
+ }
+
+ assert_raise RuntimeError, ~r"could not infer polymorphic embed from data", fn ->
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+ end
+ end
+
+ test "pass non-changeset as first argument to cast_polymorphic_embed/3 should fail" do
+ generator = :polymorphic
+
+ 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
+ end
+
+ test "cast embed after change/2 call should succeed" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ 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
+
+ assert changeset.valid?
+ assert map_size(changeset.changes) == 0
+ end
+ end
+
+ test "loading a nil embed" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ insert_result =
+ struct(reminder_module,
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder #{generator}",
+ channel: nil
+ )
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
+ |> Repo.one()
+
+ assert is_nil(reminder.channel)
+ end
+ end
+
+ test "casting a nil embed" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder #{generator}",
+ channel: nil
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
+ |> Repo.one()
+
+ assert is_nil(reminder.channel)
+ end
+ end
+
+ test "required true" 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,
+ attempts: [],
+ provider: nil
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert {:error,
+ %{
+ valid?: false,
+ changes: %{
+ channel: %{
+ valid?: false,
+ errors: [provider: {"can't be blank", [validation: :required]}]
+ }
+ }
+ }} = insert_result
+ end
+ end
+
+ test "custom changeset by passing function" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, 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,
+ attempts: [],
+ provider: %{__type__: "twilio", api_key: "somekey"},
+ custom: true
+ }
+ }
+
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.custom_changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert {:ok, reminder} = insert_result
+ assert reminder.channel.custom
+
+ %reminder_module{} = reminder
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
+ |> Repo.one()
+
+ assert sms_module == reminder.channel.__struct__
+ end
+ end
+
+ test "with option but not for all" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+ email_module = get_module(Channel.Email, generator)
+
+ sms_reminder_attrs = %{
date: ~U[2020-05-28 02:57:19Z],
text: "This is an Email reminder",
channel: %{
+ my_type_field: "email",
address: "john@example.com",
valid: true,
confirmed: false
@@ -509,10 +1404,12 @@ defmodule PolymorphicEmbedTest do
insert_result =
struct(reminder_module)
- |> reminder_module.changeset(attrs)
+ |> reminder_module.custom_changeset(sms_reminder_attrs)
|> Repo.insert()
- assert {:ok, %reminder_module{}} = insert_result
+ assert {:ok, reminder} = insert_result
+
+ %reminder_module{} = reminder
reminder =
reminder_module
@@ -522,27 +1419,272 @@ defmodule PolymorphicEmbedTest do
assert email_module == reminder.channel.__struct__
end
- test "wrong type as string adds error in changeset" do
+ test "setting embed to nil" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, generator)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ channel: nil
+ }
+
+ insert_result =
+ struct(reminder_module,
+ channel:
+ struct(sms_module,
+ number: "02/807.05.53",
+ country_code: 32
+ )
+ )
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
+ |> Repo.one()
+
+ assert is_nil(reminder.channel)
+ end
+ end
+
+ test "omitting embed field in cast" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, generator)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder #{generator}"
+ }
+
+ insert_result =
+ struct(reminder_module,
+ channel:
+ struct(sms_module,
+ number: "02/807.05.53"
+ )
+ )
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
+
+ assert {:ok, %reminder_module{}} = insert_result
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
+ |> Repo.one()
+
+ refute is_nil(reminder.channel)
+ end
+ end
+
+ test "keep existing data" 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"
+ ),
+ number: "02/807.05.53",
+ country_code: 32,
+ 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
+ |> reminder_module.changeset(%{})
+ |> Repo.insert!()
+
+ changeset =
+ reminder
+ |> reminder_module.changeset(%{
+ channel: %{
+ number: "54"
+ }
+ })
+
+ changeset |> Repo.update!()
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
+ |> Repo.one()
+
+ assert reminder.channel.result.success
+ end
+ end
+
+ test "params with string keys" 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"
+ ),
+ number: "02/807.05.53",
+ country_code: 32,
+ 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
+ |> reminder_module.changeset(%{})
+ |> Repo.insert!()
+
+ changeset =
+ reminder
+ |> reminder_module.changeset(%{
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "54"
+ }
+ })
+
+ Repo.update!(changeset)
+
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
+ |> Repo.one()
+
+ assert reminder.channel.result.success
+ end
+ end
+
+ test "missing __type__ leads to changeset error" do
generator = :polymorphic
reminder_module = get_module(Reminder, generator)
- attrs = %{
+ sms_reminder_attrs = %{
date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder",
+ text: "This is an SMS reminder",
channel: %{
- my_type_field: "unknown type"
+ 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()
+
+ assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result
+ end
+
+ test "missing __type__ nilifies" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+
+ sms_reminder_attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder",
+ 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"
+ },
+ fallback_provider: %{
+ api_key: "foo"
+ }
}
}
insert_result =
struct(reminder_module)
- |> reminder_module.changeset(attrs)
+ |> reminder_module.changeset(sms_reminder_attrs)
|> Repo.insert()
- assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result
+ assert {:ok, %{channel: %{fallback_provider: nil}}} = insert_result
end
- test "wrong type as string raises" do
+ test "missing __type__ leads to raising error" do
generator = :polymorphic
reminder_module = get_module(Reminder, generator)
@@ -554,9 +1696,21 @@ defmodule PolymorphicEmbedTest do
number: "02/807.05.53",
country_code: 1,
result: %{success: true},
- attempts: [],
+ 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__: "unknown type",
api_key: "foo"
}
}
@@ -569,641 +1723,637 @@ defmodule PolymorphicEmbedTest do
end
end
- test "pass non-changeset as first argument to cast_polymorphic_embed/3 should fail" do
+ test "cannot load the right struct" do
generator = :polymorphic
-
reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, 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
- end
-
- test "cast embed after change/2 call should succeed" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
-
- 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
-
- assert changeset.valid?
- assert map_size(changeset.changes) == 0
- end
- end
-
- test "loading a nil embed" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
-
- insert_result =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder #{generator}",
- channel: nil
+ struct(reminder_module,
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder",
+ channel:
+ struct(sms_module,
+ country_code: 1,
+ number: "02/807.05.53"
)
- |> Repo.insert()
-
- assert {:ok, %reminder_module{}} = insert_result
+ )
+ |> reminder_module.changeset(%{})
+ |> Repo.insert()
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
- |> Repo.one()
+ Ecto.Adapters.SQL.query!(
+ Repo,
+ "UPDATE reminders SET channel = jsonb_set(channel, '{my_type_field}', '\"foo\"')",
+ []
+ )
- assert is_nil(reminder.channel)
+ assert_raise RuntimeError, ~r"could not infer polymorphic embed from data .* \"foo\"", fn ->
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder")
+ |> Repo.one()
end
end
- test "casting a nil embed" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
-
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder #{generator}",
- channel: nil
- }
-
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ test "cannot load the right struct but don't raise exception" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, generator)
- assert {:ok, %reminder_module{}} = insert_result
+ struct(reminder_module,
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder",
+ channel:
+ struct(sms_module,
+ country_code: 1,
+ number: "02/807.05.53"
+ )
+ )
+ |> reminder_module.changeset(%{})
+ |> Repo.insert()
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
- |> Repo.one()
+ Ecto.Adapters.SQL.query!(
+ Repo,
+ "UPDATE reminders SET channel = jsonb_set(channel, '{my_type_field}', '\"some_deprecated_type\"')",
+ []
+ )
- assert is_nil(reminder.channel)
- end
+ assert %{channel: %{"my_type_field" => "some_deprecated_type"}} =
+ reminder_module
+ |> QueryBuilder.where(text: "This is an SMS reminder")
+ |> Repo.one()
end
- test "required true" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ test "changing type" do
+ generator = :polymorphic
+ reminder_module = get_module(Reminder, generator)
+ sms_module = get_module(Channel.SMS, 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,
- attempts: [],
- provider: nil
- }
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel: %{
+ address: "john@example.com",
+ valid: true,
+ confirmed: false
}
+ }
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(sms_reminder_attrs)
- |> Repo.insert()
-
- assert {:error,
- %{
- valid?: false,
- changes: %{
- channel: %{
- valid?: false,
- errors: [provider: {"can't be blank", [validation: :required]}]
- }
- }
- }} = insert_result
- end
- end
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
- test "custom changeset by passing MFA" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
+ assert {:ok, %reminder_module{} = reminder} = insert_result
- 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,
- attempts: [],
- provider: %{__type__: "twilio", api_key: "somekey"},
- custom: true
+ update_attrs = %{
+ date: ~U[2020-05-29 02:57:19Z],
+ text: "This is an SMS reminder",
+ channel: %{
+ my_type_field: "sms",
+ number: "02/807.05.53",
+ country_code: 1,
+ attempts: [],
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
}
}
+ }
- insert_result =
- struct(reminder_module)
- |> reminder_module.custom_changeset(sms_reminder_attrs)
- |> Repo.insert()
-
- assert {:ok, reminder} = insert_result
- assert reminder.channel.custom
+ update_result =
+ reminder
+ |> reminder_module.changeset(update_attrs)
+ |> Repo.update()
- %reminder_module{} = reminder
+ assert {:ok, %reminder_module{}} = update_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")
+ |> Repo.one()
- assert sms_module == reminder.channel.__struct__
- end
+ assert sms_module == reminder.channel.__struct__
end
- test "custom changeset by passing function" do
+ test "supports lists of polymorphic embeds" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
- sms_reminder_attrs = %{
+ attrs = %{
date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}",
+ text: "This is a reminder with multiple contexts #{generator}",
channel: %{
my_type_field: "sms",
number: "02/807.05.53",
country_code: 1,
- attempts: [],
- provider: %{__type__: "twilio", api_key: "somekey"},
- custom: true
- }
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ },
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone",
+ address: "address"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius",
+ address: "address"
+ }
+ ],
+ contexts2: nil,
+ contexts3: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone"
+ },
+ %{
+ __type__: "device",
+ ref: "56789",
+ type: "laptop"
+ }
+ ]
}
- insert_result =
+ reminder =
struct(reminder_module)
- |> reminder_module.custom_changeset2(sms_reminder_attrs)
- |> Repo.insert()
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert!()
- assert {:ok, reminder} = insert_result
- assert reminder.channel.custom
+ Enum.each(reminder.contexts, fn context ->
+ assert Map.has_key?(context, :id)
+ end)
- %reminder_module{} = reminder
+ Enum.each(reminder.contexts3, fn context ->
+ refute Map.has_key?(context, :id)
+ end)
reminder =
reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
+ |> QueryBuilder.where(text: "This is a reminder with multiple contexts #{generator}")
|> Repo.one()
- assert sms_module == reminder.channel.__struct__
- end
- end
+ assert reminder.contexts |> length() == 2
- test "with option but not for all" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
- email_module = get_module(Channel.Email, generator)
+ Enum.each(reminder.contexts, fn context ->
+ assert Map.has_key?(context, :id)
+ end)
- sms_reminder_attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder",
- channel: %{
- my_type_field: "email",
- address: "john@example.com",
- valid: true,
- confirmed: false
+ if polymorphic?(generator) do
+ assert Enum.at(reminder.contexts, 0).ref == "12345"
+ assert Enum.at(reminder.contexts, 0).type == "cellphone"
+ assert Enum.at(reminder.contexts, 1).age == "aquarius"
+ else
+ assert Enum.at(reminder.contexts, 0).address == "address"
+ assert Enum.at(reminder.contexts, 1).address == "address"
+ end
+
+ # add new list of contexts and assert that we have different ids
+
+ attrs = %{
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone",
+ address: "address"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius",
+ address: "address"
+ }
+ ]
}
- }
- insert_result =
- struct(reminder_module)
- |> reminder_module.custom_changeset2(sms_reminder_attrs)
- |> Repo.insert()
+ updated_reminder =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.update!()
- assert {:ok, reminder} = insert_result
+ assert Enum.at(reminder.contexts, 0).id != Enum.at(updated_reminder.contexts, 0).id
+ assert Enum.at(reminder.contexts, 1).id != Enum.at(updated_reminder.contexts, 1).id
- %reminder_module{} = reminder
+ # Assert that we have same ids when the provided context element has an id
+ attrs = %{
+ contexts: [
+ %{
+ __type__: "device",
+ id: Enum.at(reminder.contexts, 0).id,
+ ref: "12345",
+ type: "cellphone",
+ address: "address"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius",
+ address: "address"
+ }
+ ]
+ }
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an Email reminder")
- |> Repo.one()
+ updated_reminder =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.update!()
- assert email_module == reminder.channel.__struct__
+ assert Enum.at(reminder.contexts, 0).id == Enum.at(updated_reminder.contexts, 0).id
+ assert Enum.at(reminder.contexts, 1).id != Enum.at(updated_reminder.contexts, 1).id
+
+ # Make sure it also works for embeds without ids (`@primary_key false`)
+ attrs = %{
+ contexts3: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone"
+ },
+ %{
+ __type__: "device",
+ ref: "56789",
+ type: "laptop"
+ }
+ ]
+ }
+
+ assert {:ok, _} =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.update()
+
+ # Make sure it works for embeds with nil entries
+ attrs = %{
+ contexts2: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone"
+ },
+ %{
+ __type__: "device",
+ ref: "56789",
+ type: "laptop"
+ }
+ ]
+ }
+
+ assert {:ok, _} =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.update()
+ end
end
- test "setting embed to nil" do
+ test "generate ID for single embed in data" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
sms_module = get_module(Channel.SMS, generator)
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}",
- channel: nil
- }
-
- insert_result =
+ struct =
struct(reminder_module,
- channel:
- struct(sms_module,
- number: "02/807.05.53",
- country_code: 32
- )
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ channel: struct(sms_module)
)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
- assert {:ok, %reminder_module{}} = insert_result
+ changeset = reminder_module.changeset(struct, %{})
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
- |> Repo.one()
+ if polymorphic?(generator) do
+ assert changeset.changes.channel.id
+ else
+ assert map_size(changeset.changes) == 0
+ end
- assert is_nil(reminder.channel)
+ struct = Repo.insert!(changeset)
+
+ if polymorphic?(generator) do
+ assert changeset.changes.channel.id == struct.channel.id
+ else
+ assert struct.channel.id
+ end
end
end
- test "omitting embed field in cast" do
+ test "generate ID for single embed in changes" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
-
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder #{generator}"
- }
- insert_result =
+ struct =
struct(reminder_module,
- channel:
- struct(sms_module,
- number: "02/807.05.53"
- )
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}"
)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
- assert {:ok, %reminder_module{}} = insert_result
+ changeset =
+ reminder_module.changeset(
+ struct,
+ %{
+ channel: %{
+ my_type_field: "sms",
+ number: "111",
+ country_code: 1,
+ provider: %{
+ __type__: "twilio",
+ api_key: "foo"
+ }
+ }
+ }
+ )
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an Email reminder #{generator}")
- |> Repo.one()
+ if polymorphic?(generator) do
+ assert changeset.changes.channel.id
+ else
+ refute Map.has_key?(changeset.changes.channel, :id)
+ end
- refute is_nil(reminder.channel)
+ struct = Repo.insert!(changeset)
+
+ if polymorphic?(generator) do
+ assert changeset.changes.channel.id == struct.channel.id
+ else
+ assert struct.channel.id
+ end
end
end
- test "keep existing data" do
+ test "generate ID for list of embeds in data" 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)
+ location_module = get_module(Reminder.Context.Location, generator)
- reminder =
+ struct =
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"
- ),
- number: "02/807.05.53",
- country_code: 32,
- 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
- |> reminder_module.changeset(%{})
- |> Repo.insert!()
+ contexts: [
+ struct(location_module),
+ struct(location_module)
+ ]
+ )
- changeset =
- reminder
- |> reminder_module.changeset(%{
- channel: %{
- number: "54"
- }
- })
+ changeset = reminder_module.changeset(struct, %{})
- changeset |> Repo.update!()
+ if polymorphic?(generator) do
+ assert Enum.at(changeset.changes.contexts, 0).id
+ assert Enum.at(changeset.changes.contexts, 1).id
+ else
+ assert map_size(changeset.changes) == 0
+ end
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
- |> Repo.one()
+ struct = Repo.insert!(changeset)
- assert reminder.channel.result.success
+ if polymorphic?(generator) do
+ assert Enum.at(changeset.changes.contexts, 0).id == Enum.at(struct.contexts, 0).id
+ assert Enum.at(changeset.changes.contexts, 1).id == Enum.at(struct.contexts, 1).id
+ else
+ assert Enum.at(struct.contexts, 0).id
+ assert Enum.at(struct.contexts, 1).id
+ end
end
end
- test "params with string keys" do
+ test "generate ID for list of embeds in changes" 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 =
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"
- ),
- number: "02/807.05.53",
- country_code: 32,
- 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 #{generator}"
)
- reminder =
- reminder
- |> reminder_module.changeset(%{})
- |> Repo.insert!()
-
changeset =
- reminder
- |> reminder_module.changeset(%{
- "channel" => %{
- "my_type_field" => "sms",
- "number" => "54"
+ reminder_module.changeset(
+ struct,
+ %{
+ contexts: [
+ %{__type__: "location", address: "A"},
+ %{__type__: "location", address: "B"}
+ ]
}
- })
+ )
- Repo.update!(changeset)
+ if polymorphic?(generator) do
+ assert Enum.at(changeset.changes.contexts, 0).id
+ assert Enum.at(changeset.changes.contexts, 1).id
+ else
+ refute Map.has_key?(Enum.at(changeset.changes.contexts, 0), :id)
+ end
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder #{generator}")
- |> Repo.one()
+ struct = Repo.insert!(changeset)
- assert reminder.channel.result.success
+ if polymorphic?(generator) do
+ assert Enum.at(changeset.changes.contexts, 0).id == Enum.at(struct.contexts, 0).id
+ assert Enum.at(changeset.changes.contexts, 1).id == Enum.at(struct.contexts, 1).id
+ else
+ assert Enum.at(struct.contexts, 0).id
+ assert Enum.at(struct.contexts, 1).id
+ end
end
end
- test "missing __type__ leads to changeset error" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
+ test "validates lists of polymorphic embeds" 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",
- channel: %{
- number: "02/807.05.53",
- country_code: 1,
- result: %{success: true},
- attempts: [
- %{
- date: ~U[2020-05-28 07:27:05Z],
- result: %{success: true}
- },
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is a reminder with multiple contexts",
+ contexts: [
%{
- date: ~U[2020-05-29 07:27:05Z],
- result: %{success: false}
+ ref: "12345",
+ type: "cellphone"
},
%{
- date: ~U[2020-05-30 07:27:05Z],
- result: %{success: true}
+ age: "aquarius"
}
- ],
- 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(attrs)
+ |> Repo.insert()
- assert {:error, %Ecto.Changeset{errors: [channel: {"is invalid", []}]}} = insert_result
- end
+ if polymorphic?(generator) do
+ assert {:error, %Ecto.Changeset{valid?: false, errors: [contexts: {"is invalid", _}]}} =
+ insert_result
+ else
+ assert {:error,
+ %Ecto.Changeset{
+ valid?: false,
+ errors: errors,
+ changes: %{contexts: [%{errors: location_errors} | _]}
+ }} = insert_result
- test "missing __type__ nilifies" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
+ assert [] = errors
+ assert %{address: {"can't be blank", [validation: :required]}} = Map.new(location_errors)
+ end
- sms_reminder_attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder",
- 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"
- },
- fallback_provider: %{
- api_key: "foo"
+ if polymorphic?(generator) do
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is a reminder with multiple contexts",
+ contexts2: [
+ %{
+ ref: "12345",
+ type: "cellphone",
+ address: "address"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius",
+ address: "address"
+ }
+ ]
}
- }
- }
-
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(sms_reminder_attrs)
- |> Repo.insert()
- assert {:ok, %{channel: %{fallback_provider: nil}}} = insert_result
- end
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
- test "missing __type__ leads to raising error" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
+ assert {:ok,
+ %{
+ contexts2: [
+ %{
+ age: "aquarius"
+ }
+ ]
+ }} = insert_result
- sms_reminder_attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder",
- 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: %{
- api_key: "foo"
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is a reminder with multiple contexts",
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "12345"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius"
+ }
+ ]
}
- }
- }
- assert_raise RuntimeError, ~r"could not infer polymorphic embed from data", fn ->
- struct(reminder_module)
- |> reminder_module.changeset(sms_reminder_attrs)
- |> Repo.insert()
- end
- end
+ insert_result =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
- test "cannot load the right struct" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
+ assert {:error,
+ %Ecto.Changeset{
+ valid?: false,
+ action: :insert,
+ errors: errors,
+ changes: %{contexts: [%{errors: device_errors, action: :insert} | _]}
+ }} = insert_result
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder",
- channel:
- struct(sms_module,
- country_code: 1,
- number: "02/807.05.53"
- )
- )
- |> reminder_module.changeset(%{})
- |> Repo.insert()
+ assert [] = errors
+ assert %{type: {"can't be blank", [validation: :required]}} = Map.new(device_errors)
- Ecto.Adapters.SQL.query!(
- Repo,
- "UPDATE reminders SET channel = jsonb_set(channel, '{my_type_field}', '\"foo\"')",
- []
- )
+ device_module = get_module(Reminder.Context.Device, generator)
- assert_raise RuntimeError, ~r"could not infer polymorphic embed from data .* \"foo\"", fn ->
- reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder")
- |> Repo.one()
- end
- end
+ reminder =
+ struct(reminder_module,
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an SMS reminder #{generator}",
+ contexts: [
+ struct(device_module, ref: "12345")
+ ]
+ )
- test "changing type" do
- generator = :polymorphic
- reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
+ attrs = %{
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "54321"
+ },
+ %{
+ __type__: "age",
+ age: "aquarius"
+ }
+ ]
+ }
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an Email reminder",
- channel: %{
- address: "john@example.com",
- valid: true,
- confirmed: false
- }
- }
+ insert_result =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert()
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ assert {:error,
+ %Ecto.Changeset{
+ valid?: false,
+ action: :insert,
+ errors: errors,
+ changes: %{contexts: [%{errors: device_errors, action: :insert} | _]}
+ }} = insert_result
- assert {:ok, %reminder_module{} = reminder} = insert_result
+ assert [] = errors
+ assert %{type: {"can't be blank", [validation: :required]}} = Map.new(device_errors)
+ end
+ end
+ end
- update_attrs = %{
- date: ~U[2020-05-29 02:57:19Z],
- text: "This is an SMS reminder",
- channel: %{
- my_type_field: "sms",
- number: "02/807.05.53",
- country_code: 1,
- attempts: [],
- provider: %{
- __type__: "twilio",
- api_key: "foo"
- }
- }
- }
+ test "list of embeds defaults to []" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
- update_result =
- reminder
- |> reminder_module.changeset(update_attrs)
- |> Repo.update()
+ assert struct(reminder_module).contexts == []
+ end
+ end
- assert {:ok, %reminder_module{}} = update_result
+ test "list of embeds defaults to [] after insert" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
- reminder =
- reminder_module
- |> QueryBuilder.where(text: "This is an SMS reminder")
- |> Repo.one()
+ sms_reminder_attrs = %{
+ text: "This is an SMS reminder #{generator}",
+ date: DateTime.utc_now()
+ }
- assert sms_module == reminder.channel.__struct__
+ assert {:ok, inserted_result} =
+ struct(reminder_module)
+ |> reminder_module.changeset(sms_reminder_attrs)
+ |> Repo.insert()
+
+ assert inserted_result.contexts == []
+ end
end
- test "supports lists of polymorphic embeds" do
+ test "supports map with number keys" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is a reminder with multiple contexts #{generator}",
- channel: %{
- my_type_field: "sms",
- number: "02/807.05.53",
- country_code: 1,
- provider: %{
- __type__: "twilio",
- api_key: "foo"
+ "date" => ~U[2020-05-28 02:57:19Z],
+ "text" => "This is a reminder with multiple contexts #{generator}",
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "02/807.05.53",
+ "country_code" => 1,
+ "provider" => %{
+ "__type__" => "twilio",
+ "api_key" => "foo"
}
},
- contexts: [
- %{
- __type__: "device",
- ref: "12345",
- type: "cellphone",
- address: "address"
+ "contexts" => %{
+ "0" => %{
+ "__type__" => "device",
+ "ref" => "12345",
+ "type" => "cellphone",
+ "address" => "address"
},
- %{
- __type__: "age",
- age: "aquarius",
- address: "address"
+ "1" => %{
+ "__type__" => "age",
+ "age" => "aquarius",
+ "address" => "address"
}
- ]
+ }
}
reminder =
@@ -1238,19 +2388,19 @@ defmodule PolymorphicEmbedTest do
# add new list of contexts and assert that we have different ids
attrs = %{
- contexts: [
- %{
- __type__: "device",
- ref: "12345",
- type: "cellphone",
- address: "address"
+ "contexts" => %{
+ "0" => %{
+ "__type__" => "device",
+ "ref" => "12345",
+ "type" => "cellphone",
+ "address" => "address"
},
- %{
- __type__: "age",
- age: "aquarius",
- address: "address"
+ "1" => %{
+ "__type__" => "age",
+ "age" => "aquarius",
+ "address" => "address"
}
- ]
+ }
}
updated_reminder =
@@ -1263,332 +2413,417 @@ defmodule PolymorphicEmbedTest do
end
end
- test "generate ID for single embed in data" do
+ test "embeds_many with sort_param and drop_param" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
- sms_module = get_module(Channel.SMS, generator)
-
- struct =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}",
- channel: struct(sms_module)
- )
-
- changeset = reminder_module.changeset(struct, %{})
-
- if polymorphic?(generator) do
- assert changeset.changes.channel.id
- else
- assert map_size(changeset.changes) == 0
- end
-
- struct = Repo.insert!(changeset)
-
- if polymorphic?(generator) do
- assert changeset.changes.channel.id == struct.channel.id
- else
- assert struct.channel.id
- end
- end
- end
- test "generate ID for single embed in changes" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ attrs = %{
+ "date" => ~U[2020-05-28 02:57:19Z],
+ "text" => "This is a reminder with multiple contexts #{generator}",
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "02/807.05.53",
+ "country_code" => 1,
+ "provider" => %{
+ "__type__" => "twilio",
+ "api_key" => "foo"
+ }
+ },
+ "contexts" => %{
+ "0" => %{
+ "__type__" => "device",
+ "ref" => "12345",
+ "type" => "cellphone",
+ "address" => "address"
+ },
+ "1" => %{
+ "__type__" => "age",
+ "age" => "aquarius",
+ "address" => "address"
+ },
+ "2" => %{
+ "__type__" => "age",
+ "age" => "aquarius_drop",
+ "address" => "address_drop"
+ }
+ },
+ "contexts_drop" => ["2"],
+ "contexts_sort" => ["1", "0", "2"]
+ }
- struct =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}"
- )
+ reminder =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+ |> Repo.insert!()
- changeset =
- reminder_module.changeset(
- struct,
- %{
- channel: %{
- my_type_field: "sms",
- number: "111",
- country_code: 1,
- provider: %{
- __type__: "twilio",
- api_key: "foo"
- }
- }
- }
- )
+ Enum.each(reminder.contexts, fn context ->
+ assert context.id
+ end)
- if polymorphic?(generator) do
- assert changeset.changes.channel.id
- else
- refute Map.has_key?(changeset.changes.channel, :id)
- end
+ reminder =
+ reminder_module
+ |> QueryBuilder.where(text: "This is a reminder with multiple contexts #{generator}")
+ |> Repo.one()
- struct = Repo.insert!(changeset)
+ assert reminder.contexts |> length() == 2
+
+ Enum.each(reminder.contexts, fn context ->
+ assert context.id
+ end)
if polymorphic?(generator) do
- assert changeset.changes.channel.id == struct.channel.id
+ assert Enum.at(reminder.contexts, 1).ref == "12345"
+ assert Enum.at(reminder.contexts, 1).type == "cellphone"
+ assert Enum.at(reminder.contexts, 0).age == "aquarius"
else
- assert struct.channel.id
+ assert Enum.at(reminder.contexts, 1).address == "address"
+ assert Enum.at(reminder.contexts, 0).address == "address"
end
+
+ # add new list of contexts and assert that we have different ids
+
+ attrs = %{
+ "contexts" => %{
+ "0" => %{
+ "__type__" => "device",
+ "ref" => "12345",
+ "type" => "cellphone",
+ "address" => "address"
+ },
+ "1" => %{
+ "__type__" => "age",
+ "age" => "aquarius",
+ "address" => "address"
+ }
+ }
+ }
+
+ updated_reminder =
+ reminder
+ |> reminder_module.changeset(attrs)
+ |> Repo.update!()
+
+ assert Enum.at(reminder.contexts, 0).id != Enum.at(updated_reminder.contexts, 0).id
+ assert Enum.at(reminder.contexts, 1).id != Enum.at(updated_reminder.contexts, 1).id
end
end
- test "generate ID for list of embeds in data" do
+ test "embeds_many with new sort_param" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
- location_module = get_module(Reminder.Context.Location, generator)
-
- struct =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}",
- contexts: [
- struct(location_module),
- struct(location_module)
- ]
- )
- changeset = reminder_module.changeset(struct, %{})
-
- if polymorphic?(generator) do
- assert Enum.at(changeset.changes.contexts, 0).id
- assert Enum.at(changeset.changes.contexts, 1).id
- else
- assert map_size(changeset.changes) == 0
- end
+ attrs = %{
+ "date" => ~U[2020-05-28 02:57:19Z],
+ "text" => "This is a reminder with multiple contexts #{generator}",
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "02/807.05.53",
+ "country_code" => 1,
+ "provider" => %{
+ "__type__" => "twilio",
+ "api_key" => "foo"
+ }
+ },
+ "contexts" => %{
+ "0" => %{
+ "__type__" => "device",
+ "ref" => "12345",
+ "type" => "cellphone",
+ "address" => "address"
+ },
+ "1" => %{
+ "__type__" => "age",
+ "age" => "aquarius",
+ "address" => "address"
+ },
+ "2" => %{
+ "__type__" => "age",
+ "age" => "aquarius_drop",
+ "address" => "address_drop"
+ }
+ },
+ "contexts_drop" => ["2"],
+ "contexts_sort" => ["1", "0", "2", "new"]
+ }
- struct = Repo.insert!(changeset)
+ assert changeset =
+ %Ecto.Changeset{valid?: false} =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
- if polymorphic?(generator) do
- assert Enum.at(changeset.changes.contexts, 0).id == Enum.at(struct.contexts, 0).id
- assert Enum.at(changeset.changes.contexts, 1).id == Enum.at(struct.contexts, 1).id
- else
- assert Enum.at(struct.contexts, 0).id
- assert Enum.at(struct.contexts, 1).id
- end
+ assert Enum.at(changeset.changes.contexts, 2).errors == [
+ address: {"can't be blank", [validation: :required]}
+ ]
end
end
- test "generate ID for list of embeds in changes" do
+ test "embeds_many with sort_param but no assoc param" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
- struct =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}"
- )
-
- changeset =
- reminder_module.changeset(
- struct,
- %{
- contexts: [
- %{__type__: "location", address: "A"},
- %{__type__: "location", address: "B"}
- ]
+ attrs = %{
+ "date" => ~U[2020-05-28 02:57:19Z],
+ "text" => "This is a reminder with multiple contexts #{generator}",
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "02/807.05.53",
+ "country_code" => 1,
+ "provider" => %{
+ "__type__" => "twilio",
+ "api_key" => "foo"
}
- )
-
- if polymorphic?(generator) do
- assert Enum.at(changeset.changes.contexts, 0).id
- assert Enum.at(changeset.changes.contexts, 1).id
- else
- refute Map.has_key?(Enum.at(changeset.changes.contexts, 0), :id)
- end
+ },
+ "contexts_drop" => [],
+ "contexts_sort" => ["on"]
+ }
- struct = Repo.insert!(changeset)
+ assert changeset =
+ %Ecto.Changeset{valid?: false} =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
- if polymorphic?(generator) do
- assert Enum.at(changeset.changes.contexts, 0).id == Enum.at(struct.contexts, 0).id
- assert Enum.at(changeset.changes.contexts, 1).id == Enum.at(struct.contexts, 1).id
- else
- assert Enum.at(struct.contexts, 0).id
- assert Enum.at(struct.contexts, 1).id
- end
+ assert Enum.at(changeset.changes.contexts, 0).errors == [
+ address: {"can't be blank", [validation: :required]}
+ ]
end
end
- test "validates lists of polymorphic embeds" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ test "embeds_many with sort_param but no assoc param (sort_create function)" do
+ reminder_module = get_module(Reminder, :polymorphic)
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is a reminder with multiple contexts",
+ attrs = %{
+ "date" => ~U[2020-05-28 02:57:19Z],
+ "text" => "This is a reminder with multiple contexts",
+ "channel" => %{
+ "my_type_field" => "sms",
+ "number" => "02/807.05.53",
+ "country_code" => 1,
+ "provider" => %{
+ "__type__" => "twilio",
+ "api_key" => "foo"
+ }
+ },
+ "contexts2_drop" => [],
+ "contexts2_sort" => ["on"]
+ }
+
+ assert changeset =
+ %Ecto.Changeset{valid?: false} =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+
+ assert Enum.at(changeset.changes.contexts2, 0).errors == [
+ address: {"can't be blank", [validation: :required]}
+ ]
+ end
+
+ describe "polymorphic_embed_inputs_for/1" do
+ test "errors in form for polymorphic embed and nested embed" do
+ reminder_module = get_module(Reminder, :polymorphic)
+
+ sms_reminder_attrs = %{
+ text: "This is an SMS reminder",
contexts: [
%{
- ref: "12345",
- type: "cellphone"
- },
- %{
- age: "aquarius"
+ __type__: "device",
+ extra: %{}
}
]
}
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(sms_reminder_attrs)
- if polymorphic?(generator) do
- assert {:error, %Ecto.Changeset{valid?: false, errors: [contexts: {"is invalid", _}]}} =
- insert_result
- else
- assert {:error,
- %Ecto.Changeset{
- valid?: false,
- errors: errors,
- changes: %{contexts: [%{errors: location_errors} | _]}
- }} = insert_result
+ changeset = %{changeset | action: :insert}
- assert [] = errors
- assert %{address: {"can't be blank", [validation: :required]}} = Map.new(location_errors)
- end
+ html_string =
+ render_component(
+ &liveview_form_with_inputs_for/1,
+ %{changeset: changeset, field: :contexts}
+ )
- if polymorphic?(generator) do
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is a reminder with multiple contexts",
- contexts2: [
- %{
- ref: "12345",
- type: "cellphone",
- address: "address"
- },
- %{
- __type__: "age",
- age: "aquarius",
- address: "address"
- }
- ]
- }
+ assert String.contains?(
+ html_string,
+ "[type: {"can't be blank", [validation: :required]}]"
+ )
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ assert String.contains?(
+ html_string,
+ "[imei: {"can't be blank", [validation: :required]}]"
+ )
+ end
- assert {:ok,
- %{
- contexts2: [
- %{
- age: "aquarius"
- }
- ]
- }} = insert_result
+ test "generates forms that can be rendered (custom type field/identify_by_fields)" do
+ reminder_module = get_module(Reminder, :polymorphic)
- attrs = %{
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is a reminder with multiple contexts",
- contexts: [
- %{
- __type__: "device",
- ref: "12345"
- },
- %{
- __type__: "age",
- age: "aquarius"
- }
- ]
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel: %{
+ address: "a",
+ valid: true,
+ confirmed: true
}
+ }
- insert_result =
- struct(reminder_module)
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
- assert {:error,
- %Ecto.Changeset{
- valid?: false,
- action: :insert,
- errors: errors,
- changes: %{contexts: [%{errors: device_errors, action: :insert} | _]}
- }} = insert_result
+ html =
+ render_component(
+ &liveview_form/1,
+ %{changeset: changeset, field: :channel}
+ )
+ |> Floki.parse_fragment!()
- assert [] = errors
- assert %{type: {"can't be blank", [validation: :required]}} = Map.new(device_errors)
+ assert [input] = Floki.find(html, "#reminder_channel_my_type_field")
+ assert Floki.attribute(input, "name") == ["reminder[channel][my_type_field]"]
+ assert Floki.attribute(input, "type") == ["hidden"]
+ assert Floki.attribute(input, "value") == ["email"]
+
+ assert [input] = Floki.find(html, "#reminder_channel_number")
+ assert Floki.attribute(input, "type") == ["text"]
+ end
+
+ test "generates forms that can be rendered (custom type field)" do
+ reminder_module = get_module(Reminder, :polymorphic)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel3: %{
+ my_type_field: "email"
+ }
+ }
+
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
+
+ html =
+ render_component(
+ &liveview_form_component/1,
+ %{changeset: changeset, field: :channel3}
+ )
+ |> Floki.parse_fragment!()
- device_module = get_module(Reminder.Context.Device, generator)
+ assert [input] = Floki.find(html, ~s([name="reminder[channel3][my_type_field]"]))
+ assert Floki.attribute(input, "type") == ["hidden"]
+ assert Floki.attribute(input, "value") == ["email"]
+ end
- reminder =
- struct(reminder_module,
- date: ~U[2020-05-28 02:57:19Z],
- text: "This is an SMS reminder #{generator}",
- contexts: [
- struct(device_module, ref: "12345")
- ]
- )
+ test "generates forms that can be rendered (default type field)" do
+ reminder_module = get_module(Reminder, :polymorphic)
- attrs = %{
- contexts: [
- %{
- __type__: "device",
- ref: "54321"
- },
- %{
- __type__: "age",
- age: "aquarius"
- }
- ]
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel2: %{
+ __type__: "email",
+ address: "a",
+ valid: true,
+ confirmed: true
}
+ }
- insert_result =
- reminder
- |> reminder_module.changeset(attrs)
- |> Repo.insert()
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
- assert {:error,
- %Ecto.Changeset{
- valid?: false,
- action: :insert,
- errors: errors,
- changes: %{contexts: [%{errors: device_errors, action: :insert} | _]}
- }} = insert_result
+ html =
+ render_component(
+ &liveview_form_component/1,
+ %{changeset: changeset, field: :channel2}
+ )
+ |> Floki.parse_fragment!()
- assert [] = errors
- assert %{type: {"can't be blank", [validation: :required]}} = Map.new(device_errors)
- end
+ assert [input] = Floki.find(html, ~s([name="reminder[channel2][__type__]"]))
+ assert Floki.attribute(input, "type") == ["hidden"]
+ assert Floki.attribute(input, "value") == ["email"]
+
+ assert [input] = Floki.find(html, "#reminder_channel2_0_number")
+ assert Floki.attribute(input, "type") == ["text"]
end
end
- test "list of embeds defaults to []" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ describe "polymorphic_embed_inputs_for/2" do
+ test "generates forms that can be rendered (custom type field/identify_by_fields)" do
+ reminder_module = get_module(Reminder, :polymorphic)
- assert struct(reminder_module).contexts == []
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel: %{
+ address: "a",
+ valid: true,
+ confirmed: true
+ }
+ }
+
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
+
+ html =
+ render_component(
+ &liveview_form/1,
+ %{changeset: changeset, field: :channel}
+ )
+ |> Floki.parse_fragment!()
+
+ assert [input] = Floki.find(html, "#reminder_channel_my_type_field")
+ assert Floki.attribute(input, "name") == ["reminder[channel][my_type_field]"]
+ assert Floki.attribute(input, "type") == ["hidden"]
+ assert Floki.attribute(input, "value") == ["email"]
+
+ assert [input] = Floki.find(html, "#reminder_channel_number")
+ assert Floki.attribute(input, "type") == ["text"]
end
- end
- test "list of embeds defaults to [] after insert" do
- for generator <- @generators do
- reminder_module = get_module(Reminder, generator)
+ test "generates forms that can be rendered (custom type field)" do
+ reminder_module = get_module(Reminder, :polymorphic)
- sms_reminder_attrs = %{
- text: "This is an SMS reminder #{generator}",
- date: DateTime.utc_now()
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ channel3: %{
+ my_type_field: "email"
+ }
}
- assert {:ok, inserted_result} =
- struct(reminder_module)
- |> reminder_module.changeset(sms_reminder_attrs)
- |> Repo.insert()
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
- assert inserted_result.contexts == []
+ html =
+ render_component(
+ &liveview_form/1,
+ %{changeset: changeset, field: :channel3}
+ )
+ |> Floki.parse_fragment!()
+
+ assert [input] = Floki.find(html, "#reminder_channel3_my_type_field")
+ assert Floki.attribute(input, "name") == ["reminder[channel3][my_type_field]"]
+ assert Floki.attribute(input, "type") == ["hidden"]
+ assert Floki.attribute(input, "value") == ["email"]
end
- end
- describe "polymorphic_embed_inputs_for/2" do
- test "generates forms that can be rendered" do
+ test "generates forms that can be rendered (default type field)" do
reminder_module = get_module(Reminder, :polymorphic)
attrs = %{
date: ~U[2020-05-28 02:57:19Z],
text: "This is an Email reminder",
- channel: %{
+ channel2: %{
+ __type__: "email",
address: "a",
valid: true,
confirmed: true
@@ -1603,20 +2838,21 @@ defmodule PolymorphicEmbedTest do
html =
render_component(
&liveview_form/1,
- %{changeset: changeset, field: :channel}
+ %{changeset: changeset, field: :channel2}
)
|> Floki.parse_fragment!()
- assert [input] = Floki.find(html, "#reminder_channel___type__")
+ assert [input] = Floki.find(html, "#reminder_channel2___type__")
+ assert Floki.attribute(input, "name") == ["reminder[channel2][__type__]"]
assert Floki.attribute(input, "type") == ["hidden"]
assert Floki.attribute(input, "value") == ["email"]
- assert [input] = Floki.find(html, "#reminder_channel_number")
+ assert [input] = Floki.find(html, "#reminder_channel2_number")
assert Floki.attribute(input, "type") == ["text"]
end
end
- test "inputs_for/4" do
+ test "polymorphic_embed_inputs_for/4" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -1643,13 +2879,16 @@ defmodule PolymorphicEmbedTest do
expected_contents =
if(polymorphic?(generator),
- do:
- ~s( ),
- else:
- ~s( )
+ do: ~s"""
+
+
+ """,
+ else: ~s"""
+
+ """
)
- assert contents == expected_contents
+ assert contents == String.replace(expected_contents, "\n", "")
contents =
safe_inputs_for(
@@ -1664,17 +2903,98 @@ defmodule PolymorphicEmbedTest do
expected_contents =
if(polymorphic?(generator),
- do:
- ~s( ),
- else:
- ~s( )
+ do: ~s"""
+
+
+ """,
+ else: ~s"""
+
+ """
+ )
+
+ assert contents == String.replace(expected_contents, "\n", "")
+ end
+ end
+
+ test "polymorphic_embed_inputs_for/4 for list of embeds" do
+ for generator <- @generators do
+ reminder_module = get_module(Reminder, generator)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone",
+ address: "some address"
+ },
+ %{
+ __type__: "location",
+ age: "aquarius",
+ address: "some address"
+ }
+ ]
+ }
+
+ changeset =
+ struct(reminder_module)
+ |> reminder_module.changeset(attrs)
+
+ contents =
+ safe_inputs_for(changeset, :contexts, generator, fn f ->
+ assert f.impl == Phoenix.HTML.FormData.Ecto.Changeset
+ assert f.errors == []
+ text_input(f, :address)
+ end)
+
+ expected_contents =
+ if(polymorphic?(generator),
+ do: ~s"""
+
+
+
+
+ """,
+ else: ~s"""
+
+
+ """
+ )
+
+ assert contents == String.replace(expected_contents, "\n", "")
+
+ contents =
+ safe_inputs_for(
+ Map.put(changeset, :action, :insert),
+ :contexts,
+ generator,
+ fn f ->
+ assert f.impl == Phoenix.HTML.FormData.Ecto.Changeset
+ text_input(f, :address)
+ end
+ )
+
+ expected_contents =
+ if(polymorphic?(generator),
+ do: ~s"""
+
+
+
+
+ """,
+ else: ~s"""
+
+
+ """
)
- assert contents == expected_contents
+ assert contents == String.replace(expected_contents, "\n", "")
end
end
- test "inputs_for/4 after invalid insert" do
+ test "polymorphic_embed_inputs_for/4 after invalid insert" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -1706,13 +3026,16 @@ defmodule PolymorphicEmbedTest do
expected_contents =
if(polymorphic?(generator),
- do:
- ~s( ),
- else:
- ~s( )
+ do: ~s"""
+
+
+ """,
+ else: ~s"""
+
+ """
)
- assert contents == expected_contents
+ assert contents == String.replace(expected_contents, "\n", "")
contents =
safe_inputs_for(
@@ -1727,17 +3050,20 @@ defmodule PolymorphicEmbedTest do
expected_contents =
if(polymorphic?(generator),
- do:
- ~s( ),
- else:
- ~s( )
+ do: ~s"""
+
+
+ """,
+ else: ~s"""
+
+ """
)
- assert contents == expected_contents
+ assert contents == String.replace(expected_contents, "\n", "")
end
end
- test "inputs_for/4 after invalid insert with valid nested struct" do
+ test "polymorphic_embed_inputs_for/4 after invalid insert with valid nested struct" do
for generator <- @generators do
reminder_module = get_module(Reminder, generator)
@@ -1856,7 +3182,9 @@ defmodule PolymorphicEmbedTest do
[count: 3, validation: :length, kind: :min, type: :string]}
]
else
- assert f.errors == []
+ assert f.errors == [
+ name: {"can't be blank", [validation: :required]}
+ ]
end
"from safe_inputs_for #{generator}"
@@ -1951,7 +3279,12 @@ defmodule PolymorphicEmbedTest do
assert contents == expected_contents
assert f.impl == Phoenix.HTML.FormData.Ecto.Changeset
- assert f.errors == []
+
+ assert %{
+ number: {"can't be blank", [validation: :required]},
+ country_code: {"can't be blank", [validation: :required]},
+ provider: {"can't be blank", [validation: :required]}
+ } = Map.new(f.errors)
"from safe_inputs_for #{generator}"
end)
@@ -2044,8 +3377,57 @@ defmodule PolymorphicEmbedTest do
end
end
+ test "Form.source_data/1 and Form.source_module/1" do
+ reminder_module = get_module(Reminder, :polymorphic)
+
+ attrs = %{
+ date: ~U[2020-05-28 02:57:19Z],
+ text: "This is an Email reminder",
+ contexts: [
+ %{
+ __type__: "device",
+ ref: "12345",
+ type: "cellphone"
+ },
+ %{
+ __type__: "location",
+ age: "aquarius",
+ address: "some address"
+ }
+ ]
+ }
+
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
+
+ safe_form_for(changeset, fn _f ->
+ safe_inputs_for(changeset, :contexts, :email, :polymorphic_with_type, fn f ->
+ PolymorphicEmbed.HTML.Form.get_polymorphic_type(f[:contexts])
+
+ case PolymorphicEmbed.HTML.Form.source_data(f) do
+ %PolymorphicEmbed.Reminder.Context.Device{} ->
+ assert PolymorphicEmbed.Reminder.Context.Device ==
+ PolymorphicEmbed.HTML.Form.source_module(f)
+
+ %PolymorphicEmbed.Reminder.Context.Location{} ->
+ assert PolymorphicEmbed.Reminder.Context.Location ==
+ PolymorphicEmbed.HTML.Form.source_module(f)
+
+ _ ->
+ assert false
+ end
+
+ 1
+ end)
+
+ 1
+ end)
+ end
+
describe "Form.get_polymorphic_type/3" do
- test "returns type from changeset" do
+ test "returns type from changeset via identify_by_fields" do
reminder_module = get_module(Reminder, :polymorphic)
attrs = %{
@@ -2064,7 +3446,7 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(attrs)
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel) ==
:email
text_input(f, :text)
@@ -2087,16 +3469,33 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(%{})
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel) ==
+ :email
+
+ text_input(f, :text)
+ end)
+ end
+
+ test "returns type from map with default type field (string)" do
+ reminder_module = get_module(Reminder, :polymorphic)
+ attrs = %{"channel2" => %{"__type__" => "email"}}
+
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
+
+ safe_form_for(changeset, fn f ->
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel2) ==
:email
text_input(f, :text)
end)
end
- test "returns type from string parameters" do
+ test "returns type from map with default type field (atom)" do
reminder_module = get_module(Reminder, :polymorphic)
- attrs = %{"channel" => %{"my_type_field" => "email"}}
+ attrs = %{"channel2" => %{__type__: :email}}
changeset =
reminder_module
@@ -2104,16 +3503,16 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(attrs)
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel2) ==
:email
text_input(f, :text)
end)
end
- test "returns type from atom parameters" do
+ test "returns type from map with custom type field (string)" do
reminder_module = get_module(Reminder, :polymorphic)
- attrs = %{channel: %{my_type_field: :email}}
+ attrs = %{"channel3" => %{"my_type_field" => "email"}}
changeset =
reminder_module
@@ -2121,14 +3520,31 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(attrs)
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel3) ==
:email
text_input(f, :text)
end)
end
- test "returns type from parameters while type field is custom" do
+ test "returns type from map with custom type field (atom)" do
+ reminder_module = get_module(Reminder, :polymorphic)
+ attrs = %{"channel3" => %{my_type_field: "email"}}
+
+ changeset =
+ reminder_module
+ |> struct()
+ |> reminder_module.changeset(attrs)
+
+ safe_form_for(changeset, fn f ->
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel3) ==
+ :email
+
+ text_input(f, :text)
+ end)
+ end
+
+ test "returns nil with map when custom type field is configured and default type field is set" do
reminder_module = get_module(Reminder, :polymorphic)
attrs = %{channel: %{__type__: :email}}
@@ -2138,7 +3554,7 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(attrs)
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel) ==
nil
text_input(f, :text)
@@ -2154,7 +3570,7 @@ defmodule PolymorphicEmbedTest do
|> reminder_module.changeset(%{})
safe_form_for(changeset, fn f ->
- assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, reminder_module, :channel) ==
+ assert PolymorphicEmbed.HTML.Form.get_polymorphic_type(f, :channel) ==
nil
text_input(f, :text)
@@ -2172,7 +3588,9 @@ defmodule PolymorphicEmbedTest do
]
],
on_replace: :update,
- type_field: :my_type_field
+ type_field_name: :my_type_field,
+ array?: false,
+ default: nil
]
PolymorphicEmbed.init(opts)
@@ -2230,7 +3648,7 @@ defmodule PolymorphicEmbedTest do
defp liveview_form(assigns) do
~H"""
<.form
- let={f}
+ :let={f}
for={@changeset}
>
<%= for sms_form <- polymorphic_embed_inputs_for f, @field do %>
@@ -2241,6 +3659,37 @@ defmodule PolymorphicEmbedTest do
"""
end
+ defp liveview_form_component(assigns) do
+ ~H"""
+ <.form
+ :let={f}
+ for={@changeset}
+ >
+ <.polymorphic_embed_inputs_for field={f[@field]} :let={sms_form}>
+ <%= text_input sms_form, :number %>
+
+
+ """
+ end
+
+ defp liveview_form_with_inputs_for(assigns) do
+ ~H"""
+ <.form
+ :let={f}
+ for={@changeset}
+ >
+ <.polymorphic_embed_inputs_for field={f[@field]} :let={sms_form}>
+ <%= text_input sms_form, :number %>
+ <%= sms_form.errors |> inspect() %>
+ <.inputs_for field={sms_form[:extra]} :let={channel_form}>
+ <%= text_input channel_form, :imei %>
+ <%= channel_form.errors |> inspect() %>
+
+
+
+ """
+ end
+
defp polymorphic?(:polymorphic), do: true
defp polymorphic?(:not_polymorphic), do: false
end
diff --git a/test/support/migrations/20000101000000_create_tables.exs b/test/support/migrations/20000101000000_create_tables.exs
index 53be4e2..846c1c4 100644
--- a/test/support/migrations/20000101000000_create_tables.exs
+++ b/test/support/migrations/20000101000000_create_tables.exs
@@ -2,15 +2,31 @@ defmodule PolymorphicEmbed.CreateTables do
use Ecto.Migration
def change do
+ create table(:events) do
+ add(:embedded_reminders, :map)
+ timestamps()
+ end
+
create table(:reminders) do
add(:date, :utc_datetime, null: false)
add(:text, :text, null: false)
+ add(:type, :text, null: true)
+ add(:event_id, references(:events))
add(:channel, :map)
+ add(:channel2, :map)
+ add(:channel3, :map)
+ add(:channel4, :map)
add(:contexts, :map)
add(:contexts2, :map)
+ add(:contexts3, :map)
timestamps()
end
+
+ create table(:todos) do
+ add(:reminder_id, references(:reminders), null: false)
+ add(:embedded_reminder, :map)
+ end
end
end
diff --git a/test/support/models/not_polymorphic/channel/sms.ex b/test/support/models/not_polymorphic/channel/sms.ex
index e3379c8..6536733 100644
--- a/test/support/models/not_polymorphic/channel/sms.ex
+++ b/test/support/models/not_polymorphic/channel/sms.ex
@@ -23,13 +23,7 @@ defmodule PolymorphicEmbed.Regular.Channel.SMS do
|> validate_required([:number, :country_code])
end
- def custom_changeset(struct, attrs, _foo, _bar) do
- struct
- |> changeset(attrs)
- |> cast(attrs, [:custom])
- end
-
- def custom_changeset2(struct, attrs) do
+ def custom_changeset(struct, attrs) do
struct
|> changeset(attrs)
|> cast(attrs, [:custom])
diff --git a/test/support/models/not_polymorphic/event.ex b/test/support/models/not_polymorphic/event.ex
new file mode 100644
index 0000000..b70fba8
--- /dev/null
+++ b/test/support/models/not_polymorphic/event.ex
@@ -0,0 +1,21 @@
+defmodule PolymorphicEmbed.Regular.Event do
+ @moduledoc """
+ An (calendar) event, which can optionally have multiple reminders.
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias PolymorphicEmbed.Regular.Reminder
+
+ schema "events" do
+ has_many(:reminders, Reminder)
+ embeds_many(:embedded_reminders, Reminder)
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [])
+ |> cast_assoc(:reminders)
+ |> cast_embed(:embedded_reminders)
+ end
+end
diff --git a/test/support/models/not_polymorphic/reminder.ex b/test/support/models/not_polymorphic/reminder.ex
index 026b6da..7e94919 100644
--- a/test/support/models/not_polymorphic/reminder.ex
+++ b/test/support/models/not_polymorphic/reminder.ex
@@ -2,39 +2,43 @@ defmodule PolymorphicEmbed.Regular.Reminder do
use Ecto.Schema
use QueryBuilder
import Ecto.Changeset
+ alias PolymorphicEmbed.Regular.{Todo, Event}
schema "reminders" do
field(:date, :utc_datetime)
field(:text, :string)
+ has_one(:todo, Todo)
+ belongs_to(:event, Event)
embeds_one(:channel, PolymorphicEmbed.Regular.Channel.SMS, on_replace: :update)
- embeds_many(:contexts, PolymorphicEmbed.Regular.Reminder.Context.Location, on_replace: :delete)
+ embeds_many(:contexts, PolymorphicEmbed.Regular.Reminder.Context.Location,
+ on_replace: :delete
+ )
+
+ embeds_many(:contexts3, PolymorphicEmbed.Regular.Reminder.Context.DeviceNoId,
+ on_replace: :delete
+ )
timestamps()
end
- def changeset(struct, values) do
+ def changeset(struct, attrs) do
struct
- |> cast(values, [:date, :text])
+ |> cast(attrs, [:date, :text])
|> validate_required(:date)
|> cast_embed(:channel)
- |> cast_embed(:contexts)
- end
-
- def custom_changeset(struct, values) do
- struct
- |> cast(values, [:date, :text])
- |> cast_embed(:channel,
- with: {PolymorphicEmbed.Regular.Channel.SMS, :custom_changeset, ["foo", "bar"]}
+ |> cast_embed(:contexts,
+ sort_param: :contexts_sort,
+ drop_param: :contexts_drop
)
- |> validate_required(:date)
+ |> cast_embed(:contexts3)
end
- def custom_changeset2(struct, values) do
+ def custom_changeset(struct, attrs) do
struct
- |> cast(values, [:date, :text])
- |> cast_embed(:channel, with: &PolymorphicEmbed.Regular.Channel.SMS.custom_changeset2/2)
+ |> cast(attrs, [:date, :text])
+ |> cast_embed(:channel, with: &PolymorphicEmbed.Regular.Channel.SMS.custom_changeset/2)
|> validate_required(:date)
end
end
diff --git a/test/support/models/not_polymorphic/reminder/context/device_no_id.ex b/test/support/models/not_polymorphic/reminder/context/device_no_id.ex
new file mode 100644
index 0000000..d863796
--- /dev/null
+++ b/test/support/models/not_polymorphic/reminder/context/device_no_id.ex
@@ -0,0 +1,21 @@
+defmodule PolymorphicEmbed.Regular.Reminder.Context.DeviceNoId do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field :ref, :string
+ field :type, :string
+
+ embeds_one :extra, Extra do
+ field :imei, :string
+ end
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, ~w(ref type)a)
+ |> validate_required(~w(type)a)
+ end
+end
diff --git a/test/support/models/not_polymorphic/todo.ex b/test/support/models/not_polymorphic/todo.ex
new file mode 100644
index 0000000..ea0a103
--- /dev/null
+++ b/test/support/models/not_polymorphic/todo.ex
@@ -0,0 +1,21 @@
+defmodule PolymorphicEmbed.Regular.Todo do
+ @moduledoc """
+ A todo item, which can optionally have a single reminder.
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias PolymorphicEmbed.Regular.Reminder
+
+ schema "todos" do
+ belongs_to(:reminder, Reminder)
+ embeds_one(:embedded_reminder, Reminder)
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [])
+ |> cast_assoc(:reminder)
+ |> cast_embed(:embedded_reminder)
+ end
+end
diff --git a/test/support/models/polymorphic/channel/sms.ex b/test/support/models/polymorphic/channel/sms.ex
index e925b98..23ac803 100644
--- a/test/support/models/polymorphic/channel/sms.ex
+++ b/test/support/models/polymorphic/channel/sms.ex
@@ -41,13 +41,7 @@ defmodule PolymorphicEmbed.Channel.SMS do
|> validate_required([:number, :country_code])
end
- def custom_changeset(struct, attrs, _foo, _bar) do
- struct
- |> changeset(attrs)
- |> cast(attrs, [:custom])
- end
-
- def custom_changeset2(struct, attrs) do
+ def custom_changeset(struct, attrs) do
struct
|> changeset(attrs)
|> cast(attrs, [:custom])
diff --git a/test/support/models/polymorphic/event.ex b/test/support/models/polymorphic/event.ex
new file mode 100644
index 0000000..876036d
--- /dev/null
+++ b/test/support/models/polymorphic/event.ex
@@ -0,0 +1,21 @@
+defmodule PolymorphicEmbed.Event do
+ @moduledoc """
+ An (calendar) event, which can optionally have multiple reminders.
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias PolymorphicEmbed.Reminder
+
+ schema "events" do
+ has_many(:reminders, Reminder)
+ embeds_many(:embedded_reminders, Reminder)
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [])
+ |> cast_assoc(:reminders)
+ |> cast_embed(:embedded_reminders)
+ end
+end
diff --git a/test/support/models/polymorphic/reminder.ex b/test/support/models/polymorphic/reminder.ex
index 520f73b..73e42e8 100644
--- a/test/support/models/polymorphic/reminder.ex
+++ b/test/support/models/polymorphic/reminder.ex
@@ -3,10 +3,14 @@ defmodule PolymorphicEmbed.Reminder do
use QueryBuilder
import Ecto.Changeset
import PolymorphicEmbed
+ alias PolymorphicEmbed.{Todo, Event}
schema "reminders" do
field(:date, :utc_datetime)
field(:text, :string)
+ field(:type, :string)
+ has_one(:todo, Todo)
+ belongs_to(:event, Event)
polymorphic_embeds_one(:channel,
types: [
@@ -17,7 +21,34 @@ defmodule PolymorphicEmbed.Reminder do
]
],
on_replace: :update,
- type_field: :my_type_field
+ type_field_name: :my_type_field,
+ retain_unlisted_types_on_load: [:some_deprecated_type]
+ )
+
+ polymorphic_embeds_one(:channel2,
+ types: [
+ sms: PolymorphicEmbed.Channel.SMS,
+ email: PolymorphicEmbed.Channel.Email
+ ],
+ on_replace: :update
+ )
+
+ polymorphic_embeds_one(:channel3,
+ types: [
+ sms: PolymorphicEmbed.Channel.SMS,
+ email: PolymorphicEmbed.Channel.Email
+ ],
+ on_replace: :update,
+ type_field_name: :my_type_field
+ )
+
+ polymorphic_embeds_one(:channel4,
+ types: [
+ sms: PolymorphicEmbed.Channel.SMS,
+ email: PolymorphicEmbed.Channel.Email
+ ],
+ on_replace: :update,
+ use_parent_field_for_type: :type
)
polymorphic_embeds_many(:contexts,
@@ -39,47 +70,45 @@ defmodule PolymorphicEmbed.Reminder do
on_replace: :delete
)
+ polymorphic_embeds_many(:contexts3,
+ types: [
+ location: PolymorphicEmbed.Reminder.Context.Location,
+ age: PolymorphicEmbed.Reminder.Context.Age,
+ device: PolymorphicEmbed.Reminder.Context.DeviceNoId
+ ],
+ on_replace: :delete
+ )
+
timestamps()
end
def changeset(struct, values) do
struct
- |> cast(values, [:date, :text])
+ |> cast(values, [:date, :text, :type])
|> validate_required(:date)
|> cast_polymorphic_embed(:channel)
- |> cast_polymorphic_embed(:contexts)
- |> cast_polymorphic_embed(:contexts2)
- end
-
- def custom_changeset(struct, values) do
- struct
- |> cast(values, [:date, :text])
- |> cast_polymorphic_embed(:channel,
- with: [
- sms: {PolymorphicEmbed.Channel.SMS, :custom_changeset, ["foo", "bar"]},
- email: {PolymorphicEmbed.Channel.Email, :custom_changeset, ["foo", "bar"]}
- ]
+ |> cast_polymorphic_embed(:channel2)
+ |> cast_polymorphic_embed(:channel3)
+ |> cast_polymorphic_embed(:channel4)
+ |> cast_polymorphic_embed(:contexts,
+ sort_param: :contexts_sort,
+ default_type_on_sort_create: :location,
+ drop_param: :contexts_drop
)
- |> validate_required(:date)
- end
-
- def custom_changeset2(struct, values) do
- struct
- |> cast(values, [:date, :text])
- |> cast_polymorphic_embed(:channel,
- with: [
- sms: &PolymorphicEmbed.Channel.SMS.custom_changeset2/2
- ]
+ |> cast_polymorphic_embed(:contexts2,
+ sort_param: :contexts2_sort,
+ default_type_on_sort_create: fn -> :location end,
+ drop_param: :contexts2_drop
)
- |> validate_required(:date)
+ |> cast_polymorphic_embed(:contexts3)
end
- def custom_changeset3(struct, values) do
+ def custom_changeset(struct, values) do
struct
|> cast(values, [:date, :text])
|> cast_polymorphic_embed(:channel,
with: [
- sms: &PolymorphicEmbed.Channel.SMS.custom_changeset2/2
+ sms: &PolymorphicEmbed.Channel.SMS.custom_changeset/2
]
)
|> validate_required(:date)
diff --git a/test/support/models/polymorphic/reminder/context/device.ex b/test/support/models/polymorphic/reminder/context/device.ex
index 31684f8..9ea491d 100644
--- a/test/support/models/polymorphic/reminder/context/device.ex
+++ b/test/support/models/polymorphic/reminder/context/device.ex
@@ -6,14 +6,13 @@ defmodule PolymorphicEmbed.Reminder.Context.Device do
field :ref, :string
field :type, :string
- embeds_one :extra, Extra do
- field :imei, :string
- end
+ embeds_one :extra, PolymorphicEmbed.Reminder.Context.Extra
end
def changeset(struct, params) do
struct
|> cast(params, ~w(ref type)a)
|> validate_required(~w(type)a)
+ |> cast_embed(:extra)
end
end
diff --git a/test/support/models/polymorphic/reminder/context/device_no_id.ex b/test/support/models/polymorphic/reminder/context/device_no_id.ex
new file mode 100644
index 0000000..37b8292
--- /dev/null
+++ b/test/support/models/polymorphic/reminder/context/device_no_id.ex
@@ -0,0 +1,19 @@
+defmodule PolymorphicEmbed.Reminder.Context.DeviceNoId do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ @primary_key false
+
+ embedded_schema do
+ field :ref, :string
+ field :type, :string
+
+ embeds_one :extra, PolymorphicEmbed.Reminder.Context
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, ~w(ref type)a)
+ |> validate_required(~w(type)a)
+ end
+end
diff --git a/test/support/models/polymorphic/reminder/context/extra.ex b/test/support/models/polymorphic/reminder/context/extra.ex
new file mode 100644
index 0000000..e054c1d
--- /dev/null
+++ b/test/support/models/polymorphic/reminder/context/extra.ex
@@ -0,0 +1,14 @@
+defmodule PolymorphicEmbed.Reminder.Context.Extra do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ embedded_schema do
+ field :imei, :string
+ end
+
+ def changeset(extra, attrs) do
+ extra
+ |> cast(attrs, [:imei])
+ |> validate_required([:imei])
+ end
+end
diff --git a/test/support/models/polymorphic/todo.ex b/test/support/models/polymorphic/todo.ex
new file mode 100644
index 0000000..e7e074e
--- /dev/null
+++ b/test/support/models/polymorphic/todo.ex
@@ -0,0 +1,21 @@
+defmodule PolymorphicEmbed.Todo do
+ @moduledoc """
+ A todo item, which can optionally have a single reminder.
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias PolymorphicEmbed.Reminder
+
+ schema "todos" do
+ belongs_to(:reminder, Reminder)
+ embeds_one(:embedded_reminder, Reminder)
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [])
+ |> cast_assoc(:reminder)
+ |> cast_embed(:embedded_reminder)
+ end
+end