Skip to content

Commit

Permalink
feat: support simple redis cli command
Browse files Browse the repository at this point in the history
  • Loading branch information
okqin committed Jun 22, 2024
1 parent c76cc8e commit 65ce4d0
Show file tree
Hide file tree
Showing 11 changed files with 1,384 additions and 79 deletions.
666 changes: 666 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ publish = false
edition = "2021"

[dependencies]
anyhow = "1.0.86"
bytes = "1.6.0"
derive_more = { version = "1.0.0-beta.6", features = ["deref", "display"] }
dashmap = "5.5.3"
derive_more = { version = "1.0.0-beta.6", features = ["deref", "display", "as_ref", "from"] }
enum_dispatch = "0.3.13"
futures = { version = "0.3.30", default-features = false }
lazy_static = "1.4.0"
ordered-float = "4.2.0"
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread", "rt", "net", "io-util"] }
tokio-stream = "0.1.15"
tokio-util = { version = "0.7.11", features = ["codec"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

[dev-dependencies]
anyhow = "1.0.86"
43 changes: 43 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::sync::Arc;

use crate::RespFrame;
use dashmap::DashMap;
use derive_more::Deref;

#[derive(Debug, Clone, Deref, Default)]
pub struct Backend(Arc<BackendInner>);

#[derive(Debug, Default)]
pub struct BackendInner {
pub(crate) map: DashMap<String, RespFrame>,
pub(crate) hmap: DashMap<String, DashMap<String, RespFrame>>,
}

impl Backend {
pub fn new() -> Self {
Self::default()
}

pub fn get(&self, key: &str) -> Option<RespFrame> {
self.map.get(key).map(|v| v.value().clone())
}

pub fn set(&self, key: String, value: RespFrame) {
self.map.insert(key, value);
}

pub fn hget(&self, key: &str, field: &str) -> Option<RespFrame> {
self.hmap
.get(key)
.and_then(|v| v.get(field).map(|v| v.value().clone()))
}

pub fn hset(&self, key: String, field: String, value: RespFrame) {
let hmap = self.hmap.entry(key).or_default();
hmap.insert(field, value);
}

pub fn hgetall(&self, key: &str) -> Option<DashMap<String, RespFrame>> {
self.hmap.get(key).map(|v| v.clone())
}
}
188 changes: 188 additions & 0 deletions src/cmd/hmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use super::{
extract_args, validate_command, CommandError, CommandExecutor, HGet, HGetAll, HSet, RESP_OK,
};
use crate::{resp::RespNull, Backend, RespArray, RespBulkString, RespFrame};

impl CommandExecutor for HSet {
fn execute(self, backend: &Backend) -> RespFrame {
backend.hset(self.key, self.field, self.value);
RESP_OK.clone()
}
}

impl CommandExecutor for HGet {
fn execute(self, backend: &Backend) -> RespFrame {
match backend.hget(&self.key, &self.field) {
Some(value) => value,
None => RespFrame::Null(RespNull),
}
}
}

impl CommandExecutor for HGetAll {
fn execute(self, backend: &Backend) -> RespFrame {
let hmap = backend.hmap.get(&self.key);
match hmap {
Some(hmap) => {
let mut data = Vec::with_capacity(hmap.len());
for v in hmap.iter() {
let key = v.key().to_owned();
data.push((key, v.value().clone()));
}
if self.sort {
data.sort_by(|a, b| a.0.cmp(&b.0));
}
let ret = data
.into_iter()
.flat_map(|(k, v)| vec![RespBulkString::from(k).into(), v])
.collect::<Vec<RespFrame>>();

RespArray::new(ret).into()
}
None => RespArray::new([]).into(),
}
}
}

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.0)?,
field: String::from_utf8(field.0)?,
value,
})
}
_ => Err(CommandError::InvalidCommandArguments(
"Invalid key, field or value".to_string(),
)),
}
}
}

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.0)?,
field: String::from_utf8(field.0)?,
}),
_ => Err(CommandError::InvalidCommandArguments(
"Invalid key or field".to_string(),
)),
}
}
}

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

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

