RESP parser with byteslice and tagged arrays
String#byteslice is byte-aware (not character-aware). For a binary protocol like RESP we always want bytes. The buffer is also created with Encoding::BINARY so it never tries to interpret bytes as UTF-8.
Before you read the parser, dissect one frame by hand. When redis-cli sends PING, the bytes on the wire are 1\r\n$4\r\nPING\r\n. The first byte announces an array and the 1 says it holds one element. \r\n (that is the CRLF constant in the code below) ends every header line. Then $ announces a bulk string, 4 is the payload length in bytes, \r\n ends that header, then come the four payload bytes PING and a closing \r\n. RESP has five type bytes: + simple string, - error, : integer, $ bulk string, * array. So every value is a type byte, a header line ended by \r\n, and for a bulk string a length-prefixed run of raw bytes. That is exactly what case t switches on: read the first byte, slice up to CRLF for the header, then for $ read header.to_i more bytes.
def parse_resp(buf, cur)
return [:incomplete, cur] if cur >= buf.bytesize
t = buf.byteslice(cur, 1)
crlf_idx = buf.index(CRLF, cur)
return [:incomplete, cur] if crlf_idx.nil?
header = buf.byteslice(cur + 1, crlf_idx - cur - 1)
after = crlf_idx + 2
case t
when '$'
n = header.to_i
ep = after + n
return [:incomplete, cur] if buf.bytesize < ep + 2
[[:bulk, buf.byteslice(after, n)], ep + 2]
when '*'
n = header.to_i
items = []
c = after
n.times do
v, nc = parse_resp(buf, c)
return [:incomplete, cur] if v == :incomplete
items << v
c = nc
end
[[:array, items], c]
end
endThe RESP parser. case/when on the first byte, recursive on arrays.
Ruby has no enum. We tag tuples with a symbol: [:bulk, \u0022foo\u0022], [:int, 42], [:array, [...]]. Callers pattern-match on the first element. Less type-safe than Rust's enum but the structure is identical.
Quiz: Quiz
Loading practice…
AI prompt: Try it: hand-write a frame
Loading practice…