Current section

Files

Jump to
ecto_poly lib ecto_poly.ex
Raw

lib/ecto_poly.ex

defmodule EctoPoly do
@moduledoc """
Creates a polymorphic embedded type
"""
@doc """
# Arguments
* `types`: Keyword list of `{name, type}`. The `name` is stored in the database in order to identify what `type` to use in runtime.
* `type_field`: Name of the field used to the type of that particular object. Default is `:__type__`.
# Example
defmodule EctoPoly.TestChannelData do
use EctoPoly, types: [
sms: TestSmsChannel,
email: TestEmailChannel,
]
end
"""
defmacro __using__(opts) do
env = __CALLER__
type_field =
opts
|> Keyword.get(:type_field, :__type__)
|> Macro.expand(env)
|> Atom.to_string
types =
opts
|> Keyword.fetch!(:types)
|> Enum.map(fn {key, value} ->
{key |> Macro.expand(env), value |> Macro.expand(env)}
end)
|> Macro.expand(env)
union_type = build_union_type(types)
quote do
@behaviour Ecto.Type
@type_field unquote(type_field)
@type t :: unquote(union_type)
def type, do: :map
EctoPoly.__casters__(unquote(types))
EctoPoly.__dumpers__(unquote(types))
EctoPoly.__loaders__(unquote(types))
def load(data) when is_map(data) do
name =
data
|> Map.get(@type_field)
|> String.to_existing_atom
fields =
data
|> Map.delete(@type_field)
|> Enum.map(fn {key, value} ->
{String.to_atom(key), value}
end)
load(name, fields)
end
def cast(_), do: :error
def dump(_), do: :error
end
end
@doc false
defmacro __casters__(types) do
types
|> Enum.map(&caster/1)
end
@doc false
defmacro __dumpers__(types) do
types
|> Enum.map(&dumper/1)
end
@doc false
defmacro __loaders__(types) do
types
|> Enum.map(&loader/1)
end
defp caster({_, value_type}) do
quote do
def cast(value = %unquote(value_type){}), do: {:ok, value}
end
end
defp loader({name, value_type}) do
loader(is_schema?(value_type), name, value_type)
end
defp loader(true, name, value_type) do
quote do
defp load(unquote(name), fields) do
result =
unquote(value_type)
|> Ecto.Schema.__unsafe_load__(fields |> Map.new, &EctoPoly.load_value/2)
{:ok, result}
end
end
end
defp loader(false, name, value_type) do
quote do
defp load(unquote(name), fields) do
result =
unquote(value_type)
|> struct!(fields)
{:ok, result}
end
end
end
defp dumper({name, value_type}) do
dumper(is_schema?(value_type), name, value_type)
end
defp dumper(true, name, value_type) do
quote do
def dump(value = %unquote(value_type){}) do
fields = unquote(value_type).__schema__(:dump)
result =
value
|> EctoPoly.dump_schema(fields)
|> Map.put(@type_field, Atom.to_string(unquote(name)))
{:ok, result}
end
end
end
defp dumper(false, name, value_type) do
quote do
def dump(value = %unquote(value_type){}) do
result =
value
|> Map.from_struct
|> Map.put(@type_field, Atom.to_string(unquote(name)))
{:ok, result}
end
end
end
defp build_union_type(types) do
types
|> Enum.reduce(nil, fn (x, acc) ->
case acc do
nil ->
x
value ->
{:|, [], [value, x]}
end
end)
end
defp is_schema?(type) do
try do
type.__schema__(:query)
true
rescue
_ in UndefinedFunctionError -> false
end
end
@doc false
def dump_schema(struct, fields) do
fields
|> Enum.reduce(%{}, fn {field, {source, type}}, acc ->
value = Map.get(struct, field)
dumped = dump_value(type, value)
case dumped do
{:ok, value} ->
Map.put(acc, source, value)
:error ->
raise ArgumentError, "cannot dump `#{inspect value}` as type #{inspect type}"
end
end)
end
@doc false
def dump_value(type, value) do
with {:ok, value} <- Ecto.Type.dump(type, value),
{:ok, value} <- transform_dump(type, value)
do
{:ok, value}
else
{:error, error} ->
{:error, error}
:error ->
:error
end
end
@doc false
def load_value(type, value) do
with {:ok, value} <- transform_load(type, value),
{:ok, value} <- Ecto.Type.load(type, value)
do
{:ok, value}
else
{:error, error} ->
{:error, error}
:error ->
:error
end
end
defp transform_dump(type, value), do: do_transform_dump(Ecto.Type.type(type), value)
defp do_transform_dump(_, nil), do: {:ok, nil}
defp do_transform_dump(:decimal, value), do: {:ok, Decimal.to_string(value)}
defp do_transform_dump(:time, {hour, minute, second, microsecond}) do
result =
%Time{hour: hour, minute: minute, second: second, microsecond: {microsecond, 6}}
|> Time.to_iso8601
{:ok, result}
end
defp do_transform_dump(:naive_datetime, {{year, month, day}, {hour, minute, second, microsecond}}) do
result =
%NaiveDateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, 6}}
|> NaiveDateTime.to_iso8601
{:ok, result}
end
defp do_transform_dump(:utc_datetime, {{year, month, day}, {hour, minute, second, microsecond}}) do
result =
%DateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, 6},
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}
|> DateTime.to_iso8601
{:ok, result}
end
defp do_transform_dump({:map, type}, values), do: transform_map(type, values, &transform_dump/2)
defp do_transform_dump({:array, type}, values), do: transform_array(type, values, &transform_dump/2)
defp do_transform_dump(_, value), do: {:ok, value}
defp transform_load(type, value), do: do_transform_load(Ecto.Type.type(type), value)
defp do_transform_load(_, nil), do: {:ok, nil}
defp do_transform_load(:decimal, value), do: {:ok, Decimal.new(value)}
defp do_transform_load(:time, value) do
with {:ok, %{
hour: hour, minute: minute, second: second, microsecond: {microsecond, 6}
}} = value |> Time.from_iso8601
do
{:ok, {hour, minute, second, microsecond}}
end
end
defp do_transform_load(:naive_datetime, value) do
with {:ok, %{
year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, 6}
}} = value |> NaiveDateTime.from_iso8601
do
{:ok, {{year, month, day}, {hour, minute, second, microsecond}}}
end
end
defp do_transform_load(:utc_datetime, value) do
with {:ok, %{
year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, 6}
}, _} <- value |> DateTime.from_iso8601
do
{:ok, {{year, month, day}, {hour, minute, second, microsecond}}}
end
end
defp do_transform_load({:map, type}, values), do: transform_map(type, values, &transform_load/2)
defp do_transform_load({:array, type}, values), do: transform_array(type, values, &transform_load/2)
defp do_transform_load(_, value), do: {:ok, value}
defp transform_map(type, values, fun) do
result =
values
|> Enum.reduce_while({:ok, []}, fn {key, value}, {:ok, acc} ->
case fun.(type, value) do
{:ok, result} ->
{:cont, {:ok, [{key, result} | acc]}}
{:error, error} ->
{:halt, {:error, error}}
:error ->
{:halt, :error}
end
end)
with {:ok, result} <- result do
{:ok, result |> Map.new}
end
end
defp transform_array(type, values, fun) do
result =
values
|> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} ->
case fun.(type, value) do
{:ok, result} ->
{:cont, {:ok, [result | acc]}}
{:error, error} ->
{:halt, {:error, error}}
:error ->
{:halt, :error}
end
end)
with {:ok, result} <- result do
{:ok, result |> Enum.reverse}
end
end
end