18 releases (6 stable)

Uses new Rust 2024

new 1.2.2 May 3, 2025
1.2.1 May 2, 2025
1.1.0 Apr 26, 2025
0.2.2 Apr 12, 2025
0.1.3 Nov 27, 2024

#248 in Parser implementations

Download history 1/week @ 2025-01-14 6/week @ 2025-02-18 1/week @ 2025-02-25 248/week @ 2025-04-08 156/week @ 2025-04-15 117/week @ 2025-04-22 470/week @ 2025-04-29

991 downloads per month

MIT license

150KB
3K SLoC

stream_resp

StreamRESP is a RESP (Redis Serialization Protocol) parser fully compliant with RESP3, implemented using a finite state machine (FSM) approach. Designed for streaming scenarios.

  • Full RESP3 support: All RESP3 types are supported.
  • Optional explicit positive integer sign: Enable the explicit-positive-sign feature to support parsing integers with an explicit + sign (e.g., :+123\r\n).

Documentation

DeepWiki

Installation

To use stream_resp in your project, add the following to your Cargo.toml:

[dependencies]
stream_resp = "1"

Features

Enabling jemalloc

If you want to enable jemalloc for better memory allocation performance, you can enable the jemalloc feature in your Cargo.toml:

[dependencies]
stream_resp = { version = "1", features = ["jemalloc"] }

Enabling Explicit Positive Integer Sign (+)

By default, the parser strictly adheres to the RESP specification for integers (:<number>\r\n). However, some clients or protocols might use an explicit plus sign (:+<number>\r\n). To enable parsing of integers with an explicit +, enable the integer-plus-sign feature:

[dependencies]
stream_resp = { version = "1", features = ["explicit-positive-sign"] }

You can also enable multiple features:

[dependencies]
stream_resp = { version = "1", features = ["jemalloc", "explicit-positive-sign"] }

Usage

Here are some examples demonstrating how to use the stream_resp parser.

Example 1: Basic Usages

Creating RESP Values using From

use std::borrow::Cow;
use stream_resp::resp::RespValue;

// From String/&str (becomes SimpleString)
let simple_str: RespValue = "OK".into();
assert_eq!(simple_str, RespValue::SimpleString(Cow::Borrowed("OK")));

let simple_string: RespValue = String::from("Hello").into();
assert_eq!(simple_string, RespValue::SimpleString(Cow::Owned("Hello".to_string())));

// From i64
let integer: RespValue = 123.into();
assert_eq!(integer, RespValue::Integer(123));

// From Option<String> (becomes BulkString)
let bulk_some: RespValue = Some("data".to_string()).into();
assert_eq!(bulk_some, RespValue::BulkString(Some(Cow::Owned("data".to_string()))));

let bulk_none: RespValue = None::<String>.into();
assert_eq!(bulk_none, RespValue::BulkString(None));

// From Vec<RespValue> (becomes Array)
let array: RespValue = vec![RespValue::Integer(1), "two".into()].into();
assert_eq!(array, RespValue::Array(Some(vec![
    RespValue::Integer(1),
    RespValue::SimpleString(Cow::Borrowed("two"))
])));

// Other From implementations exist for bool, f64, Vec<(RespValue, RespValue)> (Map), etc.

Representing Redis Commands

use std::borrow::Cow;
use stream_resp::resp::RespValue;

// Representing the Redis command: SET mykey "Hello"
let command = RespValue::Array(Some(vec![
    RespValue::BulkString(Some(Cow::Borrowed("SET"))),
    RespValue::BulkString(Some(Cow::Borrowed("mykey"))),
    RespValue::BulkString(Some(Cow::Borrowed("Hello"))),
]));

// Get the RESP byte representation
let expected_bytes = b"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n";
assert_eq!(command.as_bytes(), expected_bytes);

println!("Command: {:?}", command);
println!("RESP Bytes: {:?}", String::from_utf8_lossy(&command.as_bytes()));

Converting RESP Values using Into

use std::borrow::Cow;
use stream_resp::resp::RespValue;

let simple_string = RespValue::SimpleString(Cow::Borrowed("OK"));
let ok_str: String = simple_string.into(); 
assert_eq!(ok_str, "OK");

let integer = RespValue::Integer(42);
let num: i64 = integer.into(); 
assert_eq!(num, 42);

Example 2: Streaming RESP Messages over TCP

use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use stream_resp::parser::{ParseError, Parser};
use stream_resp::resp::RespValue;

