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