diff options
| author | omagdy <omar.professional8777@gmail.com> | 2025-07-17 08:06:26 +0300 |
|---|---|---|
| committer | omagdy <omar.professional8777@gmail.com> | 2025-07-17 08:06:26 +0300 |
| commit | 38b649ea16d8ed053fd9222bfb9867e3432ee2a6 (patch) | |
| tree | a7fbde68ad869e1b74071207bdf7b7c159c7f75f | |
| parent | c880c7ad3eba9546ce95bc268218c66a128d319f (diff) | |
| download | redis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.tar.xz redis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.zip | |
test: Moved tests to seprate files under tests folder for more structure
| -rw-r--r-- | src/lib.rs | 5 | ||||
| -rw-r--r-- | src/macros.rs | 32 | ||||
| -rw-r--r-- | src/main.rs | 32 | ||||
| -rw-r--r-- | src/resp_commands.rs | 315 | ||||
| -rw-r--r-- | src/resp_parser.rs | 1123 | ||||
| -rw-r--r-- | src/shared_cache.rs | 27 | ||||
| -rw-r--r-- | tests/test_commands.rs | 312 | ||||
| -rw-r--r-- | tests/test_parse_array.rs | 130 | ||||
| -rw-r--r-- | tests/test_parse_boolean.rs | 87 | ||||
| -rw-r--r-- | tests/test_parse_bulk_string.rs | 214 | ||||
| -rw-r--r-- | tests/test_parse_double.rs | 1 | ||||
| -rw-r--r-- | tests/test_parse_integer.rs | 165 | ||||
| -rw-r--r-- | tests/test_parse_map.rs | 452 | ||||
| -rw-r--r-- | tests/test_parse_simple_error.rs | 145 | ||||
| -rw-r--r-- | tests/test_parse_simple_string.rs | 185 |
15 files changed, 1888 insertions, 1337 deletions
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<u64>, // 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<Mutex<HashMap<String, CacheEntry>>>; +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<SetCondition>, - expiry: Option<ExpiryOption>, - get_old_value: bool, + pub key: String, + pub value: String, + pub condition: Option<SetCondition>, + pub expiry: Option<ExpiryOption>, + pub get_old_value: bool, } impl SetCommand { @@ -353,303 +353,4 @@ impl From<RespType> 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<CacheEntry> { - 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<u8> { - 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 <digit>\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::<u64>() + .map_err(|_| RespError::InvalidValue)?; + + let mut map: HashMap<String, RespType> = HashMap::new(); + + let mut key_set: HashSet<String> = 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<RespType>, &[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<u8>), // $ - Array(Vec<RespType>), // * - Null(), // _ - Boolean(bool), // # - Doubles(f64), // , - BigNumbers(String), // ( - BulkErrors(Vec<RespType>), // ! - VerbatimStrings(Vec<RespType>), // = - Maps(HashMap<RespType, RespType>), // % - Attributes(Vec<RespType>), // | - Sets(HashSet<RespType>), // ~ - Pushes(Vec<RespType>), // > + SimpleString(String), // + + SimpleError(String), // - + Integer(u64), // : + BulkString(Vec<u8>), // $ + Array(Vec<RespType>), // * + Null(), // _ + Boolean(bool), // # + Doubles(f64), // , + BigNumbers(String), // ( + BulkErrors(Vec<RespType>), // ! + VerbatimStrings(Vec<RespType>), // = + Maps(HashMap<String, RespType>), // % + Attributes(Vec<RespType>), // | + Sets(HashSet<String>), // ~ + Pushes(Vec<RespType>), // > } 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::<Vec<Vec<u8>>>(); + // 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<String> = 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<f64> 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!( - |
