Skip to main content

Crate resp_rs

Crate resp_rs 

Source
Expand description

Zero-copy RESP2 and RESP3 protocol parser and serializer.

resp-rs provides high-performance parsing and serialization for the Redis Serialization Protocol (RESP), supporting both RESP2 and RESP3.

§Features

§Modules

§Quick Start

§Parsing a RESP2 command

use bytes::Bytes;
use resp_rs::resp2::{self, Frame};

// A Redis SET command on the wire
let data = Bytes::from("*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n");
let (frame, remaining) = resp2::parse_frame(data).unwrap();

assert_eq!(frame, Frame::Array(Some(vec![
    Frame::BulkString(Some(Bytes::from("SET"))),
    Frame::BulkString(Some(Bytes::from("key"))),
    Frame::BulkString(Some(Bytes::from("value"))),
])));
assert!(remaining.is_empty());

§Parsing RESP3 types

RESP3 adds null, booleans, doubles, maps, sets, and more:

use bytes::Bytes;
use resp_rs::resp3::{self, Frame};

// Simple string
let (frame, _) = resp3::parse_frame(Bytes::from("+OK\r\n")).unwrap();
assert_eq!(frame, Frame::SimpleString(Bytes::from("OK")));

// Null
let (frame, _) = resp3::parse_frame(Bytes::from("_\r\n")).unwrap();
assert_eq!(frame, Frame::Null);

// Boolean
let (frame, _) = resp3::parse_frame(Bytes::from("#t\r\n")).unwrap();
assert_eq!(frame, Frame::Boolean(true));

// Double
let (frame, _) = resp3::parse_frame(Bytes::from(",3.14\r\n")).unwrap();
assert_eq!(frame, Frame::Double(3.14));

// Integer
let (frame, _) = resp3::parse_frame(Bytes::from(":-42\r\n")).unwrap();
assert_eq!(frame, Frame::Integer(-42));

// Bulk string
let (frame, _) = resp3::parse_frame(Bytes::from("$5\r\nhello\r\n")).unwrap();
assert_eq!(frame, Frame::BulkString(Some(Bytes::from("hello"))));

// Null bulk string
let (frame, _) = resp3::parse_frame(Bytes::from("$-1\r\n")).unwrap();
assert_eq!(frame, Frame::BulkString(None));

// Array
let (frame, _) = resp3::parse_frame(Bytes::from("*2\r\n:1\r\n:2\r\n")).unwrap();
assert_eq!(frame, Frame::Array(Some(vec![Frame::Integer(1), Frame::Integer(2)])));

// Map
let data = Bytes::from("%2\r\n+name\r\n$5\r\nAlice\r\n+age\r\n:30\r\n");
let (frame, _) = resp3::parse_frame(data).unwrap();
assert_eq!(frame, Frame::Map(vec![
    (Frame::SimpleString(Bytes::from("name")), Frame::BulkString(Some(Bytes::from("Alice")))),
    (Frame::SimpleString(Bytes::from("age")), Frame::Integer(30)),
]));

// Set
let (frame, _) = resp3::parse_frame(Bytes::from("~3\r\n:1\r\n:2\r\n:3\r\n")).unwrap();
assert_eq!(frame, Frame::Set(vec![Frame::Integer(1), Frame::Integer(2), Frame::Integer(3)]));

// Big number
let (frame, _) = resp3::parse_frame(Bytes::from("(12345678901234567890\r\n")).unwrap();
assert_eq!(frame, Frame::BigNumber(Bytes::from("12345678901234567890")));

// Verbatim string
let (frame, _) = resp3::parse_frame(Bytes::from("=15\r\ntxt:hello world\r\n")).unwrap();
assert_eq!(frame, Frame::VerbatimString(Bytes::from("txt"), Bytes::from("hello world")));

// Blob error
let (frame, _) = resp3::parse_frame(Bytes::from("!5\r\nOOPS!\r\n")).unwrap();
assert_eq!(frame, Frame::BlobError(Bytes::from("OOPS!")));

// Error
let (frame, _) = resp3::parse_frame(Bytes::from("-ERR unknown\r\n")).unwrap();
assert_eq!(frame, Frame::Error(Bytes::from("ERR unknown")));

§Serialization

Convert any resp2::Frame or resp3::Frame back to wire format:

use bytes::Bytes;
use resp_rs::resp2::{Frame, frame_to_bytes};

