Skip to content

Commit ecae833

Browse files
committed
feat: support hmget sadd sismember
1 parent a4a9134 commit ecae833

File tree

25 files changed

+847
-386
lines changed

25 files changed

+847
-386
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/target
22
.DS_Store
33
/.vscode
4+
*.dump.rdb

dump.rdb

-134 Bytes
Binary file not shown.

src/backend/mod.rs

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,15 @@
11
use crate::RespFrame;
2-
use dashmap::DashMap;
2+
use dashmap::{DashMap, DashSet};
33
use std::ops::Deref;
44
use std::sync::Arc;
5-
6-
// region: --- Enums and Structs
75
#[derive(Debug, Clone)]
86
pub struct Backend(Arc<BackendInner>);
97

108
#[derive(Debug)]
119
pub struct BackendInner {
1210
pub(crate) map: DashMap<String, RespFrame>,
1311
pub(crate) hmap: DashMap<String, DashMap<String, RespFrame>>,
14-
}
15-
// endregion: --- Enums and Structs
16-
17-
// region: --- impls
18-
impl Deref for Backend {
19-
type Target = BackendInner;
20-
21-
fn deref(&self) -> &Self::Target {
22-
&self.0
23-
}
24-
}
25-
26-
impl Default for BackendInner {
27-
fn default() -> Self {
28-
Self {
29-
map: DashMap::new(),
30-
hmap: DashMap::new(),
31-
}
32-
}
33-
}
34-
35-
impl Default for Backend {
36-
fn default() -> Self {
37-
Self(Arc::new(BackendInner::default()))
38-
}
12+
pub(crate) set: DashMap<String, DashSet<String>>, // DashSet 中的元素要求实现 Eq, RespFrame 不能实现 Eq, 因此这里使用 String
3913
}
4014

