diff --git a/src/commands/get.rs b/src/commands/get.rs index c22128b..0acdd62 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -163,6 +163,20 @@ impl GetResponse { Some(result.unwrap()) } + + /// Constructs the response object from frame + pub(crate) fn from_frame(frame: F) -> Result, ResponseTypeError> + where + F: IsNullFrame + ToStringBytes, + { + if frame.is_null_frame() { + return Ok(None); + } + + Ok(Some(GetResponse::new( + frame.to_string_bytes().ok_or(ResponseTypeError {})?, + ))) + } } impl Command for GetCommand @@ -176,13 +190,7 @@ where } fn eval_response(&self, frame: F) -> Result { - if frame.is_null_frame() { - return Ok(None); - } - - Ok(Some(GetResponse::new( - frame.to_string_bytes().ok_or(ResponseTypeError {})?, - ))) + GetResponse::from_frame(frame) } } diff --git a/src/commands/hget.rs b/src/commands/hget.rs new file mode 100644 index 0000000..4309ab6 --- /dev/null +++ b/src/commands/hget.rs @@ -0,0 +1,69 @@ +use crate::commands::auth::AuthCommand; +use crate::commands::builder::{CommandBuilder, IsNullFrame, ToStringBytes}; +use crate::commands::get::GetResponse; +use crate::commands::hello::HelloCommand; +use crate::commands::{Command, ResponseTypeError}; +use crate::network::protocol::Protocol; +use crate::network::{Client, CommandErrors, Future}; +use bytes::Bytes; +use embedded_nal::TcpClientStack; +use embedded_time::Clock; + +/// Abstraction for HGET command +pub struct HashGetCommand { + /// Hash key + key: Bytes, + + /// Hash field to receive + field: Bytes, +} + +impl HashGetCommand { + pub fn new(key: K, field: F) -> Self + where + Bytes: From, + Bytes: From, + { + Self { + key: key.into(), + field: field.into(), + } + } +} + +impl Command for HashGetCommand +where + F: From + IsNullFrame + ToStringBytes, +{ + type Response = Option; + + fn encode(&self) -> F { + CommandBuilder::new("HGET").arg(&self.key).arg(&self.field).into() + } + + fn eval_response(&self, frame: F) -> Result { + GetResponse::from_frame(frame) + } +} + +impl<'a, N: TcpClientStack, C: Clock, P: Protocol> Client<'a, N, C, P> +where + AuthCommand: Command<

::FrameType>, + HelloCommand: Command<

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

::FrameType: ToStringBytes, +

::FrameType: IsNullFrame, +

