Packages
ecto
0.14.0
3.14.1
3.14.0
3.13.6
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.6
3.12.5
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.2
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.4
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.2.0-rc.1
2.2.0-rc.0
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.1.0-rc.5
2.1.0-rc.4
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.1.0-rc.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-beta.2
2.0.0-beta.1
2.0.0-beta.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.12.0-rc
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
A toolkit for data mapping and language integrated query for Elixir
Current section
Files
Jump to
Current section
Files
lib/ecto/embedded.ex
defmodule Ecto.Embedded do
@moduledoc false
alias __MODULE__
alias Ecto.Changeset
defstruct [:cardinality, :field, :owner, :embed, :on_cast, strategy: :replace]
@type t :: %Embedded{cardinality: :one | :many,
strategy: :replace | atom,
field: atom, owner: atom, embed: atom, on_cast: atom}
@doc """
Builds the embedded struct.
## Options
* `:cardinality` - tells if there is one embedded model or many
* `:strategy` - which strategy to use when storing items
* `:embed` - name of the embedded model
* `:on_cast` - the changeset function to call during casting
"""
def struct(module, name, opts) do
struct(%__MODULE__{field: name, owner: module}, opts)
end
@doc """
Casts embedded models according to the `on_cast` function.
Sets correct `state` on the returned changeset
"""
def cast(%Embedded{cardinality: :one, embed: mod, on_cast: fun}, :empty, current) do
{:ok, changeset_action(mod, fun, :empty, current), false}
end
def cast(%Embedded{cardinality: :many, embed: mod, on_cast: fun}, :empty, current) do
{:ok, Enum.map(current, &changeset_action(mod, fun, :empty, &1)), false}
end
def cast(%Embedded{cardinality: :one, embed: mod, on_cast: fun},
params, current) when is_map(params) do
{pk, param_pk} = primary_key(mod)
changeset =
if current && Map.get(current, pk) == Map.get(params, param_pk) do
changeset_action(mod, fun, params, current)
else
changeset_action(mod, fun, params, nil)
end
{:ok, changeset, changeset.valid?}
end
def cast(%Embedded{cardinality: :many, embed: mod, on_cast: fun},
params, current) when is_list(params) do
{pk, param_pk} = primary_key(mod)
current = process_current(current, pk)
changeset_fun = &changeset_action(mod, fun, &1, &2)
pk_fun = &Map.fetch(&1, param_pk)
map_changes(params, pk_fun, changeset_fun, current, [], true)
end
def cast(_embed, _params, _current) do
:error
end
@doc """
Wraps embedded models in changesets.
"""
def change(%Embedded{cardinality: :one, embed: mod}, value, current) do
do_change(value, current, mod)
end
def change(%Embedded{cardinality: :many, embed: mod},
value, current) when is_list(value) do
{pk, _} = primary_key(mod)
current = process_current(current, pk)
changeset_fun = &do_change(&1, &2, mod)
pk_fun = &model_or_changeset_pk(&1, pk)
map_changes(value, pk_fun, changeset_fun, current, [], true) |> elem(1)
end
defp model_or_changeset_pk(%Changeset{model: model}, pk), do: Map.fetch(model, pk)
defp model_or_changeset_pk(model, pk), do: Map.fetch(model, pk)
defp do_change(value, nil, mod) do
fields = mod.__schema__(:fields)
embeds = mod.__schema__(:embeds)
changeset = Changeset.change(value)
types = changeset.types
changes =
Enum.reduce(embeds, Map.take(changeset.model, fields), fn field, acc ->
{:embed, embed} = Map.get(types, field)
Map.put(acc, field, change(embed, Map.get(acc, field), nil))
end)
|> Map.merge(changeset.changes)
%{changeset | changes: changes, action: :insert}
end
defp do_change(nil, current, mod) do
# We need to mark all embeds for deletion too
changes = mod.__schema__(:embeds) |> Enum.map(&{&1, nil})
%{Changeset.change(current, changes) | action: :delete}
end
defp do_change(%Changeset{model: current} = changeset, current, _mod) do
%{changeset | action: :update}
end
defp do_change(%Changeset{}, _current, _mod) do
raise ArgumentError, "embedded changeset does not change the model already " <>
"preset in the parent model"
end
defp do_change(value, current, _mod) do
changes = Map.take(value, value.__struct__.__schema__(:fields))
%{Changeset.change(current, changes) | action: :update}
end
@doc """
Returns empty container for embed.
"""
def empty(%Embedded{cardinality: :one}), do: nil
def empty(%Embedded{cardinality: :many}), do: []
@doc """
Applies embedded changeset changes
"""
def apply_changes(%Embedded{cardinality: :one}, nil) do
nil
end
def apply_changes(%Embedded{cardinality: :one}, changeset) do
apply_changes(changeset)
end
def apply_changes(%Embedded{cardinality: :many}, changesets) do
for changeset <- changesets,
model = apply_changes(changeset),
do: model
end
defp apply_changes(%Changeset{action: :delete}), do: nil
defp apply_changes(changeset), do: Changeset.apply_changes(changeset)
@doc """
Applies given callback to all models based on changeset action
"""
def apply_callbacks(changeset, [], _adapter, _type), do: changeset
def apply_callbacks(changeset, embeds, adapter, type) do
types = changeset.types
update_in changeset.changes, fn changes ->
Enum.reduce(embeds, changes, fn name, changes ->
case Map.fetch(changes, name) do
{:ok, changeset} ->
{:embed, embed} = Map.get(types, name)
Map.put(changes, name, apply_callback(embed, changeset, adapter, type))
:error ->
changes
end
end)
end
end
defp apply_callback(%Embedded{cardinality: :one}, nil, _adapter, _type) do
nil
end
defp apply_callback(%Embedded{cardinality: :one, embed: model} = embed, changeset, adapter, type) do
apply_callback(changeset, model, embed, adapter, type)
end
defp apply_callback(%Embedded{cardinality: :many, embed: model} = embed, changesets, adapter, type) do
Enum.map(changesets, &apply_callback(&1, model, embed, adapter, type))
end
defp apply_callback(%Changeset{action: :update, changes: changes} = changeset,
_model, _embed, _adapter, _type) when changes == %{}, do: changeset
defp apply_callback(%Changeset{valid?: false}, model, _embed, _adapter, _type) do
raise ArgumentError, "changeset for #{model} is invalid, " <>
"but the parent changeset was not marked as invalid"
end
defp apply_callback(%Changeset{model: %{__struct__: model}, action: action} = changeset,
model, embed, adapter, type) do
callback = callback_for(type, action)
Ecto.Model.Callbacks.__apply__(model, callback, changeset)
|> generate_id(callback, model, embed, adapter)
|> apply_callbacks(model.__schema__(:embeds), adapter, type)
end
defp apply_callback(%Changeset{model: model}, expected, _embed, _adapter, _type) do
raise ArgumentError, "expected changeset for embedded model `#{inspect expected}`, " <>
"got: #{inspect model}"
end
defp generate_id(changeset, :before_insert, model, embed, adapter) do
{pk, _} = primary_key(model)
if Map.get(changeset.changes, pk) == nil and
Map.get(changeset.model, pk) == nil do
case model.__schema__(:autogenerate_id) do
{key, :binary_id} ->
update_in changeset.changes, &Map.put(&1, key, adapter.embed_id(embed))
other ->
raise ArgumentError, "embedded model `#{inspect model}` must have binary id " <>
"primary key with autogenerate: true, got: #{inspect other}"
end
else
changeset
end
end
defp generate_id(changeset, _callback, _model, _embed, _adapter) do
changeset
end
types = [:before, :after]
actions = [:insert, :update, :delete]
for type <- types, action <- actions do
defp callback_for(unquote(type), unquote(action)), do: unquote(:"#{type}_#{action}")
end
defp callback_for(_type, nil) do
raise ArgumentError, "embedded changeset action not set"
end
defp map_changes([], _pk, fun, current, acc, valid?) do
{previous, valid?} =
Enum.map_reduce(current, valid?, fn {_, model}, valid? ->
changeset = fun.(nil, model)
{changeset, valid? && changeset.valid?}
end)
{:ok, Enum.reverse(acc, previous), valid?}
end
defp map_changes([map | rest], pk, fun, current, acc, valid?) when is_map(map) do
case pk.(map) do
{:ok, pk_value} ->
{model, current} = Map.pop(current, pk_value)
changeset = fun.(map, model)
map_changes(rest, pk, fun, current,
[changeset | acc], valid? && changeset.valid?)
:error ->
changeset = fun.(map, nil)
map_changes(rest, pk, fun, current,
[changeset | acc], valid? && changeset.valid?)
end
end
defp map_changes(_params, _pk, _fun, _current, _acc, _valid?) do
:error
end
defp primary_key(module) do
case module.__schema__(:primary_key) do
[pk] -> {pk, Atom.to_string(pk)}
_ -> raise ArgumentError,
"embeded models must have exactly one primary key field"
end
end
defp process_current(nil, _pk),
do: %{}
defp process_current(current, pk),
do: Enum.into(current, %{}, &{Map.get(&1, pk), &1})
defp changeset_action(mod, fun, params, nil) do
changeset = apply(mod, fun, [params, mod.__struct__()])
%{changeset | action: :insert}
end
defp changeset_action(_mod, _fun, nil, model) do
%{Changeset.change(model) | action: :delete}
end
defp changeset_action(mod, fun, params, model) do
changeset = apply(mod, fun, [params, model])
%{changeset | action: :update}
end
end