4115
impl Backend {
@@ -65,5 +39,63 @@ impl Backend {
6539
pub fn hgetall(&self, key: &str) -> Option<DashMap<String, RespFrame>> {
6640
self.hmap.get(key).map(|v| v.clone())
6741
}
42+
43+
pub fn sadd(&self, key: String, members: impl Into<Vec<String>>) -> i64 {
44+
let set = self.set.entry(key).or_default();
45+
let mut cnt = 0;
46+
for member in members.into() {
47+
if set.insert(member) {
48+
cnt += 1;
49+
}
50+
}
51+
cnt
52+
}
53+
54+
pub fn sismember(&self, key: &str, value: &str) -> bool {
55+
self.set
56+
.get(key)
57+
.and_then(|v| v.get(value).map(|_| true))
58+
.unwrap_or(false)
59+
}
60+
pub fn insert_set(&self, key: String, values: Vec<String>) {
61+
let set = self.set.get_mut(&key);
62+
match set {
63+
Some(set) => {
64+
for value in values {
65+
(*set).insert(value);
66+
}
67+
}
68+
None => {
69+
let new_set = DashSet::new();
70+
for value in values {
71+
new_set.insert(value);
72+
}
73+
self.set.insert(key.to_string(), new_set);
74+
}
75+
}
76+
}
77+
}
78+
79+
impl Deref for Backend {
80+
type Target = BackendInner;
81+
82+
fn deref(&self) -> &Self::Target {
83+
&self.0
84+
}
85+
}
86+
87+
impl Default for BackendInner {
88+
fn default() -> Self {
89+
Self {
90+
map: DashMap::new(),
91+
hmap: DashMap::new(),
92+
set: DashMap::new(),
93+
}
94+
}
95+
}
96+
97+
impl Default for Backend {
98+
fn default() -> Self {
99+
Self(Arc::new(BackendInner::default()))
100+
}
68101
}
69-
// endregion: --- impls

src/cmd/command.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use enum_dispatch::enum_dispatch;
2+
use thiserror::Error;
3+
4+
use crate::{RespArray, RespError, RespFrame};
5+
6+
use super::{
7+
echo::Echo,
8+
hmap::{HGet, HGetAll, HMGet, HSet},
9+
map::{Get, Set},
10+
set::{SAdd, SIsMember},
11+
unrecognized::Unrecognized,
12+
};
13+
14+
#[enum_dispatch(CommandExecutor)]
15+
#[derive(Debug)]
16+
pub enum Command {
17+
Get(Get),
18+
Set(Set),
19+
HGet(HGet),
20+
HSet(HSet),
21+
HGetAll(HGetAll),
22+
Echo(Echo),
23+
HMGet(HMGet),
24+
SAdd(SAdd),
25+
SIsMember(SIsMember), // S 表示 Set
26+
// unrecognized command
27+
Unrecognized(Unrecognized),
28+
}
29+
30+
#[derive(Error, Debug)]
31+
pub enum CommandError {
32+
#[error("Invalid command: {0}")]
33+
InvalidCommand(String),
34+
#[error("Invalid argument: {0}")]
35+
InvalidArgument(String),
36+
#[error("{0}")]
37+
RespError(#[from] RespError),
38+
#[error("Utf8 error: {0}")]
39+
Utf8Error(#[from] std::string::FromUtf8Error),
40+
}
41+
42+
impl TryFrom<RespArray> for Command {
43+
type Error = CommandError;
44+
fn try_from(v: RespArray) -> Result<Self, Self::Error> {
45+
match v.first() {
46+
Some(RespFrame::BulkString(ref cmd)) => match cmd.as_ref() {
47+
b"get" => Ok(Get::try_from(v)?.into()),
48+
b"set" => Ok(Set::try_from(v)?.into()),
49+
b"hget" => Ok(HGet::try_from(v)?.into()),
50+
b"hset" => Ok(HSet::try_from(v)?.into()),
51+
b"hgetall" => Ok(HGetAll::try_from(v)?.into()),
52+
b"echo" => Ok(Echo::try_from(v)?.into()),
53+
b"hmget" => Ok(HMGet::try_from(v)?.into()),
54+
b"sadd" => Ok(SAdd::try_from(v)?.into()),
55+
b"sismember" => Ok(SIsMember::try_from(v)?.into()),
56+
_ => Ok(Unrecognized.into()),
57+
},
58+
_ => Err(CommandError::InvalidCommand(
59+
"Command must have a BulkString as the first argument".to_string(),
60+
)),
61+
}
62+
}
63+
}
64+
65+
impl TryFrom<RespFrame> for Command {
66+
type Error = CommandError;
67+
fn try_from(v: RespFrame) -> Result<Self, Self::Error> {
68+
match v {
69+
RespFrame::Array(array) => array.try_into(),
70+
_ => Err(CommandError::InvalidCommand(
71+
"Command must be an Array".to_string(),
72+
)),
73+
}
74+
}
75+
}

src/cmd/echo.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use crate::{Backend, RespArray, RespFrame};
2+
3+
use super::{extract_args, validate_command, CommandError, CommandExecutor};
4+
5+
// echo: https://redis.io/docs/latest/commands/echo/
6+
7+
#[derive(Debug)]
8+
pub struct Echo {
9+
message: String,
10+
}
11+
12+
impl CommandExecutor for Echo {
13+
fn execute(self, _backend: &Backend) -> RespFrame {
14+
RespFrame::BulkString(self.message.into())
15+
}
16+
}
17+
18+
impl TryFrom<RespArray> for Echo {
19+
type Error = CommandError;
20+
fn try_from(value: RespArray) -> Result<Self, Self::Error> {
21+
validate_command(&value, &["echo"], 1)?; // validate get
22+
23+
let mut args = extract_args(value, 1)?.into_iter();
24+
match args.next() {
25+
Some(RespFrame::BulkString(message)) => Ok(Echo {
26+
message: String::from_utf8(message.0)?,
27+
}),
28+
_ => Err(CommandError::InvalidArgument("Invalid message".to_string())),
29+
}
30+
}
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use crate::{BulkString, RespDecode};
36+
37+
use super::*;
38+
use anyhow::Result;
39+
use bytes::BytesMut;
40+
41+
#[test]
42+
fn test_echo_from_resp_array() -> Result<()> {
43+
let mut buf = BytesMut::new();
44+
buf.extend_from_slice(b"*2\r\n$4\r\necho\r\n$5\r\nhello\r\n");
45+
46+
let frame = RespArray::decode(&mut buf)?;
47+
48+
let result: Echo = frame.try_into()?;
49+
assert_eq!(result.message, "hello");
50+
51+
Ok(())
52+
}
53+
54+
#[test]
55+
fn test_echo_command() -> Result<()> {
56+
// let backend = Backend::new();
57+
// let cmd = Echo {
58+
// message: "hello world".to_string(),
59+
// };
60+
// let result = cmd.execute(&backend);
61+
// assert_eq!(result, RespFrame::BulkString(b"hello world".into()));
62+
63+
// Ok(())
64+
65+
let command = Echo::try_from(RespArray::new([
66+
BulkString::new("echo").into(),
67+
BulkString::new("hello").into(),
68+
]))?;
69+
assert_eq!(command.message, "hello");
70+
71+
let backend = Backend::new();
72+
let result = command.execute(&backend);
73+
assert_eq!(result, RespFrame::BulkString(b"hello".into()));
74+
Ok(())
75+
}
76+
}

0 commit comments

Comments
 (0)