Skip to content

Commit

Permalink
Implemented abstraction for HGETALL command
Browse files Browse the repository at this point in the history
  • Loading branch information
marius-meissner committed Feb 3, 2024
1 parent 73b24ef commit 7e7d640
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/commands/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<BTreeMap<Bytes, Bytes>>;
}

impl ToBytesMap for Resp2Frame {
fn to_map(&self) -> Option<BTreeMap<Bytes, Bytes>> {
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<BTreeMap<Bytes, Bytes>> {
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)
}
}
95 changes: 95 additions & 0 deletions src/commands/hgetall.rs
Original file line number Diff line number Diff line change
@@ -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<K>(key: K) -> Self
where
Bytes: From<K>,
{
Self { key: key.into() }
}
}

pub struct HashResponse {
/// Field/Value map
inner: BTreeMap<Bytes, Bytes>,
}

impl HashResponse {
/// Extracts inner map
#[allow(clippy::wrong_self_convention)]
pub fn to_map(self) -> BTreeMap<Bytes, Bytes> {
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<F>(&self, field: F) -> Option<&str>
where
Bytes: From<F>,
{
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<F> Command<F> for HashGetAllCommand
where
F: From<CommandBuilder> + ToBytesMap,
{
type Response = Option<HashResponse>;

fn encode(&self) -> F {
CommandBuilder::new("HGETALL").arg(&self.key).into()
}

fn eval_response(&self, frame: F) -> Result<Self::Response, ResponseTypeError> {
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<<P as Protocol>::FrameType>,
HelloCommand: Command<<P as Protocol>::FrameType>,
{
/// Shorthand for [HashGetAllCommand]
pub fn hgetall<K>(&'a self, key: K) -> Result<Future<'a, N, C, P, HashGetAllCommand>, CommandErrors>
where
<P as Protocol>::FrameType: ToBytesMap,
<P as Protocol>::FrameType: From<CommandBuilder>,
Bytes: From<K>,
{
self.send(HashGetAllCommand::new(key))
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
184 changes: 184 additions & 0 deletions src/commands/tests/builder.rs
Original file line number Diff line number Diff line change
@@ -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());
}
Loading

0 comments on commit 7e7d640

Please sign in to comment.