Skip to content

Commit

Permalink
feature: support cmd decode
Browse files Browse the repository at this point in the history
  • Loading branch information
luffy2025 committed Nov 30, 2024
1 parent 8fdfda6 commit 9237346
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 20 deletions.
47 changes: 47 additions & 0 deletions src/cmd/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
use crate::{RespArray, RespFrame};

#[allow(dead_code)]
pub struct Get {
key: String,
}

impl Executor for Get {
fn execute(&self) -> Result<RespFrame, CommandError> {
todo!()
}
}

// get hello
// *2\r\n$3\r\nget\r\n$5\r\nhello\r\n
impl TryFrom<RespArray> for Get {
type Error = CommandError;

fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["get"], 1)?;

let mut args = extract_args(value, 1)?.into_iter();
match args.next() {
Some(RespFrame::BulkString(key)) => Ok(Get {
key: String::from_utf8(key.to_vec())?,
}),
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::RespDecode;
use anyhow::Result;

#[test]
fn test_get_command() -> Result<()> {
let mut cmd = bytes::BytesMut::from(&b"*2\r\n$3\r\nget\r\n$5\r\nhello\r\n"[..]);
let cmd = RespArray::decode(&mut cmd)?;
let get = Get::try_from(cmd)?;
assert_eq!(get.key, "hello");
Ok(())
}
}
51 changes: 51 additions & 0 deletions src/cmd/hget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
use crate::{RespArray, RespFrame};

#[allow(dead_code)]
pub struct HGet {
key: String,
field: String,
}

impl Executor for HGet {
fn execute(&self) -> Result<RespFrame, CommandError> {
todo!()
}
}

// hget map hello
// *3\r\n$4\r\nhget\r\n$3\r\nmap\r\n$5\r\nhello\r\n
impl TryFrom<RespArray> for HGet {
type Error = CommandError;

fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["hget"], 2)?;

let mut args = extract_args(value, 1)?.into_iter();
match (args.next(), args.next()) {
(Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field))) => Ok(HGet {
key: String::from_utf8(key.to_vec())?,
field: String::from_utf8(field.to_vec())?,
}),
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::RespDecode;
use anyhow::Result;

#[test]
fn test_hget_command() -> Result<()> {
let mut cmd =
bytes::BytesMut::from(&b"*3\r\n$4\r\nhget\r\n$3\r\nmap\r\n$5\r\nhello\r\n"[..]);
let cmd = RespArray::decode(&mut cmd)?;
let get = HGet::try_from(cmd)?;
assert_eq!(get.key, "map");
assert_eq!(get.field, "hello");
Ok(())
}
}
68 changes: 68 additions & 0 deletions src/cmd/hset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
use crate::{RespArray, RespFrame};

#[allow(dead_code)]
pub struct HSet {
key: String,
field: String,
value: RespFrame,
}

impl Executor for HSet {
fn execute(&self) -> Result<RespFrame, CommandError> {
todo!()
}
}

// hset map hello world
// *4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n
impl TryFrom<RespArray> for HSet {
type Error = CommandError;

fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["hset"], 3)?;

