Skip to content

Commit

Permalink
feat: add hmget/sadd/sismember
Browse files Browse the repository at this point in the history
  • Loading branch information
kindywu committed May 7, 2024
1 parent 66597e3 commit f417643
Show file tree
Hide file tree
Showing 12 changed files with 472 additions and 51 deletions.
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/<executable file>",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
49 changes: 49 additions & 0 deletions src/cmd/echo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::{CommandError, CommandExecutor, RespArray, RespFrame};

use super::{extract_args, validate_command};

#[derive(Debug)]
pub struct Echo {
pub echo: RespFrame,
}

impl CommandExecutor for Echo {
fn execute(self) -> RespFrame {
self.echo
}
}

impl TryFrom<RespArray> for Echo {
type Error = CommandError;
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["echo"], 1, super::ArgsCheckRule::Equal)?;

let mut args = extract_args(value, 1)?.into_iter();
match args.next() {
Some(echo) => Ok(Echo { echo }),
_ => Err(CommandError::InvalidArgument("Invalid echo".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use crate::{BulkString, Command, RespArray, RespFrame};

use super::*;
use anyhow::Result;

#[test]
fn test_echo() -> Result<()> {
let frame: RespFrame = Some(RespArray::new(vec![
Some(BulkString::new("echo".to_string())).into(),
Some(BulkString::new("hello".to_string())).into(),
]))
.into();

let echo = Command::try_from(frame)?;
let ret = echo.execute();
assert_eq!(ret, Some(BulkString::new("hello".to_string())).into());
Ok(())
}
}
87 changes: 87 additions & 0 deletions src/cmd/hmget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::{BulkString, CommandError, CommandExecutor, RespArray, RespFrame, RespNull};

use super::{extract_args, validate_command};

#[derive(Debug)]
pub struct HmGet {
pub key: String,
pub members: Vec<String>,
}

impl CommandExecutor for HmGet {
fn execute(self) -> RespFrame {
println!("{:?}", self);
let mut result = RespArray::new(vec![]);
for member in self.members.iter() {
match member.as_str() {
"field1" => result.push(Some(BulkString::new("field1".to_string())).into()),
"field2" => result.push(Some(BulkString::new("field2".to_string())).into()),
_ => result.push(RespFrame::Null(RespNull {})),
}
}
RespFrame::Array(Some(result))
}
}

impl TryFrom<RespArray> for HmGet {
type Error = CommandError;
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["hmget"], 2, super::ArgsCheckRule::EqualOrGreater)?;

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

let members = args
.filter_map(|arg| {
if let RespFrame::BulkString(Some(key)) = arg {
String::from_utf8(key.0).ok()
} else {
None
}
})
.collect::<Vec<String>>();

if members.is_empty() {
return Err(CommandError::InvalidArgument("Invalid hmget".to_string()));
}

Ok(HmGet { key, members })
}
}

#[cfg(test)]
mod tests {
use crate::{BulkString, Command, RespArray, RespFrame};

use super::*;
use anyhow::Result;

#[test]
fn test_hmget() -> Result<()> {
let frame: RespFrame = Some(RespArray::new(vec![
Some(BulkString::new("hmget".to_string())).into(),
Some(BulkString::new("myhash".to_string())).into(),
Some(BulkString::new("field1".to_string())).into(),
Some(BulkString::new("field2".to_string())).into(),
Some(BulkString::new("nofield".to_string())).into(),
]))
.into();

let sadd = Command::try_from(frame)?;
let ret = sadd.execute();

assert_eq!(
ret,
Some(RespArray::new(vec![
Some(BulkString::new("field1".to_string())).into(),
Some(BulkString::new("field2".to_string())).into(),
RespNull::new().into(),
]))
.into()
);
Ok(())
}
}
76 changes: 40 additions & 36 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
mod echo;
mod hmget;
mod sadd;
mod sismember;

mod unrecognized;

use enum_dispatch::enum_dispatch;
// you could also use once_cell instead of lazy_static
use lazy_static::lazy_static;
use thiserror::Error;

use crate::{RespArray, RespError, RespFrame, SimpleString};

use self::{
echo::Echo, hmget::HmGet, sadd::SAdd, sismember::SisMember, unrecognized::Unrecognized,
};