let frame = Frame::Array(Some(vec![
    Frame::BulkString(Some(Bytes::from("GET"))),
    Frame::BulkString(Some(Bytes::from("mykey"))),
]));
let wire = frame_to_bytes(&frame);
assert_eq!(wire, Bytes::from("*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n"));

Roundtrip is guaranteed: parse_frame(frame_to_bytes(&frame)) == Ok((frame, empty)).

§Streaming parser

The resp2::Parser and resp3::Parser types buffer incremental data and yield frames as they become complete – ideal for reading from TCP sockets.

use bytes::Bytes;
use resp_rs::resp3::{Parser, Frame};

let mut parser = Parser::new();

// Simulate receiving data in chunks (e.g., from TCP)
parser.feed(Bytes::from("+HEL"));
assert!(parser.next_frame().unwrap().is_none()); // not enough data yet

parser.feed(Bytes::from("LO\r\n:42\r\n"));
// Now we have two complete frames buffered

let frame1 = parser.next_frame().unwrap().unwrap();
assert_eq!(frame1, Frame::SimpleString(Bytes::from("HELLO")));

let frame2 = parser.next_frame().unwrap().unwrap();
assert_eq!(frame2, Frame::Integer(42));

assert!(parser.next_frame().unwrap().is_none()); // buffer exhausted

§Pipelined commands

Multiple frames in a single buffer parse naturally in sequence:

use bytes::Bytes;
use resp_rs::resp2::{self, Frame};

let wire = Bytes::from("+OK\r\n$5\r\nhello\r\n:42\r\n");

let (f1, rest) = resp2::parse_frame(wire).unwrap();
assert_eq!(f1, Frame::SimpleString(Bytes::from("OK")));

let (f2, rest) = resp2::parse_frame(rest).unwrap();
assert_eq!(f2, Frame::BulkString(Some(Bytes::from("hello"))));

let (f3, rest) = resp2::parse_frame(rest).unwrap();
assert_eq!(f3, Frame::Integer(42));

assert!(rest.is_empty());

§RESP3 streaming sequences

RESP3 supports chunked/streaming data. Use resp3::parse_streaming_sequence to accumulate chunks into a complete frame:

use bytes::Bytes;
use resp_rs::resp3::{self, Frame};

// Streaming string: $?\r\n followed by chunks, terminated by ;0\r\n
let data = Bytes::from("$?\r\n;5\r\nHello\r\n;6\r\n World\r\n;0\r\n\r\n");
let (frame, _) = resp3::parse_streaming_sequence(data).unwrap();

if let Frame::StreamedString(chunks) = frame {
    assert_eq!(chunks.len(), 2);
    assert_eq!(chunks[0], Bytes::from("Hello"));
    assert_eq!(chunks[1], Bytes::from(" World"));
}

§Tokio Codec (async)

Enable the codec feature for tokio_util::codec integration:

[dependencies]
resp-rs = { version = "0.1", features = ["codec"] }
tokio = { version = "1", features = ["net"] }
tokio-util = { version = "0.7", features = ["codec"] }
futures = "0.3"  # for SinkExt / StreamExt

This provides resp2::Codec and resp3::Codec, which implement tokio_util::codec::Decoder and tokio_util::codec::Encoder. Wrap a TCP stream with tokio_util::codec::Framed for async frame-level I/O:

use resp_rs::resp2::{Codec, Frame};
use tokio::net::TcpStream;
use tokio_util::codec::Framed;
use futures::{SinkExt, StreamExt};
use bytes::Bytes;

let stream = TcpStream::connect("127.0.0.1:6379").await?;
let mut framed = Framed::new(stream, Codec::new());

// Send a PING
framed.send(Frame::Array(Some(vec![
    Frame::BulkString(Some(Bytes::from("PING"))),
]))).await?;

// Read the response
if let Some(Ok(frame)) = framed.next().await {
    println!("{frame:?}"); // SimpleString("PONG")
}

The decoder uses the same zero-copy parse_frame path internally. Errors are returned as codec::CodecError, which wraps both ParseError and std::io::Error.

§Working with Frames

Both resp2::Frame and resp3::Frame provide convenience methods for extracting typed data without manual pattern matching:

use bytes::Bytes;
use resp_rs::resp2::{self, Frame};

let data = Bytes::from("*3\r\n+SET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n");
let (frame, _) = resp2::parse_frame(data).unwrap();

