Current section

Files

Jump to
bolty lib boltx pack_stream packer.ex
Raw

lib/boltx/pack_stream/packer.ex

defprotocol Bolty.PackStream.Packer do
@moduledoc """
The `Bolty.PackStream.Packer` protocol is responsible for serializing any Elixir data
structure according to the PackStream specification.
## Serializing for structs
By default, all structures are serialized with all their fields. However, if it is
necessary that only certain fields be considered for serialization, it is necessary to
perform a derivation. Below is an example:
defmodule Book do
@derive [{Bolty.PackStream.Packer, fields: [:name]}]
defstruct [:name, :other_data]
end`
"""
@fallback_to_any true
def pack(term)
end
defimpl Bolty.PackStream.Packer, for: Atom do
use Bolty.PackStream.Markers
def pack(nil), do: <<@null_marker>>
def pack(false), do: <<@false_marker>>
def pack(true), do: <<@true_marker>>
def pack(atom) do
atom
|> Atom.to_string()
|> @protocol.BitString.pack()
end
end
defimpl Bolty.PackStream.Packer, for: BitString do
use Bolty.PackStream.Markers
def pack(binary) when is_binary(binary) do
[marker(binary), binary]
end
def pack(bits) do
throw(Bolty.Error.wrap(__MODULE__, :not_encodable, bits: bits))
end
defp marker(binary) do
size = byte_size(binary)
cond do
size <= 15 -> <<@tiny_bitstring_marker::4, size::4>>
size <= 255 -> <<@bitstring8_marker, size::8>>
size <= 65_535 -> <<@bitstring16_marker, size::16>>
size <= 4_294_967_295 -> <<@bitstring32_marker, size::32>>
true -> throw(Bolty.Error.wrap(__MODULE__, :not_encodable_too_big, bits: binary))
end
end
end
defimpl Bolty.PackStream.Packer, for: Integer do
use Bolty.PackStream.Markers
def pack(integer) when integer in -16..127 do
<<integer>>
end
def pack(integer) do
case integer do
integer when integer in @int8 ->
<<@int8_marker, integer>>
integer when integer in @int16_low when integer in @int16_high ->
<<@int16_marker, integer::16>>
integer when integer in @int32_low when integer in @int32_high ->
<<@int32_marker, integer::32>>
integer when integer in @int64_low when integer in @int64_high ->
<<@int64_marker, integer::64>>
end
end
end
defimpl Bolty.PackStream.Packer, for: Float do
use Bolty.PackStream.Markers
def pack(number) do
<<@float_marker, number::float>>
end
end
defimpl Bolty.PackStream.Packer, for: List do
use Bolty.PackStream.Markers
def pack(list) do
[marker(list), list |> Enum.map(&@protocol.pack(&1))]
end
defp marker(list) do
length = length(list)
cond do
length <= 15 -> <<@tiny_list_marker::4, length::4>>
length <= 255 -> <<@list8_marker, length::8>>
length <= 65_535 -> <<@list16_marker, length::16>>
length <= 4_294_967_295 -> <<@list32_marker, length::32>>
true -> throw(Bolty.Error.wrap(__MODULE__, :not_encodable_too_big, bits: list))
end
end
end
defimpl Bolty.PackStream.Packer, for: Map do
use Bolty.PackStream.Markers
def pack(map) do
[marker(map), map |> encode_kv()]
end
defp marker(map) do
length = map_size(map)
cond do
length <= 15 -> <<@tiny_map_marker::4, length::4>>
length <= 255 -> <<@map8_marker, length::8>>
length <= 65_535 -> <<@map16_marker, length::16>>
length <= 4_294_967_295 -> <<@map32_marker, length::32>>
true -> throw(Bolty.Error.wrap(__MODULE__, :not_encodable_too_big, bits: map))
end
end
@spec encode_kv(map()) :: binary()
defp encode_kv(map) do
Enum.reduce(map, <<>>, fn data, acc -> [acc, do_reduce_kv(data)] end)
end
@spec do_reduce_kv({atom(), any()}) :: [binary()]
defp do_reduce_kv({key, value}) do
[
@protocol.pack(key),
@protocol.pack(value)
]
end
end
defimpl Bolty.PackStream.Packer, for: Time do
use Bolty.PackStream.Markers
def pack(time) do
local_time = day_time(time)
[
<<@tiny_struct_marker::4, @local_time_struct_size::4, @local_time_signature>>,
@protocol.pack(local_time)
]
end
@spec day_time(Time.t()) :: integer()
defp day_time(time) do
Time.diff(time, ~T[00:00:00.000], :nanosecond)
end
end
defimpl Bolty.PackStream.Packer, for: Date do
use Bolty.PackStream.Markers
def pack(date) do
epoch = Date.diff(date, ~D[1970-01-01])
[<<@tiny_struct_marker::4, @date_struct_size::4, @date_signature>>, @protocol.pack(epoch)]
end
end
defimpl Bolty.PackStream.Packer, for: DateTime do
use Bolty.PackStream.Markers
def pack(datetime) do
data =
Enum.map(
decompose_datetime(DateTime.to_naive(datetime)) ++ [datetime.time_zone],
&@protocol.pack(&1)
)
[
<<@tiny_struct_marker::4, @legacy_datetime_with_zone_id_struct_size::4,
@legacy_datetime_with_zone_id_signature>>,
data
]
end
@spec decompose_datetime(Calendar.naive_datetime()) :: [integer()]
defp decompose_datetime(%NaiveDateTime{} = datetime) do
datetime_micros = NaiveDateTime.diff(datetime, ~N[1970-01-01 00:00:00.000], :microsecond)
seconds = div(datetime_micros, 1_000_000)
nanoseconds = rem(datetime_micros, 1_000_000) * 1_000
[seconds, nanoseconds]
end
end
defimpl Bolty.PackStream.Packer, for: NaiveDateTime do
use Bolty.PackStream.Markers
def pack(local_datetime) do
data =
Enum.map(
decompose_datetime(local_datetime),
&@protocol.pack(&1)
)
[<<@tiny_struct_marker::4, @local_datetime_struct_size::4, @local_datetime_signature>>, data]
end
@spec decompose_datetime(Calendar.naive_datetime()) :: [integer()]
defp decompose_datetime(%NaiveDateTime{} = datetime) do
datetime_micros = NaiveDateTime.diff(datetime, ~N[1970-01-01 00:00:00.000], :microsecond)
seconds = div(datetime_micros, 1_000_000)
nanoseconds = rem(datetime_micros, 1_000_000) * 1_000
[seconds, nanoseconds]
end
end
defimpl Bolty.PackStream.Packer, for: Bolty.Types.TimeWithTZOffset do
use Bolty.PackStream.Markers
def pack(%Bolty.Types.TimeWithTZOffset{time: time, timezone_offset: offset}) do
time_and_offset = [day_time(time), offset]
data =
Enum.map(
time_and_offset,
&@protocol.pack(&1)
)
[<<@tiny_struct_marker::4, @time_with_tz_struct_size::4, @time_with_tz_signature>>, data]
end
@spec day_time(Time.t()) :: integer()
defp day_time(time) do
Time.diff(time, ~T[00:00:00.000], :nanosecond)
end
end
defimpl Bolty.PackStream.Packer, for: Bolty.Types.DateTimeWithTZOffset do
use Bolty.PackStream.Markers
def pack(%Bolty.Types.DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset}) do
data =
Enum.map(
decompose_datetime(ndt) ++ [tz_offset],
&@protocol.pack(&1)
)
[
<<@tiny_struct_marker::4, @legacy_datetime_with_zone_offset_struct_size::4,
@legacy_datetime_with_zone_offset_signature>>,
data
]
end
@spec decompose_datetime(Calendar.naive_datetime()) :: [integer()]
defp decompose_datetime(%NaiveDateTime{} = datetime) do
datetime_micros = NaiveDateTime.diff(datetime, ~N[1970-01-01 00:00:00.000], :microsecond)
seconds = div(datetime_micros, 1_000_000)
nanoseconds = rem(datetime_micros, 1_000_000) * 1_000
[seconds, nanoseconds]
end
end
defimpl Bolty.PackStream.Packer, for: Duration do
use Bolty.PackStream.Markers
def pack(duration) do
data =
Enum.map(
compact_duration(duration),
&@protocol.pack(&1)
)
[<<@tiny_struct_marker::4, @duration_struct_size::4, @duration_signature>>, data]
end
@spec compact_duration(Duration.t()) :: [integer()]
defp compact_duration(%Duration{} = duration) do
months = 12 * duration.year + duration.month
days = 7 * duration.week + duration.day
seconds = 3_600 * duration.hour + 60 * duration.minute + duration.second
[months, days, seconds, duration.microsecond * 1_000]
end
end
defimpl Bolty.PackStream.Packer, for: Bolty.Types.Point do
use Bolty.PackStream.Markers
def pack(%Bolty.Types.Point{z: nil} = point) do
data =
Enum.map(
[point.srid, point.x, point.y],
&@protocol.pack(&1)
)
[<<@tiny_struct_marker::4, @point2d_struct_size::4, @point2d_signature>>, data]
end
def pack(%Bolty.Types.Point{} = point) do
data =
Enum.map(
[point.srid, point.x, point.y, point.z],
&@protocol.pack(&1)
)
[<<@tiny_struct_marker::4, @point3d_struct_size::4, @point3d_signature>>, data]
end
end
defimpl Bolty.PackStream.Packer, for: Any do
defmacro __deriving__(module, struct, options) do
deriving(module, struct, options)
end
def deriving(module, struct, options) do
keys = struct |> Map.from_struct() |> Map.keys()
fields = Keyword.get(options, :fields, keys)
include_struct_field? = Keyword.get(options, :include_struct_field, :__struct__ in fields)
fields_to_serialize = List.delete(fields, :__struct__)
extractor =
cond do
fields_to_serialize == keys and include_struct_field? ->
quote(do: struct |> Map.from_struct() |> Map.put("__struct__", unquote(module)))
fields_to_serialize == keys ->
quote(do: Map.from_struct(struct))
include_struct_field? ->
quote(
do:
struct
|> Map.take(unquote(fields_to_serialize))
|> Map.put("__struct__", unquote(module))
)
true ->
quote(do: Map.take(struct, unquote(fields_to_serialize)))
end
quote do
defimpl unquote(@protocol), for: unquote(module) do
def pack(struct) do
unquote(extractor)
|> @protocol.Map.pack()
end
end
end
end
def pack(%{__struct__: _} = struct) do
@protocol.Map.pack(Map.from_struct(struct))
end
def pack(term) do
raise Protocol.UndefinedError, protocol: @protocol, value: term
end
end