Current section
Files
Jump to
Current section
Files
lib/aerospike/ctx.ex
defmodule Aerospike.Ctx do
@moduledoc """
Context path steps for nested CDT operations.
Each helper returns one `{context_id, value}` pair that can be passed as
part of a `ctx` list to CDT helpers.
"""
alias Aerospike.Exp
alias Aerospike.Protocol.CDT
alias Aerospike.Protocol.MessagePack
@typedoc "A single nested-CDT navigation step."
@type step :: {non_neg_integer(), term()}
@typedoc """
Nested CDT context path.
Pass this list as `ctx:` to CDT operation builders that support nested
collection access.
"""
@type t :: [step()]
@typedoc """
List order used when a context step creates a nested list.
`:unordered` creates a normal list. `:ordered` creates an ordered list.
"""
@type list_order :: :unordered | :ordered
@typedoc """
Map order used when a context step creates a nested map.
`:unordered` keeps no ordering, `:key_ordered` orders entries by key, and
`:key_value_ordered` orders by key and value.
"""
@type map_order :: :unordered | :key_ordered | :key_value_ordered
@expression 0x04
@list_index 0x10
@list_rank 0x11
@list_value 0x13
@map_index 0x20
@map_rank 0x21
@map_key 0x22
@map_value 0x23
@doc """
Navigate to every child of the current collection.
"""
@spec all_children() :: step()
def all_children, do: all_children_with_filter(Exp.bool(true))
@doc """
Navigate to children of the current collection that match `expression`.
The expression is evaluated with Aerospike's built-in loop variables, such as
`Aerospike.Exp.int_loop_var(:value)`, bound to each candidate child.
"""
@spec all_children_with_filter(Exp.t()) :: step()
def all_children_with_filter(%Exp{} = expression), do: {@expression, expression}
@doc "Navigate into a list by absolute index."
@spec list_index(integer()) :: step()
def list_index(index) when is_integer(index), do: {@list_index, index}
@doc """
Navigate into a list by index, creating a nested list when it is missing.
`order` controls the created list order. When `pad` is `true` and `index` is
past the current end of the list, nil elements are inserted before the new
list.
"""
@spec list_index_create(integer(), list_order(), boolean()) :: step()
def list_index_create(index, order \\ :unordered, pad \\ false)
when is_integer(index) and is_boolean(pad) do
{Bitwise.bor(@list_index, list_order_flag(order, pad)), index}
end
@doc "Navigate into a list by rank."
@spec list_rank(integer()) :: step()
def list_rank(rank) when is_integer(rank), do: {@list_rank, rank}
@doc "Navigate to the list element equal to `value`."
@spec list_value(term()) :: step()
def list_value(value), do: {@list_value, value}
@doc "Navigate into a map by entry index."
@spec map_index(integer()) :: step()
def map_index(index) when is_integer(index), do: {@map_index, index}
@doc "Navigate into a map by value rank."
@spec map_rank(integer()) :: step()
def map_rank(rank) when is_integer(rank), do: {@map_rank, rank}
@doc "Navigate into the map entry for `key`."
@spec map_key(term()) :: step()
def map_key(key), do: {@map_key, key}
@doc """
Navigate into a map by key, creating a nested map when it is missing.
"""
@spec map_key_create(term(), map_order()) :: step()
def map_key_create(key, order \\ :unordered),
do: {Bitwise.bor(@map_key, map_order_flag(order)), key}
@doc "Navigate to the map entry whose value equals `value`."
@spec map_value(term()) :: step()
def map_value(value), do: {@map_value, value}
@doc "Serializes a context path to MessagePack bytes."
@spec to_bytes(t()) :: binary()
def to_bytes(ctx) when is_list(ctx), do: CDT.encode_ctx(ctx)
@doc "Deserializes MessagePack context bytes."
@spec from_bytes(binary()) :: t()
def from_bytes(bytes) when is_binary(bytes) do
bytes
|> MessagePack.unpack!()
|> from_flat_terms()
end
@doc "Serializes a context path to Base64-encoded MessagePack bytes."
@spec to_base64(t()) :: String.t()
def to_base64(ctx) when is_list(ctx), do: ctx |> to_bytes() |> Base.encode64()
@doc "Deserializes a Base64-encoded context path."
@spec from_base64(String.t()) :: t()
def from_base64(encoded) when is_binary(encoded) do
encoded
|> Base.decode64!()
|> from_bytes()
end
defp list_order_flag(:ordered, _pad), do: 0xC0
defp list_order_flag(:unordered, true), do: 0x80
defp list_order_flag(:unordered, false), do: 0x40
defp map_order_flag(:unordered), do: 0x40
defp map_order_flag(:key_ordered), do: 0x80
defp map_order_flag(:key_value_ordered), do: 0xC0
defp from_flat_terms(terms) when is_list(terms), do: from_flat_terms(terms, [])
defp from_flat_terms([], acc), do: Enum.reverse(acc)
defp from_flat_terms([@expression, wire | rest], acc) when is_binary(wire) do
from_flat_terms(rest, [{@expression, Exp.from_wire(wire)} | acc])
end
defp from_flat_terms([id, value | rest], acc) when is_integer(id) do
from_flat_terms(rest, [{id, value} | acc])
end
defp from_flat_terms(_other, _acc),
do: raise(ArgumentError, "context bytes must contain id/value pairs")
end