::FrameType: From, + Bytes: From, + Bytes: From, + { + self.send(HashGetCommand::new(key, field)) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 27af5ad..ad84a7d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,6 +5,7 @@ pub mod custom; pub mod get; pub mod hello; pub mod helpers; +mod hget; pub mod hset; pub mod ping; pub mod publish; diff --git a/src/commands/tests/hget.rs b/src/commands/tests/hget.rs new file mode 100644 index 0000000..c16275f --- /dev/null +++ b/src/commands/tests/hget.rs @@ -0,0 +1,80 @@ +use crate::commands::helpers::CmdStr; +use crate::commands::hget::HashGetCommand; +use crate::commands::Command; +use redis_protocol::resp2::types::Frame as Resp2Frame; +use redis_protocol::resp3::types::Frame as Resp3Frame; + +#[test] +fn test_encode_resp2() { + let frame: Resp2Frame = HashGetCommand::new("my_hash", "color").encode(); + + assert!(frame.is_array()); + if let Resp2Frame::Array(array) = frame { + assert_eq!(3, array.len()); + assert_eq!("HGET", array[0].to_string().unwrap()); + assert_eq!("my_hash", array[1].to_string().unwrap()); + assert_eq!("color", array[2].to_string().unwrap()); + } +} + +#[test] +fn test_encode_resp3() { + let frame: Resp3Frame = HashGetCommand::new("my_hash", "color").encode(); + + assert!(frame.is_array()); + if let Resp3Frame::Array { data, attributes: _ } = frame { + assert_eq!(3, data.len()); + assert_eq!("HGET", data[0].to_string().unwrap()); + assert_eq!("my_hash", data[1].to_string().unwrap()); + assert_eq!("color", data[2].to_string().unwrap()); + } +} + +#[test] +fn test_eval_response_resp2_key_existing() { + let response = HashGetCommand::new("my_hash", "color") + .eval_response(CmdStr::new("correct response1").to_bulk()) + .unwrap(); + + assert_eq!("correct response1", response.unwrap().as_str().unwrap()); +} + +#[test] +fn test_eval_response_resp3_key_existing() { + let response = HashGetCommand::new("my_hash", "color") + .eval_response(CmdStr::new("correct").to_blob()) + .unwrap(); + + assert_eq!("correct", response.unwrap().as_str().unwrap()); +} + +#[test] +fn test_eval_response_resp2_key_missing() { + let response = HashGetCommand::new("my_hash", "color").eval_response(Resp2Frame::Null).unwrap(); + + assert!(response.is_none()); +} + +#[test] +fn test_eval_response_resp3_key_missing() { + let response = HashGetCommand::new("my_hash", "color").eval_response(Resp3Frame::Null).unwrap(); + + assert!(response.is_none()); +} + +#[test] +fn test_eval_response_resp2_invalid_response() { + let response = HashGetCommand::new("my_hash", "color").eval_response(Resp2Frame::Array(vec![])); + + assert!(response.is_err()); +} + +#[test] +fn test_eval_response_resp3_invalid_response() { + let response = HashGetCommand::new("my_hash", "color").eval_response(Resp3Frame::Array { + data: vec![], + attributes: None, + }); + + assert!(response.is_err()); +} diff --git a/src/commands/tests/mod.rs b/src/commands/tests/mod.rs index 1d3297a..9d16c1b 100644 --- a/src/commands/tests/mod.rs +++ b/src/commands/tests/mod.rs @@ -3,6 +3,7 @@ mod bgsave; mod custom; mod get; pub(crate) mod hello; +mod hget; mod hset; mod ping; mod publish; diff --git a/src/network/tests/client.rs b/src/network/tests/client.rs index f50de27..c4c31ed 100644 --- a/src/network/tests/client.rs +++ b/src/network/tests/client.rs @@ -1054,3 +1054,63 @@ fn test_shorthand_hset_bytes_argument() { .wait() .unwrap(); } + +#[test] +fn test_shorthand_hget_str_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*3\r\n$4\r\nHGET\r\n$7\r\nmy_hash\r\n$5\r\nfield\r\n") + .response_string("test_response") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + assert_eq!( + "test_response", + client + .hget("my_hash", "field") + .unwrap() + .wait() + .unwrap() + .unwrap() + .as_str() + .unwrap() + ); +} + +#[test] +fn test_shorthand_hget_string_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*3\r\n$4\r\nHGET\r\n$7\r\nmy_hash\r\n$5\r\nfield\r\n") + .response_string("test_response") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + let response = client.hget("my_hash".to_string(), "field".to_string()).unwrap().wait(); + assert_eq!("test_response", response.unwrap().unwrap().as_str().unwrap()); +} + +#[test] +fn test_shorthand_hget_bytes_argument() { + let clock = TestClock::new(vec![]); + + let mut network = NetworkMockBuilder::default() + .send(164, "*3\r\n$4\r\nHGET\r\n$7\r\nmy_hash\r\n$5\r\nfield\r\n") + .response_string("test_response") + .into_mock(); + + let mut socket = SocketMock::new(164); + let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {}); + + let response = client + .hget(Bytes::from_static(b"my_hash"), Bytes::from_static(b"field")) + .unwrap() + .wait(); + assert_eq!("test_response", response.unwrap().unwrap().as_str().unwrap()); +}