// Extract array items
let items = frame.into_array().unwrap();
assert_eq!(items[0].as_str(), Some("SET"));
assert_eq!(items[1].as_str(), Some("key"));
assert_eq!(items[2].as_str(), Some("value"));

Available methods include:

MethodReturnsWorks on
as_bytes()Option<&Bytes>Strings, bulk strings, errors
as_str()Option<&str>String-like frames with valid UTF-8
as_integer()Option<i64>Integer
as_double()Option<f64>Double (RESP3 only)
as_boolean()Option<bool>Boolean (RESP3 only)
as_array()Option<&[Frame]>Array
as_map()Option<&[(Frame, Frame)]>Map (RESP3 only)
into_array()Result<Vec<Frame>, Frame>Array
into_bulk_string()Result<Bytes, Frame>BulkString
into_map()Result<Vec<(Frame, Frame)>, Frame>Map (RESP3 only)
is_null()boolAny frame
is_error()boolAny frame

§Redis Cluster (hash slots)

Enable the cluster feature for hash slot calculation:

[dependencies]
resp-rs = { version = "0.1", features = ["cluster"] }
use resp_rs::cluster::hash_slot;

// Keys with the same hash tag route to the same slot
assert_eq!(hash_slot(b"{user}.name"), hash_slot(b"{user}.email"));

Implements CRC16-CCITT with hash tag extraction per the Redis Cluster specification.

§no_std Support

This crate supports no_std environments (with alloc). The std feature is enabled by default; disable it for embedded or WASM targets:

[dependencies]
resp-rs = { version = "0.1", default-features = false }

All core parsing and serialization works without std. The codec feature requires std (Tokio dependency). The cluster and unsafe-internals features work in no_std.

§Performance

The parser uses offset-based internal parsing to minimize allocations. Bulk string parsing is an O(1) slice into the input buffer, not a copy.

§parse_frame vs Parser

resp2::parse_frame and resp3::parse_frame parse directly from a bytes::Bytes buffer with no overhead. The resp2::Parser and resp3::Parser wrappers add incremental buffering for TCP streams, but have roughly 2x overhead per frame due to internal BytesMut split/unsplit operations.

If you already have a complete buffer (e.g., a full response read from a socket), call parse_frame directly in a loop rather than going through Parser:

use bytes::Bytes;
use resp_rs::resp2;

let wire = Bytes::from("+OK\r\n:42\r\n$5\r\nhello\r\n");
let mut input = wire;
while !input.is_empty() {
    match resp2::parse_frame(input) {
        Ok((frame, rest)) => {
            // process frame
            input = rest;
        }
        Err(resp_rs::ParseError::Incomplete) => break, // need more data
        Err(e) => panic!("parse error: {e}"),
    }
}

Use Parser when data arrives incrementally (e.g., reading from a TCP socket in chunks) and you need to buffer partial frames across reads.

§RESP2 vs RESP3

RESP2 parsing is roughly 3x faster than RESP3 for simple types due to RESP2’s smaller type tag match (5 variants vs 16+). The gap narrows for collection-heavy workloads where per-element parsing dominates. If your application only needs RESP2, prefer the resp2 module for best performance.

§Representative timings

Measured on Apple M4 (single core, criterion):

OperationRESP2RESP3
Simple string12 ns39 ns
Bulk string13 ns39 ns
Integer25 ns47 ns
3-element array43 ns82 ns
100-element array822 ns2.0 us
5-frame pipeline (direct)107 ns129 ns
5-frame pipeline (Parser)242 ns286 ns
Streaming string (2 chunks)124 ns
Streaming array (5 elements)226 ns

Run cargo bench to reproduce on your hardware.

§Error Handling

All parsing functions return Result<_, ParseError>. The ParseError::Incomplete variant signals that more data is needed (not a protocol error), while other variants indicate malformed input.

use bytes::Bytes;
use resp_rs::{ParseError, resp2};

// Not enough data
assert_eq!(resp2::parse_frame(Bytes::from("$5\r\nhel")), Err(ParseError::Incomplete));

// Unknown type tag
assert_eq!(resp2::parse_frame(Bytes::from("X\r\n")), Err(ParseError::InvalidTag(b'X')));

// Empty input
assert_eq!(resp2::parse_frame(Bytes::new()), Err(ParseError::Incomplete));

Modules§

resp2
RESP2 protocol parser and serializer.
resp3
Zero-copy RESP3 parser.

Enums§

ParseError
Errors that can occur during RESP parsing.