From bb506e3defab6a89b061a0fa1926615f381ba526 Mon Sep 17 00:00:00 2001 From: upupnoah Date: Wed, 24 Jul 2024 05:31:47 +0700 Subject: [PATCH] feat(redis-cli): support redis cli --- Cargo.lock | 166 ++++++++++++++++++++++++++++++++ Cargo.toml | 2 + README.md | 5 + examples/enum_dispatch.rs | 1 + src/backend.rs | 65 +++++++++++++ src/cmd.rs | 147 +++++++++++++++++++++++++++++ src/cmd/hmap.rs | 194 ++++++++++++++++++++++++++++++++++++++ src/cmd/map.rs | 92 ++++++++++++++++++ src/lib.rs | 4 + src/resp.rs | 64 +++++++++++-- src/resp/encode.rs | 14 ++- 11 files changed, 742 insertions(+), 12 deletions(-) create mode 100644 src/backend.rs create mode 100644 src/cmd.rs create mode 100644 src/cmd/hmap.rs create mode 100644 src/cmd/map.rs diff --git a/Cargo.lock b/Cargo.lock index 3a52224..e6be4e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,50 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bytes" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -26,12 +64,53 @@ dependencies = [ "syn", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -50,16 +129,39 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "simple-redis" version = "0.1.0" dependencies = [ "anyhow", "bytes", + "dashmap", "enum_dispatch", + "lazy_static", "thiserror", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "syn" version = "2.0.71" @@ -96,3 +198,67 @@ name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 4ad1dbb..cdf92bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,7 @@ authors = ["Noah "] [dependencies] anyhow = "^1.0" bytes = "^1.6.1" +dashmap = "6.0.1" enum_dispatch = "^0.3.13" +lazy_static = "^1.5.0" thiserror = "^1.0.62" diff --git a/README.md b/README.md index b9d3524..e9a9ec9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # simple-redis A simple redis server implementation. + +## Project Layout + +1. Small projects use the top-level module name .rs + modulename folder. +2. Use mod.rs for medium to large projects. diff --git a/examples/enum_dispatch.rs b/examples/enum_dispatch.rs index da21222..151c9cc 100644 --- a/examples/enum_dispatch.rs +++ b/examples/enum_dispatch.rs @@ -25,6 +25,7 @@ impl DoSomething for B { println!("B"); } } + fn main() { // test enum_dispatch let apple = Types::Apple(A); diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..85a6b32 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,65 @@ +use crate::RespFrame; +use dashmap::DashMap; +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct Backend(Arc); + +#[derive(Debug)] +pub struct BackendInner { + pub(crate) map: DashMap, + pub(crate) hmap: DashMap>, +} + +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())) + } +} + +impl Backend { + pub fn new() -> Self { + Self::default() + } + + pub fn get(&self, key: &str) -> Option { + 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 { + 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> { + self.hmap.get(key).map(|v| v.clone()) + } +} diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 0000000..5aa8988 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,147 @@ +use enum_dispatch::enum_dispatch; +use lazy_static::lazy_static; +use thiserror::Error; + +use crate::{Backend, RespArray, RespError, RespFrame, SimpleString}; + +mod hmap; +mod map; + +// you could also use once_cell instead of lazy_static +lazy_static! { + static ref RESP_OK: RespFrame = SimpleString::new("OK").into(); +} + +// region: --- Traits +#[enum_dispatch] +pub trait CommandExecutor { + // fn execute(&self) -> RespFrame; + fn execute(self, backend: &Backend) -> RespFrame; +} +// endregion: --- Traits + +// region: --- Enum and Structs +#[enum_dispatch(CommandExecutor)] +#[derive(Debug)] +pub enum Command { + Get(Get), + Set(Set), + HGet(HGet), + HSet(HSet), + HGetAll(HGetAll), + // 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), +} + +#[derive(Debug)] +pub struct Get { + key: String, +} + +#[derive(Debug)] +pub struct Set { + key: String, + value: RespFrame, +} + +#[derive(Debug)] +pub struct HGet { + key: String, + field: String, +} + +#[derive(Debug)] +pub struct HSet { + key: String, + field: String, + value: RespFrame, +} + +#[derive(Debug)] +pub struct HGetAll { + key: String, + sort: bool, +} + +#[derive(Debug)] +pub struct Unrecognized; +// endregion: --- Enum and Structs + +// region: --- impls +impl CommandExecutor for Unrecognized { + fn execute(self, _: &Backend) -> RespFrame { + RESP_OK.clone() + } +} + +impl TryFrom for Command { + type Error = CommandError; + fn try_from(v: RespArray) -> Result { + 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()), + _ => Ok(Unrecognized.into()), + }, + _ => Err(CommandError::InvalidCommand( + "Command must have a BulkString as the first argument".to_string(), + )), + } + } +} +// endregion: --- impls + +// region: --- functions +fn validate_command( + value: &RespArray, + names: &[&'static str], + n_args: usize, +) -> Result<(), CommandError> { + if value.len() != n_args + names.len() { + return Err(CommandError::InvalidArgument(format!( + "{} command must have exactly {} argument", + names.join(" "), + n_args + ))); + } + + for (i, name) in names.iter().enumerate() { + match value[i] { + RespFrame::BulkString(ref cmd) => { + if cmd.as_ref().to_ascii_lowercase() != name.as_bytes() { + return Err(CommandError::InvalidCommand(format!( + "Invalid command: expected {}, got {}", + name, + String::from_utf8_lossy(cmd.as_ref()) + ))); + } + } + _ => { + return Err(CommandError::InvalidCommand( + "Command must have a BulkString as the first argument".to_string(), + )) + } + } + } + Ok(()) +} + +fn extract_args(value: RespArray, start: usize) -> Result, CommandError> { + Ok(value.0.into_iter().skip(start).collect::>()) +} +// endregion: --- functions diff --git a/src/cmd/hmap.rs b/src/cmd/hmap.rs new file mode 100644 index 0000000..3a37f11 --- /dev/null +++ b/src/cmd/hmap.rs @@ -0,0 +1,194 @@ +use crate::{BulkString, RespArray, RespFrame}; + +use super::{ + extract_args, validate_command, CommandError, CommandExecutor, HGet, HGetAll, HSet, RESP_OK, +}; + +impl CommandExecutor for HGet { + fn execute(self, backend: &crate::Backend) -> RespFrame { + match backend.hget(&self.key, &self.field) { + Some(value) => value, + None => RespFrame::Null(crate::RespNull), + } + } +} + +impl CommandExecutor for HGetAll { + fn execute(self, backend: &crate::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![BulkString::from(k).into(), v]) + .collect::>(); + + RespArray::new(ret).into() + } + None => RespArray::new([]).into(), + } + } +} + +impl CommandExecutor for HSet { + fn execute(self, backend: &crate::Backend) -> RespFrame { + backend.hset(self.key, self.field, self.value); + RESP_OK.clone() + } +} + +impl TryFrom for HGet { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + 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::InvalidArgument( + "Invalid key or field".to_string(), + )), + } + } +} + +impl TryFrom for HGetAll { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + 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::InvalidArgument("Invalid key".to_string())), + } + } +} + +impl TryFrom for HSet { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + 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::InvalidArgument( + "Invalid key, field or value".to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use crate::RespDecode; + + use super::*; + use anyhow::Result; + use bytes::BytesMut; + + #[test] + fn test_hget_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*3\r\n$4\r\nhget\r\n$3\r\nmap\r\n$5\r\nhello\r\n"); + + let frame = RespArray::decode(&mut buf)?; + + let result: HGet = frame.try_into()?; + assert_eq!(result.key, "map"); + assert_eq!(result.field, "hello"); + + Ok(()) + } + + #[test] + fn test_hgetall_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(b"*2\r\n$7\r\nhgetall\r\n$3\r\nmap\r\n"); + + let frame = RespArray::decode(&mut buf)?; + + let result: HGetAll = frame.try_into()?; + assert_eq!(result.key, "map"); + + Ok(()) + } + + #[test] + fn test_hset_from_resp_array() -> Result<()> { + let mut buf = BytesMut::new(); + buf.extend_from_slice(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 frame = RespArray::decode(&mut buf)?; + + let result: HSet = frame.try_into()?; + assert_eq!(result.key, "map"); + assert_eq!(result.field, "hello"); + assert_eq!(result.value, RespFrame::BulkString(b"world".into())); + + Ok(()) + } + + #[test] + fn test_hset_hget_hgetall_commands() -> Result<()> { + let backend = crate::Backend::new(); + let cmd = HSet { + key: "map".to_string(), + field: "hello".to_string(), + value: RespFrame::BulkString(b"world".into()), + }; + let result = cmd.execute(&backend); + assert_eq!(result, RESP_OK.clone()); + + let cmd = HSet { + key: "map".to_string(), + field: "hello1".to_string(), + value: RespFrame::BulkString(b"world1".into()), + }; + cmd.execute(&backend); + + let cmd = HGet { + key: "map".to_string(), + field: "hello".to_string(), + }; + let result = cmd.execute(&backend); + assert_eq!(result, RespFrame::BulkString(b"world".into())); + + let cmd = HGetAll { + key: "map".to_string(), + sort: true, + }; + let result = cmd.execute(&backend); + + let expected = RespArray::new([ + BulkString::from("hello").into(), + BulkString::from("world").into(), + BulkString::from("hello1").into(), + BulkString::from("world1").into(), + ]); + assert_eq!(result, expected.into()); + Ok(()) + } +} diff --git a/src/cmd/map.rs b/src/cmd/map.rs new file mode 100644 index 0000000..0e7341c --- /dev/null +++ b/src/cmd/map.rs @@ -0,0 +1,92 @@ +use crate::{ + cmd::{extract_args, validate_command, Get, Set}, + CommandError, RespArray, RespFrame, RespNull, +}; + +use super::{CommandExecutor, RESP_OK}; + +impl CommandExecutor for Get { + fn execute(self, backend: &crate::Backend) -> RespFrame { + match backend.get(&self.key) { + Some(value) => value, + None => RespFrame::Null(RespNull), + } + } +} + +impl CommandExecutor for Set { + fn execute(self, backend: &crate::Backend) -> RespFrame { + backend.set(self.key, self.value); + RESP_OK.clone() + } +} + +impl TryFrom for Get { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + validate_command(&value, &["get"], 1)?; // validate get + + 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::InvalidArgument("Invalid key".to_string())), + } + } +} + +impl TryFrom for Set { + type Error = CommandError; + fn try_from(value: RespArray) -> Result { + 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::InvalidArgument( + "Invalid key or value".to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RespDecode; + 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$5\r\nhello\r\n"); + + let frame = RespArray::decode(&mut buf)?; + // println!("{:?}", frame); + + // let result: Get = frame.try_into()?; + let result = Get::try_from(frame)?; + assert_eq!(result.key, "hello"); + + 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$5\r\nhello\r\n$5\r\nworld\r\n"); + + let frame = RespArray::decode(&mut buf)?; + + let result: Set = frame.try_into()?; + assert_eq!(result.key, "hello"); + assert_eq!(result.value, RespFrame::BulkString(b"world".into())); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 62bcc21..195a8a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ +mod backend; +mod cmd; mod resp; +pub use backend::*; +pub use cmd::*; pub use resp::*; diff --git a/src/resp.rs b/src/resp.rs index ed9d3b2..557cfc3 100644 --- a/src/resp.rs +++ b/src/resp.rs @@ -57,6 +57,7 @@ pub enum RespError { // 之所以要定义一些新的结构体, 是因为要在实现 trait 的时候, 要区分开这些类型 #[enum_dispatch(RespEncode)] +#[derive(Debug, Clone, PartialEq)] pub enum RespFrame { SimpleString(SimpleString), Error(SimpleError), @@ -67,7 +68,7 @@ pub enum RespFrame { NullArray(RespNullArray), Null(RespNull), Boolean(bool), - Double(f64), + Double(f64), // f64 can't derive Eq Map(RespMap), Set(RespSet), } @@ -78,24 +79,25 @@ pub enum RespFrame { // 3. 添加方法:你可以为 SimpleString 实现方法,这些方法特定于这种类型的字符串。 // 4. 语义清晰:在复杂的数据结构中(如你展示的 RespFrame 枚举),使用 SimpleString 而不是直接使用 String 可以使代码的意图更加明确。 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] -pub struct SimpleString(String); // Simple String, 用于存储简单字符串 +pub struct SimpleString(pub(crate) String); // Simple String, 用于存储简单字符串 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] -pub struct SimpleError(String); +pub struct SimpleError(pub(crate) String); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] -pub struct BulkString(Vec); // 单个二进制字符串, 用于存储二进制数据(最大512MB) +pub struct BulkString(pub(crate) Vec); // 单个二进制字符串, 用于存储二进制数据(最大512MB) #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub struct RespNullBulkString; - -pub struct RespArray(Vec); +#[derive(Debug, Clone, PartialEq)] +pub struct RespArray(pub(crate) Vec); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub struct RespNullArray; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub struct RespNull; -#[derive(Default)] -pub struct RespMap(BTreeMap); // 改为 BTreeMap, 用于有序的 key-value 数据 - +// 改为 BTreeMap, 用于有序的 key-value 数据 +#[derive(Default, Clone, Debug, PartialEq)] +pub struct RespMap(pub(crate) BTreeMap); // pub struct RespSet(HashSet); -pub struct RespSet(Vec); // 改为 Vec, 用于有序的集合数据 +#[derive(Debug, Clone, PartialEq)] +pub struct RespSet(pub(crate) Vec); // 改为 Vec, 用于有序的集合数据 impl SimpleString { pub fn new(s: impl Into) -> Self { @@ -133,6 +135,48 @@ impl RespSet { } } +impl From<&[u8; N]> for RespFrame { + fn from(s: &[u8; N]) -> Self { + BulkString(s.to_vec()).into() + } +} + +impl From<&[u8]> for RespFrame { + fn from(s: &[u8]) -> Self { + BulkString(s.to_vec()).into() + } +} + +impl From<&str> for RespFrame { + fn from(s: &str) -> Self { + SimpleString(s.to_string()).into() + } +} + +impl From<&str> for BulkString { + fn from(s: &str) -> Self { + BulkString(s.as_bytes().to_vec()) + } +} + +impl From for BulkString { + fn from(s: String) -> Self { + BulkString(s.into_bytes()) + } +} + +impl From<&[u8]> for BulkString { + fn from(s: &[u8]) -> Self { + BulkString(s.to_vec()) + } +} + +impl From<&[u8; N]> for BulkString { + fn from(s: &[u8; N]) -> Self { + BulkString(s.to_vec()) + } +} + // utility functions fn extract_fixed_data( buf: &mut BytesMut, diff --git a/src/resp/encode.rs b/src/resp/encode.rs index 376a9b5..cbba66c 100644 --- a/src/resp/encode.rs +++ b/src/resp/encode.rs @@ -167,6 +167,12 @@ impl Deref for BulkString { } } +impl AsRef<[u8]> for BulkString { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl Deref for RespArray { type Target = Vec; @@ -197,6 +203,10 @@ impl Deref for RespSet { } } +// enum_dispatch +// 这里因为 enum_dispatch 的原因, 会自动为变体类型生成, From for Enum_Name +// 因此当构造出变体类型的时候, 可以使用 into 方法将其转换为枚举类型, 或者 from +// 因为实现了 from 会自动实现 into, 实现了 into 会自动实现 from #[cfg(test)] mod test { use super::*; @@ -261,7 +271,7 @@ mod test { #[test] fn test_boolean_encode() { - // into 和 from 是互斥的 + // into 和 from 是互逆的 // let frame: RespFrame = true.into(); let frame = RespFrame::from(true); assert_eq!(frame.encode(), b"#t\r\n"); @@ -278,7 +288,7 @@ mod test { let frame: RespFrame = (-123.456).into(); assert_eq!(frame.encode(), b",-123.456\r\n"); - let frame: RespFrame = 1.23456e+8.into(); + let frame: RespFrame = (1.23456e+8).into(); assert_eq!(frame.encode(), b",+1.23456e8\r\n"); let frame: RespFrame = (-1.23456e-9).into();