redis_oxide/commands/
mod.rs

1//! Command builders for Redis operations
2//!
3//! This module provides type-safe command builders for Redis commands.
4
5use crate::core::{error::RedisResult, value::RespValue};
6use std::time::Duration;
7
8/// Trait for commands that can be executed
9pub trait Command {
10    /// The return type of the command
11    type Output;
12
13    /// Get the command name
14    fn command_name(&self) -> &str;
15
16    /// Get the command arguments
17    fn args(&self) -> Vec<RespValue>;
18
19    /// Parse the response into the output type
20    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output>;
21
22    /// Get the key(s) involved in this command (for cluster routing)
23    fn keys(&self) -> Vec<&[u8]>;
24}
25
26/// GET command builder
27pub struct GetCommand {
28    key: String,
29}
30
31impl GetCommand {
32    /// Create a new GET command
33    pub fn new(key: impl Into<String>) -> Self {
34        Self { key: key.into() }
35    }
36}
37
38impl Command for GetCommand {
39    type Output = Option<String>;
40
41    fn command_name(&self) -> &str {
42        "GET"
43    }
44
45    fn args(&self) -> Vec<RespValue> {
46        vec![RespValue::from(self.key.as_str())]
47    }
48
49    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
50        if response.is_null() {
51            Ok(None)
52        } else {
53            Ok(Some(response.as_string()?))
54        }
55    }
56
57    fn keys(&self) -> Vec<&[u8]> {
58        vec![self.key.as_bytes()]
59    }
60}
61
62/// SET command builder
63pub struct SetCommand {
64    key: String,
65    value: String,
66    expiration: Option<Duration>,
67    nx: bool,
68    xx: bool,
69}
70
71impl SetCommand {
72    /// Create a new SET command
73    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
74        Self {
75            key: key.into(),
76            value: value.into(),
77            expiration: None,
78            nx: false,
79            xx: false,
80        }
81    }
82
83    /// Set expiration time (EX seconds)
84    pub fn expire(mut self, duration: Duration) -> Self {
85        self.expiration = Some(duration);
86        self
87    }
88
89    /// Only set if key doesn't exist (NX)
90    pub fn only_if_not_exists(mut self) -> Self {
91        self.nx = true;
92        self
93    }
94
95    /// Only set if key exists (XX)
96    pub fn only_if_exists(mut self) -> Self {
97        self.xx = true;
98        self
99    }
100}
101
102impl Command for SetCommand {
103    type Output = bool;
104
105    fn command_name(&self) -> &str {
106        "SET"
107    }
108
109    fn args(&self) -> Vec<RespValue> {
110        let mut args = vec![
111            RespValue::from(self.key.as_str()),
112            RespValue::from(self.value.as_str()),
113        ];
114
115        if let Some(duration) = self.expiration {
116            args.push(RespValue::from("EX"));
117            args.push(RespValue::from(duration.as_secs().to_string()));
118        }
119
120        if self.nx {
121            args.push(RespValue::from("NX"));
122        }
123
124        if self.xx {
125            args.push(RespValue::from("XX"));
126        }
127
128        args
129    }
130
131    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
132        match response {
133            RespValue::SimpleString(ref s) if s == "OK" => Ok(true),
134            // NX or XX condition not met
135            _ => Ok(false),
136        }
137    }
138
139    fn keys(&self) -> Vec<&[u8]> {
140        vec![self.key.as_bytes()]
141    }
142}
143
144/// DEL command builder
145pub struct DelCommand {
146    keys: Vec<String>,
147}
148
149impl DelCommand {
150    /// Create a new DEL command
151    pub fn new(keys: Vec<String>) -> Self {
152        Self { keys }
153    }
154}
155
156impl Command for DelCommand {
157    type Output = i64;
158
159    fn command_name(&self) -> &str {
160        "DEL"
161    }
162
163    fn args(&self) -> Vec<RespValue> {
164        self.keys
165            .iter()
166            .map(|k| RespValue::from(k.as_str()))
167            .collect()
168    }
169
170    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
171        response.as_int()
172    }
173
174    fn keys(&self) -> Vec<&[u8]> {
175        self.keys.iter().map(String::as_bytes).collect()
176    }
177}
178
179/// EXISTS command builder
180pub struct ExistsCommand {
181    keys: Vec<String>,
182}
183
184impl ExistsCommand {
185    /// Create a new EXISTS command
186    pub fn new(keys: Vec<String>) -> Self {
187        Self { keys }
188    }
189}
190
191impl Command for ExistsCommand {
192    type Output = i64;
193
194    fn command_name(&self) -> &str {
195        "EXISTS"
196    }
197
198    fn args(&self) -> Vec<RespValue> {
199        self.keys
200            .iter()
201            .map(|k| RespValue::from(k.as_str()))
202            .collect()
203    }
204
205    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
206        response.as_int()
207    }
208
209    fn keys(&self) -> Vec<&[u8]> {
210        self.keys.iter().map(String::as_bytes).collect()
211    }
212}
213
214/// EXPIRE command builder
215pub struct ExpireCommand {
216    key: String,
217    seconds: i64,
218}
219
220impl ExpireCommand {
221    /// Create a new EXPIRE command
222    pub fn new(key: impl Into<String>, duration: Duration) -> Self {
223        #[allow(clippy::cast_possible_wrap)]
224        Self {
225            key: key.into(),
226            seconds: duration.as_secs() as i64,
227        }
228    }
229}
230
231impl Command for ExpireCommand {
232    type Output = bool;
233
234    fn command_name(&self) -> &str {
235        "EXPIRE"
236    }
237
238    fn args(&self) -> Vec<RespValue> {
239        vec![
240            RespValue::from(self.key.as_str()),
241            RespValue::from(self.seconds.to_string()),
242        ]
243    }
244
245    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
246        Ok(response.as_int()? == 1)
247    }
248
249    fn keys(&self) -> Vec<&[u8]> {
250        vec![self.key.as_bytes()]
251    }
252}
253
254/// TTL command builder
255pub struct TtlCommand {
256    key: String,
257}
258
259impl TtlCommand {
260    /// Create a new TTL command
261    pub fn new(key: impl Into<String>) -> Self {
262        Self { key: key.into() }
263    }
264}
265
266impl Command for TtlCommand {
267    type Output = Option<i64>;
268
269    fn command_name(&self) -> &str {
270        "TTL"
271    }
272
273    fn args(&self) -> Vec<RespValue> {
274        vec![RespValue::from(self.key.as_str())]
275    }
276
277    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
278        let ttl = response.as_int()?;
279        if ttl < 0 {
280            Ok(None) // -2 = key doesn't exist, -1 = no expiration
281        } else {
282            Ok(Some(ttl))
283        }
284    }
285
286    fn keys(&self) -> Vec<&[u8]> {
287        vec![self.key.as_bytes()]
288    }
289}
290
291/// INCR command builder
292pub struct IncrCommand {
293    key: String,
294}
295
296impl IncrCommand {
297    /// Create a new INCR command
298    pub fn new(key: impl Into<String>) -> Self {
299        Self { key: key.into() }
300    }
301}
302
303impl Command for IncrCommand {
304    type Output = i64;
305
306    fn command_name(&self) -> &str {
307        "INCR"
308    }
309
310    fn args(&self) -> Vec<RespValue> {
311        vec![RespValue::from(self.key.as_str())]
312    }
313
314    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
315        response.as_int()
316    }
317
318    fn keys(&self) -> Vec<&[u8]> {
319        vec![self.key.as_bytes()]
320    }
321}
322
323/// DECR command builder
324pub struct DecrCommand {
325    key: String,
326}
327
328impl DecrCommand {
329    /// Create a new DECR command
330    pub fn new(key: impl Into<String>) -> Self {
331        Self { key: key.into() }
332    }
333}
334
335impl Command for DecrCommand {
336    type Output = i64;
337
338    fn command_name(&self) -> &str {
339        "DECR"
340    }
341
342    fn args(&self) -> Vec<RespValue> {
343        vec![RespValue::from(self.key.as_str())]
344    }
345
346    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
347        response.as_int()
348    }
349
350    fn keys(&self) -> Vec<&[u8]> {
351        vec![self.key.as_bytes()]
352    }
353}
354
355/// INCRBY command builder
356pub struct IncrByCommand {
357    key: String,
358    increment: i64,
359}
360
361impl IncrByCommand {
362    /// Create a new INCRBY command
363    pub fn new(key: impl Into<String>, increment: i64) -> Self {
364        Self {
365            key: key.into(),
366            increment,
367        }
368    }
369}
370
371impl Command for IncrByCommand {
372    type Output = i64;
373
374    fn command_name(&self) -> &str {
375        "INCRBY"
376    }
377
378    fn args(&self) -> Vec<RespValue> {
379        vec![
380            RespValue::from(self.key.as_str()),
381            RespValue::from(self.increment.to_string()),
382        ]
383    }
384
385    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
386        response.as_int()
387    }
388
389    fn keys(&self) -> Vec<&[u8]> {
390        vec![self.key.as_bytes()]
391    }
392}
393
394/// DECRBY command builder
395pub struct DecrByCommand {
396    key: String,
397    decrement: i64,
398}
399
400impl DecrByCommand {
401    /// Create a new DECRBY command
402    pub fn new(key: impl Into<String>, decrement: i64) -> Self {
403        Self {
404            key: key.into(),
405            decrement,
406        }
407    }
408}
409
410impl Command for DecrByCommand {
411    type Output = i64;
412
413    fn command_name(&self) -> &str {
414        "DECRBY"
415    }
416
417    fn args(&self) -> Vec<RespValue> {
418        vec![
419            RespValue::from(self.key.as_str()),
420            RespValue::from(self.decrement.to_string()),
421        ]
422    }
423
424    fn parse_response(&self, response: RespValue) -> RedisResult<Self::Output> {
425        response.as_int()
426    }
427
428    fn keys(&self) -> Vec<&[u8]> {
429        vec![self.key.as_bytes()]
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_get_command() {
439        let cmd = GetCommand::new("mykey");
440        assert_eq!(cmd.command_name(), "GET");
441        assert_eq!(cmd.keys(), vec![b"mykey"]);
442    }
443
444    #[test]
445    fn test_set_command_basic() {
446        let cmd = SetCommand::new("key", "value");
447        assert_eq!(cmd.command_name(), "SET");
448        let args = cmd.args();
449        assert_eq!(args.len(), 2);
450    }
451
452    #[test]
453    fn test_set_command_with_expiration() {
454        let cmd = SetCommand::new("key", "value").expire(Duration::from_secs(60));
455        let args = cmd.args();
456        assert_eq!(args.len(), 4); // key, value, EX, 60
457    }
458
459    #[test]
460    fn test_set_command_nx() {
461        let cmd = SetCommand::new("key", "value").only_if_not_exists();
462        let args = cmd.args();
463        assert!(args.len() >= 3); // key, value, NX
464    }
465
466    #[test]
467    fn test_del_command() {
468        let cmd = DelCommand::new(vec!["key1".to_string(), "key2".to_string()]);
469        assert_eq!(cmd.command_name(), "DEL");
470        assert_eq!(cmd.keys().len(), 2);
471    }
472
473    #[test]
474    fn test_incr_command() {
475        let cmd = IncrCommand::new("counter");
476        assert_eq!(cmd.command_name(), "INCR");
477        assert_eq!(cmd.keys(), vec![b"counter"]);
478    }
479}