let mut args = extract_args(value, 1)?.into_iter();
match (args.next(), args.next(), args.next()) {
(Some(RespFrame::BulkString(key)), Some(RespFrame::BulkString(field)), Some(value)) => {
Ok(HSet {
key: String::from_utf8(key.to_vec())?,
field: String::from_utf8(field.to_vec())?,
value,
})
}

_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{BulkString, RespDecode};
use anyhow::Result;

#[test]
fn test_hset_command() -> Result<()> {
let mut cmd = bytes::BytesMut::from(
&b"*4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..],
);
let cmd = RespArray::decode(&mut cmd)?;
let set = HSet::try_from(cmd)?;
assert_eq!(set.key, "map");
assert_eq!(set.field, "hello");
assert_eq!(set.value, BulkString::new(b"world".into()).into());
Ok(())
}

#[test]
fn test_hset_command_args_not_enough() -> Result<()> {
let mut cmd =
bytes::BytesMut::from(&b"*3\r\n$4\r\nhset\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..]);
let cmd = RespArray::decode(&mut cmd)?;
let set = HSet::try_from(cmd);
assert!(set.is_err());
Ok(())
}
}
122 changes: 122 additions & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::cmd::get::Get;
use crate::cmd::hget::HGet;
use crate::cmd::hset::HSet;
use crate::cmd::set::Set;
use crate::{RespArray, RespFrame};
use thiserror::Error;

mod get;
mod hget;
mod hset;
mod set;

pub enum Command {
Get(Get),
Set(Set),
HGet(HGet),
HSet(HSet),
// HSetAll(hset_all::HSetAll),
}

#[derive(Error, Debug, PartialEq, Eq)]
pub enum CommandError {
#[error("Invalid frame: {0}")]
InvalidFrame(String),
#[error("Invalid frame type: {0}")]
InvalidCmd(String),
#[error("Invalid frame args: {0}")]
InvalidArgs(String),
#[error("From utf8 error: {0}")]
FromUtf8Error(#[from] std::string::FromUtf8Error),
}

pub trait Executor {
fn execute(&self) -> Result<RespFrame, CommandError>;
}

impl TryFrom<RespFrame> for Command {
type Error = CommandError;

fn try_from(value: RespFrame) -> Result<Self, Self::Error> {
let frame = match value {
RespFrame::Array(array) => array,
_ => return Err(CommandError::InvalidFrame("Invalid frame type".to_string())),
};

match frame.first() {
Some(RespFrame::BulkString(cmd)) => match cmd.as_ref() {
b"get" => Ok(Command::Get(Get::try_from(frame)?)),
b"set" => Ok(Command::Set(Set::try_from(frame)?)),
b"hget" => Ok(Command::HGet(HGet::try_from(frame)?)),
b"hset" => Ok(Command::HSet(HSet::try_from(frame)?)),
// b"hset_all" => Ok(Command::HSetAll(hset_all::HSetAll::new(cmd))),
_ => Err(CommandError::InvalidCmd("Invalid command".to_string())),
},
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
}
}
}

pub(crate) fn validate_command(
cmd: &RespArray,
names: &[&'static str],
args_len: usize,
) -> Result<(), CommandError> {
if cmd.len() < names.len() + args_len {
return Err(CommandError::InvalidArgs("Invalid arguments".to_string()));
}
validate_command_names(names, cmd)?;
Ok(())
}

fn validate_command_names(names: &[&'static str], cmds: &RespArray) -> Result<(), CommandError> {
for (name, arg) in names.iter().zip(cmds.iter()) {
match arg {
RespFrame::BulkString(cmd) => {
if cmd.to_ascii_lowercase() != name.as_bytes() {
return Err(CommandError::InvalidCmd(format!(
"Invalid command: expected {}, got {}",
name,
String::from_utf8_lossy(cmd)
)));
}
}
_ => {
return Err(CommandError::InvalidArgs(format!(
"Invalid arguments: expected {}, got {:?}",
name, arg
)))
}
}
}
Ok(())
}

pub(crate) fn extract_args(value: RespArray, start: usize) -> Result<Vec<RespFrame>, CommandError> {
Ok(value
.data
.into_iter()
.skip(start)
.collect::<Vec<RespFrame>>())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{BulkString, RespDecode};
use anyhow::Result;

#[test]
fn test_extract_args() -> Result<()> {
let mut cmd = bytes::BytesMut::from(
&b"*4\r\n$4\r\nhset\r\n$3\r\nmap\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..],
);
let cmd = RespArray::decode(&mut cmd)?;
let args = extract_args(cmd, 1)?;
assert_eq!(args.len(), 3);
assert_eq!(args[0], BulkString::new(b"map".to_vec()).into());
assert_eq!(args[1], BulkString::new(b"hello".to_vec()).into());
assert_eq!(args[2], BulkString::new(b"world".to_vec()).into());
Ok(())
}
}
62 changes: 62 additions & 0 deletions src/cmd/set.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::cmd::{extract_args, validate_command, CommandError, Executor};
use crate::{RespArray, RespFrame};

#[allow(dead_code)]
pub struct Set {
key: String,
value: RespFrame,
}

impl Executor for Set {
fn execute(&self) -> Result<RespFrame, CommandError> {
todo!()
}
}

// set hello world
// *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
impl TryFrom<RespArray> for Set {
type Error = CommandError;

fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["set"], 2)?;

let mut args = extract_args(value, 1)?.into_iter();
match (args.next(), args.next()) {
(Some(RespFrame::BulkString(key)), Some(value)) => {
let key = String::from_utf8(key.to_vec())?;
println!("{:?}", key);
println!("{:?}", value);
Ok(Set { key, value })
}
_ => Err(CommandError::InvalidArgs("Invalid arguments".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{BulkString, RespDecode};
use anyhow::Result;

#[test]
fn test_set_command() -> Result<()> {
let mut cmd =
bytes::BytesMut::from(&b"*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n"[..]);
let cmd = RespArray::decode(&mut cmd)?;
let set = Set::try_from(cmd)?;
assert_eq!(set.key, "hello");
assert_eq!(set.value, BulkString::new(b"world".into()).into());
Ok(())
}

#[test]
fn test_set_command_args_not_enough() -> Result<()> {
let mut cmd = bytes::BytesMut::from(&b"*2\r\n$3\r\nset\r\n$5\r\nhello\r\n"[..]);
let cmd = RespArray::decode(&mut cmd)?;
let set = Set::try_from(cmd);
assert!(set.is_err());
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod resp;

pub mod cmd;

pub use resp::*;
Loading

0 comments on commit 9237346

Please sign in to comment.