embedded_redis/commands/
builder.rs

1//! Builder for constructing RESP2/3 frames
2//!
3//! Generic wrapper mainly used as helper for creating RESP frames.
4//! However, it can also be used to execute custom/arbitrary commands. See [CustomCommand](crate::commands::custom)  for more details.
5//!
6//! # Creating generic frames
7//! The following example demonstrates the creation of command frame for [HGET](https://redis.io/commands/hget/).
8//! ```
9//! use embedded_redis::commands::builder::CommandBuilder;
10//! use redis_protocol::resp2::types::BytesFrame as Resp2Frame;
11//!
12//! let _frame: Resp2Frame = CommandBuilder::new("HGET")
13//!     .arg_static("field1")
14//!     .arg_static("foo")
15//!     .into();
16//! ```
17//! # Improved performance
18//! For best performance, especially with large amounts of data, it is recommended to use [Bytes](<https://docs.rs/bytes/latest/bytes/>).
19//! ```
20//!# use bytes::Bytes;
21//! use embedded_redis::commands::builder::CommandBuilder;
22//!# use redis_protocol::resp2::types::BytesFrame as Resp2Frame;
23//!#
24//! // Using Bytes avoids data copy, as clone() is shallow
25//! let value = Bytes::from_static("Large value".as_bytes());
26//!
27//! let _frame: Resp2Frame = CommandBuilder::new("HSET")
28//!     .arg_static("myhash")
29//!     .arg_static("field1")
30//!     .arg(&value)
31//!     .into();
32//! ```
33use crate::commands::custom::CustomCommand;
34use alloc::collections::BTreeMap;
35use alloc::string::{String, ToString};
36use alloc::vec;
37use alloc::vec::Vec;
38use bytes::Bytes;
39use redis_protocol::resp2::types::{BytesFrame as Resp2Frame, Resp2Frame as _};
40use redis_protocol::resp3::types::{BytesFrame as Resp3Frame, Resp3Frame as _};
41
42/// Builder for constructing RESP2/3 frames
43#[derive(Clone, Default)]
44pub struct CommandBuilder {
45    pub(crate) elements: Vec<Bytes>,
46}
47
48impl CommandBuilder {
49    pub fn new(keyword: &'static str) -> Self {
50        CommandBuilder {
51            elements: vec![Bytes::from_static(keyword.as_bytes())],
52        }
53    }
54
55    /// Converts builder to command ready for being sent by Client
56    pub fn to_command(self) -> CustomCommand {
57        self.into()
58    }
59
60    /// Adds a static argument
61    pub fn arg_static(mut self, arg: &'static str) -> Self {
62        self.elements.push(Bytes::from_static(arg.as_bytes()));
63        self
64    }
65
66    /// Adds a static argument
67    pub fn arg_static_option(mut self, arg: Option<&'static str>) -> Self {
68        if let Some(arg_str) = arg {
69            self.elements.push(Bytes::from_static(arg_str.as_bytes()));
70        }
71        self
72    }
73
74    /// Adds cased string of uint
75    pub fn arg_uint(mut self, arg: usize) -> Self {
76        self.elements.push(Bytes::from(arg.to_string()));
77        self
78    }
79
80    /// Adds a byte argument
81    /// Note: Besides static, the most efficient way caused by the nature how Bytes cloning is working
82    pub fn arg(mut self, arg: &Bytes) -> Self {
83        self.elements.push(arg.clone());
84        self
85    }
86
87    /// Just adding byte if option is Some
88    pub fn arg_option(mut self, arg: Option<&Bytes>) -> Self {
89        if let Some(inner) = arg {
90            self.elements.push(inner.clone());
91        }
92        self
93    }
94}
95
96impl From<CommandBuilder> for Resp2Frame {
97    fn from(builder: CommandBuilder) -> Self {
98        let mut frames = Vec::with_capacity(builder.elements.len());
99        for byte in builder.elements {
100            frames.push(Resp2Frame::BulkString(byte));
101        }
102
103        Resp2Frame::Array(frames)
104    }
105}
106
107impl From<CommandBuilder> for Resp3Frame {
108    fn from(builder: CommandBuilder) -> Self {
109        let mut frames = Vec::with_capacity(builder.elements.len());
110        for byte in builder.elements {
111            frames.push(Resp3Frame::BlobString {
112                data: byte,
113                attributes: None,
114            });
115        }
116
117        Resp3Frame::Array {
118            data: frames,
119            attributes: None,
120        }
121    }
122}
123
124impl From<CommandBuilder> for CustomCommand {
125    fn from(builder: CommandBuilder) -> Self {
126        CustomCommand::new(builder)
127    }
128}
129
130/// Unification for `to_string()` of RESP2/3 frames
131pub trait ToStringOption {
132    fn to_string_option(&self) -> Option<String>;
133}
134
135impl ToStringOption for Resp2Frame {
136    fn to_string_option(&self) -> Option<String> {
137        self.to_string()
138    }
139}
140
141impl ToStringOption for Resp3Frame {
142    fn to_string_option(&self) -> Option<String> {
143        self.to_string()
144    }
145}
146
147/// Unification for null check of RESP2/3 frames
148pub trait IsNullFrame {
149    fn is_null_frame(&self) -> bool;
150}
151
152impl IsNullFrame for Resp2Frame {
153    fn is_null_frame(&self) -> bool {
154        self == &Resp2Frame::Null
155    }
156}
157
158impl IsNullFrame for Resp3Frame {
159    fn is_null_frame(&self) -> bool {
160        self == &Resp3Frame::Null
161    }
162}
163
164/// Unification for extracting integer value of Frames
165pub trait ToInteger {
166    /// Returns the inner integer value, None in case frame is not integer type
167    fn to_integer(&self) -> Option<i64>;
168}
169
170impl ToInteger for Resp2Frame {
171    fn to_integer(&self) -> Option<i64> {
172        match self {
173            Resp2Frame::Integer(number) => Some(*number),
174            _ => None,
175        }
176    }
177}
178
179impl ToInteger for Resp3Frame {
180    fn to_integer(&self) -> Option<i64> {
181        match self {
182            Resp3Frame::Number { data, attributes: _ } => Some(*data),
183            _ => None,
184        }
185    }
186}
187
188/// Trait for string extraction of RESP2/3 frames
189pub trait ToStringBytes {
190    /// Extracts Bytes of Bulk (RESP2) or BLOB (RESP3) frames
191    /// None if frame was not Bulk/BLOB string
192    fn to_string_bytes(&self) -> Option<Bytes>;
193}
194
195impl ToStringBytes for Resp2Frame {
196    fn to_string_bytes(&self) -> Option<Bytes> {
197        match self {
198            Resp2Frame::BulkString(data) => Some(data.clone()),
199            _ => None,
200        }
201    }
202}
203
204impl ToStringBytes for Resp3Frame {
205    fn to_string_bytes(&self) -> Option<Bytes> {
206        match self {
207            Resp3Frame::BlobString { data, attributes: _ } => Some(data.clone()),
208            _ => None,
209        }
210    }
211}
212
213/// Trait for converting RESP2 arrays or RESP3 maps
214pub trait ToBytesMap {
215    /// Converts the frame to map
216    /// Returns None in case of protocol violation
217    fn to_map(&self) -> Option<BTreeMap<Bytes, Bytes>>;
218}
219
220impl ToBytesMap for Resp2Frame {
221    fn to_map(&self) -> Option<BTreeMap<Bytes, Bytes>> {
222        let mut map = BTreeMap::new();
223
224        match self {
225            Resp2Frame::Array(array) => {
226                for item in array.chunks(2) {
227                    if item.len() < 2 {
228                        return None;
229                    }
230
231                    let field = match &item[0] {
232                        Resp2Frame::SimpleString(value) | Resp2Frame::BulkString(value) => value.clone(),
233                        _ => return None,
234                    };
235
236                    let value = match &item[1] {
237                        Resp2Frame::SimpleString(value) | Resp2Frame::BulkString(value) => value.clone(),
238                        _ => return None,
239                    };
240
241                    map.insert(field, value);
242                }
243            }
244            _ => return None,
245        }
246
247        Some(map)
248    }
249}
250
251impl ToBytesMap for Resp3Frame {
252    fn to_map(&self) -> Option<BTreeMap<Bytes, Bytes>> {
253        let mut map = BTreeMap::new();
254
255        match self {
256            Resp3Frame::Map { data, attributes: _ } => {
257                for item in data {
258                    let field = match item.0 {
259                        Resp3Frame::BlobString { data, attributes: _ }
260                        | Resp3Frame::SimpleString { data, attributes: _ } => data.clone(),
261                        _ => return None,
262                    };
263
264                    let value = match item.1 {
265                        Resp3Frame::BlobString { data, attributes: _ }
266                        | Resp3Frame::SimpleString { data, attributes: _ } => data.clone(),
267                        _ => return None,
268                    };
269
270                    map.insert(field, value);
271                }
272            }
273            _ => return None,
274        }
275
276        Some(map)
277    }
278}