Skip to content

Commit

Permalink
Merge pull request #9 from upupnoah/main
Browse files Browse the repository at this point in the history
feat: support hmget sadd sismember
  • Loading branch information
upupnoah authored Jul 30, 2024
2 parents b0edb0e + ecae833 commit 99fceb0
Show file tree
Hide file tree
Showing 25 changed files with 847 additions and 386 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
.DS_Store
/.vscode
*.dump.rdb
Binary file removed dump.rdb
Binary file not shown.
90 changes: 61 additions & 29 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
use crate::RespFrame;
use dashmap::DashMap;
use dashmap::{DashMap, DashSet};
use std::ops::Deref;
use std::sync::Arc;

// region: --- Enums and Structs
#[derive(Debug, Clone)]
pub struct Backend(Arc<BackendInner>);

#[derive(Debug)]
pub struct BackendInner {
pub(crate) map: DashMap<String, RespFrame>,
pub(crate) hmap: DashMap<String, DashMap<String, RespFrame>>,
}
// endregion: --- Enums and Structs

// region: --- impls
impl Deref for Backend {
type Target = BackendInner;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Default for BackendInner {
fn default() -> Self {
Self {
map: DashMap::new(),
hmap: DashMap::new(),
}
}
}

impl Default for Backend {
fn default() -> Self {
Self(Arc::new(BackendInner::default()))
}
pub(crate) set: DashMap<String, DashSet<String>>, // DashSet 中的元素要求实现 Eq, RespFrame 不能实现 Eq, 因此这里使用 String
}

impl Backend {
Expand Down Expand Up @@ -65,5 +39,63 @@ impl Backend {
pub fn hgetall(&self, key: &str) -> Option<DashMap<String, RespFrame>> {
self.hmap.get(key).map(|v| v.clone())
}

pub fn sadd(&self, key: String, members: impl Into<Vec<String>>) -> i64 {
let set = self.set.entry(key).or_default();
let mut cnt = 0;
for member in members.into() {
if set.insert(member) {
cnt += 1;
}
}
cnt
}

pub fn sismember(&self, key: &str, value: &str) -> bool {
self.set
.get(key)
.and_then(|v| v.get(value).map(|_| true))
.unwrap_or(false)
}
pub fn insert_set(&self, key: String, values: Vec<String>) {
let set = self.set.get_mut(&key);
match set {
Some(set) => {
for value in values {
(*set).insert(value);
}
}
None => {
let new_set = DashSet::new();
for value in values {
new_set.insert(value);
}
self.set.insert(key.to_string(), new_set);
}
}
}
}

impl Deref for Backend {
type Target = BackendInner;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl Default for BackendInner {
fn default() -> Self {
Self {
map: DashMap::new(),
hmap: DashMap::new(),
set: DashMap::new(),
}
}
}

impl Default for Backend {
fn default() -> Self {
Self(Arc::new(BackendInner::default()))
}
}
// endregion: --- impls
75 changes: 75 additions & 0 deletions src/cmd/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use enum_dispatch::enum_dispatch;
use thiserror::Error;

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

use super::{
echo::Echo,
hmap::{HGet, HGetAll, HMGet, HSet},
map::{Get, Set},
set::{SAdd, SIsMember},
unrecognized::Unrecognized,
};

#[enum_dispatch(CommandExecutor)]
#[derive(Debug)]
pub enum Command {
Get(Get),
Set(Set),
HGet(HGet),
HSet(HSet),
HGetAll(HGetAll),
Echo(Echo),
HMGet(HMGet),
SAdd(SAdd),
SIsMember(SIsMember), // S 表示 Set
// unrecognized command
Unrecognized(Unrecognized),
}

#[derive(Error, Debug)]
pub enum CommandError {
#[error("Invalid command: {0}")]
InvalidCommand(String),
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("{0}")]
RespError(#[from] RespError),
#[error("Utf8 error: {0}")]
Utf8Error(#[from] std::string::FromUtf8Error),
}

impl TryFrom<RespArray> for Command {
type Error = CommandError;
fn try_from(v: RespArray) -> Result<Self, Self::Error> {
match v.first() {
Some(RespFrame::BulkString(ref cmd)) => match cmd.as_ref() {
b"get" => Ok(Get::try_from(v)?.into()),
b"set" => Ok(Set::try_from(v)?.into()),
b"hget" => Ok(HGet::try_from(v)?.into()),
b"hset" => Ok(HSet::try_from(v)?.into()),
b"hgetall" => Ok(HGetAll::try_from(v)?.into()),
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 must have a BulkString as the first argument".to_string(),
)),
}
}
}

impl TryFrom<RespFrame> for Command {
type Error = CommandError;
fn try_from(v: RespFrame) -> Result<Self, Self::Error> {
match v {
RespFrame::Array(array) => array.try_into(),
_ => Err(CommandError::InvalidCommand(
"Command must be an Array".to_string(),
)),
}
}
}
76 changes: 76 additions & 0 deletions src/cmd/echo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::{Backend, RespArray, RespFrame};

use super::{extract_args, validate_command, CommandError, CommandExecutor};

// echo: https://redis.io/docs/latest/commands/echo/

#[derive(Debug)]
pub struct Echo {
message: String,
}

impl CommandExecutor for Echo {
fn execute(self, _backend: &Backend) -> RespFrame {
RespFrame::BulkString(self.message.into())
}
}

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

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

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

use super::*;
use anyhow::Result;
use bytes::BytesMut;

#[test]
fn test_echo_from_resp_array() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(b"*2\r\n$4\r\necho\r\n$5\r\nhello\r\n");

let frame = RespArray::decode(&mut buf)?;

let result: Echo = frame.try_into()?;
assert_eq!(result.message, "hello");

Ok(())
}

#[test]
fn test_echo_command() -> Result<()> {
// let backend = Backend::new();
// let cmd = Echo {
// message: "hello world".to_string(),
// };
// let result = cmd.execute(&backend);
// assert_eq!(result, RespFrame::BulkString(b"hello world".into()));

// Ok(())

let command = Echo::try_from(RespArray::new([
BulkString::new("echo").into(),
BulkString::new("hello").into(),
]))?;
assert_eq!(command.message, "hello");

let backend = Backend::new();
let result = command.execute(&backend);
assert_eq!(result, RespFrame::BulkString(b"hello".into()));
Ok(())
}
}
Loading

0 comments on commit 99fceb0

Please sign in to comment.