zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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