Skip to content

Commit

Permalink
feat: refactoring + helpers + improved CI (#1)
Browse files Browse the repository at this point in the history
* feat: refactoring + helpers + improved CI
* chore: adds codecov badge to README
  • Loading branch information
lmammino committed Feb 28, 2024
1 parent 0db385c commit 99b8786
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 38 deletions.
45 changes: 40 additions & 5 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,43 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v4

- name: Install rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: clippy, rustfmt

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov

- uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-rust-test-${{ hashFiles('**/Cargo.lock') }}

- name: Run cargo fmt
run: cargo fmt -- --check

- name: Run clippy
run: cargo clippy -- -D warnings

- name: Run cargo check
run: cargo check --all-features --locked --release

- name: Run cargo build
run: cargo build --locked --release

- name: Generate code coverage
run: cargo llvm-cov --all-features --lcov --output-path lcov.info

- name: Upload coverage to codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/target
*.profraw
56 changes: 56 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ readme = "README.md"

[dependencies]
nom = "7.1.3"
thiserror = "1.0.57"
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
# tinyresp

[![Build Status](https://github.com/lmammino/tinyresp/actions/workflows/rust.yml/badge.svg)](https://github.com/lmammino/tinyresp/actions/workflows/rust.yml)
[![codecov](https://codecov.io/gh/lmammino/tinyresp/graph/badge.svg?token=2a5OOr6Um4)](https://codecov.io/gh/lmammino/tinyresp)
[![Crates.io](https://img.shields.io/crates/v/tinyresp.svg)](https://crates.io/crates/tinyresp)
[![docs.rs](https://docs.rs/tinyresp/badge.svg)](https://docs.rs/tinyresp)

A tiny Rust library implementing the Redis Serialization Protocol (RESP)

> ⚠️ **WARNING**: This library is still under heavy development and it is not ready for production use yet
<!-- cargo-sync-readme start -->

A simple parser for the RESP protocol
Still under heavy development
A simple parser for the RESP protocol (REdis Serialization Protocol).

For an overview of the procol check out the official
[Redis SErialization Protocol (RESP) documentation](https://redis.io/topics/protocol)

This library is written using [`nom`](https://crates.io/crates/nom) and therefore uses an incremental parsing approach.
This means that using the [`parse`] method will return a `Result` containing a tuple with 2 elements:
- The remaining input (which can be an empty string if the message was fully parsed)
- The parsed value (if the message was fully parsed)

# Example

```rust
use tinyresp::{parse_message, Value};

let message = "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
let (remaining_input, value) = parse_message(message).unwrap();
assert_eq!(remaining_input, "");
assert_eq!(value, Value::Array(vec![
Value::BulkString("hello"),
Value::BulkString("world")
]));
```

<!-- cargo-sync-readme end -->

## Contributing
Expand Down
99 changes: 70 additions & 29 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
//! A simple parser for the RESP protocol
//! Still under heavy development
//! A simple parser for the RESP protocol (REdis Serialization Protocol).
//!
//! For an overview of the procol check out the official
//! [Redis SErialization Protocol (RESP) documentation](https://redis.io/topics/protocol)
//!
//! This library is written using [`nom`](https://crates.io/crates/nom) and therefore uses an incremental parsing approach.
//! This means that using the [`parse`] method will return a `Result` containing a tuple with 2 elements:
//! - The remaining input (which can be an empty string if the message was fully parsed)
//! - The parsed value (if the message was fully parsed)
//!
//! # Example
//!
//! ```
//! use tinyresp::{parse_message, Value};
//!
//! let message = "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
//! let (remaining_input, value) = parse_message(message).unwrap();
//! assert_eq!(remaining_input, "");
//! assert_eq!(value, Value::Array(vec![
//! Value::BulkString("hello"),
//! Value::BulkString("world")
//! ]));
//! ```

use nom::{
branch::alt,
Expand All @@ -14,32 +32,42 @@ use nom::{
sequence::terminated,
IResult,
};
use std::collections::BTreeSet;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub enum Value<'a> {
SimpleString(&'a str),
SimpleError(&'a str),
Integer(i64),
BulkString(&'a str),
Array(Vec<Value<'a>>),
Null,
Boolean(bool),
Double(String),
BigNumber(&'a str),
BulkError(&'a str),
VerbatimString((&'a str, &'a str)),
Map((Vec<Value<'a>>, Vec<Value<'a>>)),
Set(BTreeSet<Value<'a>>),
Pushes(Vec<Value<'a>>),
}

mod value;
pub use value::*;

/// Parses a RESP message using an incremental parsing approach.
/// This means that the parser will return a `Result` containing a tuple with 2 elements:
/// - The remaining input (which can be an empty string if the message was fully parsed)
/// - The parsed value (if the message was fully parsed)
///
/// # Example
///
/// ```
/// use tinyresp::{parse_message, Value};
///
/// let message = "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
/// let (remaining_input, value) = parse_message(message).unwrap();
/// assert_eq!(remaining_input, "");
/// assert_eq!(value, Value::Array(vec![
/// Value::BulkString("hello"),
/// Value::BulkString("world")
/// ]));
/// ```
pub fn parse_message(input: &str) -> IResult<&str, Value> {
let (input, value) = terminated(parse_value, eof)(input)?;
Ok((input, value))
}

pub fn parse_value(input: &str) -> IResult<&str, Value> {
/// Parses a RESP message and returns the parsed value.
/// This is a convenience function that uses `parse_message` internally and returns the parsed value.
/// To get better error reporting and be able to check if there's unparsed data, use [`parse_message`] instead.
pub fn parse(input: &str) -> Result<Value, String> {
let (_, value) = parse_message(input).map_err(|e| format!("{}", e))?;
Ok(value)
}

fn parse_value(input: &str) -> IResult<&str, Value> {
alt((
parse_simple_string,
parse_simple_error,
Expand Down Expand Up @@ -165,7 +193,7 @@ fn parse_verbatim_string(input: &str) -> IResult<&str, Value> {
let (input, length) = terminated(u32, crlf)(input)?;
let (input, encoding) = terminated(take(3usize), tag(":"))(input)?;
let (input, value) = terminated(take(length as usize - 4usize), crlf)(input)?;
Ok((input, Value::VerbatimString((encoding, value))))
Ok((input, Value::VerbatimString(encoding, value)))
}

fn parse_map(input: &str) -> IResult<&str, Value> {
Expand All @@ -185,7 +213,7 @@ fn parse_map(input: &str) -> IResult<&str, Value> {
},
);

Ok((input, Value::Map((keys, values))))
Ok((input, Value::Map(keys, values)))
}

fn parse_set(input: &str) -> IResult<&str, Value> {
Expand Down Expand Up @@ -421,15 +449,15 @@ mod tests {
fn test_verbatim_string() {
assert_eq!(
parse_message("=15\r\ntxt:Some string\r\n"),
Ok(("", Value::VerbatimString(("txt", "Some string"))))
Ok(("", Value::VerbatimString("txt", "Some string")))
);
assert_eq!(
parse_message("=5\r\ntxt:1\r\n"),
Ok(("", Value::VerbatimString(("txt", "1"))))
Ok(("", Value::VerbatimString("txt", "1")))
);
assert_eq!(
parse_message("=5\r\nraw:1\r\n"),
Ok(("", Value::VerbatimString(("raw", "1"))))
Ok(("", Value::VerbatimString("raw", "1")))
);
assert!(parse_message("=5\r\nraw:1\r\nTHIS_SHOULD_NOT_BE_HERE").is_err());
}
Expand All @@ -440,10 +468,10 @@ mod tests {
parse_message("%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n"),
Ok((
"",
Value::Map((
Value::Map(
vec![Value::SimpleString("first"), Value::SimpleString("second")],
vec![Value::Integer(1), Value::Integer(2)]
))
)
))
);
}
Expand Down Expand Up @@ -477,4 +505,17 @@ mod tests {
))
);
}

#[test]
fn test_parse() {
let message = "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n";
let value = parse(message).unwrap();
assert_eq!(
value,
Value::Array(vec![Value::BulkString("hello"), Value::BulkString("world")])
);

let message = "BOGUS";
assert!(parse(message).is_err());
}
}

0 comments on commit 99b8786

Please sign in to comment.