Skip to main content

glide/commands/
stream.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Stream commands. Mirrors Python's stream command surface.
3#![allow(clippy::too_many_arguments, clippy::type_complexity)]
4
5use crate::GlideError;
6use crate::ValkeyResult;
7use crate::cmd::Cmd;
8use crate::executor::CommandExecutor;
9use crate::value::FromValkeyValue;
10use crate::value::ValkeyValue;
11use crate::value::to_glide_error;
12use crate::write::ToValkeyArgs;
13use crate::write::ValkeyWrite;
14use async_trait::async_trait;
15use bytes::Bytes;
16use std::collections::HashMap;
17
18/// A single stream entry: its ID and its field/value pairs.
19pub type StreamEntry = (String, Vec<(Bytes, Bytes)>);
20
21/// A stream trimming mode.
22///
23/// Mirrors redis-rs's `streams::StreamTrimmingMode` type.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum StreamTrimmingMode {
27    /// Trim exactly (`=`).
28    Exact,
29    /// Trim approximately (`~`).
30    Approx,
31}
32
33impl ToValkeyArgs for StreamTrimmingMode {
34    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
35        out.write_arg(match self {
36            Self::Exact => b"=",
37            Self::Approx => b"~",
38        });
39    }
40}
41
42/// A stream trim strategy.
43///
44/// Mirrors redis-rs's `streams::StreamTrimStrategy` type.
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum StreamTrimStrategy {
48    /// Evict entries while the stream is longer than the threshold (`MAXLEN`).
49    MaxLen(StreamTrimmingMode, usize, Option<usize>),
50    /// Evict entries with IDs lower than the threshold (`MINID`).
51    MinId(StreamTrimmingMode, String, Option<usize>),
52}
53
54impl StreamTrimStrategy {
55    /// Trim to at most `max_entries` entries (`MAXLEN`).
56    pub fn maxlen(trim: StreamTrimmingMode, max_entries: usize) -> Self {
57        Self::MaxLen(trim, max_entries, None)
58    }
59
60    /// Trim entries with IDs lower than `stream_id` (`MINID`).
61    pub fn minid(trim: StreamTrimmingMode, stream_id: impl Into<String>) -> Self {
62        Self::MinId(trim, stream_id.into(), None)
63    }
64
65    /// Limit the number of entries evicted in a single operation (`LIMIT`).
66    pub fn limit(self, limit: usize) -> Self {
67        match self {
68            Self::MaxLen(mode, threshold, _) => Self::MaxLen(mode, threshold, Some(limit)),
69            Self::MinId(mode, threshold, _) => Self::MinId(mode, threshold, Some(limit)),
70        }
71    }
72}
73
74impl ToValkeyArgs for StreamTrimStrategy {
75    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
76        let limit = match self {
77            Self::MaxLen(mode, threshold, limit) => {
78                out.write_arg(b"MAXLEN");
79                mode.write_valkey_args(out);
80                out.write_arg_fmt(threshold);
81                limit
82            }
83            Self::MinId(mode, threshold, limit) => {
84                out.write_arg(b"MINID");
85                mode.write_valkey_args(out);
86                out.write_arg(threshold.as_bytes());
87                limit
88            }
89        };
90        if let Some(limit) = limit {
91            out.write_arg(b"LIMIT");
92            out.write_arg_fmt(limit);
93        }
94    }
95}
96
97/// A `MAXLEN` trim threshold for `XTRIM`.
98///
99/// Mirrors redis-rs's `streams::StreamMaxlen` type.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum StreamMaxlen {
103    /// Trim to exactly this many entries (`=`).
104    Equals(usize),
105    /// Trim to approximately this many entries (`~`).
106    Approx(usize),
107}
108
109impl ToValkeyArgs for StreamMaxlen {
110    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
111        let (mode, threshold): (&[u8], _) = match *self {
112            Self::Equals(threshold) => (b"=", threshold),
113            Self::Approx(threshold) => (b"~", threshold),
114        };
115        out.write_arg(b"MAXLEN");
116        out.write_arg(mode);
117        out.write_arg_fmt(threshold);
118    }
119}
120
121/// Options for `XTRIM`.
122///
123/// Mirrors redis-rs's `streams::StreamTrimOptions` type, but does not support
124/// `set_deletion_policy` (Valkey's `XTRIM` has no `KEEPREF` / `DELREF` / `ACKED`).
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct StreamTrimOptions {
127    strategy: StreamTrimStrategy,
128}
129
130impl StreamTrimOptions {
131    /// Trim to at most `max_entries` entries (`MAXLEN`).
132    pub fn maxlen(mode: StreamTrimmingMode, max_entries: usize) -> Self {
133        Self {
134            strategy: StreamTrimStrategy::maxlen(mode, max_entries),
135        }
136    }
137
138    /// Trim entries with IDs lower than `stream_id` (`MINID`).
139    pub fn minid(mode: StreamTrimmingMode, stream_id: impl Into<String>) -> Self {
140        Self {
141            strategy: StreamTrimStrategy::minid(mode, stream_id),
142        }
143    }
144
145    /// Limit the number of entries evicted in a single operation (`LIMIT`).
146    pub fn limit(mut self, limit: usize) -> Self {
147        self.strategy = self.strategy.limit(limit);
148        self
149    }
150}
151
152impl ToValkeyArgs for StreamTrimOptions {
153    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
154        self.strategy.write_valkey_args(out);
155    }
156}
157
158/// Options for `XADD`.
159///
160/// Mirrors redis-rs's `streams::StreamAddOptions` type.
161#[derive(Debug, Clone, Default, PartialEq, Eq)]
162pub struct StreamAddOptions {
163    nomkstream: bool,
164    trim: Option<StreamTrimStrategy>,
165}
166
167impl StreamAddOptions {
168    /// Do not create the stream if it does not exist (`NOMKSTREAM`).
169    pub fn nomkstream(mut self) -> Self {
170        self.nomkstream = true;
171        self
172    }
173
174    /// Trim the stream as part of the add.
175    pub fn trim(mut self, trim: StreamTrimStrategy) -> Self {
176        self.trim = Some(trim);
177        self
178    }
179}
180
181impl ToValkeyArgs for StreamAddOptions {
182    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
183        if self.nomkstream {
184            out.write_arg(b"NOMKSTREAM");
185        }
186        self.trim.write_valkey_args(out);
187    }
188}
189
190/// Options for `XREAD` (`BLOCK`/`COUNT`).
191///
192/// Mirrors redis-rs's `streams::StreamReadOptions` type, but does not support `group` or `noack`.
193/// Use [`StreamCommands::xreadgroup`] with [`StreamReadGroupOptions`] instead.
194#[derive(Debug, Clone, Default, PartialEq, Eq)]
195pub struct StreamReadOptions {
196    block: Option<usize>,
197    count: Option<usize>,
198}
199
200impl StreamReadOptions {
201    /// Block for up to `ms` milliseconds waiting for entries (`BLOCK`).
202    pub fn block(mut self, ms: usize) -> Self {
203        self.block = Some(ms);
204        self
205    }
206
207    /// Return at most `n` entries per stream (`COUNT`).
208    pub fn count(mut self, n: usize) -> Self {
209        self.count = Some(n);
210        self
211    }
212}
213
214impl ToValkeyArgs for StreamReadOptions {
215    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
216        if let Some(block) = self.block {
217            out.write_arg(b"BLOCK");
218            out.write_arg_fmt(block);
219        }
220        if let Some(count) = self.count {
221            out.write_arg(b"COUNT");
222            out.write_arg_fmt(count);
223        }
224    }
225}
226
227/// Options for `XREADGROUP`.
228#[derive(Debug, Clone, Copy, Default)]
229pub struct StreamReadGroupOptions {
230    /// Block for up to this many milliseconds waiting for entries (`BLOCK`).
231    pub block_ms: Option<i64>,
232    /// Maximum number of entries to return per stream (`COUNT`).
233    pub count: Option<i64>,
234    /// Do not add read entries to the Pending Entries List (`NOACK`).
235    pub no_ack: bool,
236}
237
238impl ToValkeyArgs for StreamReadGroupOptions {
239    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
240        if let Some(block_ms) = self.block_ms {
241            out.write_arg(b"BLOCK");
242            out.write_arg_fmt(block_ms);
243        }
244        if let Some(count) = self.count {
245            out.write_arg(b"COUNT");
246            out.write_arg_fmt(count);
247        }
248        if self.no_ack {
249            out.write_arg(b"NOACK");
250        }
251    }
252}
253
254/// Options for `XGROUP CREATE` (`MKSTREAM`/`ENTRIESREAD`).
255///
256/// Mirrors Python `StreamGroupOptions`.
257#[derive(Debug, Clone, Copy, Default)]
258pub struct StreamGroupCreateOptions {
259    /// Create the stream if it does not exist (`MKSTREAM`).
260    pub make_stream: bool,
261    /// Number of entries already read by the group (`ENTRIESREAD`, Valkey 7+).
262    pub entries_read: Option<i64>,
263}
264
265impl ToValkeyArgs for StreamGroupCreateOptions {
266    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
267        if self.make_stream {
268            out.write_arg(b"MKSTREAM");
269        }
270        if let Some(entries_read) = self.entries_read {
271            out.write_arg(b"ENTRIESREAD");
272            out.write_arg_fmt(entries_read);
273        }
274    }
275}
276
277/// Options for `XCLAIM`.
278///
279/// Mirrors redis-rs's `streams::StreamClaimOptions` type.
280#[derive(Debug, Clone, Default, PartialEq, Eq)]
281pub struct StreamClaimOptions {
282    idle: Option<usize>,
283    time: Option<usize>,
284    retry: Option<usize>,
285    force: bool,
286    justid: bool,
287    lastid: Option<String>,
288}
289
290impl StreamClaimOptions {
291    /// Set the idle time (ms) of the claimed messages (`IDLE`).
292    pub fn idle(mut self, ms: usize) -> Self {
293        self.idle = Some(ms);
294        self
295    }
296
297    /// Set the idle time to a specific Unix time in ms (`TIME`).
298    pub fn time(mut self, ms_time: usize) -> Self {
299        self.time = Some(ms_time);
300        self
301    }
302
303    /// Set the retry counter (`RETRYCOUNT`).
304    pub fn retry(mut self, count: usize) -> Self {
305        self.retry = Some(count);
306        self
307    }
308
309    /// Create the PEL entry even if the message is not already pending (`FORCE`).
310    pub fn with_force(mut self) -> Self {
311        self.force = true;
312        self
313    }
314
315    /// Return only the claimed IDs (`JUSTID`). The reply type changes with this option.
316    pub fn with_justid(mut self) -> Self {
317        self.justid = true;
318        self
319    }
320
321    /// Set the group's last-delivered ID (`LASTID`).
322    pub fn with_lastid(mut self, lastid: impl Into<String>) -> Self {
323        self.lastid = Some(lastid.into());
324        self
325    }
326}
327
328impl ToValkeyArgs for StreamClaimOptions {
329    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
330        if let Some(idle) = self.idle {
331            out.write_arg(b"IDLE");
332            out.write_arg_fmt(idle);
333        }
334        if let Some(time) = self.time {
335            out.write_arg(b"TIME");
336            out.write_arg_fmt(time);
337        }
338        if let Some(retry) = self.retry {
339            out.write_arg(b"RETRYCOUNT");
340            out.write_arg_fmt(retry);
341        }
342        if self.force {
343            out.write_arg(b"FORCE");
344        }
345        if self.justid {
346            out.write_arg(b"JUSTID");
347        }
348        if let Some(lastid) = &self.lastid {
349            out.write_arg(b"LASTID");
350            out.write_arg(lastid.as_bytes());
351        }
352    }
353}
354
355/// Options for `XAUTOCLAIM`.
356///
357/// Mirrors redis-rs's `streams::StreamAutoClaimOptions` type.
358#[derive(Debug, Clone, Default, PartialEq, Eq)]
359pub struct StreamAutoClaimOptions {
360    count: Option<usize>,
361    justid: bool,
362}
363
364impl StreamAutoClaimOptions {
365    /// Claim at most `n` entries (`COUNT`).
366    pub fn count(mut self, n: usize) -> Self {
367        self.count = Some(n);
368        self
369    }
370
371    /// Return only the claimed IDs (`JUSTID`), leaving each claimed entry's map empty.
372    pub fn with_justid(mut self) -> Self {
373        self.justid = true;
374        self
375    }
376}
377
378impl ToValkeyArgs for StreamAutoClaimOptions {
379    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
380        if let Some(count) = self.count {
381            out.write_arg(b"COUNT");
382            out.write_arg_fmt(count);
383        }
384        if self.justid {
385            out.write_arg(b"JUSTID");
386        }
387    }
388}
389
390/// A single entry from the extended (range) form of `XPENDING`.
391#[derive(Debug, Clone)]
392pub struct XPendingEntry {
393    /// The message ID.
394    pub id: Bytes,
395    /// The consumer that currently owns the message.
396    pub consumer: Bytes,
397    /// Milliseconds since the message was last delivered.
398    pub idle_ms: i64,
399    /// Number of times the message was delivered.
400    pub delivery_count: i64,
401}
402
403impl FromValkeyValue for XPendingEntry {
404    /// Decodes `[id, consumer, idle_ms, delivery_count]`.
405    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
406        match value {
407            ValkeyValue::Array(items) if items.len() == 4 => {
408                let mut items = items.into_iter();
409                let mut next = || items.next().expect("checked length");
410                Ok(Self {
411                    id: Bytes::from_owned_valkey_value(next())?,
412                    consumer: Bytes::from_owned_valkey_value(next())?,
413                    idle_ms: i64::from_owned_valkey_value(next())?,
414                    delivery_count: i64::from_owned_valkey_value(next())?,
415                })
416            }
417            other => Err(to_glide_error(other, "Unexpected XPENDING entry.")),
418        }
419    }
420}
421
422/// A stream entry: its ID and field/value pairs.
423///
424/// Mirrors redis-rs's `streams::StreamId` type.
425#[derive(Debug, Clone, Default, PartialEq)]
426pub struct StreamId {
427    /// The entry ID.
428    pub id: String,
429    /// The entry's fields, with their values.
430    pub map: HashMap<String, ValkeyValue>,
431    /// Milliseconds since the entry was last delivered to a consumer, if reported.
432    pub milliseconds_elapsed_from_delivery: Option<usize>,
433    /// The number of times the entry was delivered, if reported.
434    pub delivered_count: Option<usize>,
435}
436
437impl StreamId {
438    /// Returns the value of `key` decoded as `T`, or `None` if it is missing or
439    /// does not decode.
440    pub fn get<T: FromValkeyValue>(&self, key: &str) -> Option<T> {
441        self.map
442            .get(key)
443            .and_then(|value| T::from_owned_valkey_value(value.clone()).ok())
444    }
445
446    /// Returns `true` if the entry has the field `key`.
447    pub fn contains_key(&self, key: &str) -> bool {
448        self.map.contains_key(key)
449    }
450
451    /// Returns the number of fields in the entry.
452    pub fn len(&self) -> usize {
453        self.map.len()
454    }
455
456    /// Returns `true` if the entry has no fields.
457    pub fn is_empty(&self) -> bool {
458        self.map.is_empty()
459    }
460}
461
462/// A stream key and its entries, as returned by `XREAD`.
463///
464/// Mirrors redis-rs's `streams::StreamKey` type.
465#[derive(Debug, Clone, Default)]
466pub struct StreamKey {
467    /// The stream key.
468    pub key: String,
469    /// The stream's entries.
470    pub ids: Vec<StreamId>,
471}
472
473/// The `XREAD` reply.
474///
475/// Mirrors redis-rs's `streams::StreamReadReply` type.
476#[derive(Debug, Clone, Default)]
477pub struct StreamReadReply {
478    /// The entries of each stream read.
479    pub keys: Vec<StreamKey>,
480}
481
482impl FromValkeyValue for StreamReadReply {
483    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
484        let keys = map_pairs(value, "Unexpected XREAD reply.")?
485            .into_iter()
486            .map(|(key, entries)| {
487                Ok(StreamKey {
488                    key: String::from_owned_valkey_value(key)?,
489                    ids: parse_stream_ids(entries)?,
490                })
491            })
492            .collect::<ValkeyResult<_>>()?;
493        Ok(Self { keys })
494    }
495}
496
497/// The `XRANGE` / `XREVRANGE` reply.
498///
499/// Mirrors redis-rs's `streams::StreamRangeReply` type.
500#[derive(Debug, Clone, Default)]
501pub struct StreamRangeReply {
502    /// The entries in the range.
503    pub ids: Vec<StreamId>,
504}
505
506impl FromValkeyValue for StreamRangeReply {
507    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
508        Ok(Self {
509            ids: parse_stream_ids(value)?,
510        })
511    }
512}
513
514/// The `XCLAIM` reply.
515///
516/// Mirrors redis-rs's `streams::StreamClaimReply` type.
517#[derive(Debug, Clone, Default)]
518pub struct StreamClaimReply {
519    /// The claimed entries.
520    pub ids: Vec<StreamId>,
521}
522
523impl FromValkeyValue for StreamClaimReply {
524    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
525        Ok(Self {
526            ids: parse_stream_ids(value)?,
527        })
528    }
529}
530
531/// The `XAUTOCLAIM` reply.
532///
533/// Mirrors redis-rs's `streams::StreamAutoClaimReply` type.
534#[derive(Debug, Clone, Default)]
535pub struct StreamAutoClaimReply {
536    /// The ID to pass as `start` to the next `XAUTOCLAIM` call (`0-0` when done).
537    pub next_stream_id: String,
538    /// The claimed entries. With `JUSTID`, each entry's map is empty.
539    pub claimed: Vec<StreamId>,
540    /// The pending IDs that were deleted from the stream (empty before Valkey 7.0).
541    pub deleted_ids: Vec<String>,
542    /// Whether nil entries (deleted from the stream; Redis 6 only) were skipped.
543    /// Only detected with `JUSTID`, since glide-core drops nil full entries.
544    pub invalid_entries: bool,
545}
546
547impl FromValkeyValue for StreamAutoClaimReply {
548    /// Decodes `[next_stream_id, claimed, deleted_ids]`, where `deleted_ids` is absent
549    /// before Valkey 7.0.
550    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
551        match value {
552            ValkeyValue::Array(mut items) if items.len() == 2 || items.len() == 3 => {
553                let deleted_ids = if items.len() == 3 {
554                    Vec::<String>::from_owned_valkey_value(items.pop().expect("checked length"))?
555                } else {
556                    Vec::new()
557                };
558                let claimed = items.pop().expect("checked length");
559                let next_stream_id =
560                    String::from_owned_valkey_value(items.pop().expect("checked length"))?;
561                let (claimed, invalid_entries) = match claimed {
562                    // `JUSTID`: an array of IDs.
563                    ValkeyValue::Array(ids) => {
564                        let count = ids.len();
565                        let claimed = ids
566                            .into_iter()
567                            .filter(|id| !matches!(id, ValkeyValue::Nil))
568                            .map(|id| {
569                                Ok(StreamId {
570                                    id: String::from_owned_valkey_value(id)?,
571                                    ..Default::default()
572                                })
573                            })
574                            .collect::<ValkeyResult<Vec<_>>>()?;
575                        let invalid_entries = claimed.len() < count;
576                        (claimed, invalid_entries)
577                    }
578                    other => (parse_stream_ids(other)?, false),
579                };
580                Ok(Self {
581                    next_stream_id,
582                    claimed,
583                    deleted_ids,
584                    invalid_entries,
585                })
586            }
587            other => Err(to_glide_error(other, "Unexpected XAUTOCLAIM reply.")),
588        }
589    }
590}
591
592/// The summary `XPENDING` reply.
593///
594/// Mirrors redis-rs's `streams::StreamPendingReply` type.
595#[derive(Debug, Clone, Default)]
596#[non_exhaustive]
597pub enum StreamPendingReply {
598    /// No entries are pending.
599    #[default]
600    Empty,
601    /// Some entries are pending.
602    Data(StreamPendingData),
603}
604
605impl StreamPendingReply {
606    /// Returns the number of pending entries.
607    pub fn count(&self) -> usize {
608        match self {
609            Self::Empty => 0,
610            Self::Data(data) => data.count,
611        }
612    }
613}
614
615impl FromValkeyValue for StreamPendingReply {
616    /// Decodes `[count, start_id, end_id, [[consumer, count], ...]]`.
617    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
618        type Summary = (
619            usize,
620            Option<String>,
621            Option<String>,
622            Option<Vec<(String, usize)>>,
623        );
624        let (count, start_id, end_id, consumers) = Summary::from_owned_valkey_value(value)?;
625        if count == 0 {
626            return Ok(Self::Empty);
627        }
628        let (Some(start_id), Some(end_id)) = (start_id, end_id) else {
629            return Err(GlideError::Request(
630                "Non-empty XPENDING reply without start and end IDs.".into(),
631            ));
632        };
633        let consumers = consumers
634            .unwrap_or_default()
635            .into_iter()
636            .map(|(name, pending)| StreamInfoConsumer {
637                name,
638                pending,
639                idle: 0,
640            })
641            .collect();
642        Ok(Self::Data(StreamPendingData {
643            count,
644            start_id,
645            end_id,
646            consumers,
647        }))
648    }
649}
650
651/// The details of a non-empty [`StreamPendingReply`].
652///
653/// Mirrors redis-rs's `streams::StreamPendingData` type.
654#[derive(Debug, Clone, Default)]
655pub struct StreamPendingData {
656    /// The number of pending entries.
657    pub count: usize,
658    /// The smallest pending entry ID.
659    pub start_id: String,
660    /// The largest pending entry ID.
661    pub end_id: String,
662    /// Every consumer with pending entries, and how many it has (`idle` is not reported).
663    pub consumers: Vec<StreamInfoConsumer>,
664}
665
666/// The extended (range) `XPENDING` reply.
667///
668/// Mirrors redis-rs's `streams::StreamPendingCountReply` type.
669#[derive(Debug, Clone, Default)]
670pub struct StreamPendingCountReply {
671    /// The pending entries.
672    pub ids: Vec<StreamPendingId>,
673}
674
675impl FromValkeyValue for StreamPendingCountReply {
676    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
677        let ids = Vec::<(String, String, usize, usize)>::from_owned_valkey_value(value)?
678            .into_iter()
679            .map(
680                |(id, consumer, last_delivered_ms, times_delivered)| StreamPendingId {
681                    id,
682                    consumer,
683                    last_delivered_ms,
684                    times_delivered,
685                },
686            )
687            .collect();
688        Ok(Self { ids })
689    }
690}
691
692/// A pending entry in a [`StreamPendingCountReply`].
693///
694/// Mirrors redis-rs's `streams::StreamPendingId` type.
695#[derive(Debug, Clone, Default)]
696pub struct StreamPendingId {
697    /// The entry ID.
698    pub id: String,
699    /// The consumer that owns the entry.
700    pub consumer: String,
701    /// Milliseconds since the entry was last delivered.
702    pub last_delivered_ms: usize,
703    /// The number of times the entry was delivered.
704    pub times_delivered: usize,
705}
706
707/// The `XINFO STREAM` reply.
708///
709/// Mirrors redis-rs's `streams::StreamInfoStreamReply` type.
710#[derive(Debug, Clone, Default)]
711pub struct StreamInfoStreamReply {
712    /// The last generated ID, which may differ from the last entry's ID.
713    pub last_generated_id: String,
714    /// The number of radix tree nodes (`radix-tree-nodes`).
715    pub radix_tree_keys: usize,
716    /// The number of consumer groups.
717    pub groups: usize,
718    /// The number of entries.
719    pub length: usize,
720    /// The first entry (default if the stream is empty).
721    pub first_entry: StreamId,
722    /// The last entry (default if the stream is empty).
723    pub last_entry: StreamId,
724}
725
726impl FromValkeyValue for StreamInfoStreamReply {
727    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
728        let mut reply = Self::default();
729        for (field, value) in map_pairs(value, "Unexpected XINFO STREAM reply.")? {
730            match String::from_owned_valkey_value(field)?.as_str() {
731                "last-generated-id" => {
732                    reply.last_generated_id = FromValkeyValue::from_owned_valkey_value(value)?
733                }
734                "radix-tree-nodes" => {
735                    reply.radix_tree_keys = FromValkeyValue::from_owned_valkey_value(value)?
736                }
737                "groups" => reply.groups = FromValkeyValue::from_owned_valkey_value(value)?,
738                "length" => reply.length = FromValkeyValue::from_owned_valkey_value(value)?,
739                "first-entry" => reply.first_entry = parse_optional_stream_id(value)?,
740                "last-entry" => reply.last_entry = parse_optional_stream_id(value)?,
741                _ => {}
742            }
743        }
744        Ok(reply)
745    }
746}
747
748/// The `XINFO CONSUMERS` reply.
749///
750/// Mirrors redis-rs's `streams::StreamInfoConsumersReply` type.
751#[derive(Debug, Clone, Default)]
752pub struct StreamInfoConsumersReply {
753    /// Every consumer in the group.
754    pub consumers: Vec<StreamInfoConsumer>,
755}
756
757impl FromValkeyValue for StreamInfoConsumersReply {
758    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
759        let consumers = list_of_map_pairs(value, "Unexpected XINFO CONSUMERS reply.")?
760            .into_iter()
761            .map(|fields| {
762                let mut consumer = StreamInfoConsumer::default();
763                for (field, value) in fields {
764                    match String::from_owned_valkey_value(field)?.as_str() {
765                        "name" => consumer.name = FromValkeyValue::from_owned_valkey_value(value)?,
766                        "pending" => {
767                            consumer.pending = FromValkeyValue::from_owned_valkey_value(value)?
768                        }
769                        "idle" => consumer.idle = FromValkeyValue::from_owned_valkey_value(value)?,
770                        _ => {}
771                    }
772                }
773                Ok(consumer)
774            })
775            .collect::<ValkeyResult<_>>()?;
776        Ok(Self { consumers })
777    }
778}
779
780/// The `XINFO GROUPS` reply.
781///
782/// Mirrors redis-rs's `streams::StreamInfoGroupsReply` type.
783#[derive(Debug, Clone, Default)]
784pub struct StreamInfoGroupsReply {
785    /// Every consumer group of the stream.
786    pub groups: Vec<StreamInfoGroup>,
787}
788
789impl FromValkeyValue for StreamInfoGroupsReply {
790    fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
791        let groups = list_of_map_pairs(value, "Unexpected XINFO GROUPS reply.")?
792            .into_iter()
793            .map(|fields| {
794                let mut group = StreamInfoGroup::default();
795                for (field, value) in fields {
796                    match String::from_owned_valkey_value(field)?.as_str() {
797                        "name" => group.name = FromValkeyValue::from_owned_valkey_value(value)?,
798                        "consumers" => {
799                            group.consumers = FromValkeyValue::from_owned_valkey_value(value)?
800                        }
801                        "pending" => {
802                            group.pending = FromValkeyValue::from_owned_valkey_value(value)?
803                        }
804                        "last-delivered-id" => {
805                            group.last_delivered_id =
806                                FromValkeyValue::from_owned_valkey_value(value)?
807                        }
808                        "entries-read" => {
809                            group.entries_read = FromValkeyValue::from_owned_valkey_value(value)?
810                        }
811                        "lag" => group.lag = FromValkeyValue::from_owned_valkey_value(value)?,
812                        _ => {}
813                    }
814                }
815                Ok(group)
816            })
817            .collect::<ValkeyResult<_>>()?;
818        Ok(Self { groups })
819    }
820}
821
822/// A consumer, as reported by `XINFO CONSUMERS` or `XPENDING`.
823///
824/// Mirrors redis-rs's `streams::StreamInfoConsumer` type.
825#[derive(Debug, Clone, Default)]
826pub struct StreamInfoConsumer {
827    /// The consumer name.
828    pub name: String,
829    /// The number of entries pending for the consumer.
830    pub pending: usize,
831    /// The consumer's idle time in milliseconds.
832    pub idle: usize,
833}
834
835/// A consumer group, as reported by `XINFO GROUPS`.
836///
837/// Mirrors redis-rs's `streams::StreamInfoGroup` type.
838#[derive(Debug, Clone, Default)]
839pub struct StreamInfoGroup {
840    /// The group name.
841    pub name: String,
842    /// The number of consumers in the group.
843    pub consumers: usize,
844    /// The number of entries delivered to the group but not yet acknowledged.
845    pub pending: usize,
846    /// The ID of the last entry delivered to the group.
847    pub last_delivered_id: String,
848    /// The logical read counter of the last entry delivered, if reported.
849    pub entries_read: Option<usize>,
850    /// The number of entries not yet delivered to the group, if known.
851    pub lag: Option<usize>,
852}
853
854/// Returns the pairs of a map reply, or none for nil.
855fn map_pairs(value: ValkeyValue, error: &str) -> ValkeyResult<Vec<(ValkeyValue, ValkeyValue)>> {
856    match value {
857        ValkeyValue::Nil => Ok(Vec::new()),
858        ValkeyValue::Map(pairs) => Ok(pairs),
859        other => Err(to_glide_error(other, error)),
860    }
861}
862
863/// Returns the pairs of each map in an array of maps (e.g. `XINFO GROUPS`).
864fn list_of_map_pairs(
865    value: ValkeyValue,
866    error: &str,
867) -> ValkeyResult<Vec<Vec<(ValkeyValue, ValkeyValue)>>> {
868    match value {
869        ValkeyValue::Nil => Ok(Vec::new()),
870        ValkeyValue::Array(items) => items
871            .into_iter()
872            .map(|item| map_pairs(item, error))
873            .collect(),
874        other => Err(to_glide_error(other, error)),
875    }
876}
877
878/// Decodes stream entries from a map of entry ID to `[[field, value], ...]`.
879fn parse_stream_ids(value: ValkeyValue) -> ValkeyResult<Vec<StreamId>> {
880    map_pairs(value, "Unexpected stream entries reply.")?
881        .into_iter()
882        .map(|(id, fields)| {
883            let fields = match fields {
884                ValkeyValue::Array(fields) => fields
885                    .into_iter()
886                    .map(|pair| match pair {
887                        ValkeyValue::Array(pair) if pair.len() == 2 => {
888                            let [field, value]: [ValkeyValue; 2] =
889                                pair.try_into().expect("checked length");
890                            Ok((field, value))
891                        }
892                        other => Err(to_glide_error(other, "Unexpected stream entry field.")),
893                    })
894                    .collect::<ValkeyResult<_>>()?,
895                other => return Err(to_glide_error(other, "Unexpected stream entry fields.")),
896            };
897            stream_id(id, fields)
898        })
899        .collect()
900}
901
902/// Decodes an `XINFO STREAM` entry, `[id, [field, value, ...]]`, or nil as the default entry.
903fn parse_optional_stream_id(value: ValkeyValue) -> ValkeyResult<StreamId> {
904    match value {
905        ValkeyValue::Nil => Ok(StreamId::default()),
906        ValkeyValue::Array(items) if items.len() == 2 => {
907            let [id, fields]: [ValkeyValue; 2] = items.try_into().expect("checked length");
908            let fields = match fields {
909                ValkeyValue::Array(fields) if fields.len() % 2 == 0 => fields,
910                other => return Err(to_glide_error(other, "Unexpected stream entry fields.")),
911            };
912            let mut fields = fields.into_iter();
913            let mut pairs = Vec::with_capacity(fields.len() / 2);
914            while let (Some(field), Some(value)) = (fields.next(), fields.next()) {
915                pairs.push((field, value));
916            }
917            stream_id(id, pairs)
918        }
919        other => Err(to_glide_error(other, "Unexpected stream entry reply.")),
920    }
921}
922
923/// Builds a stream entry from its ID and field/value pairs.
924fn stream_id(id: ValkeyValue, fields: Vec<(ValkeyValue, ValkeyValue)>) -> ValkeyResult<StreamId> {
925    let map = fields
926        .into_iter()
927        .map(|(field, value)| Ok((String::from_owned_valkey_value(field)?, value)))
928        .collect::<ValkeyResult<_>>()?;
929    Ok(StreamId {
930        id: String::from_owned_valkey_value(id)?,
931        map,
932        milliseconds_elapsed_from_delivery: None,
933        delivered_count: None,
934    })
935}
936
937/// Stream commands beyond the command table (consumer-group reads, pending ranges, ...).
938#[async_trait]
939pub trait StreamCommands: CommandExecutor {
940    /// Read from streams as part of a consumer group (`XREADGROUP`).
941    async fn xreadgroup<K: ToValkeyArgs + Send + Sync>(
942        &self,
943        group: &str,
944        consumer: &str,
945        keys_ids: &[(K, &str)],
946        options: Option<StreamReadGroupOptions>,
947    ) -> ValkeyResult<Vec<(Bytes, Vec<StreamEntry>)>> {
948        let mut cmd = Cmd::new();
949        cmd.arg("XREADGROUP").arg("GROUP").arg(group).arg(consumer);
950        cmd.arg(options);
951        cmd.arg("STREAMS");
952        for (k, _) in keys_ids {
953            cmd.arg(k);
954        }
955        for (_, id) in keys_ids {
956            cmd.arg(*id);
957        }
958        parse_stream_read(self.execute_command(cmd, None).await?)
959    }
960
961    /// Extended (range) form of `XPENDING`
962    /// (`XPENDING key group [IDLE ms] start end count [consumer]`).
963    async fn xpending_range<K: ToValkeyArgs + Send>(
964        &self,
965        key: K,
966        group: &str,
967        start: &str,
968        end: &str,
969        count: i64,
970        min_idle_time_ms: Option<i64>,
971        consumer: Option<&str>,
972    ) -> ValkeyResult<Vec<XPendingEntry>> {
973        let mut cmd = Cmd::new();
974        cmd.arg("XPENDING").arg(key).arg(group);
975        if let Some(idle) = min_idle_time_ms {
976            cmd.arg("IDLE").arg(idle);
977        }
978        cmd.arg(start).arg(end).arg(count);
979        if let Some(c) = consumer {
980            cmd.arg(c);
981        }
982        Vec::<XPendingEntry>::from_owned_valkey_value(self.execute_command(cmd, None).await?)
983    }
984
985    /// Get the full state of a stream including entries and PEL
986    /// (`XINFO STREAM ... FULL`). Returns the raw structured reply as
987    /// `(field, value)` pairs. Pass `count` to limit returned entries/PEL.
988    async fn xinfo_stream_full<K: ToValkeyArgs + Send>(
989        &self,
990        key: K,
991        count: Option<i64>,
992    ) -> ValkeyResult<Vec<(Bytes, ValkeyValue)>> {
993        let mut cmd = Cmd::new();
994        cmd.arg("XINFO").arg("STREAM").arg(key).arg("FULL");
995        if let Some(c) = count {
996            cmd.arg("COUNT").arg(c);
997        }
998        parse_field_value_map(self.execute_command(cmd, None).await?)
999    }
1000
1001    /// Set the last-delivered ID of a stream (`XSETID`).
1002    async fn xsetid<K: ToValkeyArgs + Send>(
1003        &self,
1004        key: K,
1005        last_id: &str,
1006        entries_added: Option<i64>,
1007        max_deleted_id: Option<&str>,
1008    ) -> ValkeyResult<()> {
1009        let mut cmd = Cmd::new();
1010        cmd.arg("XSETID").arg(key).arg(last_id);
1011        if let Some(e) = entries_added {
1012            cmd.arg("ENTRIESADDED").arg(e);
1013        }
1014        if let Some(m) = max_deleted_id {
1015            cmd.arg("MAXDELETEDID").arg(m);
1016        }
1017        <()>::from_owned_valkey_value(self.execute_command(cmd, None).await?)
1018    }
1019
1020    /// Create a consumer group with options (`XGROUP CREATE` with `MKSTREAM` /
1021    /// `ENTRIESREAD`).
1022    async fn xgroup_create_options<K: ToValkeyArgs + Send>(
1023        &self,
1024        key: K,
1025        group: &str,
1026        id: &str,
1027        options: &StreamGroupCreateOptions,
1028    ) -> ValkeyResult<()> {
1029        let mut cmd = Cmd::new();
1030        cmd.arg("XGROUP")
1031            .arg("CREATE")
1032            .arg(key)
1033            .arg(group)
1034            .arg(id)
1035            .arg(options);
1036        <()>::from_owned_valkey_value(self.execute_command(cmd, None).await?)
1037    }
1038
1039    /// Set the last-delivered ID for a consumer group (`XGROUP SETID`).
1040    async fn xgroup_set_id<K: ToValkeyArgs + Send>(
1041        &self,
1042        key: K,
1043        group: &str,
1044        id: &str,
1045        entries_read: Option<i64>,
1046    ) -> ValkeyResult<()> {
1047        let mut cmd = Cmd::new();
1048        cmd.arg("XGROUP").arg("SETID").arg(key).arg(group).arg(id);
1049        if let Some(e) = entries_read {
1050            cmd.arg("ENTRIESREAD").arg(e);
1051        }
1052        <()>::from_owned_valkey_value(self.execute_command(cmd, None).await?)
1053    }
1054}
1055
1056impl<T: CommandExecutor + ?Sized> StreamCommands for T {}
1057
1058/// Parse an `XRANGE`/`XREVRANGE` reply into `(id, [(field, value), ...])` entries,
1059/// handling both RESP2 (array of `[id, [f, v, ...]]`) and RESP3 (map of
1060/// `id -> [[f, v], ...]`).
1061fn parse_entries(v: ValkeyValue) -> ValkeyResult<Vec<StreamEntry>> {
1062    let pairs: Vec<(ValkeyValue, ValkeyValue)> = match v {
1063        ValkeyValue::Nil => return Ok(Vec::new()),
1064        ValkeyValue::Map(pairs) => pairs,
1065        ValkeyValue::Array(items) => {
1066            // RESP2: each item is [id, fields]. Normalize to (id, fields) pairs.
1067            let mut out = Vec::with_capacity(items.len());
1068            for entry in items {
1069                if let ValkeyValue::Array(mut parts) = entry
1070                    && parts.len() == 2
1071                {
1072                    let fields = parts.pop().unwrap();
1073                    let id = parts.pop().unwrap();
1074                    out.push((id, fields));
1075                }
1076            }
1077            out
1078        }
1079        other => {
1080            return Err(to_glide_error(other, "Unexpected stream reply."));
1081        }
1082    };
1083
1084    let mut out = Vec::with_capacity(pairs.len());
1085    for (id_val, fields_val) in pairs {
1086        let id = String::from_owned_valkey_value(id_val)?;
1087        let fv = parse_fields(fields_val)?;
1088        out.push((id, fv));
1089    }
1090    Ok(out)
1091}
1092
1093/// Parse a field/value collection that may be flat (`[f, v, f, v]`) or nested
1094/// pairs (`[[f, v], [f, v]]`).
1095fn parse_fields(v: ValkeyValue) -> ValkeyResult<Vec<(Bytes, Bytes)>> {
1096    let items = match v {
1097        ValkeyValue::Array(items) => items,
1098        ValkeyValue::Nil => return Ok(Vec::new()),
1099        other => return Ok(vec![(Bytes::from_owned_valkey_value(other)?, Bytes::new())]),
1100    };
1101    // Nested pairs form.
1102    if items
1103        .iter()
1104        .all(|it| matches!(it, ValkeyValue::Array(inner) if inner.len() == 2))
1105    {
1106        let mut out = Vec::with_capacity(items.len());
1107        for it in items {
1108            if let ValkeyValue::Array(mut pair) = it {
1109                let val = Bytes::from_owned_valkey_value(pair.pop().unwrap())?;
1110                let field = Bytes::from_owned_valkey_value(pair.pop().unwrap())?;
1111                out.push((field, val));
1112            }
1113        }
1114        return Ok(out);
1115    }
1116    // Flat form.
1117    let mut out = Vec::with_capacity(items.len() / 2);
1118    let mut iter = items.into_iter();
1119    while let (Some(f), Some(val)) = (iter.next(), iter.next()) {
1120        out.push((
1121            Bytes::from_owned_valkey_value(f)?,
1122            Bytes::from_owned_valkey_value(val)?,
1123        ));
1124    }
1125    Ok(out)
1126}
1127
1128/// Parse an `XREAD`/`XREADGROUP` reply (map or array of `[key, entries]`) into
1129/// `(stream_key, entries)` pairs.
1130fn parse_stream_read(v: ValkeyValue) -> ValkeyResult<Vec<(Bytes, Vec<StreamEntry>)>> {
1131    let pairs: Vec<(ValkeyValue, ValkeyValue)> = match v {
1132        ValkeyValue::Nil => return Ok(Vec::new()),
1133        ValkeyValue::Map(pairs) => pairs,
1134        ValkeyValue::Array(items) => {
1135            let mut out = Vec::with_capacity(items.len());
1136            for entry in items {
1137                if let ValkeyValue::Array(mut parts) = entry
1138                    && parts.len() == 2
1139                {
1140                    let entries = parts.pop().unwrap();
1141                    let key = parts.pop().unwrap();
1142                    out.push((key, entries));
1143                }
1144            }
1145            out
1146        }
1147        other => {
1148            return Err(to_glide_error(other, "Unexpected XREAD reply."));
1149        }
1150    };
1151    let mut out = Vec::with_capacity(pairs.len());
1152    for (key_val, entries_val) in pairs {
1153        let key = Bytes::from_owned_valkey_value(key_val)?;
1154        let entries = parse_entries(entries_val)?;
1155        out.push((key, entries));
1156    }
1157    Ok(out)
1158}
1159
1160/// Parse a structured reply (RESP3 map or RESP2 flat array of alternating
1161/// field/value) into `(field, value)` pairs.
1162fn parse_field_value_map(v: ValkeyValue) -> ValkeyResult<Vec<(Bytes, ValkeyValue)>> {
1163    match v {
1164        ValkeyValue::Nil => Ok(Vec::new()),
1165        ValkeyValue::Map(pairs) => pairs
1166            .into_iter()
1167            .map(|(k, val)| Ok((Bytes::from_owned_valkey_value(k)?, val)))
1168            .collect(),
1169        ValkeyValue::Array(items) => {
1170            let mut out = Vec::with_capacity(items.len() / 2);
1171            let mut iter = items.into_iter();
1172            while let (Some(k), Some(val)) = (iter.next(), iter.next()) {
1173                out.push((Bytes::from_owned_valkey_value(k)?, val));
1174            }
1175            Ok(out)
1176        }
1177        other => Err(to_glide_error(other, "Unexpected XINFO reply.")),
1178    }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183    use super::*;
1184    use crate::test_utils::assert_args;
1185    use crate::test_utils::assert_args_empty;
1186
1187    #[test]
1188    fn trim_strategy_args() {
1189        assert_args(
1190            StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 100),
1191            &["MAXLEN", "=", "100"],
1192        );
1193        assert_args(
1194            StreamTrimStrategy::maxlen(StreamTrimmingMode::Approx, 100).limit(10),
1195            &["MAXLEN", "~", "100", "LIMIT", "10"],
1196        );
1197        assert_args(
1198            StreamTrimStrategy::minid(StreamTrimmingMode::Approx, "1526985054069-0"),
1199            &["MINID", "~", "1526985054069-0"],
1200        );
1201    }
1202
1203    #[test]
1204    fn add_options_args() {
1205        assert_args_empty(StreamAddOptions::default());
1206        assert_args(
1207            StreamAddOptions::default()
1208                .nomkstream()
1209                .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 5)),
1210            &["NOMKSTREAM", "MAXLEN", "=", "5"],
1211        );
1212    }
1213
1214    #[test]
1215    fn read_group_options_args() {
1216        assert_args_empty(StreamReadGroupOptions::default());
1217        assert_args(
1218            StreamReadGroupOptions {
1219                block_ms: Some(500),
1220                count: Some(10),
1221                no_ack: true,
1222            },
1223            &["BLOCK", "500", "COUNT", "10", "NOACK"],
1224        );
1225    }
1226
1227    #[test]
1228    fn group_create_options_args() {
1229        assert_args_empty(StreamGroupCreateOptions::default());
1230        assert_args(
1231            StreamGroupCreateOptions {
1232                make_stream: true,
1233                entries_read: Some(7),
1234            },
1235            &["MKSTREAM", "ENTRIESREAD", "7"],
1236        );
1237    }
1238
1239    #[test]
1240    fn claim_options_args() {
1241        assert_args_empty(StreamClaimOptions::default());
1242        assert_args(
1243            StreamClaimOptions::default()
1244                .idle(100)
1245                .retry(3)
1246                .with_force()
1247                .with_justid()
1248                .with_lastid("5-0"),
1249            &[
1250                "IDLE",
1251                "100",
1252                "RETRYCOUNT",
1253                "3",
1254                "FORCE",
1255                "JUSTID",
1256                "LASTID",
1257                "5-0",
1258            ],
1259        );
1260        assert_args(
1261            StreamClaimOptions::default().time(1_700_000_000_000),
1262            &["TIME", "1700000000000"],
1263        );
1264    }
1265
1266    #[test]
1267    fn maxlen_args() {
1268        assert_args(StreamMaxlen::Equals(5), &["MAXLEN", "=", "5"]);
1269        assert_args(StreamMaxlen::Approx(5), &["MAXLEN", "~", "5"]);
1270    }
1271
1272    #[test]
1273    fn trim_options_args() {
1274        assert_args(
1275            StreamTrimOptions::maxlen(StreamTrimmingMode::Exact, 100),
1276            &["MAXLEN", "=", "100"],
1277        );
1278        assert_args(
1279            StreamTrimOptions::minid(StreamTrimmingMode::Approx, "1-0").limit(10),
1280            &["MINID", "~", "1-0", "LIMIT", "10"],
1281        );
1282    }
1283
1284    #[test]
1285    fn autoclaim_options_args() {
1286        assert_args_empty(StreamAutoClaimOptions::default());
1287        assert_args(
1288            StreamAutoClaimOptions::default().count(10).with_justid(),
1289            &["COUNT", "10", "JUSTID"],
1290        );
1291    }
1292
1293    #[test]
1294    fn read_options_args() {
1295        assert_args_empty(StreamReadOptions::default());
1296        assert_args(
1297            StreamReadOptions::default().count(10).block(1000),
1298            &["BLOCK", "1000", "COUNT", "10"],
1299        );
1300    }
1301
1302    fn bulk(s: &str) -> ValkeyValue {
1303        ValkeyValue::BulkString(Bytes::from(s.to_string()))
1304    }
1305
1306    fn array(items: Vec<ValkeyValue>) -> ValkeyValue {
1307        ValkeyValue::Array(items)
1308    }
1309
1310    fn entry(id: &str, field: &str, value: &str) -> StreamId {
1311        StreamId {
1312            id: id.to_string(),
1313            map: HashMap::from([(field.to_string(), bulk(value))]),
1314            ..Default::default()
1315        }
1316    }
1317
1318    #[test]
1319    fn range_reply_decoding() {
1320        // glide-core: a map of entry ID to `[[field, value], ...]`.
1321        let range = ValkeyValue::Map(vec![
1322            (bulk("1-0"), array(vec![array(vec![bulk("f"), bulk("a")])])),
1323            (bulk("2-0"), array(vec![array(vec![bulk("f"), bulk("b")])])),
1324        ]);
1325        let reply = StreamRangeReply::from_owned_valkey_value(range).unwrap();
1326        assert_eq!(
1327            reply.ids,
1328            vec![entry("1-0", "f", "a"), entry("2-0", "f", "b")]
1329        );
1330        assert!(
1331            StreamRangeReply::from_owned_valkey_value(ValkeyValue::Nil)
1332                .unwrap()
1333                .ids
1334                .is_empty()
1335        );
1336
1337        // The raw RESP2 shape is not accepted: glide-core always converts it.
1338        let raw = array(vec![array(vec![
1339            bulk("1-0"),
1340            array(vec![bulk("f"), bulk("a")]),
1341        ])]);
1342        assert!(StreamRangeReply::from_owned_valkey_value(raw).is_err());
1343
1344        // A field that is not a `[field, value]` pair.
1345        let unpaired = ValkeyValue::Map(vec![(bulk("1-0"), array(vec![bulk("f")]))]);
1346        assert!(StreamRangeReply::from_owned_valkey_value(unpaired).is_err());
1347
1348        let claim = StreamClaimReply::from_owned_valkey_value(ValkeyValue::Map(vec![(
1349            bulk("1-0"),
1350            array(vec![array(vec![bulk("f"), bulk("a")])]),
1351        )]))
1352        .unwrap();
1353        assert_eq!(claim.ids, vec![entry("1-0", "f", "a")]);
1354
1355        let id = entry("1-0", "f", "7");
1356        assert_eq!(id.get::<i64>("f"), Some(7));
1357        assert_eq!(id.get::<i64>("missing"), None);
1358        assert!(id.contains_key("f"));
1359        assert_eq!((id.len(), id.is_empty()), (1, false));
1360    }
1361
1362    #[test]
1363    fn read_reply_decoding() {
1364        // glide-core: a map of stream key to a map of entry ID to `[[field, value], ...]`.
1365        let entries = ValkeyValue::Map(vec![(
1366            bulk("1-0"),
1367            array(vec![array(vec![bulk("f"), bulk("a")])]),
1368        )]);
1369        let reply =
1370            StreamReadReply::from_owned_valkey_value(ValkeyValue::Map(vec![(bulk("s"), entries)]))
1371                .unwrap();
1372        assert_eq!(reply.keys.len(), 1);
1373        assert_eq!(reply.keys[0].key, "s");
1374        assert_eq!(reply.keys[0].ids, vec![entry("1-0", "f", "a")]);
1375
1376        assert!(
1377            Option::<StreamReadReply>::from_owned_valkey_value(ValkeyValue::Nil)
1378                .unwrap()
1379                .is_none()
1380        );
1381    }
1382
1383    #[test]
1384    fn autoclaim_reply_decoding() {
1385        let full = array(vec![
1386            bulk("2-0"),
1387            ValkeyValue::Map(vec![(
1388                bulk("1-0"),
1389                array(vec![array(vec![bulk("f"), bulk("a")])]),
1390            )]),
1391            array(vec![bulk("0-1")]),
1392        ]);
1393        let reply = StreamAutoClaimReply::from_owned_valkey_value(full).unwrap();
1394        assert_eq!(reply.next_stream_id, "2-0");
1395        assert_eq!(reply.claimed, vec![entry("1-0", "f", "a")]);
1396        assert_eq!(reply.deleted_ids, vec!["0-1"]);
1397        assert!(!reply.invalid_entries);
1398
1399        // `JUSTID`, without the deleted IDs (before Valkey 7.0) and with a nil entry.
1400        let justid = array(vec![
1401            bulk("0-0"),
1402            array(vec![bulk("1-0"), ValkeyValue::Nil]),
1403        ]);
1404        let reply = StreamAutoClaimReply::from_owned_valkey_value(justid).unwrap();
1405        assert_eq!(reply.next_stream_id, "0-0");
1406        assert_eq!(reply.claimed.len(), 1);
1407        assert_eq!(reply.claimed[0].id, "1-0");
1408        assert!(reply.claimed[0].is_empty());
1409        assert!(reply.deleted_ids.is_empty());
1410        assert!(reply.invalid_entries);
1411
1412        assert!(StreamAutoClaimReply::from_owned_valkey_value(array(vec![bulk("0-0")])).is_err());
1413    }
1414
1415    #[test]
1416    fn pending_count_reply_decoding() {
1417        let reply = StreamPendingCountReply::from_owned_valkey_value(array(vec![array(vec![
1418            bulk("1-0"),
1419            bulk("c1"),
1420            ValkeyValue::Int(100),
1421            ValkeyValue::Int(2),
1422        ])]))
1423        .unwrap();
1424        assert_eq!(reply.ids.len(), 1);
1425        let pending = &reply.ids[0];
1426        assert_eq!(
1427            (pending.id.as_str(), pending.consumer.as_str()),
1428            ("1-0", "c1")
1429        );
1430        assert_eq!(
1431            (pending.last_delivered_ms, pending.times_delivered),
1432            (100, 2)
1433        );
1434
1435        assert!(
1436            StreamPendingCountReply::from_owned_valkey_value(array(vec![]))
1437                .unwrap()
1438                .ids
1439                .is_empty()
1440        );
1441    }
1442
1443    #[test]
1444    fn pending_reply_decoding() {
1445        let empty = array(vec![
1446            ValkeyValue::Int(0),
1447            ValkeyValue::Nil,
1448            ValkeyValue::Nil,
1449            ValkeyValue::Nil,
1450        ]);
1451        let reply = StreamPendingReply::from_owned_valkey_value(empty).unwrap();
1452        assert!(matches!(reply, StreamPendingReply::Empty));
1453        assert_eq!(reply.count(), 0);
1454
1455        let data = array(vec![
1456            ValkeyValue::Int(3),
1457            bulk("1-0"),
1458            bulk("3-0"),
1459            array(vec![array(vec![bulk("c1"), bulk("3")])]),
1460        ]);
1461        let reply = StreamPendingReply::from_owned_valkey_value(data).unwrap();
1462        assert_eq!(reply.count(), 3);
1463        let StreamPendingReply::Data(data) = reply else {
1464            panic!("expected pending data");
1465        };
1466        assert_eq!(
1467            (data.start_id.as_str(), data.end_id.as_str()),
1468            ("1-0", "3-0")
1469        );
1470        assert_eq!(data.consumers.len(), 1);
1471        assert_eq!(
1472            (data.consumers[0].name.as_str(), data.consumers[0].pending),
1473            ("c1", 3)
1474        );
1475
1476        let missing_ids = array(vec![
1477            ValkeyValue::Int(1),
1478            ValkeyValue::Nil,
1479            ValkeyValue::Nil,
1480            ValkeyValue::Nil,
1481        ]);
1482        assert!(StreamPendingReply::from_owned_valkey_value(missing_ids).is_err());
1483    }
1484
1485    #[test]
1486    fn info_stream_reply_decoding() {
1487        // glide-core: a map, whose entries keep the flat `[id, [field, value, ...]]` form.
1488        let info = ValkeyValue::Map(vec![
1489            (bulk("length"), ValkeyValue::Int(2)),
1490            (bulk("radix-tree-nodes"), ValkeyValue::Int(1)),
1491            (bulk("groups"), ValkeyValue::Int(1)),
1492            (bulk("last-generated-id"), bulk("2-0")),
1493            (
1494                bulk("first-entry"),
1495                array(vec![bulk("1-0"), array(vec![bulk("f"), bulk("a")])]),
1496            ),
1497            (bulk("last-entry"), ValkeyValue::Nil),
1498            (bulk("entries-added"), ValkeyValue::Int(2)),
1499        ]);
1500        let reply = StreamInfoStreamReply::from_owned_valkey_value(info).unwrap();
1501        assert_eq!(
1502            (reply.length, reply.radix_tree_keys, reply.groups),
1503            (2, 1, 1)
1504        );
1505        assert_eq!(reply.last_generated_id, "2-0");
1506        assert_eq!(reply.first_entry, entry("1-0", "f", "a"));
1507        assert_eq!(reply.last_entry, StreamId::default());
1508
1509        // An entry with an odd number of field/value elements.
1510        let odd = ValkeyValue::Map(vec![(
1511            bulk("first-entry"),
1512            array(vec![
1513                bulk("1-0"),
1514                array(vec![bulk("f"), bulk("a"), bulk("g")]),
1515            ]),
1516        )]);
1517        assert!(StreamInfoStreamReply::from_owned_valkey_value(odd).is_err());
1518    }
1519
1520    #[test]
1521    fn info_groups_and_consumers_reply_decoding() {
1522        // glide-core: an array of maps.
1523        let groups = array(vec![ValkeyValue::Map(vec![
1524            (bulk("name"), bulk("g")),
1525            (bulk("consumers"), ValkeyValue::Int(2)),
1526            (bulk("pending"), ValkeyValue::Int(3)),
1527            (bulk("last-delivered-id"), bulk("3-0")),
1528            (bulk("entries-read"), ValkeyValue::Int(3)),
1529            (bulk("lag"), ValkeyValue::Nil),
1530        ])]);
1531        let reply = StreamInfoGroupsReply::from_owned_valkey_value(groups).unwrap();
1532        let group = &reply.groups[0];
1533        assert_eq!(
1534            (group.name.as_str(), group.consumers, group.pending),
1535            ("g", 2, 3)
1536        );
1537        assert_eq!(group.last_delivered_id, "3-0");
1538        assert_eq!((group.entries_read, group.lag), (Some(3), None));
1539
1540        let consumers = array(vec![ValkeyValue::Map(vec![
1541            (bulk("name"), bulk("c1")),
1542            (bulk("pending"), ValkeyValue::Int(1)),
1543            (bulk("idle"), ValkeyValue::Int(42)),
1544            (bulk("inactive"), ValkeyValue::Int(42)),
1545        ])]);
1546        let reply = StreamInfoConsumersReply::from_owned_valkey_value(consumers).unwrap();
1547        let consumer = &reply.consumers[0];
1548        assert_eq!(
1549            (consumer.name.as_str(), consumer.pending, consumer.idle),
1550            ("c1", 1, 42)
1551        );
1552    }
1553}