Skip to content

Commit 9237346

Browse files
committed
feature: support cmd decode
1 parent 8fdfda6 commit 9237346

File tree

15 files changed

+387
-20
lines changed

15 files changed

+387
-20
lines changed

src/cmd/get.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
2+
use crate::{RespArray, RespFrame};
3+
4+
#[allow(dead_code)]
5+
pub struct Get {
6+
key: String,
7+
}
8+
9+
impl Executor for Get {
10+
fn execute(&self) -> Result<RespFrame, CommandError> {
11+
todo!()
12+
}
13+
}
14+
15+
// get hello
16+
// *2\r\n$3\r\nget\r\n$5\r\nhello\r\n
17+
impl TryFrom<RespArray> for Get {
18+
type Error = CommandError;
19+
20+
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
21+
validate_command(&value, &["get"], 1)?;
22+
23+
let mut args = extract_args(value, 1)?.into_iter();
24+
match args.next() {
25+
Some(RespFrame::BulkString(key)) => Ok(Get {
26+
key: String::from_utf8(key.to_vec())?,
27+
}),
28+
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
29+
}
30+
}
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use super::*;
36+
use crate::RespDecode;
37+
use anyhow::Result;
38+
39+
#[test]
40+
fn test_get_command() -> Result<()> {
41+
let mut cmd = bytes::BytesMut::from(&b"*2\r\n$3\r\nget\r\n$5\r\nhello\r\n"[..]);
42+
let cmd = RespArray::decode(&mut cmd)?;
43+
let get = Get::try_from(cmd)?;
44+
assert_eq!(get.key, "hello");
45+
Ok(())
46+
}
47+
}

src/cmd/hget.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
2+
use crate::{RespArray, RespFrame};
3+
4+
#[allow(dead_code)]
5+
pub struct HGet {
6+
key: String,
7+
field: String,
8+
}
9+
10+
impl Executor for HGet {
11+
fn execute(&self) -> Result<RespFrame, CommandError> {
12+
todo!()
13+
}
14+
}
15+
16+
// hget map hello
17+
// *3\r\n$4\r\nhget\r\n$3\r\nmap\r\n$5\r\nhello\r\n
18+
impl TryFrom<RespArray> for HGet {
19+
type Error = CommandError;
20+
21+
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
22+
validate_command(&value, &["hget"], 2)?;
23+
24+
let mut args = extract_args(value, 1)?.into_iter();
25+
match (args.next(), args.next()) {
26+
(Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field))) => Ok(HGet {
27+
key: String::from_utf8(key.to_vec())?,
28+
field: String::from_utf8(field.to_vec())?,
29+
}),
30+
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
31+
}
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use super::*;
38+
use crate::RespDecode;
39+
use anyhow::Result;
40+
41+
#[test]
42+
fn test_hget_command() -> Result<()> {
43+
let mut cmd =
44+
bytes::BytesMut::from(&b"*3\r\n$4\r\nhget\r\n$3\r\nmap\r\n$5\r\nhello\r\n"[..]);
45+
let cmd = RespArray::decode(&mut cmd)?;
46+
let get = HGet::try_from(cmd)?;
47+
assert_eq!(get.key, "map");
48+
assert_eq!(get.field, "hello");
49+
Ok(())
50+
}
51+
}

