aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib.rs5
-rw-r--r--src/macros.rs32
-rw-r--r--src/main.rs32
-rw-r--r--src/resp_commands.rs315
-rw-r--r--src/resp_parser.rs1123
-rw-r--r--src/shared_cache.rs27
-rw-r--r--tests/test_commands.rs312
-rw-r--r--tests/test_parse_array.rs130
-rw-r--r--tests/test_parse_boolean.rs87
-rw-r--r--tests/test_parse_bulk_string.rs214
-rw-r--r--tests/test_parse_double.rs1
-rw-r--r--tests/test_parse_integer.rs165
-rw-r--r--tests/test_parse_map.rs452
-rw-r--r--tests/test_parse_simple_error.rs145
-rw-r--r--tests/test_parse_simple_string.rs185
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!(
- 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)
-