embedded.ex (9475B)
1 defmodule Ecto.Embedded do 2 @moduledoc """ 3 The embedding struct for `embeds_one` and `embeds_many`. 4 5 Its fields are: 6 7 * `cardinality` - The association cardinality 8 * `field` - The name of the association field on the schema 9 * `owner` - The schema where the association was defined 10 * `related` - The schema that is embedded 11 * `on_cast` - Function name to call by default when casting embeds 12 * `on_replace` - The action taken on associations when schema is replaced 13 14 """ 15 alias __MODULE__ 16 alias Ecto.Changeset 17 alias Ecto.Changeset.Relation 18 19 use Ecto.ParameterizedType 20 21 @behaviour Relation 22 @on_replace_opts [:raise, :mark_as_invalid, :delete] 23 @embeds_one_on_replace_opts @on_replace_opts ++ [:update] 24 25 defstruct [ 26 :cardinality, 27 :field, 28 :owner, 29 :related, 30 :on_cast, 31 on_replace: :raise, 32 unique: true, 33 ordered: true 34 ] 35 36 ## Parameterized API 37 38 # We treat even embed_many as maps, as that's often the 39 # most efficient format to encode them in the database. 40 @impl Ecto.ParameterizedType 41 def type(_), do: {:map, :any} 42 43 @impl Ecto.ParameterizedType 44 def init(opts) do 45 opts = Keyword.put_new(opts, :on_replace, :raise) 46 cardinality = Keyword.fetch!(opts, :cardinality) 47 48 on_replace_opts = 49 if cardinality == :one, do: @embeds_one_on_replace_opts, else: @on_replace_opts 50 51 unless opts[:on_replace] in on_replace_opts do 52 raise ArgumentError, "invalid `:on_replace` option for #{inspect Keyword.fetch!(opts, :field)}. " <> 53 "The only valid options are: " <> 54 Enum.map_join(on_replace_opts, ", ", &"`#{inspect &1}`") 55 end 56 57 struct(%Embedded{}, opts) 58 end 59 60 @impl Ecto.ParameterizedType 61 def load(nil, _fun, %{cardinality: :one}), do: {:ok, nil} 62 63 def load(value, fun, %{cardinality: :one, related: schema, field: field}) when is_map(value) do 64 {:ok, load_field(field, schema, value, fun)} 65 end 66 67 def load(nil, _fun, %{cardinality: :many}), do: {:ok, []} 68 69 def load(value, fun, %{cardinality: :many, related: schema, field: field}) when is_list(value) do 70 {:ok, Enum.map(value, &load_field(field, schema, &1, fun))} 71 end 72 73 def load(_value, _fun, _embed) do 74 :error 75 end 76 77 defp load_field(_field, schema, value, loader) when is_map(value) do 78 Ecto.Schema.Loader.unsafe_load(schema, value, loader) 79 end 80 81 defp load_field(field, _schema, value, _fun) do 82 raise ArgumentError, "cannot load embed `#{field}`, expected a map but got: #{inspect value}" 83 end 84 85 @impl Ecto.ParameterizedType 86 def dump(nil, _, _), do: {:ok, nil} 87 88 def dump(value, fun, %{cardinality: :one, related: schema, field: field}) when is_map(value) do 89 {:ok, dump_field(field, schema, value, schema.__schema__(:dump), fun, _one_embed? = true)} 90 end 91 92 def dump(value, fun, %{cardinality: :many, related: schema, field: field}) when is_list(value) do 93 types = schema.__schema__(:dump) 94 {:ok, Enum.map(value, &dump_field(field, schema, &1, types, fun, _one_embed? = false))} 95 end 96 97 def dump(_value, _fun, _embed) do 98 :error 99 end 100 101 defp dump_field(_field, schema, %{__struct__: schema} = struct, types, dumper, _one_embed?) do 102 Ecto.Schema.Loader.safe_dump(struct, types, dumper) 103 end 104 105 defp dump_field(field, schema, value, _types, _dumper, one_embed?) do 106 one_or_many = 107 if one_embed?, 108 do: "a struct #{inspect schema} value", 109 else: "a list of #{inspect schema} struct values" 110 111 raise ArgumentError, 112 "cannot dump embed `#{field}`, expected #{one_or_many} but got: #{inspect value}" 113 end 114 115 @impl Ecto.ParameterizedType 116 def cast(nil, %{cardinality: :one}), do: {:ok, nil} 117 def cast(%{__struct__: schema} = struct, %{cardinality: :one, related: schema}) do 118 {:ok, struct} 119 end 120 121 def cast(nil, %{cardinality: :many}), do: {:ok, []} 122 def cast(value, %{cardinality: :many, related: schema}) when is_list(value) do 123 if Enum.all?(value, &Kernel.match?(%{__struct__: ^schema}, &1)) do 124 {:ok, value} 125 else 126 :error 127 end 128 end 129 130 def cast(_value, _embed) do 131 :error 132 end 133 134 @impl Ecto.ParameterizedType 135 def embed_as(_, _), do: :dump 136 137 ## End of parameterized API 138 139 # Callback invoked by repository to prepare embeds. 140 # 141 # It replaces the changesets for embeds inside changes 142 # by actual structs so it can be dumped by adapters and 143 # loaded into the schema struct afterwards. 144 @doc false 145 def prepare(changeset, embeds, adapter, repo_action) do 146 %{changes: changes, types: types, repo: repo} = changeset 147 prepare(Map.take(changes, embeds), types, adapter, repo, repo_action) 148 end 149 150 defp prepare(embeds, _types, _adapter, _repo, _repo_action) when embeds == %{} do 151 embeds 152 end 153 154 defp prepare(embeds, types, adapter, repo, repo_action) do 155 Enum.reduce embeds, embeds, fn {name, changeset_or_changesets}, acc -> 156 {:embed, embed} = Map.get(types, name) 157 Map.put(acc, name, prepare_each(embed, changeset_or_changesets, adapter, repo, repo_action)) 158 end 159 end 160 161 defp prepare_each(%{cardinality: :one}, nil, _adapter, _repo, _repo_action) do 162 nil 163 end 164 165 defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, repo, repo_action) do 166 action = check_action!(changeset.action, repo_action, embed) 167 changeset = run_prepare(changeset, repo) 168 to_struct(changeset, action, embed, adapter) 169 end 170 171 defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, repo, repo_action) do 172 for changeset <- changesets, 173 action = check_action!(changeset.action, repo_action, embed), 174 changeset = run_prepare(changeset, repo), 175 prepared = to_struct(changeset, action, embed, adapter), 176 do: prepared 177 end 178 179 defp to_struct(%Changeset{valid?: false}, _action, 180 %{related: schema}, _adapter) do 181 raise ArgumentError, "changeset for embedded #{inspect schema} is invalid, " <> 182 "but the parent changeset was not marked as invalid" 183 end 184 185 defp to_struct(%Changeset{data: %{__struct__: actual}}, _action, 186 %{related: expected}, _adapter) when actual != expected do 187 raise ArgumentError, "expected changeset for embedded schema `#{inspect expected}`, " <> 188 "got: #{inspect actual}" 189 end 190 191 defp to_struct(%Changeset{changes: changes, data: schema}, :update, 192 _embed, _adapter) when changes == %{} do 193 schema 194 end 195 196 defp to_struct(%Changeset{}, :delete, _embed, _adapter) do 197 nil 198 end 199 200 defp to_struct(%Changeset{data: data} = changeset, action, %{related: schema}, adapter) do 201 %{data: struct, changes: changes} = changeset = 202 maybe_surface_changes(changeset, data, schema, action) 203 204 embeds = prepare(changeset, schema.__schema__(:embeds), adapter, action) 205 206 changes 207 |> Map.merge(embeds) 208 |> autogenerate_id(struct, action, schema, adapter) 209 |> autogenerate(action, schema) 210 |> apply_embeds(struct) 211 end 212 213 defp maybe_surface_changes(changeset, data, schema, :insert) do 214 Relation.surface_changes(changeset, data, schema.__schema__(:fields)) 215 end 216 217 defp maybe_surface_changes(changeset, _data, _schema, _action) do 218 changeset 219 end 220 221 defp run_prepare(changeset, repo) do 222 changeset = %{changeset | repo: repo} 223 224 Enum.reduce(Enum.reverse(changeset.prepare), changeset, fn fun, acc -> 225 case fun.(acc) do 226 %Ecto.Changeset{} = acc -> acc 227 other -> 228 raise "expected function #{inspect fun} given to Ecto.Changeset.prepare_changes/2 " <> 229 "to return an Ecto.Changeset, got: `#{inspect other}`" 230 end 231 end) 232 end 233 234 defp apply_embeds(changes, struct) do 235 struct(struct, changes) 236 end 237 238 defp check_action!(:replace, action, %{on_replace: :delete} = embed), 239 do: check_action!(:delete, action, embed) 240 defp check_action!(:update, :insert, %{related: schema}), 241 do: raise(ArgumentError, "got action :update in changeset for embedded #{inspect schema} while inserting") 242 defp check_action!(action, _, _), do: action 243 244 defp autogenerate_id(changes, _struct, :insert, schema, adapter) do 245 case schema.__schema__(:autogenerate_id) do 246 {key, _source, :binary_id} -> 247 Map.put_new_lazy(changes, key, fn -> adapter.autogenerate(:embed_id) end) 248 {_key, :id} -> 249 raise ArgumentError, "embedded schema `#{inspect schema}` cannot autogenerate `:id` primary keys, " <> 250 "those are typically used for auto-incrementing constraints. " <> 251 "Maybe you meant to use `:binary_id` instead?" 252 nil -> 253 changes 254 end 255 end 256 257 defp autogenerate_id(changes, struct, :update, _schema, _adapter) do 258 for {_, nil} <- Ecto.primary_key(struct) do 259 raise Ecto.NoPrimaryKeyValueError, struct: struct 260 end 261 changes 262 end 263 264 defp autogenerate(changes, action, schema) do 265 autogen_fields = action |> action_to_auto() |> schema.__schema__() 266 267 Enum.reduce(autogen_fields, changes, fn {fields, {mod, fun, args}}, acc -> 268 case Enum.reject(fields, &Map.has_key?(changes, &1)) do 269 [] -> 270 acc 271 272 fields -> 273 generated = apply(mod, fun, args) 274 Enum.reduce(fields, acc, &Map.put(&2, &1, generated)) 275 end 276 end) 277 end 278 279 defp action_to_auto(:insert), do: :autogenerate 280 defp action_to_auto(:update), do: :autoupdate 281 282 @impl Relation 283 def build(%Embedded{related: related}, _owner) do 284 related.__struct__ 285 end 286 287 def preload_info(_embed) do 288 :embed 289 end 290 end