Skip to main content

ember_protocol/
command.rs

1//! Command parsing from RESP3 frames.
2//!
3//! Converts a parsed [`Frame`] (expected to be an array) into a typed
4//! [`Command`] enum. This keeps protocol-level concerns separate from
5//! the engine that actually executes commands.
6
7use bytes::Bytes;
8
9use crate::error::ProtocolError;
10use crate::types::Frame;
11
12/// Expiration option for the SET command.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum SetExpire {
15    /// EX seconds — expire after N seconds.
16    Ex(u64),
17    /// PX milliseconds — expire after N milliseconds.
18    Px(u64),
19}
20
21/// A parsed client command, ready for execution.
22#[derive(Debug, Clone, PartialEq)]
23pub enum Command {
24    /// PING with an optional message. Returns PONG or echoes the message.
25    Ping(Option<Bytes>),
26
27    /// ECHO <message>. Returns the message back to the client.
28    Echo(Bytes),
29
30    /// GET <key>. Returns the value or nil.
31    Get { key: String },
32
33    /// SET <key> <value> [EX seconds | PX milliseconds] [NX | XX].
34    Set {
35        key: String,
36        value: Bytes,
37        expire: Option<SetExpire>,
38        /// Only set the key if it does not already exist.
39        nx: bool,
40        /// Only set the key if it already exists.
41        xx: bool,
42    },
43
44    /// INCR <key>. Increments the integer value of a key by 1.
45    Incr { key: String },
46
47    /// DECR <key>. Decrements the integer value of a key by 1.
48    Decr { key: String },
49
50    /// DEL <key> [key ...]. Returns the number of keys removed.
51    Del { keys: Vec<String> },
52
53    /// EXISTS <key> [key ...]. Returns the number of keys that exist.
54    Exists { keys: Vec<String> },
55
56    /// MGET <key> [key ...]. Returns the values for all specified keys.
57    MGet { keys: Vec<String> },
58
59    /// MSET <key> <value> [key value ...]. Sets multiple key-value pairs.
60    MSet { pairs: Vec<(String, Bytes)> },
61
62    /// EXPIRE <key> <seconds>. Sets a TTL on an existing key.
63    Expire { key: String, seconds: u64 },
64
65    /// TTL <key>. Returns remaining time-to-live in seconds.
66    Ttl { key: String },
67
68    /// PERSIST <key>. Removes the expiration from a key.
69    Persist { key: String },
70
71    /// PTTL <key>. Returns remaining time-to-live in milliseconds.
72    Pttl { key: String },
73
74    /// PEXPIRE <key> <milliseconds>. Sets a TTL in milliseconds on an existing key.
75    Pexpire { key: String, milliseconds: u64 },
76
77    /// DBSIZE. Returns the number of keys in the database.
78    DbSize,
79
80    /// INFO [section]. Returns server info. Currently only supports "keyspace".
81    Info { section: Option<String> },
82
83    /// BGSAVE. Triggers a background snapshot.
84    BgSave,
85
86    /// BGREWRITEAOF. Triggers an AOF rewrite (snapshot + truncate).
87    BgRewriteAof,
88
89    /// FLUSHDB. Removes all keys from the database.
90    FlushDb,
91
92    /// SCAN <cursor> [MATCH pattern] [COUNT count]. Iterates keys.
93    Scan {
94        cursor: u64,
95        pattern: Option<String>,
96        count: Option<usize>,
97    },
98
99    /// LPUSH <key> <value> [value ...]. Pushes values to the head of a list.
100    LPush { key: String, values: Vec<Bytes> },
101
102    /// RPUSH <key> <value> [value ...]. Pushes values to the tail of a list.
103    RPush { key: String, values: Vec<Bytes> },
104
105    /// LPOP <key>. Pops a value from the head of a list.
106    LPop { key: String },
107
108    /// RPOP <key>. Pops a value from the tail of a list.
109    RPop { key: String },
110
111    /// LRANGE <key> <start> <stop>. Returns a range of elements by index.
112    LRange { key: String, start: i64, stop: i64 },
113
114    /// LLEN <key>. Returns the length of a list.
115    LLen { key: String },
116
117    /// TYPE <key>. Returns the type of the value stored at key.
118    Type { key: String },
119
120    /// ZADD <key> [NX|XX] [GT|LT] [CH] <score> <member> [score member ...].
121    ZAdd {
122        key: String,
123        flags: ZAddFlags,
124        members: Vec<(f64, String)>,
125    },
126
127    /// ZREM <key> <member> [member ...]. Removes members from a sorted set.
128    ZRem { key: String, members: Vec<String> },
129
130    /// ZSCORE <key> <member>. Returns the score of a member.
131    ZScore { key: String, member: String },
132
133    /// ZRANK <key> <member>. Returns the rank of a member (0-based).
134    ZRank { key: String, member: String },
135
136    /// ZCARD <key>. Returns the cardinality (number of members) of a sorted set.
137    ZCard { key: String },
138
139    /// ZRANGE <key> <start> <stop> [WITHSCORES]. Returns a range by rank.
140    ZRange {
141        key: String,
142        start: i64,
143        stop: i64,
144        with_scores: bool,
145    },
146
147    /// A command we don't recognize (yet).
148    Unknown(String),
149}
150
151/// Flags for the ZADD command.
152#[derive(Debug, Clone, Default, PartialEq)]
153pub struct ZAddFlags {
154    /// Only add new members, don't update existing scores.
155    pub nx: bool,
156    /// Only update existing members, don't add new ones.
157    pub xx: bool,
158    /// Only update when new score > current score.
159    pub gt: bool,
160    /// Only update when new score < current score.
161    pub lt: bool,
162    /// Return count of changed members (added + updated) instead of just added.
163    pub ch: bool,
164}
165
166impl Eq for ZAddFlags {}
167
168impl Command {
169    /// Parses a [`Frame`] into a [`Command`].
170    ///
171    /// Expects an array frame where the first element is the command name
172    /// (as a bulk or simple string) and the rest are arguments.
173    pub fn from_frame(frame: Frame) -> Result<Command, ProtocolError> {
174        let frames = match frame {
175            Frame::Array(frames) => frames,
176            _ => {
177                return Err(ProtocolError::InvalidCommandFrame(
178                    "expected array frame".into(),
179                ));
180            }
181        };
182
183        if frames.is_empty() {
184            return Err(ProtocolError::InvalidCommandFrame(
185                "empty command array".into(),
186            ));
187        }
188
189        let name = extract_string(&frames[0])?;
190        let name_upper = name.to_ascii_uppercase();
191
192        match name_upper.as_str() {
193            "PING" => parse_ping(&frames[1..]),
194            "ECHO" => parse_echo(&frames[1..]),
195            "GET" => parse_get(&frames[1..]),
196            "SET" => parse_set(&frames[1..]),
197            "INCR" => parse_incr(&frames[1..]),
198            "DECR" => parse_decr(&frames[1..]),
199            "DEL" => parse_del(&frames[1..]),
200            "EXISTS" => parse_exists(&frames[1..]),
201            "MGET" => parse_mget(&frames[1..]),
202            "MSET" => parse_mset(&frames[1..]),
203            "EXPIRE" => parse_expire(&frames[1..]),
204            "TTL" => parse_ttl(&frames[1..]),
205            "PERSIST" => parse_persist(&frames[1..]),
206            "PTTL" => parse_pttl(&frames[1..]),
207            "PEXPIRE" => parse_pexpire(&frames[1..]),
208            "DBSIZE" => parse_dbsize(&frames[1..]),
209            "INFO" => parse_info(&frames[1..]),
210            "BGSAVE" => parse_bgsave(&frames[1..]),
211            "BGREWRITEAOF" => parse_bgrewriteaof(&frames[1..]),
212            "FLUSHDB" => parse_flushdb(&frames[1..]),
213            "SCAN" => parse_scan(&frames[1..]),
214            "LPUSH" => parse_lpush(&frames[1..]),
215            "RPUSH" => parse_rpush(&frames[1..]),
216            "LPOP" => parse_lpop(&frames[1..]),
217            "RPOP" => parse_rpop(&frames[1..]),
218            "LRANGE" => parse_lrange(&frames[1..]),
219            "LLEN" => parse_llen(&frames[1..]),
220            "TYPE" => parse_type(&frames[1..]),
221            "ZADD" => parse_zadd(&frames[1..]),
222            "ZREM" => parse_zrem(&frames[1..]),
223            "ZSCORE" => parse_zscore(&frames[1..]),
224            "ZRANK" => parse_zrank(&frames[1..]),
225            "ZCARD" => parse_zcard(&frames[1..]),
226            "ZRANGE" => parse_zrange(&frames[1..]),
227            _ => Ok(Command::Unknown(name)),
228        }
229    }
230}
231
232/// Extracts a UTF-8 string from a Bulk or Simple frame.
233fn extract_string(frame: &Frame) -> Result<String, ProtocolError> {
234    match frame {
235        Frame::Bulk(data) => String::from_utf8(data.to_vec()).map_err(|_| {
236            ProtocolError::InvalidCommandFrame("command name is not valid utf-8".into())
237        }),
238        Frame::Simple(s) => Ok(s.clone()),
239        _ => Err(ProtocolError::InvalidCommandFrame(
240            "expected bulk or simple string for command name".into(),
241        )),
242    }
243}
244
245/// Extracts raw bytes from a Bulk or Simple frame.
246fn extract_bytes(frame: &Frame) -> Result<Bytes, ProtocolError> {
247    match frame {
248        Frame::Bulk(data) => Ok(data.clone()),
249        Frame::Simple(s) => Ok(Bytes::from(s.clone().into_bytes())),
250        _ => Err(ProtocolError::InvalidCommandFrame(
251            "expected bulk or simple string argument".into(),
252        )),
253    }
254}
255
256/// Parses a string argument as a positive u64.
257fn parse_u64(frame: &Frame, cmd: &str) -> Result<u64, ProtocolError> {
258    let s = extract_string(frame)?;
259    s.parse::<u64>().map_err(|_| {
260        ProtocolError::InvalidCommandFrame(format!("value is not a valid integer for '{cmd}'"))
261    })
262}
263
264fn parse_ping(args: &[Frame]) -> Result<Command, ProtocolError> {
265    match args.len() {
266        0 => Ok(Command::Ping(None)),
267        1 => {
268            let msg = extract_bytes(&args[0])?;
269            Ok(Command::Ping(Some(msg)))
270        }
271        _ => Err(ProtocolError::WrongArity("PING".into())),
272    }
273}
274
275fn parse_echo(args: &[Frame]) -> Result<Command, ProtocolError> {
276    if args.len() != 1 {
277        return Err(ProtocolError::WrongArity("ECHO".into()));
278    }
279    let msg = extract_bytes(&args[0])?;
280    Ok(Command::Echo(msg))
281}
282
283fn parse_get(args: &[Frame]) -> Result<Command, ProtocolError> {
284    if args.len() != 1 {
285        return Err(ProtocolError::WrongArity("GET".into()));
286    }
287    let key = extract_string(&args[0])?;
288    Ok(Command::Get { key })
289}
290
291fn parse_set(args: &[Frame]) -> Result<Command, ProtocolError> {
292    if args.len() < 2 {
293        return Err(ProtocolError::WrongArity("SET".into()));
294    }
295
296    let key = extract_string(&args[0])?;
297    let value = extract_bytes(&args[1])?;
298
299    let mut expire = None;
300    let mut nx = false;
301    let mut xx = false;
302    let mut idx = 2;
303
304    while idx < args.len() {
305        let flag = extract_string(&args[idx])?.to_ascii_uppercase();
306        match flag.as_str() {
307            "NX" => {
308                nx = true;
309                idx += 1;
310            }
311            "XX" => {
312                xx = true;
313                idx += 1;
314            }
315            "EX" => {
316                idx += 1;
317                if idx >= args.len() {
318                    return Err(ProtocolError::WrongArity("SET".into()));
319                }
320                let amount = parse_u64(&args[idx], "SET")?;
321                if amount == 0 {
322                    return Err(ProtocolError::InvalidCommandFrame(
323                        "invalid expire time in 'SET' command".into(),
324                    ));
325                }
326                expire = Some(SetExpire::Ex(amount));
327                idx += 1;
328            }
329            "PX" => {
330                idx += 1;
331                if idx >= args.len() {
332                    return Err(ProtocolError::WrongArity("SET".into()));
333                }
334                let amount = parse_u64(&args[idx], "SET")?;
335                if amount == 0 {
336                    return Err(ProtocolError::InvalidCommandFrame(
337                        "invalid expire time in 'SET' command".into(),
338                    ));
339                }
340                expire = Some(SetExpire::Px(amount));
341                idx += 1;
342            }
343            _ => {
344                return Err(ProtocolError::InvalidCommandFrame(format!(
345                    "unsupported SET option '{flag}'"
346                )));
347            }
348        }
349    }
350
351    if nx && xx {
352        return Err(ProtocolError::InvalidCommandFrame(
353            "XX and NX options at the same time are not compatible".into(),
354        ));
355    }
356
357    Ok(Command::Set {
358        key,
359        value,
360        expire,
361        nx,
362        xx,
363    })
364}
365
366fn parse_incr(args: &[Frame]) -> Result<Command, ProtocolError> {
367    if args.len() != 1 {
368        return Err(ProtocolError::WrongArity("INCR".into()));
369    }
370    let key = extract_string(&args[0])?;
371    Ok(Command::Incr { key })
372}
373
374fn parse_decr(args: &[Frame]) -> Result<Command, ProtocolError> {
375    if args.len() != 1 {
376        return Err(ProtocolError::WrongArity("DECR".into()));
377    }
378    let key = extract_string(&args[0])?;
379    Ok(Command::Decr { key })
380}
381
382fn parse_del(args: &[Frame]) -> Result<Command, ProtocolError> {
383    if args.is_empty() {
384        return Err(ProtocolError::WrongArity("DEL".into()));
385    }
386    let keys = args
387        .iter()
388        .map(extract_string)
389        .collect::<Result<Vec<_>, _>>()?;
390    Ok(Command::Del { keys })
391}
392
393fn parse_exists(args: &[Frame]) -> Result<Command, ProtocolError> {
394    if args.is_empty() {
395        return Err(ProtocolError::WrongArity("EXISTS".into()));
396    }
397    let keys = args
398        .iter()
399        .map(extract_string)
400        .collect::<Result<Vec<_>, _>>()?;
401    Ok(Command::Exists { keys })
402}
403
404fn parse_mget(args: &[Frame]) -> Result<Command, ProtocolError> {
405    if args.is_empty() {
406        return Err(ProtocolError::WrongArity("MGET".into()));
407    }
408    let keys = args
409        .iter()
410        .map(extract_string)
411        .collect::<Result<Vec<_>, _>>()?;
412    Ok(Command::MGet { keys })
413}
414
415fn parse_mset(args: &[Frame]) -> Result<Command, ProtocolError> {
416    if args.is_empty() || !args.len().is_multiple_of(2) {
417        return Err(ProtocolError::WrongArity("MSET".into()));
418    }
419    let mut pairs = Vec::with_capacity(args.len() / 2);
420    for chunk in args.chunks(2) {
421        let key = extract_string(&chunk[0])?;
422        let value = extract_bytes(&chunk[1])?;
423        pairs.push((key, value));
424    }
425    Ok(Command::MSet { pairs })
426}
427
428fn parse_expire(args: &[Frame]) -> Result<Command, ProtocolError> {
429    if args.len() != 2 {
430        return Err(ProtocolError::WrongArity("EXPIRE".into()));
431    }
432    let key = extract_string(&args[0])?;
433    let seconds = parse_u64(&args[1], "EXPIRE")?;
434
435    if seconds == 0 {
436        return Err(ProtocolError::InvalidCommandFrame(
437            "invalid expire time in 'EXPIRE' command".into(),
438        ));
439    }
440
441    Ok(Command::Expire { key, seconds })
442}
443
444fn parse_ttl(args: &[Frame]) -> Result<Command, ProtocolError> {
445    if args.len() != 1 {
446        return Err(ProtocolError::WrongArity("TTL".into()));
447    }
448    let key = extract_string(&args[0])?;
449    Ok(Command::Ttl { key })
450}
451
452fn parse_persist(args: &[Frame]) -> Result<Command, ProtocolError> {
453    if args.len() != 1 {
454        return Err(ProtocolError::WrongArity("PERSIST".into()));
455    }
456    let key = extract_string(&args[0])?;
457    Ok(Command::Persist { key })
458}
459
460fn parse_pttl(args: &[Frame]) -> Result<Command, ProtocolError> {
461    if args.len() != 1 {
462        return Err(ProtocolError::WrongArity("PTTL".into()));
463    }
464    let key = extract_string(&args[0])?;
465    Ok(Command::Pttl { key })
466}
467
468fn parse_pexpire(args: &[Frame]) -> Result<Command, ProtocolError> {
469    if args.len() != 2 {
470        return Err(ProtocolError::WrongArity("PEXPIRE".into()));
471    }
472    let key = extract_string(&args[0])?;
473    let milliseconds = parse_u64(&args[1], "PEXPIRE")?;
474
475    if milliseconds == 0 {
476        return Err(ProtocolError::InvalidCommandFrame(
477            "invalid expire time in 'PEXPIRE' command".into(),
478        ));
479    }
480
481    Ok(Command::Pexpire { key, milliseconds })
482}
483
484fn parse_dbsize(args: &[Frame]) -> Result<Command, ProtocolError> {
485    if !args.is_empty() {
486        return Err(ProtocolError::WrongArity("DBSIZE".into()));
487    }
488    Ok(Command::DbSize)
489}
490
491fn parse_info(args: &[Frame]) -> Result<Command, ProtocolError> {
492    match args.len() {
493        0 => Ok(Command::Info { section: None }),
494        1 => {
495            let section = extract_string(&args[0])?;
496            Ok(Command::Info {
497                section: Some(section),
498            })
499        }
500        _ => Err(ProtocolError::WrongArity("INFO".into())),
501    }
502}
503
504fn parse_bgsave(args: &[Frame]) -> Result<Command, ProtocolError> {
505    if !args.is_empty() {
506        return Err(ProtocolError::WrongArity("BGSAVE".into()));
507    }
508    Ok(Command::BgSave)
509}
510
511fn parse_bgrewriteaof(args: &[Frame]) -> Result<Command, ProtocolError> {
512    if !args.is_empty() {
513        return Err(ProtocolError::WrongArity("BGREWRITEAOF".into()));
514    }
515    Ok(Command::BgRewriteAof)
516}
517
518fn parse_flushdb(args: &[Frame]) -> Result<Command, ProtocolError> {
519    if !args.is_empty() {
520        return Err(ProtocolError::WrongArity("FLUSHDB".into()));
521    }
522    Ok(Command::FlushDb)
523}
524
525fn parse_scan(args: &[Frame]) -> Result<Command, ProtocolError> {
526    if args.is_empty() {
527        return Err(ProtocolError::WrongArity("SCAN".into()));
528    }
529
530    let cursor = parse_u64(&args[0], "SCAN")?;
531    let mut pattern = None;
532    let mut count = None;
533    let mut idx = 1;
534
535    while idx < args.len() {
536        let flag = extract_string(&args[idx])?.to_ascii_uppercase();
537        match flag.as_str() {
538            "MATCH" => {
539                idx += 1;
540                if idx >= args.len() {
541                    return Err(ProtocolError::WrongArity("SCAN".into()));
542                }
543                pattern = Some(extract_string(&args[idx])?);
544                idx += 1;
545            }
546            "COUNT" => {
547                idx += 1;
548                if idx >= args.len() {
549                    return Err(ProtocolError::WrongArity("SCAN".into()));
550                }
551                let n = parse_u64(&args[idx], "SCAN")?;
552                count = Some(n as usize);
553                idx += 1;
554            }
555            _ => {
556                return Err(ProtocolError::InvalidCommandFrame(format!(
557                    "unsupported SCAN option '{flag}'"
558                )));
559            }
560        }
561    }
562
563    Ok(Command::Scan {
564        cursor,
565        pattern,
566        count,
567    })
568}
569
570/// Parses a string argument as an i64. Used by LRANGE for start/stop indices.
571fn parse_i64(frame: &Frame, cmd: &str) -> Result<i64, ProtocolError> {
572    let s = extract_string(frame)?;
573    s.parse::<i64>().map_err(|_| {
574        ProtocolError::InvalidCommandFrame(format!("value is not a valid integer for '{cmd}'"))
575    })
576}
577
578fn parse_lpush(args: &[Frame]) -> Result<Command, ProtocolError> {
579    if args.len() < 2 {
580        return Err(ProtocolError::WrongArity("LPUSH".into()));
581    }
582    let key = extract_string(&args[0])?;
583    let values = args[1..]
584        .iter()
585        .map(extract_bytes)
586        .collect::<Result<Vec<_>, _>>()?;
587    Ok(Command::LPush { key, values })
588}
589
590fn parse_rpush(args: &[Frame]) -> Result<Command, ProtocolError> {
591    if args.len() < 2 {
592        return Err(ProtocolError::WrongArity("RPUSH".into()));
593    }
594    let key = extract_string(&args[0])?;
595    let values = args[1..]
596        .iter()
597        .map(extract_bytes)
598        .collect::<Result<Vec<_>, _>>()?;
599    Ok(Command::RPush { key, values })
600}
601
602fn parse_lpop(args: &[Frame]) -> Result<Command, ProtocolError> {
603    if args.len() != 1 {
604        return Err(ProtocolError::WrongArity("LPOP".into()));
605    }
606    let key = extract_string(&args[0])?;
607    Ok(Command::LPop { key })
608}
609
610fn parse_rpop(args: &[Frame]) -> Result<Command, ProtocolError> {
611    if args.len() != 1 {
612        return Err(ProtocolError::WrongArity("RPOP".into()));
613    }
614    let key = extract_string(&args[0])?;
615    Ok(Command::RPop { key })
616}
617
618fn parse_lrange(args: &[Frame]) -> Result<Command, ProtocolError> {
619    if args.len() != 3 {
620        return Err(ProtocolError::WrongArity("LRANGE".into()));
621    }
622    let key = extract_string(&args[0])?;
623    let start = parse_i64(&args[1], "LRANGE")?;
624    let stop = parse_i64(&args[2], "LRANGE")?;
625    Ok(Command::LRange { key, start, stop })
626}
627
628fn parse_llen(args: &[Frame]) -> Result<Command, ProtocolError> {
629    if args.len() != 1 {
630        return Err(ProtocolError::WrongArity("LLEN".into()));
631    }
632    let key = extract_string(&args[0])?;
633    Ok(Command::LLen { key })
634}
635
636fn parse_type(args: &[Frame]) -> Result<Command, ProtocolError> {
637    if args.len() != 1 {
638        return Err(ProtocolError::WrongArity("TYPE".into()));
639    }
640    let key = extract_string(&args[0])?;
641    Ok(Command::Type { key })
642}
643
644/// Parses a string argument as an f64 score.
645fn parse_f64(frame: &Frame, cmd: &str) -> Result<f64, ProtocolError> {
646    let s = extract_string(frame)?;
647    let v = s.parse::<f64>().map_err(|_| {
648        ProtocolError::InvalidCommandFrame(format!("value is not a valid float for '{cmd}'"))
649    })?;
650    if v.is_nan() {
651        return Err(ProtocolError::InvalidCommandFrame(format!(
652            "NaN is not a valid score for '{cmd}'"
653        )));
654    }
655    Ok(v)
656}
657
658fn parse_zadd(args: &[Frame]) -> Result<Command, ProtocolError> {
659    // ZADD key [NX|XX] [GT|LT] [CH] score member [score member ...]
660    if args.len() < 3 {
661        return Err(ProtocolError::WrongArity("ZADD".into()));
662    }
663
664    let key = extract_string(&args[0])?;
665    let mut flags = ZAddFlags::default();
666    let mut idx = 1;
667
668    // parse optional flags before score/member pairs
669    while idx < args.len() {
670        let s = extract_string(&args[idx])?.to_ascii_uppercase();
671        match s.as_str() {
672            "NX" => {
673                flags.nx = true;
674                idx += 1;
675            }
676            "XX" => {
677                flags.xx = true;
678                idx += 1;
679            }
680            "GT" => {
681                flags.gt = true;
682                idx += 1;
683            }
684            "LT" => {
685                flags.lt = true;
686                idx += 1;
687            }
688            "CH" => {
689                flags.ch = true;
690                idx += 1;
691            }
692            _ => break,
693        }
694    }
695
696    // NX and XX are mutually exclusive
697    if flags.nx && flags.xx {
698        return Err(ProtocolError::InvalidCommandFrame(
699            "XX and NX options at the same time are not compatible".into(),
700        ));
701    }
702    // GT and LT are mutually exclusive
703    if flags.gt && flags.lt {
704        return Err(ProtocolError::InvalidCommandFrame(
705            "GT and LT options at the same time are not compatible".into(),
706        ));
707    }
708
709    // remaining args must be score/member pairs
710    let remaining = &args[idx..];
711    if remaining.is_empty() || !remaining.len().is_multiple_of(2) {
712        return Err(ProtocolError::WrongArity("ZADD".into()));
713    }
714
715    let mut members = Vec::with_capacity(remaining.len() / 2);
716    for pair in remaining.chunks(2) {
717        let score = parse_f64(&pair[0], "ZADD")?;
718        let member = extract_string(&pair[1])?;
719        members.push((score, member));
720    }
721
722    Ok(Command::ZAdd {
723        key,
724        flags,
725        members,
726    })
727}
728
729fn parse_zcard(args: &[Frame]) -> Result<Command, ProtocolError> {
730    if args.len() != 1 {
731        return Err(ProtocolError::WrongArity("ZCARD".into()));
732    }
733    let key = extract_string(&args[0])?;
734    Ok(Command::ZCard { key })
735}
736
737fn parse_zrem(args: &[Frame]) -> Result<Command, ProtocolError> {
738    if args.len() < 2 {
739        return Err(ProtocolError::WrongArity("ZREM".into()));
740    }
741    let key = extract_string(&args[0])?;
742    let members = args[1..]
743        .iter()
744        .map(extract_string)
745        .collect::<Result<Vec<_>, _>>()?;
746    Ok(Command::ZRem { key, members })
747}
748
749fn parse_zscore(args: &[Frame]) -> Result<Command, ProtocolError> {
750    if args.len() != 2 {
751        return Err(ProtocolError::WrongArity("ZSCORE".into()));
752    }
753    let key = extract_string(&args[0])?;
754    let member = extract_string(&args[1])?;
755    Ok(Command::ZScore { key, member })
756}
757
758fn parse_zrank(args: &[Frame]) -> Result<Command, ProtocolError> {
759    if args.len() != 2 {
760        return Err(ProtocolError::WrongArity("ZRANK".into()));
761    }
762    let key = extract_string(&args[0])?;
763    let member = extract_string(&args[1])?;
764    Ok(Command::ZRank { key, member })
765}
766
767fn parse_zrange(args: &[Frame]) -> Result<Command, ProtocolError> {
768    if args.len() < 3 || args.len() > 4 {
769        return Err(ProtocolError::WrongArity("ZRANGE".into()));
770    }
771    let key = extract_string(&args[0])?;
772    let start = parse_i64(&args[1], "ZRANGE")?;
773    let stop = parse_i64(&args[2], "ZRANGE")?;
774
775    let with_scores = if args.len() == 4 {
776        let opt = extract_string(&args[3])?.to_ascii_uppercase();
777        if opt != "WITHSCORES" {
778            return Err(ProtocolError::InvalidCommandFrame(format!(
779                "unsupported ZRANGE option '{opt}'"
780            )));
781        }
782        true
783    } else {
784        false
785    };
786
787    Ok(Command::ZRange {
788        key,
789        start,
790        stop,
791        with_scores,
792    })
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798
799    /// Helper: build an array frame from bulk strings.
800    fn cmd(parts: &[&str]) -> Frame {
801        Frame::Array(
802            parts
803                .iter()
804                .map(|s| Frame::Bulk(Bytes::from(s.to_string())))
805                .collect(),
806        )
807    }
808
809    // --- ping ---
810
811    #[test]
812    fn ping_no_args() {
813        assert_eq!(
814            Command::from_frame(cmd(&["PING"])).unwrap(),
815            Command::Ping(None),
816        );
817    }
818
819    #[test]
820    fn ping_with_message() {
821        assert_eq!(
822            Command::from_frame(cmd(&["PING", "hello"])).unwrap(),
823            Command::Ping(Some(Bytes::from("hello"))),
824        );
825    }
826
827    #[test]
828    fn ping_case_insensitive() {
829        assert_eq!(
830            Command::from_frame(cmd(&["ping"])).unwrap(),
831            Command::Ping(None),
832        );
833        assert_eq!(
834            Command::from_frame(cmd(&["Ping"])).unwrap(),
835            Command::Ping(None),
836        );
837    }
838
839    #[test]
840    fn ping_too_many_args() {
841        let err = Command::from_frame(cmd(&["PING", "a", "b"])).unwrap_err();
842        assert!(matches!(err, ProtocolError::WrongArity(_)));
843    }
844
845    // --- echo ---
846
847    #[test]
848    fn echo() {
849        assert_eq!(
850            Command::from_frame(cmd(&["ECHO", "test"])).unwrap(),
851            Command::Echo(Bytes::from("test")),
852        );
853    }
854
855    #[test]
856    fn echo_missing_arg() {
857        let err = Command::from_frame(cmd(&["ECHO"])).unwrap_err();
858        assert!(matches!(err, ProtocolError::WrongArity(_)));
859    }
860
861    // --- get ---
862
863    #[test]
864    fn get_basic() {
865        assert_eq!(
866            Command::from_frame(cmd(&["GET", "mykey"])).unwrap(),
867            Command::Get {
868                key: "mykey".into()
869            },
870        );
871    }
872
873    #[test]
874    fn get_no_args() {
875        let err = Command::from_frame(cmd(&["GET"])).unwrap_err();
876        assert!(matches!(err, ProtocolError::WrongArity(_)));
877    }
878
879    #[test]
880    fn get_too_many_args() {
881        let err = Command::from_frame(cmd(&["GET", "a", "b"])).unwrap_err();
882        assert!(matches!(err, ProtocolError::WrongArity(_)));
883    }
884
885    #[test]
886    fn get_case_insensitive() {
887        assert_eq!(
888            Command::from_frame(cmd(&["get", "k"])).unwrap(),
889            Command::Get { key: "k".into() },
890        );
891    }
892
893    // --- set ---
894
895    #[test]
896    fn set_basic() {
897        assert_eq!(
898            Command::from_frame(cmd(&["SET", "key", "value"])).unwrap(),
899            Command::Set {
900                key: "key".into(),
901                value: Bytes::from("value"),
902                expire: None,
903                nx: false,
904                xx: false,
905            },
906        );
907    }
908
909    #[test]
910    fn set_with_ex() {
911        assert_eq!(
912            Command::from_frame(cmd(&["SET", "key", "val", "EX", "10"])).unwrap(),
913            Command::Set {
914                key: "key".into(),
915                value: Bytes::from("val"),
916                expire: Some(SetExpire::Ex(10)),
917                nx: false,
918                xx: false,
919            },
920        );
921    }
922
923    #[test]
924    fn set_with_px() {
925        assert_eq!(
926            Command::from_frame(cmd(&["SET", "key", "val", "PX", "5000"])).unwrap(),
927            Command::Set {
928                key: "key".into(),
929                value: Bytes::from("val"),
930                expire: Some(SetExpire::Px(5000)),
931                nx: false,
932                xx: false,
933            },
934        );
935    }
936
937    #[test]
938    fn set_ex_case_insensitive() {
939        assert_eq!(
940            Command::from_frame(cmd(&["set", "k", "v", "ex", "5"])).unwrap(),
941            Command::Set {
942                key: "k".into(),
943                value: Bytes::from("v"),
944                expire: Some(SetExpire::Ex(5)),
945                nx: false,
946                xx: false,
947            },
948        );
949    }
950
951    #[test]
952    fn set_nx_flag() {
953        assert_eq!(
954            Command::from_frame(cmd(&["SET", "key", "val", "NX"])).unwrap(),
955            Command::Set {
956                key: "key".into(),
957                value: Bytes::from("val"),
958                expire: None,
959                nx: true,
960                xx: false,
961            },
962        );
963    }
964
965    #[test]
966    fn set_xx_flag() {
967        assert_eq!(
968            Command::from_frame(cmd(&["SET", "key", "val", "XX"])).unwrap(),
969            Command::Set {
970                key: "key".into(),
971                value: Bytes::from("val"),
972                expire: None,
973                nx: false,
974                xx: true,
975            },
976        );
977    }
978
979    #[test]
980    fn set_nx_with_ex() {
981        assert_eq!(
982            Command::from_frame(cmd(&["SET", "key", "val", "EX", "10", "NX"])).unwrap(),
983            Command::Set {
984                key: "key".into(),
985                value: Bytes::from("val"),
986                expire: Some(SetExpire::Ex(10)),
987                nx: true,
988                xx: false,
989            },
990        );
991    }
992
993    #[test]
994    fn set_nx_before_ex() {
995        assert_eq!(
996            Command::from_frame(cmd(&["SET", "key", "val", "NX", "PX", "5000"])).unwrap(),
997            Command::Set {
998                key: "key".into(),
999                value: Bytes::from("val"),
1000                expire: Some(SetExpire::Px(5000)),
1001                nx: true,
1002                xx: false,
1003            },
1004        );
1005    }
1006
1007    #[test]
1008    fn set_nx_xx_conflict() {
1009        let err = Command::from_frame(cmd(&["SET", "k", "v", "NX", "XX"])).unwrap_err();
1010        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1011    }
1012
1013    #[test]
1014    fn set_nx_case_insensitive() {
1015        assert_eq!(
1016            Command::from_frame(cmd(&["set", "k", "v", "nx"])).unwrap(),
1017            Command::Set {
1018                key: "k".into(),
1019                value: Bytes::from("v"),
1020                expire: None,
1021                nx: true,
1022                xx: false,
1023            },
1024        );
1025    }
1026
1027    #[test]
1028    fn set_missing_value() {
1029        let err = Command::from_frame(cmd(&["SET", "key"])).unwrap_err();
1030        assert!(matches!(err, ProtocolError::WrongArity(_)));
1031    }
1032
1033    #[test]
1034    fn set_invalid_expire_value() {
1035        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX", "notanum"])).unwrap_err();
1036        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1037    }
1038
1039    #[test]
1040    fn set_zero_expire() {
1041        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX", "0"])).unwrap_err();
1042        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1043    }
1044
1045    #[test]
1046    fn set_unknown_flag() {
1047        let err = Command::from_frame(cmd(&["SET", "k", "v", "ZZ", "10"])).unwrap_err();
1048        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1049    }
1050
1051    #[test]
1052    fn set_incomplete_expire() {
1053        // EX without a value
1054        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX"])).unwrap_err();
1055        assert!(matches!(err, ProtocolError::WrongArity(_)));
1056    }
1057
1058    // --- del ---
1059
1060    #[test]
1061    fn del_single() {
1062        assert_eq!(
1063            Command::from_frame(cmd(&["DEL", "key"])).unwrap(),
1064            Command::Del {
1065                keys: vec!["key".into()]
1066            },
1067        );
1068    }
1069
1070    #[test]
1071    fn del_multiple() {
1072        assert_eq!(
1073            Command::from_frame(cmd(&["DEL", "a", "b", "c"])).unwrap(),
1074            Command::Del {
1075                keys: vec!["a".into(), "b".into(), "c".into()]
1076            },
1077        );
1078    }
1079
1080    #[test]
1081    fn del_no_args() {
1082        let err = Command::from_frame(cmd(&["DEL"])).unwrap_err();
1083        assert!(matches!(err, ProtocolError::WrongArity(_)));
1084    }
1085
1086    // --- exists ---
1087
1088    #[test]
1089    fn exists_single() {
1090        assert_eq!(
1091            Command::from_frame(cmd(&["EXISTS", "key"])).unwrap(),
1092            Command::Exists {
1093                keys: vec!["key".into()]
1094            },
1095        );
1096    }
1097
1098    #[test]
1099    fn exists_multiple() {
1100        assert_eq!(
1101            Command::from_frame(cmd(&["EXISTS", "a", "b"])).unwrap(),
1102            Command::Exists {
1103                keys: vec!["a".into(), "b".into()]
1104            },
1105        );
1106    }
1107
1108    #[test]
1109    fn exists_no_args() {
1110        let err = Command::from_frame(cmd(&["EXISTS"])).unwrap_err();
1111        assert!(matches!(err, ProtocolError::WrongArity(_)));
1112    }
1113
1114    // --- mget ---
1115
1116    #[test]
1117    fn mget_single() {
1118        assert_eq!(
1119            Command::from_frame(cmd(&["MGET", "key"])).unwrap(),
1120            Command::MGet {
1121                keys: vec!["key".into()]
1122            },
1123        );
1124    }
1125
1126    #[test]
1127    fn mget_multiple() {
1128        assert_eq!(
1129            Command::from_frame(cmd(&["MGET", "a", "b", "c"])).unwrap(),
1130            Command::MGet {
1131                keys: vec!["a".into(), "b".into(), "c".into()]
1132            },
1133        );
1134    }
1135
1136    #[test]
1137    fn mget_no_args() {
1138        let err = Command::from_frame(cmd(&["MGET"])).unwrap_err();
1139        assert!(matches!(err, ProtocolError::WrongArity(_)));
1140    }
1141
1142    // --- mset ---
1143
1144    #[test]
1145    fn mset_single_pair() {
1146        assert_eq!(
1147            Command::from_frame(cmd(&["MSET", "key", "val"])).unwrap(),
1148            Command::MSet {
1149                pairs: vec![("key".into(), Bytes::from("val"))]
1150            },
1151        );
1152    }
1153
1154    #[test]
1155    fn mset_multiple_pairs() {
1156        assert_eq!(
1157            Command::from_frame(cmd(&["MSET", "a", "1", "b", "2"])).unwrap(),
1158            Command::MSet {
1159                pairs: vec![
1160                    ("a".into(), Bytes::from("1")),
1161                    ("b".into(), Bytes::from("2")),
1162                ]
1163            },
1164        );
1165    }
1166
1167    #[test]
1168    fn mset_no_args() {
1169        let err = Command::from_frame(cmd(&["MSET"])).unwrap_err();
1170        assert!(matches!(err, ProtocolError::WrongArity(_)));
1171    }
1172
1173    #[test]
1174    fn mset_odd_args() {
1175        // missing value for second key
1176        let err = Command::from_frame(cmd(&["MSET", "a", "1", "b"])).unwrap_err();
1177        assert!(matches!(err, ProtocolError::WrongArity(_)));
1178    }
1179
1180    // --- expire ---
1181
1182    #[test]
1183    fn expire_basic() {
1184        assert_eq!(
1185            Command::from_frame(cmd(&["EXPIRE", "key", "60"])).unwrap(),
1186            Command::Expire {
1187                key: "key".into(),
1188                seconds: 60,
1189            },
1190        );
1191    }
1192
1193    #[test]
1194    fn expire_wrong_arity() {
1195        let err = Command::from_frame(cmd(&["EXPIRE", "key"])).unwrap_err();
1196        assert!(matches!(err, ProtocolError::WrongArity(_)));
1197    }
1198
1199    #[test]
1200    fn expire_invalid_seconds() {
1201        let err = Command::from_frame(cmd(&["EXPIRE", "key", "abc"])).unwrap_err();
1202        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1203    }
1204
1205    #[test]
1206    fn expire_zero_seconds() {
1207        let err = Command::from_frame(cmd(&["EXPIRE", "key", "0"])).unwrap_err();
1208        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1209    }
1210
1211    // --- ttl ---
1212
1213    #[test]
1214    fn ttl_basic() {
1215        assert_eq!(
1216            Command::from_frame(cmd(&["TTL", "key"])).unwrap(),
1217            Command::Ttl { key: "key".into() },
1218        );
1219    }
1220
1221    #[test]
1222    fn ttl_wrong_arity() {
1223        let err = Command::from_frame(cmd(&["TTL"])).unwrap_err();
1224        assert!(matches!(err, ProtocolError::WrongArity(_)));
1225    }
1226
1227    // --- dbsize ---
1228
1229    #[test]
1230    fn dbsize_basic() {
1231        assert_eq!(
1232            Command::from_frame(cmd(&["DBSIZE"])).unwrap(),
1233            Command::DbSize,
1234        );
1235    }
1236
1237    #[test]
1238    fn dbsize_case_insensitive() {
1239        assert_eq!(
1240            Command::from_frame(cmd(&["dbsize"])).unwrap(),
1241            Command::DbSize,
1242        );
1243    }
1244
1245    #[test]
1246    fn dbsize_extra_args() {
1247        let err = Command::from_frame(cmd(&["DBSIZE", "extra"])).unwrap_err();
1248        assert!(matches!(err, ProtocolError::WrongArity(_)));
1249    }
1250
1251    // --- info ---
1252
1253    #[test]
1254    fn info_no_section() {
1255        assert_eq!(
1256            Command::from_frame(cmd(&["INFO"])).unwrap(),
1257            Command::Info { section: None },
1258        );
1259    }
1260
1261    #[test]
1262    fn info_with_section() {
1263        assert_eq!(
1264            Command::from_frame(cmd(&["INFO", "keyspace"])).unwrap(),
1265            Command::Info {
1266                section: Some("keyspace".into())
1267            },
1268        );
1269    }
1270
1271    #[test]
1272    fn info_too_many_args() {
1273        let err = Command::from_frame(cmd(&["INFO", "a", "b"])).unwrap_err();
1274        assert!(matches!(err, ProtocolError::WrongArity(_)));
1275    }
1276
1277    // --- bgsave ---
1278
1279    #[test]
1280    fn bgsave_basic() {
1281        assert_eq!(
1282            Command::from_frame(cmd(&["BGSAVE"])).unwrap(),
1283            Command::BgSave,
1284        );
1285    }
1286
1287    #[test]
1288    fn bgsave_case_insensitive() {
1289        assert_eq!(
1290            Command::from_frame(cmd(&["bgsave"])).unwrap(),
1291            Command::BgSave,
1292        );
1293    }
1294
1295    #[test]
1296    fn bgsave_extra_args() {
1297        let err = Command::from_frame(cmd(&["BGSAVE", "extra"])).unwrap_err();
1298        assert!(matches!(err, ProtocolError::WrongArity(_)));
1299    }
1300
1301    // --- bgrewriteaof ---
1302
1303    #[test]
1304    fn bgrewriteaof_basic() {
1305        assert_eq!(
1306            Command::from_frame(cmd(&["BGREWRITEAOF"])).unwrap(),
1307            Command::BgRewriteAof,
1308        );
1309    }
1310
1311    #[test]
1312    fn bgrewriteaof_case_insensitive() {
1313        assert_eq!(
1314            Command::from_frame(cmd(&["bgrewriteaof"])).unwrap(),
1315            Command::BgRewriteAof,
1316        );
1317    }
1318
1319    #[test]
1320    fn bgrewriteaof_extra_args() {
1321        let err = Command::from_frame(cmd(&["BGREWRITEAOF", "extra"])).unwrap_err();
1322        assert!(matches!(err, ProtocolError::WrongArity(_)));
1323    }
1324
1325    // --- flushdb ---
1326
1327    #[test]
1328    fn flushdb_basic() {
1329        assert_eq!(
1330            Command::from_frame(cmd(&["FLUSHDB"])).unwrap(),
1331            Command::FlushDb,
1332        );
1333    }
1334
1335    #[test]
1336    fn flushdb_case_insensitive() {
1337        assert_eq!(
1338            Command::from_frame(cmd(&["flushdb"])).unwrap(),
1339            Command::FlushDb,
1340        );
1341    }
1342
1343    #[test]
1344    fn flushdb_extra_args() {
1345        let err = Command::from_frame(cmd(&["FLUSHDB", "extra"])).unwrap_err();
1346        assert!(matches!(err, ProtocolError::WrongArity(_)));
1347    }
1348
1349    // --- lpush ---
1350
1351    #[test]
1352    fn lpush_single() {
1353        assert_eq!(
1354            Command::from_frame(cmd(&["LPUSH", "list", "val"])).unwrap(),
1355            Command::LPush {
1356                key: "list".into(),
1357                values: vec![Bytes::from("val")],
1358            },
1359        );
1360    }
1361
1362    #[test]
1363    fn lpush_multiple() {
1364        assert_eq!(
1365            Command::from_frame(cmd(&["LPUSH", "list", "a", "b", "c"])).unwrap(),
1366            Command::LPush {
1367                key: "list".into(),
1368                values: vec![Bytes::from("a"), Bytes::from("b"), Bytes::from("c")],
1369            },
1370        );
1371    }
1372
1373    #[test]
1374    fn lpush_no_value() {
1375        let err = Command::from_frame(cmd(&["LPUSH", "key"])).unwrap_err();
1376        assert!(matches!(err, ProtocolError::WrongArity(_)));
1377    }
1378
1379    #[test]
1380    fn lpush_case_insensitive() {
1381        assert!(matches!(
1382            Command::from_frame(cmd(&["lpush", "k", "v"])).unwrap(),
1383            Command::LPush { .. }
1384        ));
1385    }
1386
1387    // --- rpush ---
1388
1389    #[test]
1390    fn rpush_single() {
1391        assert_eq!(
1392            Command::from_frame(cmd(&["RPUSH", "list", "val"])).unwrap(),
1393            Command::RPush {
1394                key: "list".into(),
1395                values: vec![Bytes::from("val")],
1396            },
1397        );
1398    }
1399
1400    #[test]
1401    fn rpush_no_value() {
1402        let err = Command::from_frame(cmd(&["RPUSH", "key"])).unwrap_err();
1403        assert!(matches!(err, ProtocolError::WrongArity(_)));
1404    }
1405
1406    // --- lpop ---
1407
1408    #[test]
1409    fn lpop_basic() {
1410        assert_eq!(
1411            Command::from_frame(cmd(&["LPOP", "list"])).unwrap(),
1412            Command::LPop { key: "list".into() },
1413        );
1414    }
1415
1416    #[test]
1417    fn lpop_wrong_arity() {
1418        let err = Command::from_frame(cmd(&["LPOP"])).unwrap_err();
1419        assert!(matches!(err, ProtocolError::WrongArity(_)));
1420    }
1421
1422    // --- rpop ---
1423
1424    #[test]
1425    fn rpop_basic() {
1426        assert_eq!(
1427            Command::from_frame(cmd(&["RPOP", "list"])).unwrap(),
1428            Command::RPop { key: "list".into() },
1429        );
1430    }
1431
1432    // --- lrange ---
1433
1434    #[test]
1435    fn lrange_basic() {
1436        assert_eq!(
1437            Command::from_frame(cmd(&["LRANGE", "list", "0", "-1"])).unwrap(),
1438            Command::LRange {
1439                key: "list".into(),
1440                start: 0,
1441                stop: -1,
1442            },
1443        );
1444    }
1445
1446    #[test]
1447    fn lrange_wrong_arity() {
1448        let err = Command::from_frame(cmd(&["LRANGE", "list", "0"])).unwrap_err();
1449        assert!(matches!(err, ProtocolError::WrongArity(_)));
1450    }
1451
1452    #[test]
1453    fn lrange_invalid_index() {
1454        let err = Command::from_frame(cmd(&["LRANGE", "list", "abc", "0"])).unwrap_err();
1455        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1456    }
1457
1458    // --- llen ---
1459
1460    #[test]
1461    fn llen_basic() {
1462        assert_eq!(
1463            Command::from_frame(cmd(&["LLEN", "list"])).unwrap(),
1464            Command::LLen { key: "list".into() },
1465        );
1466    }
1467
1468    #[test]
1469    fn llen_wrong_arity() {
1470        let err = Command::from_frame(cmd(&["LLEN"])).unwrap_err();
1471        assert!(matches!(err, ProtocolError::WrongArity(_)));
1472    }
1473
1474    // --- type ---
1475
1476    #[test]
1477    fn type_basic() {
1478        assert_eq!(
1479            Command::from_frame(cmd(&["TYPE", "key"])).unwrap(),
1480            Command::Type { key: "key".into() },
1481        );
1482    }
1483
1484    #[test]
1485    fn type_wrong_arity() {
1486        let err = Command::from_frame(cmd(&["TYPE"])).unwrap_err();
1487        assert!(matches!(err, ProtocolError::WrongArity(_)));
1488    }
1489
1490    #[test]
1491    fn type_case_insensitive() {
1492        assert!(matches!(
1493            Command::from_frame(cmd(&["type", "k"])).unwrap(),
1494            Command::Type { .. }
1495        ));
1496    }
1497
1498    // --- general ---
1499
1500    #[test]
1501    fn unknown_command() {
1502        assert_eq!(
1503            Command::from_frame(cmd(&["FOOBAR", "arg"])).unwrap(),
1504            Command::Unknown("FOOBAR".into()),
1505        );
1506    }
1507
1508    #[test]
1509    fn non_array_frame() {
1510        let err = Command::from_frame(Frame::Simple("PING".into())).unwrap_err();
1511        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1512    }
1513
1514    #[test]
1515    fn empty_array() {
1516        let err = Command::from_frame(Frame::Array(vec![])).unwrap_err();
1517        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1518    }
1519
1520    // --- zadd ---
1521
1522    #[test]
1523    fn zadd_basic() {
1524        let parsed = Command::from_frame(cmd(&["ZADD", "board", "100", "alice"])).unwrap();
1525        match parsed {
1526            Command::ZAdd {
1527                key,
1528                flags,
1529                members,
1530            } => {
1531                assert_eq!(key, "board");
1532                assert_eq!(flags, ZAddFlags::default());
1533                assert_eq!(members, vec![(100.0, "alice".into())]);
1534            }
1535            other => panic!("expected ZAdd, got {other:?}"),
1536        }
1537    }
1538
1539    #[test]
1540    fn zadd_multiple_members() {
1541        let parsed =
1542            Command::from_frame(cmd(&["ZADD", "board", "100", "alice", "200", "bob"])).unwrap();
1543        match parsed {
1544            Command::ZAdd { members, .. } => {
1545                assert_eq!(members.len(), 2);
1546                assert_eq!(members[0], (100.0, "alice".into()));
1547                assert_eq!(members[1], (200.0, "bob".into()));
1548            }
1549            other => panic!("expected ZAdd, got {other:?}"),
1550        }
1551    }
1552
1553    #[test]
1554    fn zadd_with_flags() {
1555        let parsed = Command::from_frame(cmd(&["ZADD", "z", "NX", "CH", "100", "alice"])).unwrap();
1556        match parsed {
1557            Command::ZAdd { flags, .. } => {
1558                assert!(flags.nx);
1559                assert!(flags.ch);
1560                assert!(!flags.xx);
1561                assert!(!flags.gt);
1562                assert!(!flags.lt);
1563            }
1564            other => panic!("expected ZAdd, got {other:?}"),
1565        }
1566    }
1567
1568    #[test]
1569    fn zadd_gt_flag() {
1570        let parsed = Command::from_frame(cmd(&["zadd", "z", "gt", "100", "alice"])).unwrap();
1571        match parsed {
1572            Command::ZAdd { flags, .. } => assert!(flags.gt),
1573            other => panic!("expected ZAdd, got {other:?}"),
1574        }
1575    }
1576
1577    #[test]
1578    fn zadd_nx_xx_conflict() {
1579        let err = Command::from_frame(cmd(&["ZADD", "z", "NX", "XX", "100", "alice"])).unwrap_err();
1580        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1581    }
1582
1583    #[test]
1584    fn zadd_gt_lt_conflict() {
1585        let err = Command::from_frame(cmd(&["ZADD", "z", "GT", "LT", "100", "alice"])).unwrap_err();
1586        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1587    }
1588
1589    #[test]
1590    fn zadd_wrong_arity() {
1591        let err = Command::from_frame(cmd(&["ZADD", "z"])).unwrap_err();
1592        assert!(matches!(err, ProtocolError::WrongArity(_)));
1593    }
1594
1595    #[test]
1596    fn zadd_odd_score_member_count() {
1597        // one score without a member
1598        let err = Command::from_frame(cmd(&["ZADD", "z", "100"])).unwrap_err();
1599        assert!(matches!(err, ProtocolError::WrongArity(_)));
1600    }
1601
1602    #[test]
1603    fn zadd_invalid_score() {
1604        let err = Command::from_frame(cmd(&["ZADD", "z", "notanum", "alice"])).unwrap_err();
1605        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1606    }
1607
1608    #[test]
1609    fn zadd_nan_score() {
1610        let err = Command::from_frame(cmd(&["ZADD", "z", "nan", "alice"])).unwrap_err();
1611        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1612    }
1613
1614    #[test]
1615    fn zadd_negative_score() {
1616        let parsed = Command::from_frame(cmd(&["ZADD", "z", "-50.5", "alice"])).unwrap();
1617        match parsed {
1618            Command::ZAdd { members, .. } => {
1619                assert_eq!(members[0].0, -50.5);
1620            }
1621            other => panic!("expected ZAdd, got {other:?}"),
1622        }
1623    }
1624
1625    // --- zrem ---
1626
1627    #[test]
1628    fn zrem_basic() {
1629        assert_eq!(
1630            Command::from_frame(cmd(&["ZREM", "z", "alice"])).unwrap(),
1631            Command::ZRem {
1632                key: "z".into(),
1633                members: vec!["alice".into()],
1634            },
1635        );
1636    }
1637
1638    #[test]
1639    fn zrem_multiple() {
1640        let parsed = Command::from_frame(cmd(&["ZREM", "z", "a", "b", "c"])).unwrap();
1641        match parsed {
1642            Command::ZRem { members, .. } => assert_eq!(members.len(), 3),
1643            other => panic!("expected ZRem, got {other:?}"),
1644        }
1645    }
1646
1647    #[test]
1648    fn zrem_wrong_arity() {
1649        let err = Command::from_frame(cmd(&["ZREM", "z"])).unwrap_err();
1650        assert!(matches!(err, ProtocolError::WrongArity(_)));
1651    }
1652
1653    // --- zscore ---
1654
1655    #[test]
1656    fn zscore_basic() {
1657        assert_eq!(
1658            Command::from_frame(cmd(&["ZSCORE", "z", "alice"])).unwrap(),
1659            Command::ZScore {
1660                key: "z".into(),
1661                member: "alice".into(),
1662            },
1663        );
1664    }
1665
1666    #[test]
1667    fn zscore_wrong_arity() {
1668        let err = Command::from_frame(cmd(&["ZSCORE", "z"])).unwrap_err();
1669        assert!(matches!(err, ProtocolError::WrongArity(_)));
1670    }
1671
1672    // --- zrank ---
1673
1674    #[test]
1675    fn zrank_basic() {
1676        assert_eq!(
1677            Command::from_frame(cmd(&["ZRANK", "z", "alice"])).unwrap(),
1678            Command::ZRank {
1679                key: "z".into(),
1680                member: "alice".into(),
1681            },
1682        );
1683    }
1684
1685    #[test]
1686    fn zrank_wrong_arity() {
1687        let err = Command::from_frame(cmd(&["ZRANK", "z"])).unwrap_err();
1688        assert!(matches!(err, ProtocolError::WrongArity(_)));
1689    }
1690
1691    // --- zcard ---
1692
1693    #[test]
1694    fn zcard_basic() {
1695        assert_eq!(
1696            Command::from_frame(cmd(&["ZCARD", "z"])).unwrap(),
1697            Command::ZCard { key: "z".into() },
1698        );
1699    }
1700
1701    #[test]
1702    fn zcard_wrong_arity() {
1703        let err = Command::from_frame(cmd(&["ZCARD"])).unwrap_err();
1704        assert!(matches!(err, ProtocolError::WrongArity(_)));
1705    }
1706
1707    // --- zrange ---
1708
1709    #[test]
1710    fn zrange_basic() {
1711        assert_eq!(
1712            Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1"])).unwrap(),
1713            Command::ZRange {
1714                key: "z".into(),
1715                start: 0,
1716                stop: -1,
1717                with_scores: false,
1718            },
1719        );
1720    }
1721
1722    #[test]
1723    fn zrange_with_scores() {
1724        assert_eq!(
1725            Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1", "WITHSCORES"])).unwrap(),
1726            Command::ZRange {
1727                key: "z".into(),
1728                start: 0,
1729                stop: -1,
1730                with_scores: true,
1731            },
1732        );
1733    }
1734
1735    #[test]
1736    fn zrange_withscores_case_insensitive() {
1737        assert_eq!(
1738            Command::from_frame(cmd(&["zrange", "z", "0", "-1", "withscores"])).unwrap(),
1739            Command::ZRange {
1740                key: "z".into(),
1741                start: 0,
1742                stop: -1,
1743                with_scores: true,
1744            },
1745        );
1746    }
1747
1748    #[test]
1749    fn zrange_invalid_option() {
1750        let err = Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1", "BADOPT"])).unwrap_err();
1751        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1752    }
1753
1754    #[test]
1755    fn zrange_wrong_arity() {
1756        let err = Command::from_frame(cmd(&["ZRANGE", "z", "0"])).unwrap_err();
1757        assert!(matches!(err, ProtocolError::WrongArity(_)));
1758    }
1759
1760    // --- incr ---
1761
1762    #[test]
1763    fn incr_basic() {
1764        assert_eq!(
1765            Command::from_frame(cmd(&["INCR", "counter"])).unwrap(),
1766            Command::Incr {
1767                key: "counter".into()
1768            },
1769        );
1770    }
1771
1772    #[test]
1773    fn incr_wrong_arity() {
1774        let err = Command::from_frame(cmd(&["INCR"])).unwrap_err();
1775        assert!(matches!(err, ProtocolError::WrongArity(_)));
1776    }
1777
1778    // --- decr ---
1779
1780    #[test]
1781    fn decr_basic() {
1782        assert_eq!(
1783            Command::from_frame(cmd(&["DECR", "counter"])).unwrap(),
1784            Command::Decr {
1785                key: "counter".into()
1786            },
1787        );
1788    }
1789
1790    #[test]
1791    fn decr_wrong_arity() {
1792        let err = Command::from_frame(cmd(&["DECR"])).unwrap_err();
1793        assert!(matches!(err, ProtocolError::WrongArity(_)));
1794    }
1795
1796    // --- persist ---
1797
1798    #[test]
1799    fn persist_basic() {
1800        assert_eq!(
1801            Command::from_frame(cmd(&["PERSIST", "key"])).unwrap(),
1802            Command::Persist { key: "key".into() },
1803        );
1804    }
1805
1806    #[test]
1807    fn persist_case_insensitive() {
1808        assert_eq!(
1809            Command::from_frame(cmd(&["persist", "key"])).unwrap(),
1810            Command::Persist { key: "key".into() },
1811        );
1812    }
1813
1814    #[test]
1815    fn persist_wrong_arity() {
1816        let err = Command::from_frame(cmd(&["PERSIST"])).unwrap_err();
1817        assert!(matches!(err, ProtocolError::WrongArity(_)));
1818    }
1819
1820    // --- pttl ---
1821
1822    #[test]
1823    fn pttl_basic() {
1824        assert_eq!(
1825            Command::from_frame(cmd(&["PTTL", "key"])).unwrap(),
1826            Command::Pttl { key: "key".into() },
1827        );
1828    }
1829
1830    #[test]
1831    fn pttl_wrong_arity() {
1832        let err = Command::from_frame(cmd(&["PTTL"])).unwrap_err();
1833        assert!(matches!(err, ProtocolError::WrongArity(_)));
1834    }
1835
1836    // --- pexpire ---
1837
1838    #[test]
1839    fn pexpire_basic() {
1840        assert_eq!(
1841            Command::from_frame(cmd(&["PEXPIRE", "key", "5000"])).unwrap(),
1842            Command::Pexpire {
1843                key: "key".into(),
1844                milliseconds: 5000,
1845            },
1846        );
1847    }
1848
1849    #[test]
1850    fn pexpire_wrong_arity() {
1851        let err = Command::from_frame(cmd(&["PEXPIRE", "key"])).unwrap_err();
1852        assert!(matches!(err, ProtocolError::WrongArity(_)));
1853    }
1854
1855    #[test]
1856    fn pexpire_zero_millis() {
1857        let err = Command::from_frame(cmd(&["PEXPIRE", "key", "0"])).unwrap_err();
1858        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1859    }
1860
1861    #[test]
1862    fn pexpire_invalid_millis() {
1863        let err = Command::from_frame(cmd(&["PEXPIRE", "key", "notanum"])).unwrap_err();
1864        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1865    }
1866
1867    // --- scan ---
1868
1869    #[test]
1870    fn scan_basic() {
1871        assert_eq!(
1872            Command::from_frame(cmd(&["SCAN", "0"])).unwrap(),
1873            Command::Scan {
1874                cursor: 0,
1875                pattern: None,
1876                count: None,
1877            },
1878        );
1879    }
1880
1881    #[test]
1882    fn scan_with_match() {
1883        assert_eq!(
1884            Command::from_frame(cmd(&["SCAN", "0", "MATCH", "user:*"])).unwrap(),
1885            Command::Scan {
1886                cursor: 0,
1887                pattern: Some("user:*".into()),
1888                count: None,
1889            },
1890        );
1891    }
1892
1893    #[test]
1894    fn scan_with_count() {
1895        assert_eq!(
1896            Command::from_frame(cmd(&["SCAN", "42", "COUNT", "100"])).unwrap(),
1897            Command::Scan {
1898                cursor: 42,
1899                pattern: None,
1900                count: Some(100),
1901            },
1902        );
1903    }
1904
1905    #[test]
1906    fn scan_with_match_and_count() {
1907        assert_eq!(
1908            Command::from_frame(cmd(&["SCAN", "0", "MATCH", "*:data", "COUNT", "50"])).unwrap(),
1909            Command::Scan {
1910                cursor: 0,
1911                pattern: Some("*:data".into()),
1912                count: Some(50),
1913            },
1914        );
1915    }
1916
1917    #[test]
1918    fn scan_count_before_match() {
1919        assert_eq!(
1920            Command::from_frame(cmd(&["SCAN", "0", "COUNT", "10", "MATCH", "foo*"])).unwrap(),
1921            Command::Scan {
1922                cursor: 0,
1923                pattern: Some("foo*".into()),
1924                count: Some(10),
1925            },
1926        );
1927    }
1928
1929    #[test]
1930    fn scan_case_insensitive() {
1931        assert_eq!(
1932            Command::from_frame(cmd(&["scan", "0", "match", "x*", "count", "5"])).unwrap(),
1933            Command::Scan {
1934                cursor: 0,
1935                pattern: Some("x*".into()),
1936                count: Some(5),
1937            },
1938        );
1939    }
1940
1941    #[test]
1942    fn scan_wrong_arity() {
1943        let err = Command::from_frame(cmd(&["SCAN"])).unwrap_err();
1944        assert!(matches!(err, ProtocolError::WrongArity(_)));
1945    }
1946
1947    #[test]
1948    fn scan_invalid_cursor() {
1949        let err = Command::from_frame(cmd(&["SCAN", "notanum"])).unwrap_err();
1950        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1951    }
1952
1953    #[test]
1954    fn scan_invalid_count() {
1955        let err = Command::from_frame(cmd(&["SCAN", "0", "COUNT", "bad"])).unwrap_err();
1956        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1957    }
1958
1959    #[test]
1960    fn scan_unknown_flag() {
1961        let err = Command::from_frame(cmd(&["SCAN", "0", "BADOPT", "val"])).unwrap_err();
1962        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
1963    }
1964
1965    #[test]
1966    fn scan_match_missing_pattern() {
1967        let err = Command::from_frame(cmd(&["SCAN", "0", "MATCH"])).unwrap_err();
1968        assert!(matches!(err, ProtocolError::WrongArity(_)));
1969    }
1970
1971    #[test]
1972    fn scan_count_missing_value() {
1973        let err = Command::from_frame(cmd(&["SCAN", "0", "COUNT"])).unwrap_err();
1974        assert!(matches!(err, ProtocolError::WrongArity(_)));
1975    }
1976}