Skip to content

Commit

Permalink
Merge pull request #9 from atlas-aero/hset
Browse files Browse the repository at this point in the history
HSET abstraction
  • Loading branch information
marius-meissner authored Feb 3, 2024
2 parents 263609b + d6f0795 commit 74e26af
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 1 deletion.
159 changes: 159 additions & 0 deletions src/commands/hset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! Abstraction of HSET command.
//!
//! For general information about this command, see the [Redis documentation](<https://redis.io/commands/hset/>).
//!
//! # 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::hset::HashSetCommand;
//!# use embedded_redis::commands::publish::PublishCommand;
//!# 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.send(CommandBuilder::new("DEL").arg_static("my_hash").to_command()).unwrap().wait().unwrap();
//!
//! let command = HashSetCommand::new("my_hash".into(), "color".into(), "green".into());
//! let response = client.send(command).unwrap().wait().unwrap();
//!
//! // Returns the number of added fields
//! assert_eq!(1, response)
//! ```
//! # Setting multiple fields at once
//! ```
//!# 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::hset::HashSetCommand;
//!# use embedded_redis::commands::publish::PublishCommand;
//!# 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.send(CommandBuilder::new("DEL").arg_static("my_hash").to_command()).unwrap().wait().unwrap();
//!#
//! let command = HashSetCommand::multiple("my_hash".into(), [
//! ("color".into(), "green".into()),
//! ("material".into(), "stone".into())
//! ]);
//! let response = client.send(command).unwrap().wait().unwrap();
//!
//! // Returns the number of added fields
//! assert_eq!(2, response)
//! ```
//! # Shorthand
//! [Client](Client#method.hset) 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::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(SetCommand::new("test_key", "test_value")).unwrap().wait();
//!#
//! // Using &str arguments
//! let _ = client.hset("hash", "field", "value");
//!
//! // Using String arguments
//! let _ = client.hset("hash".to_string(), "field".to_string(), "value".to_string());
//!
//! // Using Bytes arguments
//! let _ = client.hset(Bytes::from_static(b"hash"), Bytes::from_static(b"field"), Bytes::from_static(b"value"));
//! ```
use crate::commands::auth::AuthCommand;
use crate::commands::builder::{CommandBuilder, ToInteger};
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 of HSET command
pub struct HashSetCommand<const N: usize> {
/// Hash key
key: Bytes,

/// Field/Value paris
fields: [(Bytes, Bytes); N],
}

impl HashSetCommand<1> {
pub fn new(key: Bytes, field: Bytes, value: Bytes) -> Self {
Self {
key,
fields: [(field, value)],
}
}
}

impl<const N: usize> HashSetCommand<N> {
/// Constructs a new command with multiple field/value paris
pub fn multiple(key: Bytes, fields: [(Bytes, Bytes); N]) -> Self {
Self { key, fields }
}
}

impl<F: From<CommandBuilder> + ToInteger, const N: usize> Command<F> for HashSetCommand<N> {
type Response = i64;

fn encode(&self) -> F {
let mut builder = CommandBuilder::new("HSET").arg(&self.key);

for (field, value) in &self.fields {
builder = builder.arg(field).arg(value);
}

builder.into()
}

fn eval_response(&self, frame: F) -> Result<Self::Response, ResponseTypeError> {
frame.to_integer().ok_or(ResponseTypeError {})
}
}

impl<'a, N: TcpClientStack, C: Clock, P: Protocol> Client<'a, N, C, P>
where
AuthCommand: Command<<P as Protocol>::FrameType>,
HelloCommand: Command<<P as Protocol>::FrameType>,
{
/// Shorthand for [HashSetCommand]
/// For setting multiple fields, use [HashSetCommand] directly instead
pub fn hset<K, F, V>(
&'a self,
key: K,
field: F,
value: V,
) -> Result<Future<'a, N, C, P, HashSetCommand<1>>, CommandErrors>
where
Bytes: From<K>,
Bytes: From<F>,
Bytes: From<V>,
<P as Protocol>::FrameType: ToInteger,
<P as Protocol>::FrameType: From<CommandBuilder>,
{
self.send(HashSetCommand::new(key.into(), field.into(), value.into()))
}
}
2 changes: 1 addition & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ pub mod custom;
pub mod get;
pub mod hello;
pub mod helpers;
pub mod hset;
pub mod ping;
pub mod publish;
pub mod set;

#[cfg(test)]
pub(crate) mod tests;

Expand Down
113 changes: 113 additions & 0 deletions src/commands/tests/hset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use crate::commands::hset::HashSetCommand;
use crate::commands::Command;
use redis_protocol::resp2::types::Frame as Resp2Frame;
use redis_protocol::resp3::types::Frame as Resp3Frame;

#[test]
fn test_encode_single_field_resp2() {
let frame: Resp2Frame = HashSetCommand::new("my_hash".into(), "color".into(), "green".into()).encode();

assert!(frame.is_array());
if let Resp2Frame::Array(array) = frame {
assert_eq!(4, array.len());
assert_eq!("HSET", array[0].to_string().unwrap());
assert_eq!("my_hash", array[1].to_string().unwrap());
assert_eq!("color", array[2].to_string().unwrap());
assert_eq!("green", array[3].to_string().unwrap());
}
}

