Skip to content

Commit

Permalink
Rename :type_field to :type_field_name
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuprog committed Jun 3, 2024
1 parent 8b9ff8a commit 945861d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 32 deletions.
70 changes: 41 additions & 29 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ defmodule PolymorphicEmbed do
@type t() :: any()

require Logger
require PolymorphicEmbed.OptionsValidator

alias Ecto.Changeset
alias PolymorphicEmbed.OptionsValidator

defmacro polymorphic_embeds_one(field_name, opts) do
opts = Keyword.update!(opts, :types, &expand_alias(&1, __CALLER__))
Expand Down Expand Up @@ -74,6 +76,16 @@ defmodule PolymorphicEmbed do

@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 v5
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)"
Expand All @@ -100,16 +112,13 @@ defmodule PolymorphicEmbed do
}
end)

type_field = Keyword.get(opts, :type_field, :__type__)

%{
default: Keyword.get(opts, :default, nil),
on_replace: Keyword.fetch!(opts, :on_replace),
on_type_not_found: Keyword.get(opts, :on_type_not_found, :changeset_error),
nilify_unlisted_types_on_load: Keyword.get(opts, :nilify_unlisted_types_on_load, []),
retain_unlisted_types_on_load: Keyword.get(opts, :retain_unlisted_types_on_load, []),
type_field: type_field |> to_string(),
type_field_atom: type_field,
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
Expand Down Expand Up @@ -201,15 +210,15 @@ defmodule PolymorphicEmbed do

defp sort_create(%{sort_param: _} = cast_opts, field_opts) do
default_type = Map.get(cast_opts, :default_type_on_sort_create)
type_field_atom = Map.fetch!(field_opts, :type_field_atom)
type_field_name = Map.fetch!(field_opts, :type_field_name)
types_metadata = Map.fetch!(field_opts, :types_metadata)

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_atom => first_type}
%{type_field_name => first_type}

_ ->
default_type =
Expand All @@ -223,7 +232,7 @@ defmodule PolymorphicEmbed do
raise "Incorrect type atom #{inspect(default_type)}"
end

%{type_field_atom => default_type}
%{type_field_name => default_type}
end
end

Expand Down Expand Up @@ -299,15 +308,15 @@ defmodule PolymorphicEmbed do
%{
types_metadata: types_metadata,
on_type_not_found: on_type_not_found,
type_field: type_field
type_field_name: type_field_name
} = 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(params, type_field, types_metadata, data_for_field) do
case action_and_struct(params, type_field_name, types_metadata, data_for_field) do
:type_not_found when on_type_not_found == :raise ->
raise_cannot_infer_type_from_data(params)

Expand Down Expand Up @@ -335,8 +344,8 @@ defmodule PolymorphicEmbed do
end
end

defp action_and_struct(params, type_field, types_metadata, data_for_field) do
case get_polymorphic_module_from_map(params, type_field, types_metadata) do
defp action_and_struct(params, type_field_name, types_metadata, data_for_field) do
case get_polymorphic_module_from_map(params, type_field_name, types_metadata) do
nil ->
if data_for_field do
{:update, data_for_field}
Expand All @@ -360,14 +369,14 @@ defmodule PolymorphicEmbed do
%{
types_metadata: types_metadata,
on_type_not_found: on_type_not_found,
type_field: type_field
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 get_polymorphic_module_from_map(params, type_field, types_metadata) do
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)

Expand Down Expand Up @@ -448,18 +457,18 @@ defmodule PolymorphicEmbed do
def do_load(data, _loader, field_opts) do
%{
types_metadata: types_metadata,
type_field: type_field
type_field_name: type_field_name
} = field_opts

case get_polymorphic_module_from_map(data, type_field, types_metadata) do
case get_polymorphic_module_from_map(data, type_field_name, types_metadata) do
nil ->
retain_type_list = Map.get(field_opts, :retain_unlisted_types_on_load, [])
nilify_type_list = Map.get(field_opts, :nilify_unlisted_types_on_load, [])
retain_type_list =
Map.fetch!(field_opts, :retain_unlisted_types_on_load) |> Enum.map(&to_string(&1))

retain_type_list = Enum.map(retain_type_list, &to_string(&1))
nilify_type_list = Enum.map(nilify_type_list, &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)
type = Map.get(data, type_field_name |> to_string)

cond do
type in retain_type_list ->
Expand Down Expand Up @@ -488,7 +497,7 @@ defmodule PolymorphicEmbed do

def dump(%module{} = struct, dumper, %{
types_metadata: types_metadata,
type_field_atom: type_field_atom
type_field_name: type_field_name
}) do
case module.__schema__(:autogenerate_id) do
{key, _source, :binary_id} ->
Expand All @@ -504,7 +513,7 @@ defmodule PolymorphicEmbed do
struct
|> map_from_struct()
# use the atom instead of string form for mongodb
|> Map.put(type_field_atom, do_get_polymorphic_type(module, types_metadata))
|> Map.put(type_field_name, do_get_polymorphic_type(module, types_metadata))

dumper.(:map, map)
end
Expand All @@ -516,21 +525,24 @@ defmodule PolymorphicEmbed do
end

def get_polymorphic_module(schema, field, type_or_data) do
%{types_metadata: types_metadata, type_field: type_field} = get_field_opts(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) ->
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) ->
get_polymorphic_module_for_type(type, types_metadata)
end
end

defp get_polymorphic_module_from_map(%{} = attrs, type_field, types_metadata) do
defp get_polymorphic_module_from_map(%{} = attrs, type_field_name, types_metadata) do
attrs = attrs |> convert_map_keys_to_string()
type_field_name_as_string = to_string(type_field_name)

type = Enum.find_value(attrs, fn {key, value} -> key == type_field && value end)
type =
Enum.find_value(attrs, fn {key, value} -> key == type_field_name_as_string && value end)

if type do
get_polymorphic_module_for_type(type, types_metadata)
Expand Down
4 changes: 2 additions & 2 deletions lib/polymorphic_embed/html/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
%schema{} = source_changeset.data

field_opts = PolymorphicEmbed.get_field_opts(schema, field)
type_field_atom = Map.get(field_opts, :type_field_atom, :__type__)
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)

Expand All @@ -108,7 +108,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
errors: errors,
data: data,
params: params,
hidden: [{type_field_atom, to_string(type)}],
hidden: [{type_field_name, to_string(type)}],
options: options
}
end)
Expand Down
69 changes: 69 additions & 0 deletions lib/polymorphic_embed/options_validator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule PolymorphicEmbed.OptionsValidator do
require Logger

@known_options_names [
:types,
:on_replace,
:type_field,
:type_field_name,
:on_type_not_found,
:retain_unlisted_types_on_load,
:nilify_unlisted_types_on_load,
# Ecto
:field,
:schema,
:default
]
@valid_on_replace_options [:update, :delete]
@valid_on_type_not_found_options [:raise, :changeset_error, :nilify, :ignore]

def validate!(options) do
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

unless Keyword.fetch!(options, :on_replace) in @valid_on_replace_options do
raise(
"`:on_replace` must be set to `:update` for a single polymorphic embed or `: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

# TODO remove in v5
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` 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` 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` must be a list of types as atoms."
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
2 changes: 1 addition & 1 deletion test/support/models/polymorphic/reminder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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]
)

Expand Down

0 comments on commit 945861d

Please sign in to comment.