defmodule Exandra.EmbeddedType do
opts_schema = [
cardinality: [
type: :atom,
required: false,
doc:
"`:one` for a regular UDT column, or `:many` for a frozen list of UDTs. default is `:one`"
],
using: [
type: :atom,
required: true,
doc: "The Ecto.Schema to use for the UDT."
],
field: [
type: :atom,
doc: false
],
schema: [
type: :atom,
doc: false
]
]
@moduledoc """
`Ecto.ParameterizedType` for **User-Defined Types** (UDTs) with Ecto.Schemas.
## Options
#{NimbleOptions.docs(opts_schema)}
## Examples
defmodule PageView do
use Ecto.Schema
use Exandra.Embedded
schema "page_views" do
field :url, :string
embedded_type :view_meta, ViewMeta
end
def changeset(entity, params) do
entity
|> cast(params, [:url, :view_meta])
end
end
"""
use Ecto.ParameterizedType
defstruct [
:cardinality,
:field,
:using
]
@type t :: any()
@opts_schema NimbleOptions.new!(opts_schema)
# Made public for testing.
@doc false
def params(embed), do: %{embed: embed}
@impl Ecto.ParameterizedType
def init(opts) do
NimbleOptions.validate!(opts, @opts_schema)
cardinality = Keyword.get(opts, :cardinality, :one)
opts = Keyword.put(opts, :cardinality, cardinality)
struct(__MODULE__, opts)
end
@impl Ecto.ParameterizedType
def type(_), do: :exandra_embedded_type
@impl Ecto.ParameterizedType
def cast(nil, %{cardinality: :one}), do: {:ok, nil}
def cast(nil, %{cardinality: :many}), do: {:ok, []}
def cast(%struct{} = data, %{cardinality: :one, using: struct}), do: {:ok, data}
def cast(data, %{cardinality: :one, using: struct}) do
autogenerated_fields =
(struct.__schema__(:primary_key) ++ struct.__schema__(:autogenerate_fields))
|> Enum.into(%{}, fn field ->
type = struct.__schema__(:type, field)
{field, Exandra.autogenerate(type)}
end)
struct
|> struct(%{})
|> struct.changeset(data)
|> Ecto.Changeset.apply_action(:insert)
|> case do
{:ok, casted} ->
{:ok, Map.merge(casted, autogenerated_fields)}
{:error, _changeset} ->
:error
end
end
def cast(data, %{cardinality: :many, using: struct}) do
data
|> Enum.reduce_while([], fn datum, acc ->
case cast(datum, %{cardinality: :one, using: struct}) do
{:ok, casted} -> {:cont, [casted | acc]}
:error -> {:halt, :error}
end
end)
|> case do
:error -> :error
casted_list -> {:ok, Enum.reverse(casted_list)}
end
end
@impl Ecto.ParameterizedType
def load(nil, _loader, %{cardinality: :one}), do: {:ok, nil}
def load(nil, _loader, %{cardinality: :many}), do: {:ok, []}
def load(value, loader, %{cardinality: :one, using: struct}) do
{:ok, Ecto.Schema.Loader.unsafe_load(struct, value, loader)}
end
def load(value, loader, %{cardinality: :many, using: struct}) do
{:ok, Enum.map(value, &Ecto.Schema.Loader.unsafe_load(struct, &1, loader))}
end
def load(_data, _loader, _opts), do: :error
@impl Ecto.ParameterizedType
def dump(nil, _dumper, _opts), do: {:ok, nil}
def dump(%struct{} = data, dumper, %{cardinality: :one, using: struct}) do
{:ok, dump_field(data, struct.__schema__(:dump), dumper)}
end
def dump(data, dumper, %{cardinality: :many, using: struct}) do
{:ok, Enum.map(data, &dump_field(&1, struct.__schema__(:dump), dumper))}
end
def dump(_data, _dumper, _opts), do: :error
defp dump_field(data, types, dumper) do
data
|> Ecto.Schema.Loader.safe_dump(types, dumper)
|> Map.new(fn {field, dumped} -> {Atom.to_string(field), dumped} end)
end
# From Ecto.Type.
@doc false
@impl Ecto.ParameterizedType
def embed_as(_format, _params), do: :dump
end