#[cfg(test)]
mod tests {
use super::*;
use crate::{resp::RespDecoder, RespBulkString};
use anyhow::Result;
use bytes::BytesMut;

#[test]
fn test_hset_command() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(
b"*4\r\n$4\r\nhset\r\n$6\r\nmyhash\r\n$5\r\nfield\r\n$5\r\nvalue\r\n",
);
let input = RespArray::decode(&mut buf)?;

let cmd = HSet::try_from(input)?;
assert_eq!(cmd.key, "myhash");
assert_eq!(cmd.field, "field");
assert_eq!(
cmd.value,
RespFrame::BulkString(RespBulkString::new("value"))
);

Ok(())
}

#[test]
fn test_hget_command() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(b"*3\r\n$4\r\nhget\r\n$6\r\nmyhash\r\n$5\r\nfield\r\n");
let input = RespArray::decode(&mut buf)?;
let cmd = HGet::try_from(input)?;
assert_eq!(cmd.key, "myhash");
assert_eq!(cmd.field, "field");

Ok(())
}

#[test]
fn test_hgetall_command() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(b"*2\r\n$7\r\nhgetall\r\n$6\r\nmyhash\r\n");
let input = RespArray::decode(&mut buf)?;

let cmd = HGetAll::try_from(input)?;
assert_eq!(cmd.key, "myhash");
Ok(())
}

#[test]
fn test_hgetall_cmd_execute() {
let backend = Backend::new();
let cmd = HSet {
key: "family".to_string(),
field: "name".to_string(),
value: RespFrame::BulkString(RespBulkString::new("Vic")),
};
let resp = cmd.execute(&backend);
assert_eq!(resp, RESP_OK.clone());

let cmd = HSet {
key: "family".to_string(),
field: "age".to_string(),
value: RespFrame::Integer(10.into()),
};
let resp = cmd.execute(&backend);
assert_eq!(resp, RESP_OK.clone());

let cmd = HGetAll {
key: "family".to_string(),
sort: true,
};
let resp = cmd.execute(&backend);
assert_eq!(
resp,
RespArray::new([
RespBulkString::from("age").into(),
RespFrame::Integer(10),
RespBulkString::from("name").into(),
RespFrame::BulkString("Vic".into()),
])
.into()
);
}
}
105 changes: 105 additions & 0 deletions src/cmd/map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use super::{extract_args, validate_command, CommandError, CommandExecutor, Get, Set, RESP_OK};
use crate::{
resp::{RespArray, RespNull},
Backend, RespFrame,
};

impl CommandExecutor for Get {
fn execute(self, backend: &Backend) -> RespFrame {
match backend.get(&self.key) {
Some(value) => value,
None => RespFrame::Null(RespNull),
}
}
}

impl CommandExecutor for Set {
fn execute(self, backend: &Backend) -> RespFrame {
backend.set(self.key, self.value);
RESP_OK.clone()
}
}

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)) => Ok(Set {
key: String::from_utf8(key.0)?,
value,
}),
_ => Err(CommandError::InvalidCommandArguments(
"Invalid key or value".to_string(),
)),
}
}
}

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.0)?,
}),
_ => Err(CommandError::InvalidCommandArguments(
"Invalid key".to_string(),
)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{resp::RespDecoder, RespBulkString};
use anyhow::Result;
use bytes::BytesMut;

#[test]
fn test_get_from_resp_array() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(b"*2\r\n$3\r\nget\r\n$4\r\nname\r\n");
let frame = RespArray::decode(&mut buf)?;
let get = Get::try_from(frame)?;
assert_eq!(get.key, "name");
Ok(())
}

#[test]
fn test_set_from_resp_array() -> Result<()> {
let mut buf = BytesMut::new();
buf.extend_from_slice(b"*3\r\n$3\r\nset\r\n$4\r\nname\r\n$7\r\nvictory\r\n");
let frame = RespArray::decode(&mut buf)?;
let set = Set::try_from(frame)?;
assert_eq!(set.key, "name");
assert_eq!(
set.value,
RespFrame::BulkString(RespBulkString::new("victory"))
);
Ok(())
}

#[test]
fn test_set_and_get_cmd_execute() {
let backend = Backend::new();
let cmd = Set {
key: "name".to_string(),
value: RespFrame::BulkString("victory".into()),
};
let resp = cmd.execute(&backend);
assert_eq!(resp, RESP_OK.clone());

let cmd = Get {
key: "name".to_string(),
};
let resp = cmd.execute(&backend);
assert_eq!(resp, RespFrame::BulkString("victory".into()));
}
}
Loading

0 comments on commit 65ce4d0

Please sign in to comment.