src/cmd/hset.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
2+
use crate::{RespArray, RespFrame};
3+
4+
#[allow(dead_code)]
5+
pub struct HSet {
6+
key: String,
7+
field: String,
8+
value: RespFrame,
9+
}
10+
11+
impl Executor for HSet {
12+
fn execute(&self) -> Result<RespFrame, CommandError> {
13+
todo!()
14+
}
15+
}
16+
17+
// hset map hello world
18+
// *4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n
19+
impl TryFrom<RespArray> for HSet {
20+
type Error = CommandError;
21+
22+
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
23+
validate_command(&value, &["hset"], 3)?;
24+
25+
let mut args = extract_args(value, 1)?.into_iter();
26+
match (args.next(), args.next(), args.next()) {
27+
(Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field)), Some(value)) => {
28+
Ok(HSet {
29+
key: String::from_utf8(key.to_vec())?,
30+
field: String::from_utf8(field.to_vec())?,
31+
value,
32+
})
33+
}
34+
35+
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
36+
}
37+
}
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
use crate::{BulkString, RespDecode};
44+
use anyhow::Result;
45+
46+
#[test]
47+
fn test_hset_command() -> Result<()> {
48+
let mut cmd = bytes::BytesMut::from(
49+
&b"*4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..],
50+
);
51+
let cmd = RespArray::decode(&mut cmd)?;
52+
let set = HSet::try_from(cmd)?;
53+
assert_eq!(set.key, "map");
54+
assert_eq!(set.field, "hello");
55+
assert_eq!(set.value, BulkString::new(b"world".into()).into());
56+
Ok(())
57+
}
58+
59+
#[test]
60+
fn test_hset_command_args_not_enough() -> Result<()> {
61+
let mut cmd =
62+
bytes::BytesMut::from(&b"*3\r\n$4\r\nhset\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..]);
63+
let cmd = RespArray::decode(&mut cmd)?;
64+
let set = HSet::try_from(cmd);
65+
assert!(set.is_err());
66+
Ok(())
67+
}
68+
}