fn handle_client(mut stream: TcpStream) {
    let mut parser = Parser::new(100, 1000);
    let mut buffer = [0; 512];

    loop {
        match stream.read(&mut buffer) {
            Ok(0) => break, // Connection closed
            Ok(n) => {
                parser.read_buf(&buffer[..n]);
                while let Ok(Some((resp, _))) = parser.try_parse() {
                    println!("Parsed RESP value: {:?}", resp);
                    // Echo the parsed RESP value back to the client
                    let response = format!("{:?}\r\n", resp);
                    stream.write_all(response.as_bytes()).unwrap();
                }
            }
            Err(e) => {
                eprintln!("Failed to read from socket: {:?}", e);
                break;
            }
        }
    }
}

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:6379")?;
    println!("Server listening on port 6379");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                std::thread::spawn(|| {
                    handle_client(stream);
                });
            }
            Err(e) => {
                eprintln!("Failed to accept connection: {:?}", e);
            }
        }
    }

    Ok(())
}

Example 3: Parsing Complete RESP Messages

use std::borrow::Cow;
use stream_resp::parser::{ParseError, Parser};
use stream_resp::resp::RespValue;

fn main() {
    let mut parser = Parser::new(100, 1000);

    parser.read_buf(b"+OK\r\n");
    let result = match parser.try_parse() {
        Ok(Some(val)) => val,
        Ok(None) => panic!("Expected complete value"),
        Err(e) => panic!("Parse error: {:?}", e),
    };
    assert_eq!(result, RespValue::SimpleString(Cow::Borrowed("OK")));

    parser.read_buf(b"+Hello World\r\n");
    let result = match parser.try_parse() {
        Ok(Some(val)) => val,
        Ok(None) => panic!("Expected complete value"),
        Err(e) => panic!("Parse error: {:?}", e),
    };
    assert_eq!(
        result,
        RespValue::SimpleString(Cow::Borrowed("Hello World"))
    );
}

Example 4: Parsing Incomplete RESP Messages in Chunks

use std::borrow::Cow;
use stream_resp::parser::{ParseError, Parser};
use stream_resp::resp::RespValue;

fn main() {
    {
        let mut parser = Parser::new(100, 1000);

        // First chunk: type marker
        parser.read_buf(b"$5");
        let result = parser.try_parse();
        assert_eq!(result, Err(ParseError::UnexpectedEof));

        // Second chunk: length and data
        parser.read_buf(b"\\r\\nhello");
        let result = parser.try_parse();
        assert_eq!(result, Err(ParseError::NotEnoughData));

        // Third chunk: terminator
        parser.read_buf(b"\\r\\n");
        let result = parser.try_parse();
        assert_eq!(
            result,
            Ok(Some(RespValue::BulkString(Some(Cow::Borrowed("hello")))))
        );
    }

    // Simple array chunked transfer
    {
        let mut parser = Parser::new(100, 1000);

        // First chunk: array length
        parser.read_buf(b"*2");
        _ = parser.try_parse();

        // Second chunk: array length terminator and first element start
        parser.read_buf(b"\\r\\n:1");
        _ = parser.try_parse();

        // Third chunk: first element terminator
        parser.read_buf(b"\\r\\n");
        _ = parser.try_parse();

        // Fourth chunk: second element
        parser.read_buf(b":2\\r\\n");
        let result = parser.try_parse();
        assert_eq!(
            result,
            Ok(Some(RespValue::Array(Some(vec![
                RespValue::Integer(1),
                RespValue::Integer(2)
            ]))))
        );
    }
}

Benchmarks

The project includes performance benchmarks for the RESP parser. To run the benchmarks:

# Run all benchmarks
cargo bench

# Run a specific benchmark group
cargo bench --bench parser_benchmark -- "RESP Parser"

# Run a specific benchmark test
cargo bench --bench parser_benchmark -- "RESP Parser/parse/simple_string"

The benchmark results will be displayed in your terminal. Detailed HTML reports will be generated in the target/criterion directory, which you can open in a web browser to see more detailed performance information.

Viewing Benchmark Results

After running the benchmarks, you can find detailed reports in:

target/criterion/report/index.html

The benchmarks measure parsing performance across various RESP data types and scenarios including:

  • Simple strings, errors, integers
  • Bulk strings (empty, null, large)
  • Arrays (simple, nested, large)
  • Real-world commands
  • Batched commands
  • Chunked parsing

Benchmark results will be displayed in the terminal and detailed HTML reports are generated in the target/criterion directory.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Dependencies

~1.6–2.4MB
~40K SLoC