lazy_static! {
static ref RESP_OK: RespFrame = SimpleString::new("OK").into();
}
Expand All @@ -30,29 +41,13 @@ pub trait CommandExecutor {
#[derive(Debug)]
pub enum Command {
Echo(Echo),

SAdd(SAdd),
SisMember(SisMember),
HmGet(HmGet),
// unrecognized command
Unrecognized(Unrecognized),
}

#[derive(Debug)]
pub struct Echo {
echo: RespFrame,
}
impl CommandExecutor for Echo {
fn execute(self) -> RespFrame {
self.echo
}
}

#[derive(Debug)]
pub struct Unrecognized;
impl CommandExecutor for Unrecognized {
fn execute(self) -> RespFrame {
RESP_OK.clone()
}
}

impl TryFrom<RespFrame> for Command {
type Error = CommandError;
fn try_from(v: RespFrame) -> Result<Self, Self::Error> {
Expand All @@ -75,6 +70,9 @@ impl TryFrom<RespArray> for Command {
Some(RespFrame::BulkString(ref cmd)) => match cmd {
Some(cmd) => match cmd.as_ref() {
b"echo" => Ok(Echo::try_from(v)?.into()),
b"hmget" => Ok(HmGet::try_from(v)?.into()),
b"sadd" => Ok(SAdd::try_from(v)?.into()),
b"sismember" => Ok(SisMember::try_from(v)?.into()),
_ => Ok(Unrecognized.into()),
},
_ => Err(CommandError::InvalidCommand("Command is null".to_string())),
Expand All @@ -86,30 +84,36 @@ impl TryFrom<RespArray> for Command {
}
}

impl TryFrom<RespArray> for Echo {
type Error = CommandError;
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["echo"], 1)?;

let mut args = extract_args(value, 1)?.into_iter();
match args.next() {
Some(echo) => Ok(Echo { echo }),
_ => Err(CommandError::InvalidArgument("Invalid echo".to_string())),
}
}
enum ArgsCheckRule {
Equal,
EqualOrGreater,
}

fn validate_command(
value: &RespArray,
names: &[&'static str],
n_args: usize,
rule: ArgsCheckRule,
) -> Result<(), CommandError> {
if value.len() != n_args + names.len() {
return Err(CommandError::InvalidArgument(format!(
"{} command must have exactly {} argument",
names.join(" "),
n_args
)));
match rule {
ArgsCheckRule::Equal => {
if value.len() != n_args + names.len() {
return Err(CommandError::InvalidArgument(format!(
"{} command must have exactly {} argument",
names.join(" "),
n_args
)));
}
}
ArgsCheckRule::EqualOrGreater => {
if value.len() < n_args + names.len() {
return Err(CommandError::InvalidArgument(format!(
"{} command must have minimum required {} argument",
names.join(" "),
n_args
)));
}
}
}

for (i, name) in names.iter().enumerate() {
Expand Down
62 changes: 62 additions & 0 deletions src/cmd/sadd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::{CommandError, CommandExecutor, RespArray, RespFrame};

use super::{extract_args, validate_command};

#[derive(Debug)]
pub struct SAdd {
pub key: String,
pub members: RespArray,
}

impl CommandExecutor for SAdd {
fn execute(self) -> RespFrame {
println!("{:?}", self);
let len = self.members.len() as i64;
len.into()
}
}

impl TryFrom<RespArray> for SAdd {
type Error = CommandError;
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(&value, &["sadd"], 2, super::ArgsCheckRule::EqualOrGreater)?;

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

let mut members = RespArray::new(vec![]);
for arg in args {
members.push(arg)
}

Ok(SAdd { key, members })
}
}

#[cfg(test)]
mod tests {
use crate::{BulkString, Command, RespArray, RespFrame};

use super::*;
use anyhow::Result;

#[test]
fn test_sadd() -> Result<()> {
let frame: RespFrame = Some(RespArray::new(vec![
Some(BulkString::new("sadd".to_string())).into(),
Some(BulkString::new("myset".to_string())).into(),
Some(BulkString::new("A".to_string())).into(),
Some(BulkString::new("B".to_string())).into(),
Some(BulkString::new("C".to_string())).into(),
]))
.into();

let sadd = Command::try_from(frame)?;
let ret = sadd.execute();
assert_eq!(ret, 3.into());
Ok(())
}
}
60 changes: 60 additions & 0 deletions src/cmd/sismember.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::{CommandError, CommandExecutor, RespArray, RespFrame};

use super::{extract_args, validate_command};

#[derive(Debug)]
pub struct SisMember {
pub key: String,
pub member: RespFrame,
}

impl CommandExecutor for SisMember {
fn execute(self) -> RespFrame {
println!("{:?}", self);
1.into()
}
}

impl TryFrom<RespArray> for SisMember {
type Error = CommandError;
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
validate_command(
&value,
&["sismember"],
2,
super::ArgsCheckRule::EqualOrGreater,
)?;

let mut args = extract_args(value, 1)?.into_iter();
match (args.next(), args.next()) {
(Some(RespFrame::BulkString(Some(key))), Some(member)) => Ok(SisMember {
key: String::from_utf8(key.0)?,
member,
}),
_ => Err(CommandError::InvalidArgument("Invalid echo".to_string())),
}
}
}

#[cfg(test)]
mod tests {
use crate::{BulkString, Command, RespArray, RespFrame};

use super::*;
use anyhow::Result;

#[test]
fn test_sismember() -> Result<()> {
let frame: RespFrame = Some(RespArray::new(vec![
Some(BulkString::new("sismember".to_string())).into(),
Some(BulkString::new("myset".to_string())).into(),
Some(BulkString::new("A".to_string())).into(),
]))
.into();

let sis_member = Command::try_from(frame)?;
let ret = sis_member.execute();
assert_eq!(ret, 1.into());
Ok(())
}
}
Loading

0 comments on commit f417643

Please sign in to comment.