1defmodule Ecto.Embedded do
2  @moduledoc false
3  alias __MODULE__
4  alias Ecto.Changeset
5
6  @type t :: %Embedded{cardinality: :one | :many,
7                       on_replace: :raise | :mark_as_invalid | :delete,
8                       field: atom,
9                       owner: atom,
10                       on_cast: nil | fun,
11                       related: atom,
12                       unique: boolean}
13
14  @behaviour Ecto.Changeset.Relation
15  @on_replace_opts [:raise, :mark_as_invalid, :delete]
16  @embeds_one_on_replace_opts @on_replace_opts ++ [:update]
17  defstruct [:cardinality, :field, :owner, :related, :on_cast, on_replace: :raise, unique: true]
18
19  @doc """
20  Builds the embedded struct.
21
22  ## Options
23
24    * `:cardinality` - tells if there is one embedded schema or many
25    * `:related` - name of the embedded schema
26    * `:on_replace` - the action taken on embeds when the embed is replaced
27
28  """
29  def struct(module, name, opts) do
30    opts = Keyword.put_new(opts, :on_replace, :raise)
31    cardinality = Keyword.fetch!(opts, :cardinality)
32    on_replace_opts = if cardinality == :one, do: @embeds_one_on_replace_opts, else: @on_replace_opts
33
34    unless opts[:on_replace] in on_replace_opts do
35      raise ArgumentError, "invalid `:on_replace` option for #{inspect name}. " <>
36        "The only valid options are: " <>
37        Enum.map_join(@on_replace_opts, ", ", &"`#{inspect &1}`")
38    end
39
40    struct(%Embedded{field: name, owner: module}, opts)
41  end
42
43  @doc """
44  Callback invoked by repository to prepare embeds.
45
46  It replaces the changesets for embeds inside changes
47  by actual structs so it can be dumped by adapters and
48  loaded into the schema struct afterwards.
49  """
50  def prepare(changeset, adapter, repo_action) do
51    %{changes: changes, data: %{__struct__: schema}, types: types} = changeset
52    prepare(Map.take(changes, schema.__schema__(:embeds)), types, adapter, repo_action)
53  end
54
55  defp prepare(embeds, _types, _adapter, _repo_action) when embeds == %{} do
56    embeds
57  end
58
59  defp prepare(embeds, types, adapter, repo_action) do
60    Enum.reduce embeds, embeds, fn {name, changeset}, acc ->
61      {:embed, embed} = Map.get(types, name)
62      Map.put(acc, name, prepare_each(embed, changeset, adapter, repo_action))
63    end
64  end
65
66  defp prepare_each(%{cardinality: :one}, nil, _adapter, _repo_action) do
67    nil
68  end
69
70  defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, repo_action) do
71    action = check_action!(changeset.action, repo_action, embed)
72    to_struct(changeset, action, embed, adapter)
73  end
74
75  defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, repo_action) do
76    for changeset <- changesets,
77        action = check_action!(changeset.action, repo_action, embed),
78        prepared = to_struct(changeset, action, embed, adapter),
79        do: prepared
80  end
81
82  defp to_struct(%Changeset{valid?: false}, _action,
83                 %{related: schema}, _adapter) do
84    raise ArgumentError, "changeset for embedded #{inspect schema} is invalid, " <>
85                         "but the parent changeset was not marked as invalid"
86  end
87
88  defp to_struct(%Changeset{data: %{__struct__: actual}}, _action,
89                 %{related: expected}, _adapter) when actual != expected do
90    raise ArgumentError, "expected changeset for embedded schema `#{inspect expected}`, " <>
91                         "got: #{inspect actual}"
92  end
93
94  defp to_struct(%Changeset{changes: changes, data: schema}, :update,
95                 _embed, _adapter) when changes == %{} do
96    schema
97  end
98
99  defp to_struct(%Changeset{}, :delete, _embed, _adapter) do
100    nil
101  end
102
103  defp to_struct(%Changeset{} = changeset, action, %{related: schema}, adapter) do
104    %{data: struct, changes: changes} = changeset
105    embeds = prepare(changeset, adapter, action)
106
107    changes
108    |> Map.merge(embeds)
109    |> autogenerate_id(struct, action, schema, adapter)
110    |> autogenerate(action, schema)
111    |> apply_embeds(struct)
112  end
113
114  defp apply_embeds(changes, struct) do
115    struct(struct, changes)
116  end
117
118  defp check_action!(:replace, action, %{on_replace: :delete} = embed),
119    do: check_action!(:delete, action, embed)
120  defp check_action!(:update, :insert, %{related: schema}),
121    do: raise(ArgumentError, "got action :update in changeset for embedded #{inspect schema} while inserting")
122  defp check_action!(:delete, :insert, %{related: schema}),
123    do: raise(ArgumentError, "got action :delete in changeset for embedded #{inspect schema} while inserting")
124  defp check_action!(action, _, _), do: action
125
126  defp autogenerate_id(changes, _struct, :insert, schema, adapter) do
127    case schema.__schema__(:autogenerate_id) do
128      {key, _source, :binary_id} ->
129        Map.put_new_lazy(changes, key, fn -> adapter.autogenerate(:embed_id) end)
130      {_key, :id} ->
131        raise ArgumentError, "embedded schema `#{inspect schema}` cannot autogenerate `:id` primary keys, " <>
132                             "those are typically used for auto-incrementing constraints. " <>
133                             "Maybe you meant to use `:binary_id` instead?"
134      nil ->
135        changes
136    end
137  end
138
139  defp autogenerate_id(changes, struct, :update, _schema, _adapter) do
140    for {_, nil} <- Ecto.primary_key(struct) do
141      raise Ecto.NoPrimaryKeyValueError, struct: struct
142    end
143    changes
144  end
145
146  defp autogenerate(changes, action, schema) do
147    Enum.reduce schema.__schema__(action_to_auto(action)), changes, fn
148      {k, {mod, fun, args}}, acc ->
149        case Map.fetch(acc, k) do
150          {:ok, _} -> acc
151          :error   -> Map.put(acc, k, apply(mod, fun, args))
152        end
153    end
154  end
155
156  defp action_to_auto(:insert), do: :autogenerate
157  defp action_to_auto(:update), do: :autoupdate
158
159  @doc """
160  Callback invoked to build relations.
161  """
162  def build(%Embedded{related: related}) do
163    related.__struct__
164  end
165end
166