-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from atlas-aero/hset
HSET abstraction
- Loading branch information
Showing
5 changed files
with
340 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ mod bgsave; | |
mod custom; | ||
mod get; | ||
pub(crate) mod hello; | ||
mod hset; | ||
mod ping; | ||
mod publish; | ||
mod set; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters