101 lines
3.2 KiB
Elixir
101 lines
3.2 KiB
Elixir
|
defmodule AshCubDB.Serde do
|
||
|
@moduledoc """
|
||
|
Handle serialising and deserialising of records into CubDB.
|
||
|
"""
|
||
|
|
||
|
alias Ash.{Resource, Type}
|
||
|
alias AshCubDB.Info
|
||
|
alias Ecto.Schema.Metadata
|
||
|
|
||
|
@doc """
|
||
|
Serialise the record into key and value tuples for storage in CubDB.
|
||
|
"""
|
||
|
@spec serialise(Resource.record()) :: {:ok, tuple, tuple} | {:error, any}
|
||
|
def serialise(record) do
|
||
|
{key_layout, data_layout} =
|
||
|
record.__struct__
|
||
|
|> Info.field_layout()
|
||
|
|
||
|
with {:ok, key} <- serialise_with_layout(record, key_layout),
|
||
|
{:ok, data} <- serialise_with_layout(record, data_layout) do
|
||
|
{:ok, key, data}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc false
|
||
|
@spec deserialise!(Resource.t(), {tuple, tuple}) :: Resource.record() | no_return
|
||
|
def deserialise!(resource, {key, data}) do
|
||
|
case deserialise(resource, key, data) do
|
||
|
{:ok, record} -> record
|
||
|
{:error, reason} -> raise reason
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@doc """
|
||
|
Convert the key and data back into a record..
|
||
|
"""
|
||
|
@spec deserialise(Resource.t(), tuple, tuple) :: {:ok, Resource.record()} | {:error, any}
|
||
|
def deserialise(resource, key, data) do
|
||
|
{key_layout, data_layout} = Info.field_layout(resource)
|
||
|
|
||
|
with {:ok, key_map} <- deserialise_with_layout(resource, key, key_layout),
|
||
|
{:ok, data_map} <- deserialise_with_layout(resource, data, data_layout) do
|
||
|
attrs = Map.merge(key_map, data_map)
|
||
|
record = struct(resource, attrs)
|
||
|
|
||
|
{:ok, %{record | __meta__: %Metadata{state: :loaded, schema: resource}}}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp serialise_with_layout(record, layout) do
|
||
|
layout
|
||
|
|> Tuple.to_list()
|
||
|
|> Enum.reduce_while({:ok, {}}, fn attr, {:ok, result} ->
|
||
|
with {:ok, value} <- fetch_record_attribute(record, attr),
|
||
|
{:ok, attr} <- fetch_attribute_definition(record.__struct__, attr),
|
||
|
{:ok, casted} <- Type.dump_to_native(attr.type, value, attr.constraints) do
|
||
|
{:cont, {:ok, Tuple.append(result, casted)}}
|
||
|
else
|
||
|
:error -> {:halt, {:error, "Failed to dump value as type `#{attr.type}`"}}
|
||
|
{:error, reason} -> {:halt, {:error, reason}}
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
defp deserialise_with_layout(resource, data, layout) do
|
||
|
layout
|
||
|
|> Tuple.to_list()
|
||
|
|> Enum.zip(Tuple.to_list(data))
|
||
|
|> Enum.reduce_while({:ok, %{}}, fn {attr, value}, {:ok, result} ->
|
||
|
with {:ok, attr} <- fetch_attribute_definition(resource, attr),
|
||
|
{:ok, value} <- Type.cast_stored(attr.type, value, attr.constraints) do
|
||
|
{:cont, {:ok, Map.put(result, attr.name, value)}}
|
||
|
else
|
||
|
:error -> {:halt, {:error, "Failed to load `#{inspect(value)}`."}}
|
||
|
{:error, reason} -> {:halt, {:error, reason}}
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
defp fetch_record_attribute(record, attribute_name) do
|
||
|
case Map.fetch(record, attribute_name) do
|
||
|
{:ok, value} ->
|
||
|
{:ok, value}
|
||
|
|
||
|
:error ->
|
||
|
{:error,
|
||
|
"Unable to retreive attribute `#{attribute_name}` from resource `#{inspect(record.__struct__)}`"}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
defp fetch_attribute_definition(resource, attribute_name) do
|
||
|
case Resource.Info.attribute(resource, attribute_name) do
|
||
|
nil ->
|
||
|
{:error, "Attribute `#{attribute_name}` not found on resource `#{inspect(resource)}`"}
|
||
|
|
||
|
attribute ->
|
||
|
{:ok, attribute}
|
||
|
end
|
||
|
end
|
||
|
end
|