Current section
Files
Jump to
Current section
Files
lib/resp.ex
defmodule Resp do
@moduledoc """
RESP2 and RESP3 protocol parser and encoder for Elixir.
Supports the full RESP2 and RESP3 specifications including all aggregate
and streamed types. Continuation-based parsing for TCP streaming.
## Parsing
{:ok, arr, ""} = Resp.parse("*2\\r\\n$3\\r\\nGET\\r\\n$3\\r\\nkey\\r\\n")
arr.data
#=> [%Resp.Bulk{data: "GET"}, %Resp.Bulk{data: "key"}]
## Encoding
Resp.encode_string("OK")
#=> "+OK\\r\\n"
"""
defmodule ParseError do
defexception [:message]
@type t :: %__MODULE__{message: String.t()}
end
@crlf "\r\n"
# ───────────────────────────────────
# Parsing
# ───────────────────────────────────
@doc """
Parses a RESP command from binary data.
Returns `{:ok, %Resp.Array{}, rest}` for RESP2 arrays and inline/telnet commands.
Returns `{:continuation, fun}` when more data is needed.
Raises `Resp.ParseError` on protocol violations.
"""
@spec parse(binary()) :: {:ok, Resp.Array.t(), binary()} | {:continuation, function()}
def parse(data)
def parse(<<?*, rest::binary>>) do
parse_resp_array(rest)
end
def parse(<<>>) do
{:continuation, &parse/1}
end
def parse(data) do
parse_telnet(data)
end
defp parse_resp_array(data) do
resolve_cont(parse_integer(data), fn
-1, rest -> {:ok, %Resp.Array{type: :null, data: nil, count: -1}, rest}
0, rest -> {:ok, %Resp.Array{type: :array, data: [], count: 0}, rest}
count, rest -> parse_bulk_args(rest, count, [])
end)
end
defp parse_bulk_args(data, 0, acc) do
{:ok, %Resp.Array{type: :array, data: Enum.reverse(acc), count: length(acc)}, data}
end
defp parse_bulk_args(data, n, acc) do
resolve_cont(parse_bulk_len(data), fn
-1, rest ->
parse_bulk_args(rest, n - 1, [nil | acc])
size, rest ->
parse_bulk_body(rest, size, [], fn data, rest ->
parse_bulk_args(rest, n - 1, [%Resp.Bulk{type: :bulk, data: data, count: 0} | acc])
end)
end)
end
defp parse_bulk_len(<<?$, rest::binary>>), do: parse_integer(rest)
defp parse_bulk_len(<<>>), do: {:continuation, &parse_bulk_len/1}
defp parse_bulk_body(data, 0, buf, ok) do
resolve_cont(expect_crlf(data), fn :ok, rest ->
ok.(IO.iodata_to_binary(buf), rest)
end)
end
defp parse_bulk_body(data, size_left, buf, ok) when size_left > 0 do
dsize = byte_size(data)
if dsize >= size_left do
<<chunk::binary-size(size_left), rest::binary>> = data
resolve_cont(expect_crlf(rest), fn :ok, rest ->
ok.(IO.iodata_to_binary([buf, chunk]), rest)
end)
else
if dsize > 0 do
{:continuation, &parse_bulk_body(&1, size_left - dsize, [buf, data], ok)}
else
{:continuation, &parse_bulk_body(&1, size_left, buf, ok)}
end
end
end
# ── Telnet ─────────────────────────
defp parse_telnet(data) do
resolve_cont(read_line(data), fn line, rest ->
args = tokenize(line)
bulks = Enum.map(args, &%Resp.Bulk{type: :bulk, data: &1, count: 0})
{:ok, %Resp.Array{type: :array, data: bulks, count: length(bulks)}, rest}
end)
end
defp read_line(data, acc \\ "")
defp read_line(<<?\r, ?\n, rest::binary>>, acc), do: {:ok, acc, rest}
defp read_line(<<?\n, rest::binary>>, acc), do: {:ok, acc, rest}
defp read_line(<<>>, acc), do: {:continuation, &read_line(&1, acc)}
defp read_line(<<c, rest::binary>>, acc), do: read_line(rest, <<acc::binary, c>>)
defp tokenize(line), do: tokenize(line, [], [])
defp tokenize(<<>>, cur, args), do: finish_token(cur, args)
defp tokenize(<<?\s, rest::binary>>, cur, args),
do: tokenize(trim_spaces(rest), [], finish_token(cur, args))
defp tokenize(<<?", rest::binary>>, _cur, args), do: parse_quoted(rest, ?", [], args)
defp tokenize(<<?', rest::binary>>, _cur, args), do: parse_quoted(rest, ?', [], args)
defp tokenize(<<c, rest::binary>>, cur, args), do: tokenize(rest, [cur, c], args)
defp trim_spaces(<<?\s, rest::binary>>), do: trim_spaces(rest)
defp trim_spaces(data), do: data
defp finish_token([], args), do: args
defp finish_token(cur, args), do: args ++ [IO.iodata_to_binary(cur)]
defp parse_quoted(<<q, rest::binary>>, q, quoted, args) do
tokenize(trim_spaces(rest), [], args ++ [IO.iodata_to_binary(quoted)])
end
defp parse_quoted(<<?\\, rest::binary>>, q, quoted, args),
do: parse_quoted_esc(rest, q, quoted, args)
defp parse_quoted(<<c, rest::binary>>, q, quoted, args),
do: parse_quoted(rest, q, [quoted, c], args)
defp parse_quoted(<<>>, _q, _quoted, _args), do: {:continuation, &{:continuation, &1}}
defp parse_quoted_esc(<<c, rest::binary>>, q, quoted, args) do
parse_quoted(rest, q, [quoted, escape(c)], args)
end
defp parse_quoted_esc(<<>>, _q, _quoted, _args), do: {:continuation, &{:continuation, &1}}
defp escape(?n), do: ?\n
defp escape(?r), do: ?\r
defp escape(?t), do: ?\t
defp escape(c), do: c
# ── RESP3 parse_value ──────────────
@doc """
Parses any RESP2 or RESP3 typed value from binary data.
Returns `{:ok, %Resp.*{}, rest}` where the struct type matches the type byte.
Returns `{:continuation, fun}` when more data is needed.
"""
@spec parse_value(binary()) ::
{:ok,
Resp.String.t()
| Resp.Error.t()
| Resp.Integer.t()
| Resp.Bulk.t()
| Resp.Null.t()
| Resp.Array.t()
| Resp.Bool.t()
| Resp.Double.t()
| Resp.BigNumber.t()
| Resp.BlobError.t()
| Resp.VerbatimString.t()
| Resp.Map.t()
| Resp.Set.t()
| Resp.Push.t()
| Resp.Attribute.t(), binary()}
| {:continuation, function()}
def parse_value(data)
def parse_value(<<?+, rest::binary>>) do
resolve_cont(read_line(rest, ""), fn line, rest ->
{:ok,
%Resp.String{
type: :string,
raw: <<?+, line::binary, @crlf::binary>>,
data: line,
count: 0
}, rest}
end)
end
def parse_value(<<?-, rest::binary>>) do
resolve_cont(read_line(rest, ""), fn line, rest ->
{:ok,
%Resp.Error{type: :error, raw: <<?-, line::binary, @crlf::binary>>, data: line, count: 0},
rest}
end)
end
def parse_value(<<?:, rest::binary>>) do
resolve_cont(read_line(rest, ""), fn line, rest ->
{:ok,
%Resp.Integer{
type: :integer,
raw: <<?:, line::binary, @crlf::binary>>,
data: line,
count: 0
}, rest}
end)
end
def parse_value(<<?$, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
-1, rest ->
{:ok, %Resp.Null{type: :null, raw: "$-1\r\n", data: nil, version: :resp2, count: 0}, rest}
0, rest ->
resolve_cont(expect_crlf(rest), fn :ok, rest ->
{:ok, %Resp.Bulk{type: :bulk, raw: "$0\r\n\r\n", data: "", count: 0}, rest}
end)
size, rest ->
parse_bulk_body(rest, size, [], fn data, body_rest ->
raw = IO.iodata_to_binary([<<"$#{size}\r\n">>, data, @crlf])
{:ok, %Resp.Bulk{type: :bulk, raw: raw, data: data, count: 0}, body_rest}
end)
end)
end
def parse_value(<<?*, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
-1, rest -> {:ok, %Resp.Array{type: :null, data: nil, count: -1}, rest}
0, rest -> {:ok, %Resp.Array{type: :array, data: [], count: 0}, rest}
count, rest -> parse_elems(rest, :array, count, [])
end)
end
def parse_value(<<?_, rest::binary>>) do
resolve_cont(expect_crlf(rest), fn :ok, rest ->
{:ok, %Resp.Null{type: :null, raw: "_\r\n", data: nil, version: :resp3, count: 0}, rest}
end)
end
def parse_value(<<?#, rest::binary>>) do
case rest do
<<?t, @crlf, rest::binary>> -> {:ok, %Resp.Bool{type: :bool, data: true, count: 0}, rest}
<<?f, @crlf, rest::binary>> -> {:ok, %Resp.Bool{type: :bool, data: false, count: 0}, rest}
<<?t>> -> {:continuation, &finish_bool(&1, true)}
<<?f>> -> {:continuation, &finish_bool(&1, false)}
<<>> -> {:continuation, &parse_value/1}
_ -> raise %ParseError{message: "invalid boolean value: #{inspect(rest)}"}
end
end
def parse_value(<<?,, rest::binary>>) do
resolve_cont(read_line(rest, ""), fn line, rest ->
{:ok, %Resp.Double{type: :double, data: line, count: 0}, rest}
end)
end
def parse_value(<<?(, rest::binary>>) do
resolve_cont(read_line(rest, ""), fn line, rest ->
{:ok, %Resp.BigNumber{type: :big_number, data: line, count: 0}, rest}
end)
end
def parse_value(<<?!, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
-1, rest ->
{:ok, %Resp.BlobError{type: :blob_error, data: nil, count: 0}, rest}
0, rest ->
resolve_cont(expect_crlf(rest), fn :ok, rest ->
{:ok, %Resp.BlobError{type: :blob_error, data: "", count: 0}, rest}
end)
size, rest ->
parse_bulk_body(rest, size, [], fn data, body_rest ->
{:ok, %Resp.BlobError{type: :blob_error, data: data, count: 0}, body_rest}
end)
end)
end
def parse_value(<<?=, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
-1, rest ->
{:ok, %Resp.VerbatimString{type: :verbatim_string, data: nil, count: 0}, rest}
0, rest ->
resolve_cont(expect_crlf(rest), fn :ok, rest ->
{:ok, %Resp.VerbatimString{type: :verbatim_string, data: "", count: 0}, rest}
end)
size, rest ->
parse_bulk_body(rest, size, [], fn data, v_rest ->
{:ok, %Resp.VerbatimString{type: :verbatim_string, data: data, count: 0}, v_rest}
end)
end)
end
def parse_value(<<?%, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
0, rest -> {:ok, %Resp.Map{type: :map, data: [], count: 0}, rest}
count, rest -> parse_elems(rest, :map, count * 2, [])
end)
end
def parse_value(<<?~, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
0, rest -> {:ok, %Resp.Set{type: :set, data: [], count: 0}, rest}
count, rest -> parse_elems(rest, :set, count, [])
end)
end
def parse_value(<<?>, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
0, rest -> {:ok, %Resp.Push{type: :push, data: [], count: 0}, rest}
count, rest -> parse_elems(rest, :push, count, [])
end)
end
def parse_value(<<?|, rest::binary>>) do
resolve_cont(parse_integer(rest), fn
0, rest -> {:ok, %Resp.Attribute{type: :attribute, data: [], count: 0}, rest}
count, rest -> parse_elems(rest, :attribute, count * 2, [])
end)
end
def parse_value(<<?$, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_string_chunk(&1, []), rest}
end
def parse_value(<<?*, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_array_chunk(&1, []), rest}
end
def parse_value(<<?%, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_array_chunk(&1, []), rest}
end
def parse_value(<<?~, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_array_chunk(&1, []), rest}
end
def parse_value(<<?>, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_array_chunk(&1, []), rest}
end
def parse_value(<<?|, ??, ?\r, ?\n, rest::binary>>) do
{:streamed, &stream_array_chunk(&1, []), rest}
end
def parse_value(<<>>) do
{:continuation, &parse_value/1}
end
def parse_value(<<byte, _::binary>>) do
raise %ParseError{message: "unknown type byte: #{inspect(<<byte>>)}"}
end
defp finish_bool(<<?\r, ?\n, rest::binary>>, val),
do: {:ok, %Resp.Bool{type: :bool, data: val, count: 0}, rest}
defp finish_bool(<<>>, _val), do: {:continuation, &finish_bool(&1, true)}
# ── Aggregate helpers ─────────────
defp parse_elems(data, type, 0, acc) do
count = if type == :map or type == :attribute, do: div(length(acc), 2), else: length(acc)
mod = struct_for_type(type)
{:ok, struct(mod, %{type: type, data: Enum.reverse(acc), count: count}), data}
end
defp parse_elems(data, type, n, acc) do
resolve_cont(parse_value(data), fn elem, rest ->
parse_elems(rest, type, n - 1, [elem | acc])
end)
end
defp struct_for_type(:array), do: Resp.Array
defp struct_for_type(:map), do: Resp.Map
defp struct_for_type(:set), do: Resp.Set
defp struct_for_type(:push), do: Resp.Push
defp struct_for_type(:attribute), do: Resp.Attribute
# ── Streamed type chunking ─────────
defp stream_string_chunk(<<?., ?\r, ?\n, rest::binary>>, _acc), do: {:done, rest}
defp stream_string_chunk(<<?$, ?-, ?1, ?\r, ?\n, rest::binary>>, _acc), do: {:done, rest}
defp stream_string_chunk(data, _acc) do
case parse_value(data) do
{:ok, %Resp.Bulk{data: chunk}, rest} -> {:chunk, chunk, rest}
{:continuation, _} -> {:continuation, &stream_string_chunk(&1, [])}
end
end
defp stream_array_chunk(<<?., ?\r, ?\n, rest::binary>>, _acc), do: {:done, rest}
defp stream_array_chunk(data, _acc) do
case parse_value(data) do
{:ok, elem, rest} -> {:chunk, elem, rest}
{:continuation, _} -> {:continuation, &stream_array_chunk(&1, [])}
end
end
# ── Stream entry point ─────────────
@doc """
Parses a streamed RESP type header and returns a chunk function.
Returns `{:streamed, chunk_fun, rest}` where `chunk_fun` is called with
subsequent data and returns `{:chunk, data, rest}` or `{:done, rest}`.
"""
@spec parse_stream(binary()) :: {:streamed, function(), binary()} | {:continuation, function()}
def parse_stream(data) do
case data do
<<?$, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_string_chunk(&1, []), rest}
<<?*, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest}
<<?%, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest}
<<?~, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest}
<<?>, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest}
<<?|, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest}
<<>> -> {:continuation, &parse_stream/1}
end
end
# ── Integer parsing ────────────────
for n <- 1..18 do
defp parse_integer(<<digits::binary-size(unquote(n)), @crlf, rest::binary>> = bin) do
case Integer.parse(digits) do
{int, ""} -> {:ok, int, rest}
_ -> parse_int_split(bin)
end
end
end
defp parse_integer(<<?-, rest::binary>>) do
resolve_cont(parse_uint(rest), fn n, rest -> {:ok, -n, rest} end)
end
defp parse_integer(<<?+, rest::binary>>) do
resolve_cont(parse_uint(rest), fn n, rest -> {:ok, n, rest} end)
end
defp parse_integer(data), do: parse_int_split(data)
defp parse_int_split(""), do: {:continuation, &parse_integer/1}
defp parse_int_split(<<?-, rest::binary>>) do
resolve_cont(parse_uint(rest), fn n, rest -> {:ok, -n, rest} end)
end
defp parse_int_split(<<?+, rest::binary>>) do
resolve_cont(parse_uint(rest), fn n, rest -> {:ok, n, rest} end)
end
defp parse_int_split(data), do: parse_uint(data)
defp parse_uint(<<d, _::binary>> = data) when d in ?0..?9 do
resolve_cont(parse_digits(data, 0), fn n, rest ->
resolve_cont(expect_crlf(rest), fn :ok, rest -> {:ok, n, rest} end)
end)
end
defp parse_uint(<<nd, _::binary>>),
do: raise(%ParseError{message: "expected digit, got: #{inspect(<<nd>>)}"})
defp parse_uint(""), do: {:continuation, &parse_uint/1}
defp parse_digits(<<d, rest::binary>>, acc) when d in ?0..?9,
do: parse_digits(rest, acc * 10 + (d - ?0))
defp parse_digits(<<@crlf, _::binary>> = rest, acc), do: {:ok, acc, rest}
defp parse_digits(<<?\r>>, acc), do: {:continuation, &finish_crlf(&1, acc)}
defp parse_digits(<<>>, acc), do: {:continuation, &parse_digits(&1, acc)}
defp parse_digits(<<nd, _::binary>>, _acc),
do: raise(%ParseError{message: "expected digit or CRLF, got: #{inspect(<<nd>>)}"})
defp finish_crlf(<<?\n, rest::binary>>, acc), do: {:ok, acc, <<?\r, ?\n, rest::binary>>}
defp finish_crlf(<<>>, _acc), do: {:continuation, &finish_crlf(&1, 0)}
# ── CRLF ───────────────────────────
defp expect_crlf(<<@crlf, rest::binary>>), do: {:ok, :ok, rest}
defp expect_crlf(<<?\r>>), do: {:continuation, &expect_crlf_cont/1}
defp expect_crlf(<<>>), do: {:continuation, &expect_crlf/1}
defp expect_crlf(bin),
do:
raise(%ParseError{
message: "expected CRLF, got: #{inspect(binary_part(bin, 0, min(byte_size(bin), 4)))}"
})
defp expect_crlf_cont(<<?\n, rest::binary>>), do: {:ok, :ok, rest}
defp expect_crlf_cont(<<>>), do: {:continuation, &expect_crlf_cont/1}
# ── Continuation helper ────────────
defp resolve_cont({:ok, val, rest}, ok) when is_function(ok, 2), do: ok.(val, rest)
defp resolve_cont({:continuation, cont}, ok) do
{:continuation, fn new_data -> resolve_cont(cont.(new_data), ok) end}
end
# ───────────────────────────────────
# Encoding
# ───────────────────────────────────
@doc "Encodes a simple string."
@spec encode_string(String.t()) :: iodata()
def encode_string(s), do: [?+, s, @crlf]
@doc "Encodes an error."
@spec encode_error(String.t()) :: iodata()
def encode_error(msg), do: [?-, msg, @crlf]
@doc "Encodes an integer."
@spec encode_integer(integer()) :: iodata()
def encode_integer(n), do: [?:, Integer.to_string(n), @crlf]
@doc "Encodes a bulk string."
@spec encode_bulk(binary()) :: iodata()
def encode_bulk(data), do: [size_prefix(?$, byte_size(data)), data, @crlf]
@doc "Encodes RESP2 null (`$-1\\r\\n`)."
@spec encode_null() :: iodata()
def encode_null, do: [?$, ?-, ?1, @crlf]
@doc "Encodes RESP3 null (`_\\r\\n`)."
@spec encode_null3() :: iodata()
def encode_null3, do: [?_, @crlf]
@doc "Encodes an array header."
@spec encode_array(integer()) :: iodata()
def encode_array(n), do: size_prefix(?*, n)
@doc "Encodes a RESP3 boolean."
@spec encode_bool(boolean()) :: iodata()
def encode_bool(true), do: [?#, ?t, @crlf]
def encode_bool(false), do: [?#, ?f, @crlf]
@doc "Encodes a RESP3 double."
@spec encode_double(float() | :infinity | :neg_infinity | :nan) :: iodata()
def encode_double(:infinity), do: ",inf\r\n"
def encode_double(:neg_infinity), do: ",-inf\r\n"
def encode_double(:nan), do: ",nan\r\n"
def encode_double(f) when is_float(f) do
[?,, encode_double_val(f), @crlf]
end
defp encode_double_val(f) do
if f == 0.0 do
if neg_zero?(f), do: "-0", else: "0"
else
try do
t = trunc(f)
if f == t, do: Integer.to_string(t), else: Float.to_string(f)
rescue
ArithmeticError -> Float.to_string(f)
end
end
end
defp neg_zero?(f) do
<<sign::1, _::11, _::52>> = <<f::float>>
sign == 1
end
@doc "Encodes a RESP3 big number."
@spec encode_big_number(String.t()) :: iodata()
def encode_big_number(s), do: [?(, s, @crlf]
@doc "Encodes a RESP3 bulk error."
@spec encode_blob_error(String.t()) :: iodata()
def encode_blob_error(msg) do
[?!, Integer.to_string(byte_size(msg)), @crlf, msg, @crlf]
end
@doc "Encodes a RESP3 verbatim string with encoding prefix."
@spec encode_verbatim_string(String.t(), String.t()) :: iodata()
def encode_verbatim_string(enc, s) do
payload = <<enc::binary, ?:, s::binary>>
[?=, Integer.to_string(byte_size(payload)), @crlf, payload, @crlf]
end
@doc "Encodes a RESP3 map header."
@spec encode_map(integer()) :: iodata()
def encode_map(n), do: size_prefix(?%, n)
@doc "Encodes a RESP3 set header."
@spec encode_set(integer()) :: iodata()
def encode_set(n), do: size_prefix(?~, n)
@doc "Encodes a RESP3 push header."
@spec encode_push(integer()) :: iodata()
def encode_push(n), do: size_prefix(?>, n)
@doc "Encodes a RESP3 attribute header."
@spec encode_attribute(integer()) :: iodata()
def encode_attribute(n), do: size_prefix(?|, n)
@doc "Encodes raw data (passthrough)."
@spec encode_raw(iodata()) :: iodata()
def encode_raw(data), do: data
@doc """
Packs a list of arguments as a RESP2 array of bulk strings.
"""
@spec pack([String.Chars.t()]) :: iodata()
def pack(items) when is_list(items), do: pack(items, [], 0)
defp pack([item | rest], acc, count) do
s = to_string(item)
pack(rest, [[?$, Integer.to_string(byte_size(s)), @crlf, s, @crlf] | acc], count + 1)
end
defp pack([], acc, count) do
[?*, Integer.to_string(count), @crlf, Enum.reverse(acc)]
end
@doc """
Generates server-info response for the HELLO command.
`:resp2` returns an array, `:resp3` returns a map.
"""
@spec hello(:resp2 | :resp3) :: iodata()
def hello(:resp2) do
info = server_info()
[
encode_array(length(info))
| Enum.flat_map(info, fn {k, v} ->
[encode_bulk(to_string(k)), encode_bulk(to_string(v))]
end)
]
end
def hello(:resp3) do
info = server_info()
[
encode_map(length(info))
| Enum.flat_map(info, fn {k, v} ->
[encode_bulk(to_string(k)), encode_bulk(to_string(v))]
end)
]
end
defp server_info do
[
server: "resp.ex",
version: "0.1.0",
proto: 3,
id: System.system_time(:second),
mode: "standalone",
role: "master",
modules: []
]
end
@doc """
Encodes any Elixir value to its RESP representation (default RESP3 mode).
See `encode_any/2` for mode-aware encoding.
"""
def encode_any(value), do: encode_any(value, :resp3)
@doc """
Encodes any Elixir value with mode awareness.
In RESP2 mode: nil → `$-1`, booleans → integers.
In RESP3 mode: nil → `_`, booleans → `#t`/`#f`.
"""
@spec encode_any(term(), :resp2 | :resp3) :: iodata()
def encode_any(nil, :resp2), do: encode_null()
def encode_any(nil, :resp3), do: encode_null3()
def encode_any(true, :resp3), do: encode_bool(true)
def encode_any(false, :resp3), do: encode_bool(false)
def encode_any(true, _mode), do: encode_integer(1)
def encode_any(false, _mode), do: encode_integer(0)
def encode_any(n, _mode) when is_integer(n), do: encode_integer(n)
def encode_any(f, _mode) when is_float(f), do: encode_double(f)
def encode_any(s, _mode) when is_binary(s), do: encode_bulk(s)
def encode_any(%Resp.String{data: v}, _mode), do: encode_string(v)
def encode_any(%Resp.Error{data: msg}, _mode), do: encode_error(msg)
def encode_any(list, _mode) when is_list(list) do
[encode_array(length(list)) | Enum.map(list, &encode_any(&1, :resp2))]
end
def encode_any(map, :resp2) when is_map(map) do
pairs = Map.to_list(map)
[
size_prefix(?*, length(pairs) * 2)
| Enum.flat_map(pairs, fn {k, v} -> [encode_any(k, :resp2), encode_any(v, :resp2)] end)
]
end
def encode_any(map, :resp3) when is_map(map) do
pairs = Map.to_list(map)
[
encode_map(length(pairs))
| Enum.flat_map(pairs, fn {k, v} -> [encode_any(k, :resp2), encode_any(v, :resp2)] end)
]
end
def encode_any(%MapSet{} = set, _mode) do
elems = MapSet.to_list(set)
[encode_set(length(elems)) | Enum.map(elems, &encode_any(&1, :resp2))]
end
defp size_prefix(byte, n) when n >= 0 and n <= 9, do: [byte, ?0 + n, @crlf]
defp size_prefix(byte, n), do: [byte, Integer.to_string(n), @crlf]
end