#[test]
fn test_encode_single_field_resp3() {
let frame: Resp3Frame = HashSetCommand::new("my_hash".into(), "color".into(), "green".into()).encode();

if let Resp3Frame::Array { data, attributes: _ } = frame {
assert_eq!(4, data.len());
assert_eq!("HSET", data[0].to_string().unwrap());
assert_eq!("my_hash", data[1].to_string().unwrap());
assert_eq!("color", data[2].to_string().unwrap());
assert_eq!("green", data[3].to_string().unwrap());
}
}

#[test]
fn test_encode_multiple_fields_resp2() {
let frame: Resp2Frame = HashSetCommand::multiple(
"my_hash".into(),
[
("gender".into(), "male".into()),
("material".into(), "wood".into()),
],
)
.encode();

if let Resp2Frame::Array(array) = frame {
assert_eq!(6, array.len());
assert_eq!("HSET", array[0].to_string().unwrap());
assert_eq!("my_hash", array[1].to_string().unwrap());
assert_eq!("gender", array[2].to_string().unwrap());
assert_eq!("male", array[3].to_string().unwrap());
assert_eq!("material", array[4].to_string().unwrap());
assert_eq!("wood", array[5].to_string().unwrap());
}
}

#[test]
fn test_encode_multiple_fields_resp3() {
let frame: Resp3Frame = HashSetCommand::multiple(
"my_hash".into(),
[
("gender".into(), "male".into()),
("material".into(), "wood".into()),
],
)
.encode();

if let Resp3Frame::Array { data, attributes: _ } = frame {
assert_eq!(6, data.len());
assert_eq!("HSET", data[0].to_string().unwrap());
assert_eq!("my_hash", data[1].to_string().unwrap());
assert_eq!("gender", data[2].to_string().unwrap());
assert_eq!("male", data[3].to_string().unwrap());
assert_eq!("material", data[4].to_string().unwrap());
assert_eq!("wood", data[5].to_string().unwrap());
}
}

#[test]
fn test_eval_response_resp2_success() {
let command = HashSetCommand::new("my_hash".into(), "color".into(), "green".into());
let response = command.eval_response(Resp2Frame::Integer(2));

assert_eq!(2, response.unwrap());
}

#[test]
fn test_eval_response_resp3_success() {
let command = HashSetCommand::new("my_hash".into(), "color".into(), "green".into());
let response = command.eval_response(Resp3Frame::Number {
data: 3,
attributes: None,
});

assert_eq!(3, response.unwrap());
}

#[test]
fn test_eval_response_resp2_invalid_response() {
let command = HashSetCommand::new("my_hash".into(), "color".into(), "green".into());
let response = command.eval_response(Resp2Frame::BulkString("3".into()));

assert!(response.is_err());
}

#[test]
fn test_eval_response_resp3_invalid_response() {
let command = HashSetCommand::new("my_hash".into(), "color".into(), "green".into());
let response = command.eval_response(Resp3Frame::BlobString {
data: "test".into(),
attributes: None,
});

assert!(response.is_err());
}
1 change: 1 addition & 0 deletions src/commands/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod bgsave;
mod custom;
mod get;
pub(crate) mod hello;
mod hset;
mod ping;
mod publish;
mod set;
66 changes: 66 additions & 0 deletions src/network/tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,69 @@ fn test_shorthand_bgsave_scheduled() {

client.bgsave(true).unwrap().wait().unwrap();
}

#[test]
fn test_shorthand_hset_str_argument() {
let clock = TestClock::new(vec![]);

let mut network = NetworkMockBuilder::default()
.send(
164,
"*4\r\n$4\r\nHSET\r\n$7\r\nmy_hash\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n",
)
.response(":1\r\n")
.into_mock();

let mut socket = SocketMock::new(164);
let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {});

client.hset("my_hash", "color", "green").unwrap().wait().unwrap();
}

#[test]
fn test_shorthand_hset_string_argument() {
let clock = TestClock::new(vec![]);

let mut network = NetworkMockBuilder::default()
.send(
164,
"*4\r\n$4\r\nHSET\r\n$7\r\nmy_hash\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n",
)
.response(":1\r\n")
.into_mock();

let mut socket = SocketMock::new(164);
let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {});

client
.hset("my_hash".to_string(), "color".to_string(), "green".to_string())
.unwrap()
.wait()
.unwrap();
}

#[test]
fn test_shorthand_hset_bytes_argument() {
let clock = TestClock::new(vec![]);

let mut network = NetworkMockBuilder::default()
.send(
164,
"*4\r\n$4\r\nHSET\r\n$7\r\nmy_hash\r\n$5\r\ncolor\r\n$5\r\ngreen\r\n",
)
.response(":1\r\n")
.into_mock();

let mut socket = SocketMock::new(164);
let client = create_mocked_client(&mut network, &mut socket, &clock, Resp2 {});

client
.hset(
Bytes::from_static(b"my_hash"),
Bytes::from_static(b"color"),
Bytes::from_static(b"green"),
)
.unwrap()
.wait()
.unwrap();
}

0 comments on commit 74e26af

Please sign in to comment.