From 38b649ea16d8ed053fd9222bfb9867e3432ee2a6 Mon Sep 17 00:00:00 2001 From: omagdy Date: Thu, 17 Jul 2025 08:06:26 +0300 Subject: test: Moved tests to seprate files under tests folder for more structure --- src/lib.rs | 5 + src/macros.rs | 32 +- src/main.rs | 32 +- src/resp_commands.rs | 315 +---------- src/resp_parser.rs | 1123 +++++-------------------------------- src/shared_cache.rs | 27 + tests/test_commands.rs | 312 +++++++++++ tests/test_parse_array.rs | 130 +++++ tests/test_parse_boolean.rs | 87 +++ tests/test_parse_bulk_string.rs | 214 +++++++ tests/test_parse_double.rs | 1 + tests/test_parse_integer.rs | 165 ++++++ tests/test_parse_map.rs | 452 +++++++++++++++ tests/test_parse_simple_error.rs | 145 +++++ tests/test_parse_simple_string.rs | 185 ++++++ 15 files changed, 1888 insertions(+), 1337 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/shared_cache.rs create mode 100644 tests/test_commands.rs create mode 100644 tests/test_parse_array.rs create mode 100644 tests/test_parse_boolean.rs create mode 100644 tests/test_parse_bulk_string.rs create mode 100644 tests/test_parse_double.rs create mode 100644 tests/test_parse_integer.rs create mode 100644 tests/test_parse_map.rs create mode 100644 tests/test_parse_simple_error.rs create mode 100644 tests/test_parse_simple_string.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bf4f302 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +#[macro_use] +pub mod macros; +pub mod resp_commands; +pub mod resp_parser; +pub mod shared_cache; diff --git a/src/macros.rs b/src/macros.rs index bc4f572..9fddef7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -3,65 +3,65 @@ macro_rules! resp { // Null: resp!(null) (null) => { - $crate::RespType::Null().to_resp_bytes() + RespType::Null().to_resp_bytes() }; // Simple String: resp!("PONG") or resp!(simple "PONG") (simple $s:expr) => { - $crate::RespType::SimpleString($s.to_string()).to_resp_bytes() + RespType::SimpleString($s.to_string()).to_resp_bytes() }; ($s:expr) => { - $crate::RespType::SimpleString($s.to_string()).to_resp_bytes() + RespType::SimpleString($s.to_string()).to_resp_bytes() }; // Simple Error: resp!(error "ERR message") (error $s:expr) => { - $crate::RespType::SimpleError($s.to_string()).to_resp_bytes() + RespType::SimpleError($s.to_string()).to_resp_bytes() }; // Integer: resp!(int 123) (int $i:expr) => { - $crate::RespType::Integer($i).to_resp_bytes() + RespType::Integer($i).to_resp_bytes() }; // Bulk String: resp!(bulk "hello") or resp!(bulk vec![104, 101, 108, 108, 111]) (bulk $s:expr) => { - $crate::RespType::BulkString($s.into()).to_resp_bytes() + RespType::BulkString($s.into()).to_resp_bytes() }; // Array: resp!(array [resp!("one"), resp!(int 2)]) (array [$($elem:expr),*]) => { - $crate::RespType::Array(vec![$($elem),*]).to_resp_bytes() + RespType::Array(vec![$($elem),*]).to_resp_bytes() }; // Boolean: resp!(bool true) (bool $b:expr) => { - $crate::RespType::Boolean($b).to_resp_bytes() + RespType::Boolean($b).to_resp_bytes() }; // Double: resp!(double 3.14) (double $d:expr) => { - $crate::RespType::Doubles($d).to_resp_bytes() + RespType::Doubles($d).to_resp_bytes() }; // Big Number: resp!(bignumber "123456789") (bignumber $n:expr) => { - $crate::RespType::BigNumbers($n.to_string()).to_resp_bytes() + RespType::BigNumbers($n.to_string()).to_resp_bytes() }; // Bulk Error: resp!(bulkerror [resp!("err1"), resp!("err2")]) (bulkerror [$($elem:expr),*]) => { - $crate::RespType::BulkErrors(vec![$($elem),*]).to_resp_bytes() + RespType::BulkErrors(vec![$($elem),*]).to_resp_bytes() }; // Verbatim String: resp!(verbatim [resp!("txt"), resp!("example")]) (verbatim [$($elem:expr),*]) => { - $crate::RespType::VerbatimStrings(vec![$($elem),*]).to_resp_bytes() + RespType::VerbatimStrings(vec![$($elem),*]).to_resp_bytes() }; // Map: resp!(map {resp!("key") => resp!("value")}) (map {$($key:expr => $value:expr),*}) => { - $crate::RespType::Maps({ + RespType::Maps({ let mut map = HashMap::new(); $(map.insert($key, $value);)* map @@ -70,12 +70,12 @@ macro_rules! resp { // Attributes: resp!(attributes [resp!("key"), resp!("value")]) (attributes [$($elem:expr),*]) => { - $crate::RespType::Attributes(vec![$($elem),*]).to_resp_bytes() + RespType::Attributes(vec![$($elem),*]).to_resp_bytes() }; // Set: resp!(set [resp!("one"), resp!("two")]) (set [$($elem:expr),*]) => { - $crate::RespType::Sets({ + RespType::Sets({ let mut set = HashSet::new(); $(set.insert($elem);)* set @@ -84,6 +84,6 @@ macro_rules! resp { // Push: resp!(push [resp!("event"), resp!("data")]) (push [$($elem:expr),*]) => { - $crate::RespType::Pushes(vec![$($elem),*]).to_resp_bytes() + RespType::Pushes(vec![$($elem),*]).to_resp_bytes() }; } diff --git a/src/main.rs b/src/main.rs index c6c8720..6fb16b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,35 +8,9 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -#[macro_use] -mod macros; -mod resp_commands; -mod resp_parser; - -use resp_commands::RedisCommands; -use resp_parser::{parse, RespType}; - -#[derive(Debug, Clone)] -pub struct CacheEntry { - pub value: String, - pub expires_at: Option, // Unix timestamp in milliseconds -} - -impl CacheEntry { - pub fn is_expired(&self) -> bool { - if let Some(expiry) = self.expires_at { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - now > expiry - } else { - false - } - } -} - -pub type SharedCache = Arc>>; +use codecrafters_redis::resp_commands::RedisCommands; +use codecrafters_redis::resp_parser::{parse, RespType}; +use codecrafters_redis::shared_cache::*; fn spawn_cleanup_thread(cache: SharedCache) { let cache_clone = cache.clone(); diff --git a/src/resp_commands.rs b/src/resp_commands.rs index 2663cc5..4d01507 100644 --- a/src/resp_commands.rs +++ b/src/resp_commands.rs @@ -1,5 +1,5 @@ -use crate::{macros::*, resp_parser::*, CacheEntry, SharedCache}; -use std::collections::{HashMap, HashSet}; +use crate::{resp_parser::*, shared_cache::*}; +use std::collections::HashMap; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, Clone)] @@ -42,11 +42,11 @@ pub enum ExpiryOption { /// GET -- Return the old string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. #[derive(Debug, Clone)] pub struct SetCommand { - key: String, - value: String, - condition: Option, - expiry: Option, - get_old_value: bool, + pub key: String, + pub value: String, + pub condition: Option, + pub expiry: Option, + pub get_old_value: bool, } impl SetCommand { @@ -353,303 +353,4 @@ impl From for RedisCommands { } #[cfg(test)] -mod tests { - use super::*; - use std::sync::{Arc, Mutex}; - use std::thread; - - // Test Helpers & Mocks - - /// Creates a new, empty shared cache for each test. - fn new_cache() -> SharedCache { - Arc::new(Mutex::new(HashMap::new())) - } - - /// Builds a `RespType::Array` from string slices to simplify parser tests. - fn build_command_from_str_slice(args: &[&str]) -> RespType { - let resp_args = args - .iter() - .map(|s| RespType::BulkString(s.to_string().into_bytes())) - .collect(); - RespType::Array(resp_args) - } - - /// A helper to get a value directly from the cache for assertions. - fn get_from_cache(cache: &SharedCache, key: &str) -> Option { - cache.lock().unwrap().get(key).cloned() - } - - /// Tests for the `RedisCommands::from(RespType)` parser logic. - mod command_parser_tests { - use super::*; - - #[test] - fn test_parse_ping() { - let cmd = build_command_from_str_slice(&["PING"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::PING)); - } - - #[test] - fn test_parse_ping_case_insensitive() { - let cmd = build_command_from_str_slice(&["pInG"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::PING)); - } - - #[test] - fn test_parse_ping_with_extra_args_is_invalid() { - let cmd = build_command_from_str_slice(&["PING", "extra"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - - #[test] - fn test_parse_echo() { - let cmd = build_command_from_str_slice(&["ECHO", "hello world"]); - match RedisCommands::from(cmd) { - RedisCommands::ECHO(s) => assert_eq!(s, "hello world"), - _ => panic!("Expected ECHO command"), - } - } - - #[test] - fn test_parse_echo_no_args_is_invalid() { - let cmd = build_command_from_str_slice(&["ECHO"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - - #[test] - fn test_parse_get() { - let cmd = build_command_from_str_slice(&["GET", "mykey"]); - match RedisCommands::from(cmd) { - RedisCommands::GET(k) => assert_eq!(k, "mykey"), - _ => panic!("Expected GET command"), - } - } - - #[test] - fn test_parse_simple_set() { - let cmd = build_command_from_str_slice(&["SET", "mykey", "myvalue"]); - match RedisCommands::from(cmd) { - RedisCommands::SET(c) => { - assert_eq!(c.key, "mykey"); - assert_eq!(c.value, "myvalue"); - assert!(c.condition.is_none() && c.expiry.is_none() && !c.get_old_value); - } - _ => panic!("Expected SET command"), - } - } - - #[test] - fn test_parse_set_with_all_options() { - let cmd = build_command_from_str_slice(&["SET", "k", "v", "NX", "PX", "5000", "GET"]); - match RedisCommands::from(cmd) { - RedisCommands::SET(c) => { - assert!(matches!(c.condition, Some(SetCondition::NotExists))); - assert!(matches!(c.expiry, Some(ExpiryOption::Milliseconds(5000)))); - assert!(c.get_old_value); - } - _ => panic!("Expected SET command"), - } - } - - #[test] - fn test_parse_set_options_case_insensitive() { - let cmd = build_command_from_str_slice(&["set", "k", "v", "nx", "px", "100"]); - match RedisCommands::from(cmd) { - RedisCommands::SET(c) => { - assert!(matches!(c.condition, Some(SetCondition::NotExists))); - assert!(matches!(c.expiry, Some(ExpiryOption::Milliseconds(100)))); - } - _ => panic!("Expected SET command"), - } - } - - #[test] - fn test_parse_set_invalid_option_value() { - let cmd = build_command_from_str_slice(&["SET", "k", "v", "EX", "not-a-number"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - - #[test] - fn test_parse_set_option_missing_value() { - let cmd = build_command_from_str_slice(&["SET", "k", "v", "PX"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - - #[test] - fn test_parse_unknown_command() { - let cmd = build_command_from_str_slice(&["UNKNOWN", "foo"]); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - - #[test] - fn test_parse_not_an_array_is_invalid() { - let cmd = RespType::SimpleString("SET k v".into()); - assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid)); - } - } - - /// Tests for the command execution logic in `RedisCommands::execute`. - mod command_execution_tests { - use super::*; - - /// Helper to parse and execute a command against a cache. - fn run_command(cache: &SharedCache, args: &[&str]) -> Vec { - let command = RedisCommands::from(build_command_from_str_slice(args)); - command.execute(Arc::clone(cache)) - } - - #[test] - fn test_execute_ping() { - let cache = new_cache(); - let result = run_command(&cache, &["PING"]); - assert_eq!(result, b"+PONG\r\n"); - } - - #[test] - fn test_execute_echo() { - let cache = new_cache(); - // Note: the provided code has a bug, it returns a Simple String, not a Bulk String. - // A correct implementation would return `resp!(bulk "hello")`. - let result = run_command(&cache, &["ECHO", "hello"]); - assert_eq!(result, b"+hello\r\n"); - } - - #[test] - fn test_execute_get_non_existent() { - let cache = new_cache(); - let result = run_command(&cache, &["GET", "mykey"]); - assert_eq!(result, b"$-1\r\n"); // Null Bulk String - } - - #[test] - fn test_execute_set_and_get() { - let cache = new_cache(); - let set_result = run_command(&cache, &["SET", "mykey", "myvalue"]); - assert_eq!(set_result, b"+OK\r\n"); - - let get_result = run_command(&cache, &["GET", "mykey"]); - assert_eq!(get_result, b"$7\r\nmyvalue\r\n"); - } - - #[test] - fn test_execute_set_nx() { - let cache = new_cache(); - // Should succeed when key doesn't exist - assert_eq!(run_command(&cache, &["SET", "k", "v1", "NX"]), b"+OK\r\n"); - assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v1"); - - // Should fail when key exists - assert_eq!(run_command(&cache, &["SET", "k", "v2", "NX"]), b"$-1\r\n"); - assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v1"); // Value is unchanged - } - - #[test] - fn test_execute_set_xx() { - let cache = new_cache(); - // Should fail when key doesn't exist - assert_eq!(run_command(&cache, &["SET", "k", "v1", "XX"]), b"$-1\r\n"); - assert!(get_from_cache(&cache, "k").is_none()); - - // Pre-populate and should succeed - run_command(&cache, &["SET", "k", "v1"]); - assert_eq!(run_command(&cache, &["SET", "k", "v2", "XX"]), b"+OK\r\n"); - assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v2"); - } - - #[test] - fn test_execute_set_with_get_option() { - let cache = new_cache(); - run_command(&cache, &["SET", "mykey", "old"]); - - // Note: This test will fail with the provided code, which incorrectly returns - // a Simple String `+old\r\n`. The test correctly expects a Bulk String. - let result = run_command(&cache, &["SET", "mykey", "new", "GET"]); - assert_eq!(result, b"$3\r\nold\r\n"); - assert_eq!(get_from_cache(&cache, "mykey").unwrap().value, "new"); - } - - #[test] - fn test_execute_set_get_on_non_existent() { - let cache = new_cache(); - // Note: This test will fail with the provided code, which incorrectly - // returns `+OK\r\n`. The spec requires a nil reply. - let result = run_command(&cache, &["SET", "mykey", "new", "GET"]); - assert_eq!(result, b"$-1\r\n"); - assert!(get_from_cache(&cache, "mykey").is_some()); - } - - #[test] - fn test_expiry_with_px_and_cleanup() { - let cache = new_cache(); - run_command(&cache, &["SET", "mykey", "val", "PX", "50"]); - - assert!(get_from_cache(&cache, "mykey").is_some()); - thread::sleep(Duration::from_millis(60)); - - // GET on an expired key should return nil and trigger cleanup - assert_eq!(run_command(&cache, &["GET", "mykey"]), b"$-1\r\n"); - assert!(get_from_cache(&cache, "mykey").is_none()); - } - - #[test] - fn test_keepttl() { - let cache = new_cache(); - run_command(&cache, &["SET", "mykey", "v1", "EX", "2"]); - let expiry1 = get_from_cache(&cache, "mykey").unwrap().expires_at; - - thread::sleep(Duration::from_millis(100)); - run_command(&cache, &["SET", "mykey", "v2", "KEEPTTL"]); - - let entry2 = get_from_cache(&cache, "mykey").unwrap(); - assert_eq!(entry2.value, "v2"); // Value is updated - assert_eq!(entry2.expires_at, expiry1); // TTL is retained - } - } - - /// Unit tests for the `SetCommand` helper methods. - mod set_command_tests { - use super::*; - - #[test] - fn test_calculate_expiry_seconds() { - let cmd = SetCommand::new("k".into(), "v".into()) - .with_expiry(Some(ExpiryOption::Seconds(10))); - let expiry = cmd.calculate_expiry_time().unwrap(); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - let delta = expiry.saturating_sub(now); - // Allow a small delta for execution time variance - assert!((9990..=10010).contains(&delta), "Delta was {}", delta); - } - - #[test] - fn test_calculate_expiry_at_seconds() { - let ts_secs = 1893456000; // 2030-01-01 00:00:00 UTC - let cmd = SetCommand::new("k".into(), "v".into()) - .with_expiry(Some(ExpiryOption::ExpiresAtSeconds(ts_secs))); - let expiry = cmd.calculate_expiry_time().unwrap(); - assert_eq!(expiry, ts_secs * 1000); - } - - #[test] - fn test_calculate_expiry_at_milliseconds() { - let ts_ms = 1893456000123; - let cmd = SetCommand::new("k".into(), "v".into()) - .with_expiry(Some(ExpiryOption::ExpiresAtMilliseconds(ts_ms))); - let expiry = cmd.calculate_expiry_time().unwrap(); - assert_eq!(expiry, ts_ms); - } - - #[test] - fn test_calculate_expiry_for_none_and_keepttl() { - let cmd_keepttl = - SetCommand::new("k".into(), "v".into()).with_expiry(Some(ExpiryOption::KeepTtl)); - assert!(cmd_keepttl.calculate_expiry_time().is_none()); - - let cmd_none = SetCommand::new("k".into(), "v".into()).with_expiry(None); - assert!(cmd_none.calculate_expiry_time().is_none()); - } - } -} +mod tests {} diff --git a/src/resp_parser.rs b/src/resp_parser.rs index 0313679..0563188 100644 --- a/src/resp_parser.rs +++ b/src/resp_parser.rs @@ -194,7 +194,7 @@ impl RespError { } } -fn parse_simple_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_simple_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != SIMPLE_STRING { @@ -218,7 +218,7 @@ fn parse_simple_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_simple_errors(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_simple_errors(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != SIMPLE_ERROR { @@ -242,7 +242,7 @@ fn parse_simple_errors(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_integers(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_integers(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != INTEGER { @@ -299,12 +299,16 @@ pub fn parse(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { let (parsed, remain) = parse_nulls(bytes)?; Ok((parsed, remain)) } + MAPS => { + let (parsed, remain) = parse_maps(bytes)?; + Ok((parsed, remain)) + } _ => Err(RespError::InvalidDataType), } } -fn parse_array(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_array(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != ARRAY { @@ -343,7 +347,7 @@ fn parse_array(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_bulk_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_bulk_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != BULK_STRING { @@ -389,7 +393,7 @@ fn parse_bulk_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_nulls(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_nulls(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != NULL { @@ -413,7 +417,7 @@ fn parse_nulls(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_boolean(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_boolean(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != BOOLEAN { @@ -444,7 +448,7 @@ fn parse_boolean(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_doubles(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { +pub fn parse_doubles(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { match bytes { [first, rest @ ..] => { if *first != DOUBLES { @@ -467,51 +471,105 @@ fn parse_doubles(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { } } -fn parse_big_numbers() { - todo!() +pub fn parse_maps(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != MAPS { + return Err(RespError::InvalidDataType); + } + + // this would consume the \r\n + let (consumed, mut remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + // should equal the digit + let length = String::from_utf8_lossy(consumed) + .parse::() + .map_err(|_| RespError::InvalidValue)?; + + let mut map: HashMap = HashMap::new(); + + let mut key_set: HashSet = HashSet::new(); + + // I mean this is pretty unredable but it is what it is :/ + // The redundant !remained.is_empty() is because the parse function should handle the + // empty bytes but that would mean I refactor the parse to return and (Option, &[u8]) + // Which is kind of a lot of work now so this works for now I should probably do this for arrray parsing I think + for _ in 0..length { + if !remained.is_empty() { + if !remained.is_empty() { + let (key, rest) = parse(remained)?; + key_set.insert(key.to_resp_string()); + dbg!(&key); + remained = rest; + if !remained.is_empty() { + let (value, rest) = parse(remained)?; + dbg!(&value); + remained = rest; + map.insert(key.to_resp_string(), value); + } + } + } + } + + // I need this because if the user sent the same key it should override and the check + // for unexpected end fails because it would expect the length of the map that was intended by length variable + if map.len() != key_set.len() { + return Err(RespError::UnexpectedEnd); + } + + let consumed = RespType::Maps(map); + + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } } -fn parse_sets() { +pub fn parse_big_numbers() { todo!() } -fn parse_maps() { +pub fn parse_sets() { todo!() } -fn parse_verbatim_string() { +pub fn parse_verbatim_string() { todo!() } -fn parse_bulk_errors() { +pub fn parse_bulk_errors() { todo!() } -fn parse_attributes() { +pub fn parse_attributes() { todo!() } -fn parse_pushes() { +pub fn parse_pushes() { todo!() } #[derive(Debug, Clone)] pub enum RespType { - SimpleString(String), // + - SimpleError(String), // - - Integer(u64), // : - BulkString(Vec), // $ - Array(Vec), // * - Null(), // _ - Boolean(bool), // # - Doubles(f64), // , - BigNumbers(String), // ( - BulkErrors(Vec), // ! - VerbatimStrings(Vec), // = - Maps(HashMap), // % - Attributes(Vec), // | - Sets(HashSet), // ~ - Pushes(Vec), // > + SimpleString(String), // + + SimpleError(String), // - + Integer(u64), // : + BulkString(Vec), // $ + Array(Vec), // * + Null(), // _ + Boolean(bool), // # + Doubles(f64), // , + BigNumbers(String), // ( + BulkErrors(Vec), // ! + VerbatimStrings(Vec), // = + Maps(HashMap), // % + Attributes(Vec), // | + Sets(HashSet), // ~ + Pushes(Vec), // > } impl RespType { @@ -560,6 +618,54 @@ impl RespType { } } } + + pub fn to_resp_string(&self) -> String { + match self { + RespType::SimpleString(s) => format!("{}", s), + RespType::SimpleError(s) => format!("{}", s), + RespType::Integer(i) => format!("{}", i), + RespType::BulkString(bytes) => { + let s = String::from_utf8_lossy(bytes); + format!("{}", s) + } + RespType::Array(arr) => { + let elements = arr + .iter() + .map(|e| e.to_resp_bytes()) + .collect::>>(); + // TODO: Implement proper Display for elements because this will definitely not + // work + format!("{:?}", elements) + } + // this is just a hack because the platform uses RESP2 in RESP3 it should be "_\r\n" + RespType::Null() => "-1".to_string(), + RespType::Boolean(b) => format!("{}", if *b { "t" } else { "f" }), + RespType::Doubles(d) => format!("{}", d), + RespType::BigNumbers(n) => format!("{}", n), + RespType::Maps(map) => { + let pairs: Vec = map + .iter() + .map(|(key, value)| format!("{}: {}", key, value.to_resp_string())) + .collect(); + format!("{{{}}}", pairs.join(", ")) + } + RespType::BulkErrors(errors) => { + todo!() + } + RespType::VerbatimStrings(strings) => { + todo!() + } + RespType::Attributes(attrs) => { + todo!() + } + RespType::Sets(set) => { + todo!() + } + RespType::Pushes(pushes) => { + todo!() + } + } + } } impl PartialEq for RespType { @@ -576,7 +682,7 @@ impl PartialEq for RespType { (RespType::BigNumbers(a), RespType::BigNumbers(b)) => a == b, (RespType::BulkErrors(a), RespType::BulkErrors(b)) => a == b, (RespType::VerbatimStrings(a), RespType::VerbatimStrings(b)) => a == b, - // (RespType::Maps(a), RespType::Maps(b)) => a == b, + (RespType::Maps(a), RespType::Maps(b)) => a == b, (RespType::Attributes(a), RespType::Attributes(b)) => a == b, // (RespType::Sets(a), RespType::Sets(b)) => a == b, (RespType::Pushes(a), RespType::Pushes(b)) => a == b, @@ -667,956 +773,3 @@ impl PartialEq for RespType { } } } - -// Test module -#[cfg(test)] -mod tests { - use super::*; - - mod simple_strings_tests { - use super::*; - - #[test] - fn test_valid_simple_strings() { - // Basic valid cases - assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); - assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); - assert_eq!( - parse_simple_strings(b"+Hello World\r\n").unwrap().0, - "Hello World" - ); - - // Empty string - assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); - - // String with spaces and special characters (but no \r or \n) - assert_eq!( - parse_simple_strings(b"+Hello, World! 123\r\n").unwrap().0, - "Hello, World! 123" - ); - - // String with various ASCII characters - assert_eq!( - parse_simple_strings(b"+!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") - .unwrap() - .0, - "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " - ); - - // Unicode characters (should work with UTF-8) - assert_eq!( - parse_simple_strings(b"+\xc3\xa9\xc3\xa1\xc3\xb1\r\n") - .unwrap() - .0, - "éáñ" - ); - } - - #[test] - fn test_invalid_prefix() { - // Missing '+' prefix - assert_eq!( - parse_simple_strings(b"OK\r\n").err().unwrap().message(), - "WRONGTYPE Operation against a key holding the wrong kind of value" - ); - - // Wrong prefix - assert_eq!( - parse_simple_strings(b"-Error\r\n").err().unwrap().message(), - "WRONGTYPE Operation against a key holding the wrong kind of value" - ); - assert_eq!( - parse_simple_strings(b":123\r\n").err().unwrap().message(), - "WRONGTYPE Operation against a key holding the wrong kind of value" - ); - assert_eq!( - parse_simple_strings(b"$5\r\nhello\r\n") - .err() - .unwrap() - .message(), - "WRONGTYPE Operation against a key holding the wrong kind of value" - ); - } - - #[test] - fn test_missing_crlf_terminator() { - // No CRLF at all - assert_eq!( - parse_simple_strings(b"+OK").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \r - assert_eq!( - parse_simple_strings(b"+OK\r").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \n - assert_eq!( - parse_simple_strings(b"+OK\n").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Wrong order (\n\r instead of \r\n) - assert_eq!( - parse_simple_strings(b"+OK\n\r").err().unwrap().message(), - "ERR Unexpected end of input" - ); - } - - #[test] - fn test_invalid_characters_in_content() { - // Contains \r in content - assert_eq!( - parse_simple_strings(b"+Hello\rWorld\r\n") - .err() - .unwrap() - .message(), - "ERR invalid value" - ); - - // Contains \n in content - assert_eq!( - parse_simple_strings(b"+Hello\nWorld\r\n") - .err() - .unwrap() - .message(), - "ERR invalid value" - ); - } - - #[test] - fn test_empty_input() { - assert_eq!( - parse_simple_strings(b"").err().unwrap().message(), - "ERR Empty data" - ); - } - - #[test] - fn test_with_trailing_data() { - // RESP simple string with extra data after CRLF (should be ignored) - assert_eq!(parse_simple_strings(b"+OK\r\nextra_data").unwrap().0, "OK"); - assert_eq!( - parse_simple_strings(b"+PONG\r\n+another_string\r\n") - .unwrap() - .0, - "PONG" - ); - } - - #[test] - fn test_real_world_redis_responses() { - // Common Redis simple string responses - assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); - assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); - assert_eq!(parse_simple_strings(b"+QUEUED\r\n").unwrap().0, "QUEUED"); - - // Redis status responses - assert_eq!( - parse_simple_strings(b"+Background saving started\r\n") - .unwrap() - .0, - "Background saving started" - ); - assert_eq!( - parse_simple_strings(b"+Background saving successfully finished\r\n") - .unwrap() - .0, - "Background saving successfully finished" - ); - } - - #[test] - fn test_edge_cases() { - // Just the prefix and CRLF - assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); - - // Long string - let long_string = "a".repeat(1000); - let mut input = b"+".to_vec(); - input.extend_from_slice(long_string.as_bytes()); - input.extend_from_slice(b"\r\n"); - assert_eq!(parse_simple_strings(&input).unwrap().0, long_string); - - // String with only spaces - assert_eq!(parse_simple_strings(b"+ \r\n").unwrap().0, " "); - - // String with tabs and other whitespace - assert_eq!(parse_simple_strings(b"+\t \t\r\n").unwrap().0, "\t \t"); - } - - #[test] - fn test_binary_safety_within_limits() { - // Non-UTF8 bytes (but no \r or \n) - let mut input = b"+".to_vec(); - input.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // Invalid UTF-8 - input.extend_from_slice(b"\r\n"); - - // Should handle invalid UTF-8 gracefully with replacement characters - if let RespType::SimpleString(data) = parse_simple_strings(&input).unwrap().0 { - assert!(!data.is_empty()); // Should contain replacement characters - } - } - } - - mod simple_errors_tests { - use super::*; - - #[test] - fn test_valid_simple_errors() { - // Basic valid cases - assert_eq!( - parse_simple_errors(b"-ERR unknown command\r\n").unwrap().0, - "ERR unknown command" - ); - assert_eq!( - parse_simple_errors(b"-WRONGTYPE\r\n").unwrap().0, - "WRONGTYPE" - ); - assert_eq!( - parse_simple_errors(b"-ERR syntax error\r\n").unwrap().0, - "ERR syntax error" - ); - - // Empty error string - assert_eq!(parse_simple_errors(b"-\r\n").unwrap().0, ""); - - // Error with spaces and special characters (but no \r or \n) - assert_eq!( - parse_simple_errors(b"-ERR invalid key: 'test123'\r\n") - .unwrap() - .0, - "ERR invalid key: 'test123'" - ); - - // Error with various ASCII characters - assert_eq!( - parse_simple_errors(b"-ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") - .unwrap() - .0, - "ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " - ); - - // Unicode characters in error message - assert_eq!( - parse_simple_errors(b"-ERR \xc3\xa9\xc3\xa1\xc3\xb1\r\n") - .unwrap() - .0, - "ERR éáñ" - ); - - // Common Redis error patterns - assert_eq!( - parse_simple_errors(b"-NOAUTH Authentication required\r\n") - .unwrap() - .0, - "NOAUTH Authentication required" - ); - assert_eq!( - parse_simple_errors( - b"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n" - ) - .unwrap() - .0, - "WRONGTYPE Operation against a key holding the wrong kind of value" - ); - } - - #[test] - fn test_invalid_simple_errors() { - // Wrong data type marker - assert_eq!( - parse_simple_errors(b"+OK\r\n").err().unwrap().message(), - "ERR Invalid data type" - ); - - // Contains \r in content - assert_eq!( - parse_simple_errors(b"-ERR invalid\r character\r\n") - .err() - .unwrap() - .message(), - "ERR invalid value" - ); - - // Contains \n in content - assert_eq!( - parse_simple_errors(b"-ERR invalid\n character\r\n") - .err() - .unwrap() - .message(), - "ERR invalid value" - ); - - // Missing \r\n terminator - assert_eq!( - parse_simple_errors(b"-ERR no terminator") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Only \r without \n - assert_eq!( - parse_simple_errors(b"-ERR only carriage return\r") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Only \n without \r - assert_eq!( - parse_simple_errors(b"-ERR only newline\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Empty input - assert_eq!( - parse_simple_errors(b"").err().unwrap().message(), - "ERR Empty data" - ); - - // Just the marker without content - assert_eq!( - parse_simple_errors(b"-").err().unwrap().message(), - "ERR Unexpected end of input" - ); - } - - #[test] - fn test_simple_error_remaining_bytes() { - // Test that remaining bytes are correctly returned - let (error, remaining) = parse_simple_errors(b"-ERR test\r\nnext data").unwrap(); - assert_eq!(error, "ERR test"); - assert_eq!(remaining, b"next data"); - - // Test with multiple commands - let (error, remaining) = parse_simple_errors(b"-WRONGTYPE\r\n+OK\r\n").unwrap(); - assert_eq!(error, "WRONGTYPE"); - assert_eq!(remaining, b"+OK\r\n"); - - // Test with no remaining data - let (error, remaining) = parse_simple_errors(b"-ERR final\r\n").unwrap(); - assert_eq!(error, "ERR final"); - assert_eq!(remaining, b""); - } - } - - mod integeres_tests { - use super::*; - - #[test] - fn test_valid_integers() { - // Basic valid cases - assert_eq!(parse_integers(b":0\r\n").unwrap().0, 0u64); - assert_eq!(parse_integers(b":1\r\n").unwrap().0, 1u64); - assert_eq!(parse_integers(b":42\r\n").unwrap().0, 42u64); - assert_eq!(parse_integers(b":1000\r\n").unwrap().0, 1000u64); - - assert_eq!(parse_integers(b":+42\r\n").unwrap().0, 42u64); - - // Large numbers - assert_eq!( - parse_integers(b":9223372036854775807\r\n").unwrap().0, - 9223372036854775807u64 - ); - assert_eq!( - parse_integers(b":18446744073709551615\r\n").unwrap().0, - 18446744073709551615u64 - ); // u64::MAX - - // Edge cases - assert_eq!(parse_integers(b":123456789\r\n").unwrap().0, 123456789u64); - - // Numbers with leading zeros (should still parse correctly) - assert_eq!(parse_integers(b":0000042\r\n").unwrap().0, 42u64); - assert_eq!(parse_integers(b":00000\r\n").unwrap().0, 0u64); - } - - #[test] - fn test_invalid_integers() { - // Wrong data type marker - assert_eq!( - parse_integers(b"+42\r\n").err().unwrap().message(), - "ERR Invalid data type" - ); - - // Negative numbers (not valid for u64) - assert_eq!( - parse_integers(b":-42\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Non-numeric content - assert_eq!( - parse_integers(b":abc\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Mixed numeric and non-numeric - assert_eq!( - parse_integers(b":42abc\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Empty integer - assert_eq!( - parse_integers(b":\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Contains \r in content - assert_eq!( - parse_integers(b":42\r23\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Contains \n in content - assert_eq!( - parse_integers(b":42\n23\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Missing \r\n terminator - assert_eq!( - parse_integers(b":42").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \r without \n - assert_eq!( - parse_integers(b":42\r").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \n without \r - assert_eq!( - parse_integers(b":42\n").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Empty input - assert_eq!( - parse_integers(b"").err().unwrap().message(), - "ERR Empty data" - ); - - // Just the marker without content - assert_eq!( - parse_integers(b":").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Number too large for u64 - assert_eq!( - parse_integers(b":18446744073709551616\r\n") // u64::MAX + 1 - .err() - .unwrap() - .message(), - "ERR invalid value" - ); - - // Floating point numbers - assert_eq!( - parse_integers(b":42.5\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Scientific notation - assert_eq!( - parse_integers(b":1e5\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Hexadecimal numbers - assert_eq!( - parse_integers(b":0x42\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Whitespace - assert_eq!( - parse_integers(b": 42\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - assert_eq!( - parse_integers(b":42 \r\n").err().unwrap().message(), - "ERR invalid value" - ); - } - - #[test] - fn test_integer_remaining_bytes() { - // Test that remaining bytes are correctly returned - let (integer, remaining) = parse_integers(b":42\r\nnext data").unwrap(); - assert_eq!(integer, 42u64); - assert_eq!(remaining, b"next data"); - - // Test with multiple commands - let (integer, remaining) = parse_integers(b":1337\r\n+OK\r\n").unwrap(); - assert_eq!(integer, 1337u64); - assert_eq!(remaining, b"+OK\r\n"); - - // Test with no remaining data - let (integer, remaining) = parse_integers(b":999\r\n").unwrap(); - assert_eq!(integer, 999u64); - assert_eq!(remaining, b""); - - // Test with zero and remaining data - let (integer, remaining) = parse_integers(b":0\r\n-ERR test\r\n").unwrap(); - assert_eq!(integer, 0u64); - assert_eq!(remaining, b"-ERR test\r\n"); - } - } - - #[cfg(test)] - mod bulk_string_tests { - use super::*; - - #[test] - fn test_valid_bulk_strings() { - // basic valid cases - assert_eq!(parse_bulk_strings(b"$2\r\nok\r\n").unwrap().0, "ok"); - assert_eq!(parse_bulk_strings(b"$4\r\npong\r\n").unwrap().0, "pong"); - assert_eq!( - parse_bulk_strings(b"$11\r\nhello world\r\n").unwrap().0, - "hello world" - ); - - // empty string - assert_eq!(parse_bulk_strings(b"$0\r\n\r\n").unwrap().0, ""); - - // string with special characters (including \r and \n - allowed in bulk strings) - assert_eq!( - parse_bulk_strings(b"$13\r\nhello\r\nworld!\r\n").unwrap().0, - "hello\r\nworld!" - ); - - // string with various ascii characters - assert_eq!( - parse_bulk_strings(b"$30\r\n!@#$%^&*()_+-={}[]|\\:;\"'<>?,./\r\n") - .unwrap() - .0, - "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./" - ); - - // large string - let large_content = "x".repeat(1000); - let large_bulk = format!("$1000\r\n{}\r\n", large_content); - if let RespType::BulkString(bulk) = parse_bulk_strings(large_bulk.as_bytes()).unwrap().0 - { - } - - assert_eq!( - parse_bulk_strings(large_bulk.as_bytes()).unwrap().0, - large_content - ); - - // string with only whitespace - assert_eq!(parse_bulk_strings(b"$3\r\n \r\n").unwrap().0, " "); - - // string with tabs and newlines - assert_eq!( - parse_bulk_strings(b"$7\r\nhe\tllo\n\r\n").unwrap().0, - "he\tllo\n" - ); - } - - #[test] - fn test_null_bulk_string() { - // Null bulk string - let (result, remaining) = parse_bulk_strings(b"$-1\r\n").unwrap(); - assert_eq!(result, RespType::Null()); - assert_eq!(remaining, b""); - - // Null bulk string with remaining data - let (result, remaining) = parse_bulk_strings(b"$-1\r\n+OK\r\n").unwrap(); - assert_eq!(result, RespType::Null()); - assert_eq!(remaining, b"+OK\r\n"); - } - - #[test] - fn test_invalid_bulk_strings() { - // Wrong data type marker - assert_eq!( - parse_bulk_strings(b"+OK\r\n").err().unwrap().message(), - "ERR Invalid data type" - ); - - // Invalid length format - assert_eq!( - parse_bulk_strings(b"$abc\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Negative length (other than -1) - assert_eq!( - parse_bulk_strings(b"$-5\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Missing length - assert_eq!( - parse_bulk_strings(b"$\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Missing first \r\n after length - assert_eq!( - parse_bulk_strings(b"$5hello\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Content shorter than declared length - assert_eq!( - parse_bulk_strings(b"$5\r\nhi\r\n").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Content longer than declared length (missing final \r\n) - assert_eq!( - parse_bulk_strings(b"$2\r\nhello\r\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Missing final \r\n - assert_eq!( - parse_bulk_strings(b"$5\r\nhello").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \r without \n at the end - assert_eq!( - parse_bulk_strings(b"$5\r\nhello\r") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Only \n without \r at the end - assert_eq!( - parse_bulk_strings(b"$5\r\nhello\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Empty input - assert_eq!( - parse_bulk_strings(b"").err().unwrap().message(), - "ERR Empty data" - ); - - // Just the marker - assert_eq!( - parse_bulk_strings(b"$").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Length too large for available data - assert_eq!( - parse_bulk_strings(b"$100\r\nshort\r\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Zero length but with content - assert_eq!( - parse_bulk_strings(b"$0\r\nhello\r\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - } - - #[test] - fn test_bulk_string_remaining_bytes() { - // Test that remaining bytes are correctly returned - let (string, remaining) = parse_bulk_strings(b"$5\r\nhello\r\nnext data").unwrap(); - assert_eq!(string, "hello"); - assert_eq!(remaining, b"next data"); - - // Test with multiple commands - let (string, remaining) = parse_bulk_strings(b"$4\r\ntest\r\n:42\r\n").unwrap(); - assert_eq!(string, "test"); - assert_eq!(remaining, b":42\r\n"); - - // Test with no remaining data - let (string, remaining) = parse_bulk_strings(b"$3\r\nend\r\n").unwrap(); - assert_eq!(string, "end"); - assert_eq!(remaining, b""); - - // Test null string with remaining data - let (result, remaining) = parse_bulk_strings(b"$-1\r\n+PONG\r\n").unwrap(); - assert_eq!(result, RespType::Null()); - assert_eq!(remaining, b"+PONG\r\n"); - } - - #[test] - fn test_bulk_string_edge_cases() { - // String that contains the exact sequence that would end it - assert_eq!( - parse_bulk_strings(b"$8\r\ntest\r\n\r\n\r\n").unwrap().0, - "test\r\n" - ); - - // String with only \r\n - assert_eq!(parse_bulk_strings(b"$2\r\n\r\n\r\n").unwrap().0, "\r\n"); - - // String that starts with numbers - assert_eq!(parse_bulk_strings(b"$5\r\n12345\r\n").unwrap().0, "12345"); - - // String with control characters - assert_eq!( - parse_bulk_strings(b"$5\r\n\x01\x02\x03\x04\x05\r\n") - .unwrap() - .0, - "\x01\x02\x03\x04\x05" - ); - - // Maximum length value (within reason) - let content = "a".repeat(65535); - let bulk = format!("$65535\r\n{}\r\n", content); - assert_eq!(parse_bulk_strings(bulk.as_bytes()).unwrap().0, content); - } - } - - mod array_tests { - use super::*; - - #[test] - fn test_valid_arrays() { - // Simple array with strings - let arr = vec![ - RespType::BulkString("hello".into()), - RespType::BulkString("world".into()), - ]; - assert_eq!( - parse_array(b"*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n") - .unwrap() - .0, - RespType::Array(arr) - ); - - // Array with mixed types - let arr = vec![ - RespType::Integer(42), - RespType::SimpleString("OK".to_string()), - RespType::Null(), - ]; - assert_eq!( - parse_array(b"*3\r\n:42\r\n+OK\r\n_\r\n").unwrap().0, - RespType::Array(arr) - ); - - // Nested array - let arr = vec![ - RespType::Array(vec![ - RespType::BulkString("nested".into()), - RespType::Integer(123), - ]), - RespType::SimpleError("ERR test".to_string()), - ]; - assert_eq!( - parse_array(b"*2\r\n*2\r\n$6\r\nnested\r\n:123\r\n-ERR test\r\n") - .unwrap() - .0, - RespType::Array(arr) - ); - - // Empty array - let arr = vec![]; - assert_eq!(parse_array(b"*0\r\n").unwrap().0, RespType::Array(arr)); - } - - #[test] - fn test_invalid_arrays() { - // Wrong data type marker - assert_eq!( - parse_array(b"+2\r\n$5\r\nhello\r\n") - .err() - .unwrap() - .message(), - "ERR Invalid data type" - ); - - // Missing \r\n terminator - assert_eq!( - parse_array(b"*2\r\n$5\r\nhello\r\n$5\r\nworld") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Invalid length - assert_eq!( - parse_array(b"*-1\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Non-numeric length - assert_eq!( - parse_array(b"*abc\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Incomplete array elements - assert_eq!( - parse_array(b"*2\r\n$5\r\nhello\r\n") - .err() - .unwrap() - .message(), - "ERR Unexpected end of input" - ); - - // Empty input - assert_eq!(parse_array(b"").err().unwrap().message(), "ERR Empty data"); - - // Just the marker - assert_eq!( - parse_array(b"*").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Invalid element type - assert_eq!( - parse_array(b"*1\r\n@invalid\r\n").err().unwrap().message(), - "ERR Invalid data type" - ); - } - - #[test] - fn test_array_remaining_bytes() { - // Test with remaining data - let arr = vec![RespType::BulkString("test".into()), RespType::Integer(99)]; - let (value, remaining) = parse_array(b"*2\r\n$4\r\ntest\r\n:99\r\n+OK\r\n").unwrap(); - assert_eq!(value, RespType::Array(arr)); - assert_eq!(remaining, b"+OK\r\n"); - - // Test with no remaining data - let arr = vec![RespType::SimpleString("PONG".to_string())]; - let (value, remaining) = parse_array(b"*1\r\n+PONG\r\n").unwrap(); - assert_eq!(value, RespType::Array(arr)); - assert_eq!(remaining, b""); - - // Test with multiple commands - let arr = vec![RespType::Null()]; - let (value, remaining) = parse_array(b"*1\r\n_\r\n*0\r\n").unwrap(); - assert_eq!(value, RespType::Array(arr)); - assert_eq!(remaining, b"*0\r\n"); - - // Test with empty array and remaining data - let arr = vec![]; - let (value, remaining) = parse_array(b"*0\r\n-ERR test\r\n").unwrap(); - assert_eq!(value, RespType::Array(arr)); - assert_eq!(remaining, b"-ERR test\r\n"); - } - } - - mod boolean_tests { - use super::*; - - #[test] - fn test_valid_booleans() { - // Basic true value - assert_eq!(parse_boolean(b"#t\r\n").unwrap().0, RespType::Boolean(true)); - - // Basic false value - assert_eq!( - parse_boolean(b"#f\r\n").unwrap().0, - RespType::Boolean(false) - ); - } - - #[test] - fn test_invalid_booleans() { - // Wrong data type marker - assert_eq!( - parse_boolean(b":t\r\n").err().unwrap().message(), - "ERR Invalid data type" - ); - - // Invalid boolean value - assert_eq!( - parse_boolean(b"#x\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Missing \r\n terminator - assert_eq!( - parse_boolean(b"#t").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Only \r without \n - assert_eq!( - parse_boolean(b"#t\r").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Empty input - assert_eq!( - parse_boolean(b"").err().unwrap().message(), - "ERR Empty data" - ); - - // Just the marker - assert_eq!( - parse_boolean(b"#").err().unwrap().message(), - "ERR Unexpected end of input" - ); - - // Case sensitivity - assert_eq!( - parse_boolean(b"#T\r\n").err().unwrap().message(), - "ERR invalid value" - ); - - // Extra content - assert_eq!( - parse_boolean(b"#ttrue\r\n").err().unwrap().message(), - "ERR Unexpected end of input" - ); - } - - #[test] - fn test_boolean_remaining_bytes() { - // Test with remaining data - let (value, remaining) = parse_boolean(b"#t\r\n+OK\r\n").unwrap(); - assert_eq!(value, RespType::Boolean(true)); - assert_eq!(remaining, b"+OK\r\n"); - - // Test with no remaining data - let (value, remaining) = parse_boolean(b"#f\r\n").unwrap(); - assert_eq!(value, RespType::Boolean(false)); - assert_eq!(remaining, b""); - - // Test with multiple commands - let (value, remaining) = parse_boolean(b"#t\r\n:42\r\n").unwrap(); - assert_eq!(value, RespType::Boolean(true)); - assert_eq!(remaining, b":42\r\n"); - - // Test with false and remaining data - let (value, remaining) = parse_boolean(b"#f\r\n-ERR test\r\n").unwrap(); - assert_eq!(value, RespType::Boolean(false)); - assert_eq!(remaining, b"-ERR test\r\n"); - } - } -} diff --git a/src/shared_cache.rs b/src/shared_cache.rs new file mode 100644 index 0000000..4d6aec2 --- /dev/null +++ b/src/shared_cache.rs @@ -0,0 +1,27 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub struct CacheEntry { + pub value: String, + pub expires_at: Option, // Unix timestamp in milliseconds +} + +impl CacheEntry { + pub fn is_expired(&self) -> bool { + if let Some(expiry) = self.expires_at { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + now > expiry + } else { + false + } + } +} + +pub type SharedCache = Arc>>; diff --git a/tests/test_commands.rs b/tests/test_commands.rs new file mode 100644 index 0000000..10e6c9c --- /dev/null +++ b/tests/test_commands.rs @@ -0,0 +1,312 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; + +use codecrafters_redis::resp_parser::RespType; +use codecrafters_redis::shared_cache::{CacheEntry, SharedCache}; + +// Test Helpers & Mocks +/// Creates a new, empty shared cache for each test. +fn new_cache() -> SharedCache { + Arc::new(Mutex::new(HashMap::new())) +} + +/// Builds a `RespType::Array` from string slices to simplify parser tests. +fn build_command_from_str_slice(args: &[&str]) -> RespType { + let resp_args = args + .iter() + .map(|s| RespType::BulkString(s.to_string().into_bytes())) + .collect(); + RespType::Array(resp_args) +} + +/// A helper to get a value directly from the cache for assertions. +fn get_from_cache(cache: &SharedCache, key: &str) -> Option { + cache.lock().unwrap().get(key).cloned() +} + +/// Tests for the `RedisCommands::from(RespType)` parser logic. +mod command_parser_tests { + use codecrafters_redis::resp_commands::ExpiryOption; + use codecrafters_redis::resp_commands::RedisCommands; + use codecrafters_redis::resp_commands::SetCondition; + use codecrafters_redis::resp_parser::RespType; + + use super::*; + + #[test] + fn test_parse_ping() { + let cmd = build_command_from_str_slice(&["PING"]); + assert!(matches!(RedisCommands::from(cmd), RedisCommands::PING)); + } + + #[test] + fn test_parse_ping_case_insensitive() { + let cmd = build_command_from_str_slice(&["pInG"]); +