src/cmd/mod.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use crate::cmd::get::Get;
2+
use crate::cmd::hget::HGet;
3+
use crate::cmd::hset::HSet;
4+
use crate::cmd::set::Set;
5+
use crate::{RespArray, RespFrame};
6+
use thiserror::Error;
7+
8+
mod get;
9+
mod hget;
10+
mod hset;
11+
mod set;
12+
13+
pub enum Command {
14+
Get(Get),
15+
Set(Set),
16+
HGet(HGet),
17+
HSet(HSet),
18+
// HSetAll(hset_all::HSetAll),
19+
}
20+
21+
#[derive(Error, Debug, PartialEq, Eq)]
22+
pub enum CommandError {
23+
#[error("Invalid frame: {0}")]
24+
InvalidFrame(String),
25+
#[error("Invalid frame type: {0}")]
26+
InvalidCmd(String),
27+
#[error("Invalid frame args: {0}")]
28+
InvalidArgs(String),
29+
#[error("From utf8 error: {0}")]
30+
FromUtf8Error(#[from] std::string::FromUtf8Error),
31+
}
32+
33+
pub trait Executor {
34+
fn execute(&self) -> Result<RespFrame, CommandError>;
35+
}
36+
37+
impl TryFrom<RespFrame> for Command {
38+
type Error = CommandError;
39+
40+
fn try_from(value: RespFrame) -> Result<Self, Self::Error> {
41+
let frame = match value {
42+
RespFrame::Array(array) => array,
43+
_ => return Err(CommandError::InvalidFrame("Invalid frame type".to_string())),
44+
};
45+
46+
match frame.first() {
47+
Some(RespFrame::BulkString(cmd)) => match cmd.as_ref() {
48+
b"get" => Ok(Command::Get(Get::try_from(frame)?)),
49+
b"set" => Ok(Command::Set(Set::try_from(frame)?)),
50+
b"hget" => Ok(Command::HGet(HGet::try_from(frame)?)),
51+
b"hset" => Ok(Command::HSet(HSet::try_from(frame)?)),
52+
// b"hset_all" => Ok(Command::HSetAll(hset_all::HSetAll::new(cmd))),
53+
_ => Err(CommandError::InvalidCmd("Invalid command".to_string())),
54+
},
55+
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
56+
}
57+
}
58+
}
59+
60+
pub(crate) fn validate_command(
61+
cmd: &RespArray,
62+
names: &[&'static str],
63+
args_len: usize,
64+
) -> Result<(), CommandError> {
65+
if cmd.len() < names.len() + args_len {
66+
return Err(CommandError::InvalidArgs("Invalid arguments".to_string()));
67+
}
68+
validate_command_names(names, cmd)?;
69+
Ok(())
70+
}
71+
72+
fn validate_command_names(names: &[&'static str], cmds: &RespArray) -> Result<(), CommandError> {
73+
for (name, arg) in names.iter().zip(cmds.iter()) {
74+
match arg {
75+
RespFrame::BulkString(cmd) => {
76+
if cmd.to_ascii_lowercase() != name.as_bytes() {
77+
return Err(CommandError::InvalidCmd(format!(
78+
"Invalid command: expected {}, got {}",
79+
name,
80+
String::from_utf8_lossy(cmd)
81+
)));
82+
}
83+
}
84+
_ => {
85+
return Err(CommandError::InvalidArgs(format!(
86+
"Invalid arguments: expected {}, got {:?}",
87+
name, arg
88+
)))
89+
}
90+
}
91+
}
92+
Ok(())
93+
}
94+
95+
pub(crate) fn extract_args(value: RespArray, start: usize) -> Result<Vec<RespFrame>, CommandError> {
96+
Ok(value
97+
.data
98+
.into_iter()
99+
.skip(start)
100+
.collect::<Vec<RespFrame>>())
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
use crate::{BulkString, RespDecode};
107+
use anyhow::Result;
108+
109+
#[test]
110+
fn test_extract_args() -> Result<()> {
111+
let mut cmd = bytes::BytesMut::from(
112+
&b"*4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..],
113+
);
114+
let cmd = RespArray::decode(&mut cmd)?;
115+
let args = extract_args(cmd, 1)?;
116+
assert_eq!(args.len(), 3);
117+
assert_eq!(args[0], BulkString::new(b"map".to_vec()).into());
118+
assert_eq!(args[1], BulkString::new(b"hello".to_vec()).into());
119+
assert_eq!(args[2], BulkString::new(b"world".to_vec()).into());
120+
Ok(())
121+
}
122+
}

src/cmd/set.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
2+
use crate::{RespArray, RespFrame};
3+
4+
#[allow(dead_code)]
5+
pub struct Set {
6+
key: String,
7+
value: RespFrame,
8+
}
9+
10+
impl Executor for Set {
11+
fn execute(&self) -> Result<RespFrame, CommandError> {
12+
todo!()
13+
}
14+
}
15+
16+
// set hello world
17+
// *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
18+
impl TryFrom<RespArray> for Set {
19+
type Error = CommandError;
20+
21+
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
22+
validate_command(&value, &["set"], 2)?;
23+
24+
let mut args = extract_args(value, 1)?.into_iter();
25+
match (args.next(), args.next()) {
26+
(Some(RespFrame::BulkString(key)), Some(value)) => {
27+
let key = String::from_utf8(key.to_vec())?;
28+
println!("{:?}", key);
29+
println!("{:?}", value);
30+
Ok(Set { key, value })
31+
}
32+
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
33+
}
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use super::*;
40+
use crate::{BulkString, RespDecode};
41+
use anyhow::Result;
42+
43+
#[test]
44+
fn test_set_command() -> Result<()> {
45+
let mut cmd =
46+
bytes::BytesMut::from(&b"*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..]);
47+
let cmd = RespArray::decode(&mut cmd)?;
48+
let set = Set::try_from(cmd)?;
49+
assert_eq!(set.key, "hello");
50+
assert_eq!(set.value, BulkString::new(b"world".into()).into());
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn test_set_command_args_not_enough() -> Result<()> {
56+
let mut cmd = bytes::BytesMut::from(&b"*2\r\n$3\r\nset\r\n$5\r\nhello\r\n"[..]);
57+
let cmd = RespArray::decode(&mut cmd)?;
58+
let set = Set::try_from(cmd);
59+
assert!(set.is_err());
60+
Ok(())
61+
}
62+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
mod resp;
22

3+
pub mod cmd;
4+
35
pub use resp::*;

0 commit comments

Comments
 (0)