redis_rs 0.9.0

Simple redis client library
Documentation
use crate::enums::RedisParseError;
use crate::enums::{RedisError, RedisResult};
pub use crate::response::{
    RedisResponse,
    RedisResponse::{Array, BulkString, Error, Integer, SimpleString},
};
use std::str::Bytes;

pub fn parse_command(command: &str) -> Result<String, RedisError> {
    let mut tokens = Vec::<String>::new();
    let mut cur_token = String::new();
    let mut quoted = false;

    for item in command.as_bytes() {
        let item = *item as char;
        match item {
            ' ' if !quoted => {
                tokens.push(cur_token.clone());
                cur_token.clear()
            }
            '\'' if !quoted => {
                quoted = true;
            }
            '\'' if quoted => {
                quoted = false;
                tokens.push(cur_token.clone());
                cur_token.clear();
            }
            _ => cur_token.push(item),
        }
    }

    if !cur_token.is_empty() {
        tokens.push(cur_token);
    }

    if quoted && command.contains('\'') {
        return Err(Box::new(RedisParseError {
            contents: "Bad request format, Mismatch quotes".to_string(),
        }));
    }

    let mut output = String::new();

    output.push_str(&format!("*{}\r\n", tokens.len()));

    for token in tokens {
        let length = token.len();
        output.push_str(&format!("${}\r\n{}\r\n", length, token));
    }

    Ok(output)
}

#[doc(hidden)]
pub fn parse_response(response: &str) -> RedisResult<RedisResponse> {
    let mut bytes = response.bytes();

    let first_byte = match bytes.next() {
        Some(value) => value,
        None => {
            return Err(Box::new(RedisParseError {
                contents: "Error reading response data".to_string(),
            }))
        }
    };

    let response = match first_byte as char {
        '+' => parse_simple_string(&mut bytes),
        '-' => parse_error(&mut bytes),
        ':' => parse_integer(&mut bytes),
        '$' => parse_bulk_string(&mut bytes),
        '*' => parse_array(&mut bytes),
        _ => {
            return Err(Box::new(RedisParseError {
                contents: format!("unexpected byte {}, in response", first_byte),
            }))
        }
    }?;

    Ok(response)
}

#[doc(hidden)]
fn parse_error(bytes: &mut std::str::Bytes) -> RedisResult<RedisResponse> {
    let error_string = read_to_carriage_return(bytes);

    Ok(Error(error_string))
}

#[doc(hidden)]
fn parse_integer(bytes: &mut Bytes) -> RedisResult<RedisResponse> {
    let integer_value = read_to_carriage_return(bytes);

    let parsed_integer: i32 = match integer_value.parse() {
        Ok(value) => value,
        Err(_) => {
            return Err(Box::new(RedisParseError {
                contents: format!("Error parsing {} as integer", integer_value),
            }))
        }
    };

    Ok(Integer(parsed_integer))
}

#[doc(hidden)]
fn parse_bulk_string(bytes: &mut Bytes) -> RedisResult<RedisResponse> {
    let integer_value = read_to_carriage_return(bytes);
    let mut string = String::new();

    let mut num_bytes: i32 = match integer_value.parse() {
        Ok(value) => value,
        Err(_) => {
            return Err(Box::new(RedisParseError {
                contents: format!("Error parsing {} to integer", integer_value.clone()),
            }))
        }
    };

    for byte in bytes.skip(1) {
        if num_bytes == 0 {
            break;
        }

        string.push(byte as char);

        num_bytes -= 1;
    }

    Ok(BulkString(string))
}

#[doc(hidden)]
fn parse_array(_bytes: &mut Bytes) -> RedisResult<RedisResponse> {
    todo!()
}

#[doc(hidden)]
fn parse_simple_string(bytes: &mut Bytes) -> RedisResult<RedisResponse> {
    let string = read_to_carriage_return(bytes);
    Ok(SimpleString(string))
}

#[doc(hidden)]
fn read_to_carriage_return(bytes: &mut Bytes) -> String {
    let mut string = String::new();

    for c in bytes {
        let c = c as char;

        if let '\r' = c {
            break;
        }

        string.push(c);
    }

    string
}

#[cfg(test)]
mod test {
    use std::net::TcpStream;

    use crate::{connection::Connection, enums::RedisError};
    use parse::{parse_response, RedisResponse};

    fn create_connection(addr: &str) -> Result<TcpStream, RedisError> {
        Ok(TcpStream::connect(addr)?)
    }

    const HOST: &'static str = "127.0.0.1";
    const PORT: u16 = 6379;
    const ADDR: &'static str = "127.0.0.1:6379";

    use crate::parse;

    #[test]
    fn test_parse_command() {
        let parsed_command = parse::parse_command("GET FOO").unwrap();

        assert_eq!("*2\r\n$3\r\nGET\r\n$3\r\nFOO\r\n", parsed_command);
    }

    #[test]
    fn test_parse_quotes_handled_properly() {
        let stream = create_connection(ADDR).unwrap();
        let mut client = Connection::new(HOST, PORT, stream);

        let _ = client.send_raw_request("set myvalue 'a custom value'");

        let key = "myvalue";
        let response = client.get(key).unwrap();

        assert_eq!(
            response,
            RedisResponse::BulkString(String::from("a custom value"))
        );
    }

    #[test]
    fn test_parse_response() {
        let data = String::from("+OK\r\n");

        let response = parse_response(&data).unwrap();

        assert_eq!(response, RedisResponse::SimpleString(String::from("OK")));
    }

    #[test]
    fn test_error_response() {
        let data = String::from("-ERROR\r\n");

        let response = parse_response(&data).unwrap();
        assert_eq!(response, RedisResponse::Error(String::from("ERROR")));
    }
}