From 7e7d64095c5bed3de1a66f89dcf439a4d27c56df Mon Sep 17 00:00:00 2001 From: Marius Meissner Date: Sat, 3 Feb 2024 14:17:37 +0100 Subject: [PATCH 1/3] Implemented abstraction for HGETALL command --- src/commands/builder.rs | 69 +++++++++++++ src/commands/hgetall.rs | 95 ++++++++++++++++++ src/commands/mod.rs | 1 + src/commands/tests/builder.rs | 184 ++++++++++++++++++++++++++++++++++ src/commands/tests/hgetall.rs | 100 ++++++++++++++++++ src/commands/tests/mod.rs | 2 + src/network/tests/client.rs | 75 ++++++++++++++ 7 files changed, 526 insertions(+) create mode 100644 src/commands/hgetall.rs create mode 100644 src/commands/tests/builder.rs create mode 100644 src/commands/tests/hgetall.rs diff --git a/src/commands/builder.rs b/src/commands/builder.rs index 48666aa..410b404 100644 --- a/src/commands/builder.rs +++ b/src/commands/builder.rs @@ -31,10 +31,12 @@ //! .into(); //! ``` use crate::commands::custom::CustomCommand; +use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use bytes::Bytes; +use redis_protocol::resp2::prelude::Frame; use redis_protocol::resp2::types::Frame as Resp2Frame; use redis_protocol::resp3::types::Frame as Resp3Frame; @@ -208,3 +210,70 @@ impl ToStringBytes for Resp3Frame { } } } + +/// Trait for converting RESP2 arrays or RESP3 maps +pub trait ToBytesMap { + /// Converts the frame to map + /// Returns None in case of protocol violation + fn to_map(&self) -> Option>; +} + +impl ToBytesMap for Resp2Frame { + fn to_map(&self) -> Option> { + let mut map = BTreeMap::new(); + + match self { + Frame::Array(array) => { + for item in array.chunks(2) { + if item.len() < 2 { + return None; + } + + let field = match &item[0] { + Frame::SimpleString(value) | Frame::BulkString(value) => value.clone(), + _ => return None, + }; + + let value = match &item[1] { + Frame::SimpleString(value) | Frame::BulkString(value) => value.clone(), + _ => return None, + }; + + map.insert(field, value); + } + } + _ => return None, + } + + Some(map) + } +} + +impl ToBytesMap for Resp3Frame { + fn to_map(&self) -> Option> { + let mut map = BTreeMap::new(); + + match self { + Resp3Frame::Map { data, attributes: _ } => { + for item in data { + let field = match item.0 { + Resp3Frame::BlobString { data, attributes: _ } + | Resp3Frame::SimpleString { data, attributes: _ } => data.clone(), + _ => return None, + }; + + let value = match item.1 { + Resp3Frame::BlobString { data, attributes: _ } + | Resp3Frame::SimpleString { data, attributes: _ } => data.clone(), + _ => return None, + }; + + map.insert(field, value); + } + } + _ => return None, + } + + Some(map) + } +} diff --git a/src/commands/hgetall.rs b/src/commands/hgetall.rs new file mode 100644 index 0000000..c579784 --- /dev/null +++ b/src/commands/hgetall.rs @@ -0,0 +1,95 @@ +use crate::commands::auth::AuthCommand; +use crate::commands::builder::{CommandBuilder, ToBytesMap}; +use crate::commands::hello::HelloCommand; +use crate::commands::{Command, ResponseTypeError}; +use crate::network::protocol::Protocol; +use crate::network::{Client, CommandErrors, Future}; +use alloc::collections::BTreeMap; +use bytes::Bytes; +use embedded_nal::TcpClientStack; +use embedded_time::Clock; + +/// Abstraction for HGETALL command +pub struct HashGetAllCommand { + /// Hash key + key: Bytes, +} + +impl HashGetAllCommand { + pub fn new(key: K) -> Self + where + Bytes: From, + { + Self { key: key.into() } + } +} + +pub struct HashResponse { + /// Field/Value map + inner: BTreeMap, +} + +impl HashResponse { + /// Extracts inner map + #[allow(clippy::wrong_self_convention)] + pub fn to_map(self) -> BTreeMap { + self.inner + } + + /// Returns the given field as &str. Returns None in case field is missing or value has invalid UTF8 encoding + pub fn get_str(&self, field: F) -> Option<&str> + where + Bytes: From, + { + let field: Bytes = field.into(); + + match self.inner.get(&field) { + None => None, + Some(value) => match core::str::from_utf8(value) { + Ok(value) => Some(value), + Err(_) => None, + }, + } + } +} + +impl Command for HashGetAllCommand +where + F: From + ToBytesMap, +{ + type Response = Option; + + fn encode(&self) -> F { + CommandBuilder::new("HGETALL").arg(&self.key).into() + } + + fn eval_response(&self, frame: F) -> Result { + let map = frame.to_map(); + + if map.is_none() { + return Err(ResponseTypeError {}); + } + + if map.as_ref().unwrap().is_empty() { + return Ok(None); + } + + Ok(Some(HashResponse { inner: map.unwrap() })) + } +} + +impl<'a, N: TcpClientStack, C: Clock, P: Protocol> Client<'a, N, C, P> +where + AuthCommand: Command<

