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(<>) 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(<>), 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 <> = 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(<>, acc), do: {:ok, acc, rest} defp read_line(<>, acc), do: {:ok, acc, rest} defp read_line(<<>>, acc), do: {:continuation, &read_line(&1, acc)} defp read_line(<>, acc), do: read_line(rest, <>) defp tokenize(line), do: tokenize(line, [], []) defp tokenize(<<>>, cur, args), do: finish_token(cur, args) defp tokenize(<>, cur, args), do: tokenize(trim_spaces(rest), [], finish_token(cur, args)) defp tokenize(<>, _cur, args), do: parse_quoted(rest, ?", [], args) defp tokenize(<>, _cur, args), do: parse_quoted(rest, ?', [], args) defp tokenize(<>, cur, args), do: tokenize(rest, [cur, c], args) defp trim_spaces(<>), 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, quoted, args) do tokenize(trim_spaces(rest), [], args ++ [IO.iodata_to_binary(quoted)]) end defp parse_quoted(<>, q, quoted, args), do: parse_quoted_esc(rest, q, quoted, args) defp parse_quoted(<>, 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(<>, 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(<>) do resolve_cont(read_line(rest, ""), fn line, rest -> {:ok, %Resp.String{ type: :string, raw: <>, data: line, count: 0 }, rest} end) end def parse_value(<>) do resolve_cont(read_line(rest, ""), fn line, rest -> {:ok, %Resp.Error{type: :error, raw: <>, data: line, count: 0}, rest} end) end def parse_value(<>) do resolve_cont(read_line(rest, ""), fn line, rest -> {:ok, %Resp.Integer{ type: :integer, raw: <>, data: line, count: 0 }, rest} end) end def parse_value(<>) 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(<>) 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(<>) 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(<>) do case rest do <> -> {:ok, %Resp.Bool{type: :bool, data: true, count: 0}, rest} <> -> {:ok, %Resp.Bool{type: :bool, data: false, count: 0}, rest} <> -> {:continuation, &finish_bool(&1, true)} <> -> {:continuation, &finish_bool(&1, false)} <<>> -> {:continuation, &parse_value/1} _ -> raise %ParseError{message: "invalid boolean value: #{inspect(rest)}"} end end def parse_value(<>) do resolve_cont(read_line(rest, ""), fn line, rest -> {:ok, %Resp.Double{type: :double, data: line, count: 0}, rest} end) end def parse_value(<>) 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(<>) 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(<>) 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(<>) 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(<>) 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(<>) 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(<>) do {:streamed, &stream_string_chunk(&1, []), rest} end def parse_value(<>) do {:streamed, &stream_array_chunk(&1, []), rest} end def parse_value(<>) do {:streamed, &stream_array_chunk(&1, []), rest} end def parse_value(<>) 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 {:streamed, &stream_array_chunk(&1, []), rest} end def parse_value(<<>>) do {:continuation, &parse_value/1} end def parse_value(<>) do raise %ParseError{message: "unknown type byte: #{inspect(<>)}"} end defp finish_bool(<>, 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(<>, _acc), do: {:done, rest} defp stream_string_chunk(<>, _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(<>, _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 <> -> {:streamed, &stream_string_chunk(&1, []), rest} <> -> {:streamed, &stream_array_chunk(&1, []), rest} <> -> {:streamed, &stream_array_chunk(&1, []), rest} <> -> {:streamed, &stream_array_chunk(&1, []), rest} <, ??, ?\r, ?\n, rest::binary>> -> {:streamed, &stream_array_chunk(&1, []), rest} <> -> {:streamed, &stream_array_chunk(&1, []), rest} <<>> -> {:continuation, &parse_stream/1} end end # ── Integer parsing ──────────────── for n <- 1..18 do defp parse_integer(<> = bin) do case Integer.parse(digits) do {int, ""} -> {:ok, int, rest} _ -> parse_int_split(bin) end end end defp parse_integer(<>) do resolve_cont(parse_uint(rest), fn n, rest -> {:ok, -n, rest} end) end defp parse_integer(<>) 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(<>) do resolve_cont(parse_uint(rest), fn n, rest -> {:ok, -n, rest} end) end defp parse_int_split(<>) 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(<> = 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(<>), do: raise(%ParseError{message: "expected digit, got: #{inspect(<>)}"}) defp parse_uint(""), do: {:continuation, &parse_uint/1} defp parse_digits(<>, 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(<>, acc), do: {:continuation, &finish_crlf(&1, acc)} defp parse_digits(<<>>, acc), do: {:continuation, &parse_digits(&1, acc)} defp parse_digits(<>, _acc), do: raise(%ParseError{message: "expected digit or CRLF, got: #{inspect(<>)}"}) defp finish_crlf(<>, acc), do: {:ok, acc, <>} defp finish_crlf(<<>>, _acc), do: {:continuation, &finish_crlf(&1, 0)} # ── CRLF ─────────────────────────── defp expect_crlf(<<@crlf, rest::binary>>), do: {:ok, :ok, rest} defp expect_crlf(<>), 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(<>), 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 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 = <> [?=, 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