Current section
Files
Jump to
Current section
Files
lib/aerospike/protocol/info.ex
defmodule Aerospike.Protocol.Info do
@moduledoc false
alias Aerospike.Protocol.Message
@doc """
Encodes an info request from a list of command strings.
Commands are joined with newlines and a trailing newline is appended.
The result is wrapped in a Message with type 1 (info).
## Examples
iex> Aerospike.Protocol.Info.encode_request(["namespaces"])
<<2, 1, 0, 0, 0, 0, 0, 11, "namespaces\\n">>
iex> Aerospike.Protocol.Info.encode_request(["node", "build"])
<<2, 1, 0, 0, 0, 0, 0, 11, "node\\nbuild\\n">>
iex> Aerospike.Protocol.Info.encode_request([])
<<2, 1, 0, 0, 0, 0, 0, 0>>
"""
@spec encode_request([String.t()]) :: binary()
def encode_request([]), do: Message.encode_info("")
def encode_request(commands) when is_list(commands) do
payload = Enum.join(commands, "\n") <> "\n"
Message.encode_info(payload)
end
@doc """
Decodes an info response body into a map of key-value pairs.
The response format is newline-separated entries where each entry is either:
- `key\\tvalue` (tab-separated key and value)
- `key` (key only, value is empty string)
## Examples
iex> Aerospike.Protocol.Info.decode_response("namespaces\\ttest;memory\\n")
{:ok, %{"namespaces" => "test;memory"}}
iex> Aerospike.Protocol.Info.decode_response("node\\tBB9050011AC4202\\nbuild\\t7.0.0.0\\n")
{:ok, %{"node" => "BB9050011AC4202", "build" => "7.0.0.0"}}
iex> Aerospike.Protocol.Info.decode_response("status\\n")
{:ok, %{"status" => ""}}
iex> Aerospike.Protocol.Info.decode_response("")
{:ok, %{}}
"""
@spec decode_response(binary()) :: {:ok, map()}
def decode_response(body) when is_binary(body) do
result =
body
|> String.trim_trailing("\n")
|> case do
"" -> %{}
trimmed -> parse_entries(trimmed)
end
{:ok, result}
end
defp parse_entries(data) do
data
|> String.split("\n")
|> Enum.reduce(%{}, fn entry, acc ->
case String.split(entry, "\t", parts: 2) do
[key, value] -> Map.put(acc, key, value)
[key] -> Map.put(acc, key, "")
end
end)
end
@doc """
Decodes a complete info response message (header + body).
First decodes the message envelope, validates it's an info message (type 1),
then parses the body.
## Examples
iex> msg = <<2, 1, 0, 0, 0, 0, 0, 16, "namespaces\\ttest\\n">>
iex> Aerospike.Protocol.Info.decode_message(msg)
{:ok, %{"namespaces" => "test"}}
"""
@spec decode_message(binary()) ::
{:ok, map()} | {:error, :incomplete_header | :incomplete_body | :invalid_message_type}
def decode_message(data) do
with {:ok, {_version, type, body}} <- Message.decode(data) do
if type == Message.type_info() do
decode_response(body)
else
{:error, :invalid_message_type}
end
end
end
@typedoc """
A capability token captured from the server's `features` info reply.
Recognised tokens (`compression`, `pipelining`) become atoms so call
sites can pattern-match. Every other token is preserved as
`{:unknown, raw_string}` so future probes (e.g. diagnostic logging or
a wider allow-list in a later phase) can still see what the server
advertised.
"""
@type feature :: atom() | {:unknown, String.t()}
# Recognised tokens become atoms so callers can pattern-match. Anything
# outside this list is preserved as `{:unknown, raw}` so a diagnostic
# log or a future wider allow-list can still see what the server
# advertised. Keep this list aligned with the call sites that actually
# branch on a feature — adding a token here without a consumer is
# noise.
@recognised_features %{
"compression" => :compression,
"pipelining" => :pipelining
}
@doc """
Parses a `features` info-key value into a set of capability tokens.
The reply body is a `;`-separated list of feature names. Empty
segments (leading, trailing, or doubled separators) are dropped.
Recognised tokens become atoms; unrecognised tokens are preserved
as `{:unknown, raw}` tuples.
## Examples
iex> Aerospike.Protocol.Info.parse_features("compression;pipelining")
MapSet.new([:compression, :pipelining])
iex> Aerospike.Protocol.Info.parse_features("")
MapSet.new()
iex> Aerospike.Protocol.Info.parse_features("compression;peers;;batch-index")
MapSet.new([:compression, {:unknown, "batch-index"}, {:unknown, "peers"}])
"""
@spec parse_features(binary()) :: MapSet.t(feature())
def parse_features(value) when is_binary(value) do
value
|> String.split(";", trim: true)
|> Enum.reduce(MapSet.new(), fn raw, acc ->
token = raw |> String.trim() |> classify_feature()
if token == :empty, do: acc, else: MapSet.put(acc, token)
end)
end
defp classify_feature(""), do: :empty
defp classify_feature(raw) do
case Map.fetch(@recognised_features, raw) do
{:ok, atom} -> atom
:error -> {:unknown, raw}
end
end
end