::FrameType>, + HelloCommand: Command<

::FrameType>, +{ + /// Shorthand for [HashGetAllCommand] + pub fn hgetall(&'a self, key: K) -> Result, CommandErrors> + where +

::FrameType: ToBytesMap, +

::FrameType: From, + Bytes: From, + { + self.send(HashGetAllCommand::new(key)) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 93d0a37..9a868ce 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod get; pub mod hello; pub mod helpers; pub mod hget; +pub mod hgetall; pub mod hset; pub mod ping; pub mod publish; diff --git a/src/commands/tests/builder.rs b/src/commands/tests/builder.rs new file mode 100644 index 0000000..9bf0df2 --- /dev/null +++ b/src/commands/tests/builder.rs @@ -0,0 +1,184 @@ +use crate::commands::builder::ToBytesMap; +use bytes::Bytes; +use redis_protocol::resp2::types::Frame as Resp2Frame; +use redis_protocol::resp3::types::{Frame as Resp3Frame, FrameMap}; + +#[test] +fn to_bytes_map_resp2_simple_string() { + let frame: Resp2Frame = Resp2Frame::Array(vec![ + Resp2Frame::SimpleString("color".into()), + Resp2Frame::SimpleString("green".into()), + Resp2Frame::SimpleString("material".into()), + Resp2Frame::SimpleString("wood".into()), + ]); + let map = frame.to_map().unwrap(); + + assert_eq!(2, map.len()); + assert_eq!("green", map.get(&Bytes::from_static(b"color")).unwrap()); + assert_eq!("wood", map.get(&Bytes::from_static(b"material")).unwrap()); +} + +#[test] +fn to_bytes_map_resp2_bulk_string() { + let frame: Resp2Frame = Resp2Frame::Array(vec![ + Resp2Frame::BulkString("color".into()), + Resp2Frame::BulkString("green".into()), + Resp2Frame::BulkString("material".into()), + Resp2Frame::BulkString("wood".into()), + ]); + let map = frame.to_map().unwrap(); + + assert_eq!(2, map.len()); + assert_eq!("green", map.get(&Bytes::from_static(b"color")).unwrap()); + assert_eq!("wood", map.get(&Bytes::from_static(b"material")).unwrap()); +} + +#[test] +fn to_bytes_map_resp2_no_array() { + assert!(Resp2Frame::SimpleString("test".into()).to_map().is_none()); +} + +#[test] +fn to_bytes_map_resp2_missing_value() { + let frame: Resp2Frame = Resp2Frame::Array(vec![ + Resp2Frame::SimpleString("color".into()), + Resp2Frame::SimpleString("green".into()), + Resp2Frame::SimpleString("material".into()), + ]); + + assert!(frame.to_map().is_none()); +} + +#[test] +fn to_bytes_map_resp2_field_not_string() { + let frame: Resp2Frame = Resp2Frame::Array(vec![ + Resp2Frame::Array(vec![]), + Resp2Frame::SimpleString("green".into()), + ]); + + assert!(frame.to_map().is_none()); +} + +#[test] +fn to_bytes_map_resp2_value_not_string() { + let frame: Resp2Frame = Resp2Frame::Array(vec![ + Resp2Frame::SimpleString("color".into()), + Resp2Frame::Array(vec![]), + ]); + + assert!(frame.to_map().is_none()); +} + +#[test] +fn to_bytes_map_resp3_simple_string() { + let frame: Resp3Frame = Resp3Frame::Map { + data: FrameMap::from([ + ( + Resp3Frame::SimpleString { + data: "color".into(), + attributes: None, + }, + Resp3Frame::SimpleString { + data: "green".into(), + attributes: None, + }, + ), + ( + Resp3Frame::SimpleString { + data: "material".into(), + attributes: None, + }, + Resp3Frame::SimpleString { + data: "wood".into(), + attributes: None, + }, + ), + ]), + attributes: None, + }; + let map = frame.to_map().unwrap(); + + assert_eq!(2, map.len()); + assert_eq!("green", map.get(&Bytes::from_static(b"color")).unwrap()); + assert_eq!("wood", map.get(&Bytes::from_static(b"material")).unwrap()); +} + +#[test] +fn to_bytes_map_resp3_blob_string() { + let frame: Resp3Frame = Resp3Frame::Map { + data: FrameMap::from([ + ( + Resp3Frame::BlobString { + data: "color".into(), + attributes: None, + }, + Resp3Frame::BlobString { + data: "green".into(), + attributes: None, + }, + ), + ( + Resp3Frame::BlobString { + data: "material".into(), + attributes: None, + }, + Resp3Frame::BlobString { + data: "wood".into(), + attributes: None, + }, + ), + ]), + attributes: None, + }; + let map = frame.to_map().unwrap(); + + assert_eq!(2, map.len()); + assert_eq!("green", map.get(&Bytes::from_static(b"color")).unwrap()); + assert_eq!("wood", map.get(&Bytes::from_static(b"material")).unwrap()); +} + +#[test] +fn to_bytes_map_resp3_no_array() { + assert!(Resp3Frame::BlobString { + data: "test".into(), + attributes: None, + } + .to_map() + .is_none()); +} + +#[test] +fn to_bytes_map_resp3_field_not_string() { + let frame: Resp3Frame = Resp3Frame::Map { + data: FrameMap::from([( + Resp3Frame::Number { + data: 0, + attributes: None, + }, + Resp3Frame::SimpleString { + data: "green".into(), + attributes: None, + }, + )]), + attributes: None, + }; + assert!(frame.to_map().is_none()); +} + +#[test] +fn to_bytes_map_resp3_value_not_string() { + let frame: Resp3Frame = Resp3Frame::Map { + data: FrameMap::from([( + Resp3Frame::SimpleString { + data: "color".into(), + attributes: None, + }, + Resp3Frame::Number { + data: 0, + attributes: None, + }, + )]), + attributes: None, + }; + assert!(frame.to_map().is_none()); +} diff --git a/src/commands/tests/hgetall.rs b/src/commands/tests/hgetall.rs new file mode 100644 index 0000000..1b358bd --- /dev/null +++ b/src/commands/tests/hgetall.rs @@ -0,0 +1,100 @@ +use crate::commands::hgetall::HashGetAllCommand; +use crate::commands::Command; +use redis_protocol::resp2::types::Frame as Resp2Frame; +use redis_protocol::resp3::prelude::FrameMap; +use redis_protocol::resp3::types::Frame as Resp3Frame; + +#[test] +fn test_encode_resp2() { + let frame: Resp2Frame = HashGetAllCommand::new("my_hash").encode(); + + assert!(frame.is_array()); + if let Resp2Frame::Array(array) = frame { + assert_eq!(2, array.len()); + assert_eq!("HGETALL", array[0].to_string().unwrap()); + assert_eq!("my_hash", array[1].to_string().unwrap()); + } +} + +#[test] +fn test_encode_resp3() { + let frame: Resp3Frame = HashGetAllCommand::new("my_hash").encode(); + + assert!(frame.is_array()); + if let Resp3Frame::Array { data, attributes: _ } = frame { + assert_eq!(2, data.len()); + assert_eq!("HGETALL", data[0].to_string().unwrap()); + assert_eq!("my_hash", data[1].to_string().unwrap()); + } +} + +#[test] +fn test_eval_response_resp2_key_existing() { + let response = HashGetAllCommand::new("my_hash") + .eval_response(Resp2Frame::Array(vec![ + Resp2Frame::SimpleString("color".into()), + Resp2Frame::SimpleString("green".into()), + ])) + .unwrap(); + + assert_eq!("green", response.unwrap().get_str("color").unwrap()); +} + +#[test] +fn test_eval_response_resp3_key_existing() { + let response = HashGetAllCommand::new("my_hash") + .eval_response(Resp3Frame::Map { + data: FrameMap::from([( + Resp3Frame::BlobString { + data: "color".into(), + attributes: None, + }, + Resp3Frame::BlobString { + data: "green".into(), + attributes: None, + }, + )]), + attributes: None, + }) + .unwrap(); + + assert_eq!("green", response.unwrap().get_str("color").unwrap()); +} + +#[test] +fn test_eval_response_resp2_key_missing() { + let response = HashGetAllCommand::new("my_hash") + .eval_response(Resp2Frame::Array(vec![])) + .unwrap(); + + assert!(response.is_none()); +} + +#[test] +fn test_eval_response_resp3_key_missing() { + let response = HashGetAllCommand::new("my_hash") + .eval_response(Resp3Frame::Map { + data: Default::default(), + attributes: None, + }) + .unwrap(); + + assert!(response.is_none()); +} + +#[test] +fn test_eval_response_resp2_invalid_response() { + let response = HashGetAllCommand::new("my_hash").eval_response(Resp2Frame::SimpleString("wrong".into())); + + assert!(response.is_err()); +} + +#[test] +fn test_eval_response_resp3_invalid_response() { + let response = HashGetAllCommand::new("my_hash").eval_response(Resp3Frame::SimpleString { + data: "wrong".into(), + attributes: None, + }); + + assert!(response.is_err()); +} diff --git a/src/commands/tests/mod.rs b/src/commands/tests/mod.rs index 9d16c1b..5fb6889 100644 --- a/src/commands/tests/mod.rs +++ b/src/commands/tests/mod.rs @@ -1,9 +1,11 @@ mod auth; mod bgsave; +mod builder; mod custom; mod get; pub(crate) mod hello; mod hget; +mod hgetall; mod hset; mod ping; mod publish; diff --git a/src/network/tests/client.rs b/src/network/tests/client.rs index c4c31ed..0ccb861 100644 --- a/src/network/tests/client.rs +++ b/src/network/tests/client.rs @@ -1114,3 +1114,78 @@ fn test_shorthand_hget_bytes_argument() { .wait(); assert_eq!("test_response", response.unwrap().unwrap().as_str().unwrap()); } + +#[test] +fn test_shorthand_hgetall_str_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*2\r\n$7\r\nHGETALL\r\n$7\r\nmy_hash\r\n") + .response("*2\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + assert_eq!( + "green", + client + .hgetall("my_hash") + .unwrap() + .wait() + .unwrap() + .unwrap() + .get_str("color") + .unwrap() + ); +} + +#[test] +fn test_shorthand_hgetall_string_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*2\r\n$7\r\nHGETALL\r\n$7\r\nmy_hash\r\n") + .response("*2\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + assert_eq!( + "green", + client + .hgetall("my_hash".to_string()) + .unwrap() + .wait() + .unwrap() + .unwrap() + .get_str("color") + .unwrap() + ); +} + +#[test] +fn test_shorthand_hgetall_bytes_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*2\r\n$7\r\nHGETALL\r\n$7\r\nmy_hash\r\n") + .response("*2\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + assert_eq!( + "green", + client + .hgetall(Bytes::from_static(b"my_hash")) + .unwrap() + .wait() + .unwrap() + .unwrap() + .get_str("color") + .unwrap() + ); +} From 65b6fd267cd673af23ba42d055ba9e4cebe170ed Mon Sep 17 00:00:00 2001 From: Marius Meissner Date: Sat, 3 Feb 2024 14:24:50 +0100 Subject: [PATCH 2/3] Added docs for HGETALL command --- src/commands/hgetall.rs | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/commands/hgetall.rs b/src/commands/hgetall.rs index c579784..895d593 100644 --- a/src/commands/hgetall.rs +++ b/src/commands/hgetall.rs @@ -1,3 +1,87 @@ +//! Abstraction of HGETALL command. +//! +//! For general information about this command, see the [Redis documentation](). +//! +//! # Using command object +//! ``` +//!# use core::str::FromStr; +//!# use embedded_nal::SocketAddr; +//!# use std_embedded_nal::Stack; +//!# use std_embedded_time::StandardClock; +//!# use embedded_redis::commands::builder::CommandBuilder; +//!# use embedded_redis::commands::hgetall::HashGetAllCommand; +//!# use embedded_redis::network::ConnectionHandler; +//!# +//! let mut stack = Stack::default(); +//! let clock = StandardClock::default(); +//! +//! let mut connection_handler = ConnectionHandler::resp2(SocketAddr::from_str("127.0.0.1:6379").unwrap()); +//! let client = connection_handler.connect(&mut stack, Some(&clock)).unwrap(); +//! client.hset("test_all_hash", "color", "green").unwrap().wait().unwrap(); +//! client.hset("test_all_hash", "material", "wood").unwrap().wait().unwrap(); +//! +//! let command = HashGetAllCommand::new("test_all_hash"); +//! let response = client.send(command).unwrap().wait().unwrap().unwrap(); +//! +//! assert_eq!("green", response.get_str("color").unwrap()); +//! assert_eq!("wood", response.get_str("material").unwrap()); +//! ``` +//! +//! # Missing key or field +//! In case key or field is missing. [None] is returned. +//! ``` +//!# use core::str::FromStr; +//!# use embedded_nal::SocketAddr; +//!# use std_embedded_nal::Stack; +//!# use std_embedded_time::StandardClock; +//!# use embedded_redis::commands::builder::CommandBuilder; +//!# use embedded_redis::commands::hgetall::HashGetAllCommand; +//!# use embedded_redis::network::ConnectionHandler; +//!# +//!# let mut stack = Stack::default(); +//!# let clock = StandardClock::default(); +//!# +//!# let mut connection_handler = ConnectionHandler::resp2(SocketAddr::from_str("127.0.0.1:6379").unwrap()); +//!# let client = connection_handler.connect(&mut stack, Some(&clock)).unwrap(); +//!# +//! let command = HashGetAllCommand::new("not_existing"); +//! let response = client.send(command).unwrap().wait().unwrap(); +//! +//! assert!(response.is_none()) +//! ``` +//! +//! # Shorthand +//! [Client](Client#method.hgetall) provides a shorthand method for this command. +//! ``` +//!# use core::str::FromStr; +//!# use bytes::Bytes; +//!# use embedded_nal::SocketAddr; +//!# use std_embedded_nal::Stack; +//!# use std_embedded_time::StandardClock; +//!# use embedded_redis::commands::hset::HashSetCommand; +//!# use embedded_redis::commands::set::SetCommand; +//!# use embedded_redis::network::ConnectionHandler; +//!# +//!# let mut stack = Stack::default(); +//!# let clock = StandardClock::default(); +//!# +//!# let mut connection_handler = ConnectionHandler::resp2(SocketAddr::from_str("127.0.0.1:6379").unwrap()); +//!# let client = connection_handler.connect(&mut stack, Some(&clock)).unwrap(); +//!# +//!# let _ = client.send(HashSetCommand::new("multi_hash_key", "first_field", "green")).unwrap().wait(); +//!# let _ = client.send(HashSetCommand::new("multi_hash_key", "second_field", "wood")).unwrap().wait(); +//!# +//! // Using &str arguments +//! let response = client.hgetall("multi_hash_key").unwrap().wait().unwrap().unwrap(); +//! assert_eq!("green", response.get_str("first_field").unwrap()); +//! assert_eq!("wood", response.get_str("second_field").unwrap()); +//! +//! // Using String arguments +//! let _ = client.hgetall("multi_hash_key".to_string()); +//! +//! // Using Bytes arguments +//! let _ = client.hgetall(Bytes::from_static(b"multi_hash_key")); +//! ``` use crate::commands::auth::AuthCommand; use crate::commands::builder::{CommandBuilder, ToBytesMap}; use crate::commands::hello::HelloCommand; From 63918c59e8aceda9f2bc9cb771dfa8cd6a97c8f3 Mon Sep 17 00:00:00 2001 From: Marius Meissner Date: Sat, 3 Feb 2024 14:26:33 +0100 Subject: [PATCH 3/3] Removed obsolete imports in docs --- src/commands/hget.rs | 5 ----- src/commands/hset.rs | 3 --- 2 files changed, 8 deletions(-) diff --git a/src/commands/hget.rs b/src/commands/hget.rs index 70c7bf6..f3d2d42 100644 --- a/src/commands/hget.rs +++ b/src/commands/hget.rs @@ -10,8 +10,6 @@ //!# use std_embedded_time::StandardClock; //!# use embedded_redis::commands::builder::CommandBuilder; //!# use embedded_redis::commands::hget::HashGetCommand; -//!# use embedded_redis::commands::hset::HashSetCommand; -//!# use embedded_redis::commands::publish::PublishCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //! let mut stack = Stack::default(); @@ -36,8 +34,6 @@ //!# use std_embedded_time::StandardClock; //!# use embedded_redis::commands::builder::CommandBuilder; //!# use embedded_redis::commands::hget::HashGetCommand; -//!# use embedded_redis::commands::hset::HashSetCommand; -//!# use embedded_redis::commands::publish::PublishCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //!# let mut stack = Stack::default(); @@ -61,7 +57,6 @@ //!# use std_embedded_nal::Stack; //!# use std_embedded_time::StandardClock; //!# use embedded_redis::commands::hset::HashSetCommand; -//!# use embedded_redis::commands::set::SetCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //!# let mut stack = Stack::default(); diff --git a/src/commands/hset.rs b/src/commands/hset.rs index 3c89a59..f96461c 100644 --- a/src/commands/hset.rs +++ b/src/commands/hset.rs @@ -10,7 +10,6 @@ //!# use std_embedded_time::StandardClock; //!# use embedded_redis::commands::builder::CommandBuilder; //! use embedded_redis::commands::hset::HashSetCommand; -//!# use embedded_redis::commands::publish::PublishCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //! let mut stack = Stack::default(); @@ -34,7 +33,6 @@ //!# use std_embedded_time::StandardClock; //!# use embedded_redis::commands::builder::CommandBuilder; //!# use embedded_redis::commands::hset::HashSetCommand; -//!# use embedded_redis::commands::publish::PublishCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //!# let mut stack = Stack::default(); @@ -61,7 +59,6 @@ //!# use embedded_nal::SocketAddr; //!# use std_embedded_nal::Stack; //!# use std_embedded_time::StandardClock; -//!# use embedded_redis::commands::set::SetCommand; //!# use embedded_redis::network::ConnectionHandler; //!# //!# let mut stack = Stack::default();