Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HSET abstraction #9

Merged
merged 2 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
Loading