Skip to main content

redis/commands/
streams.rs

1//! Defines types to use with the streams commands.
2
3#[cfg(feature = "streams")]
4use crate::{
5    FromRedisValue, RedisWrite, ToRedisArgs, Value,
6    errors::{ParsingError, invalid_type_error},
7    types::HashMap,
8};
9use crate::{from_redis_value, from_redis_value_ref, types::ToSingleRedisArg};
10
11// Stream Maxlen Enum
12
13/// Utility enum for passing `MAXLEN [= or ~] [COUNT]`
14/// arguments into `StreamCommands`.
15/// The enum value represents the count.
16#[derive(PartialEq, Eq, Clone, Debug, Copy)]
17#[non_exhaustive]
18pub enum StreamMaxlen {
19    /// Match an exact count
20    Equals(usize),
21    /// Match an approximate count
22    Approx(usize),
23}
24
25impl ToRedisArgs for StreamMaxlen {
26    fn write_redis_args<W>(&self, out: &mut W)
27    where
28        W: ?Sized + RedisWrite,
29    {
30        let (ch, val) = match *self {
31            StreamMaxlen::Equals(v) => ("=", v),
32            StreamMaxlen::Approx(v) => ("~", v),
33        };
34        out.write_arg(b"MAXLEN");
35        out.write_arg(ch.as_bytes());
36        val.write_redis_args(out);
37    }
38}
39
40/// Utility enum for passing the trim mode`[=|~]`
41/// arguments into `StreamCommands`.
42#[derive(Debug)]
43#[non_exhaustive]
44pub enum StreamTrimmingMode {
45    /// Match an exact count
46    Exact,
47    /// Match an approximate count
48    Approx,
49}
50
51impl ToRedisArgs for StreamTrimmingMode {
52    fn write_redis_args<W>(&self, out: &mut W)
53    where
54        W: ?Sized + RedisWrite,
55    {
56        match self {
57            Self::Exact => out.write_arg(b"="),
58            Self::Approx => out.write_arg(b"~"),
59        };
60    }
61}
62
63/// Utility enum for passing `<MAXLEN|MINID> [=|~] threshold [LIMIT count]`
64/// arguments into `StreamCommands`.
65/// The enum values the trimming mode (=|~), the threshold, and the optional limit
66#[derive(Debug)]
67#[non_exhaustive]
68pub enum StreamTrimStrategy {
69    /// Evicts entries as long as the streams length exceeds threshold.  With an optional limit.
70    MaxLen(StreamTrimmingMode, usize, Option<usize>),
71    /// Evicts entries with IDs lower than threshold, where threshold is a stream ID With an optional limit.
72    MinId(StreamTrimmingMode, String, Option<usize>),
73}
74
75impl StreamTrimStrategy {
76    /// Define a MAXLEN trim strategy with the given maximum number of entries
77    pub fn maxlen(trim: StreamTrimmingMode, max_entries: usize) -> Self {
78        Self::MaxLen(trim, max_entries, None)
79    }
80
81    /// Defines a MINID trim strategy with the given minimum stream ID
82    pub fn minid(trim: StreamTrimmingMode, stream_id: impl Into<String>) -> Self {
83        Self::MinId(trim, stream_id.into(), None)
84    }
85
86    /// Set a limit to the number of records to trim in a single operation
87    pub fn limit(self, limit: usize) -> Self {
88        match self {
89            StreamTrimStrategy::MaxLen(m, t, _) => StreamTrimStrategy::MaxLen(m, t, Some(limit)),
90            StreamTrimStrategy::MinId(m, t, _) => StreamTrimStrategy::MinId(m, t, Some(limit)),
91        }
92    }
93}
94
95impl ToRedisArgs for StreamTrimStrategy {
96    fn write_redis_args<W>(&self, out: &mut W)
97    where
98        W: ?Sized + RedisWrite,
99    {
100        let limit = match self {
101            StreamTrimStrategy::MaxLen(m, t, limit) => {
102                out.write_arg(b"MAXLEN");
103                m.write_redis_args(out);
104                t.write_redis_args(out);
105                limit
106            }
107            StreamTrimStrategy::MinId(m, t, limit) => {
108                out.write_arg(b"MINID");
109                m.write_redis_args(out);
110                t.write_redis_args(out);
111                limit
112            }
113        };
114        if let Some(limit) = limit {
115            out.write_arg(b"LIMIT");
116            limit.write_redis_args(out);
117        }
118    }
119}
120
121/// Builder options for [`xtrim_options`] command
122///
123/// [`xtrim_options`]: ../trait.Commands.html#method.xtrim_options
124///
125#[derive(Debug)]
126pub struct StreamTrimOptions {
127    strategy: StreamTrimStrategy,
128    deletion_policy: Option<StreamDeletionPolicy>,
129}
130
131impl StreamTrimOptions {
132    /// Define a MAXLEN trim strategy with the given maximum number of entries
133    pub fn maxlen(mode: StreamTrimmingMode, max_entries: usize) -> Self {
134        Self {
135            strategy: StreamTrimStrategy::maxlen(mode, max_entries),
136            deletion_policy: None,
137        }
138    }
139
140    /// Defines a MINID trim strategy with the given minimum stream ID
141    pub fn minid(mode: StreamTrimmingMode, stream_id: impl Into<String>) -> Self {
142        Self {
143            strategy: StreamTrimStrategy::minid(mode, stream_id),
144            deletion_policy: None,
145        }
146    }
147
148    /// Set a limit to the number of records to trim in a single operation
149    pub fn limit(mut self, limit: usize) -> Self {
150        self.strategy = self.strategy.limit(limit);
151        self
152    }
153
154    /// Set the deletion policy for the XTRIM operation
155    pub fn set_deletion_policy(mut self, deletion_policy: StreamDeletionPolicy) -> Self {
156        self.deletion_policy = Some(deletion_policy);
157        self
158    }
159}
160
161impl ToRedisArgs for StreamTrimOptions {
162    fn write_redis_args<W>(&self, out: &mut W)
163    where
164        W: ?Sized + RedisWrite,
165    {
166        self.strategy.write_redis_args(out);
167        if let Some(deletion_policy) = self.deletion_policy.as_ref() {
168            deletion_policy.write_redis_args(out);
169        }
170    }
171}
172
173/// Idempotency mode for stream message production
174///
175/// Supports idempotent message processing to prevent duplicate entries.
176/// See [Redis Streams Idempotency](https://redis.io/docs/latest/develop/data-types/streams/idempotency/)
177#[derive(Debug, Clone)]
178pub enum StreamIdempotencyMode {
179    /// Manual mode: Producer provides both producer ID (pid) and idempotent ID (iid)
180    ///
181    /// Example: `XADD key IDMP producer-1 iid-1 * field value`
182    Manual {
183        /// Producer ID - unique identifier for the message producer
184        producer_id: String,
185        /// Idempotent ID - unique identifier for this specific message
186        idempotent_id: String,
187    },
188    /// Automatic mode: Producer provides only producer ID (pid), Redis generates iid from message content
189    ///
190    /// Example: `XADD key IDMPAUTO producer-1 * field value`
191    Automatic {
192        /// Producer ID - unique identifier for the message producer
193        producer_id: String,
194    },
195}
196
197impl ToRedisArgs for StreamIdempotencyMode {
198    fn write_redis_args<W>(&self, out: &mut W)
199    where
200        W: ?Sized + RedisWrite,
201    {
202        match self {
203            StreamIdempotencyMode::Manual {
204                producer_id,
205                idempotent_id,
206            } => {
207                out.write_arg(b"IDMP");
208                out.write_arg(producer_id.as_bytes());
209                out.write_arg(idempotent_id.as_bytes());
210            }
211            StreamIdempotencyMode::Automatic { producer_id } => {
212                out.write_arg(b"IDMPAUTO");
213                out.write_arg(producer_id.as_bytes());
214            }
215        }
216    }
217}
218
219/// Minimum value for IDMP-DURATION parameter (1 second)
220pub const IDMP_DURATION_MIN: u32 = 1;
221/// Maximum value for IDMP-DURATION parameter (86400 seconds = 24 hours)
222pub const IDMP_DURATION_MAX: u32 = 86400;
223/// Minimum value for IDMP-MAXSIZE parameter (1 entry)
224pub const IDMP_MAXSIZE_MIN: u16 = 1;
225/// Maximum value for IDMP-MAXSIZE parameter (10000 entries)
226pub const IDMP_MAXSIZE_MAX: u16 = 10000;
227
228/// Configuration options for [`xcfgset`] command
229///
230/// Configures idempotency parameters for a stream.
231/// Use the constructor methods to create an instance with at least one parameter set,
232/// or use the setter methods to add parameters to an existing instance.
233///
234/// [`xcfgset`]: ../trait.Commands.html#method.xcfgset
235///
236/// # Example
237/// ```no_run
238/// use redis::{Commands, streams::StreamConfigOptions};
239/// # let client = redis::Client::open("redis://127.0.0.1/").unwrap();
240/// # let mut con = client.get_connection().unwrap();
241///
242/// // Create with idempotency duration in seconds, optionally add maxsize
243/// let opts1 = StreamConfigOptions::with_idempotency_seconds(300)
244///     .unwrap()
245///     .idempotency_maxsize(1000)
246///     .unwrap();
247/// let _: String = con.xcfgset("key", &opts1).unwrap();
248///
249/// // Or create with maxsize only and optionally add idempotency duration in seconds
250/// let opts2 = StreamConfigOptions::with_idempotency_maxsize(500)
251///     .unwrap()
252///     .idempotency_seconds(300)
253///     .unwrap();
254/// let _: String = con.xcfgset("key", &opts2).unwrap();
255/// ```
256#[derive(Debug)]
257pub struct StreamConfigOptions {
258    /// Duration in seconds that each idempotent ID is kept
259    ///
260    /// Valid range: `1..=86400` (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`])
261    idmp_duration: Option<u32>,
262    /// Maximum number of idempotent IDs kept per producer
263    ///
264    /// Valid range: `1..=10000` (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`])
265    idmp_maxsize: Option<u16>,
266}
267
268impl StreamConfigOptions {
269    /// Create configuration options with IDMP-DURATION parameter
270    ///
271    /// Sets the duration in seconds that each idempotent ID (iid) is kept
272    /// in the stream's IDMP map. Default: 100 seconds.
273    ///
274    /// # Errors
275    /// Returns an error if seconds is not in the valid range `1..=86400`
276    /// (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`])
277    pub fn with_idempotency_seconds(seconds: u32) -> Result<Self, String> {
278        if !(IDMP_DURATION_MIN..=IDMP_DURATION_MAX).contains(&seconds) {
279            return Err(format!(
280                "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX} seconds, got: {seconds}"
281            ));
282        }
283        Ok(Self {
284            idmp_duration: Some(seconds),
285            idmp_maxsize: None,
286        })
287    }
288
289    /// Create configuration options with IDMP-MAXSIZE parameter
290    ///
291    /// Sets the maximum number of most recent idempotent IDs kept for each
292    /// producer in the stream's IDMP map. Default: 100 entries.
293    ///
294    /// # Errors
295    /// Returns an error if size is not in the valid range `1..=10000`
296    /// (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`])
297    pub fn with_idempotency_maxsize(size: u16) -> Result<Self, String> {
298        if !(IDMP_MAXSIZE_MIN..=IDMP_MAXSIZE_MAX).contains(&size) {
299            return Err(format!(
300                "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX} entries, got: {size}"
301            ));
302        }
303        Ok(Self {
304            idmp_duration: None,
305            idmp_maxsize: Some(size),
306        })
307    }
308
309    /// Set or update the IDMP-DURATION parameter
310    ///
311    /// # Errors
312    /// Returns an error if seconds is not in the valid range `1..=86400`
313    /// (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`])
314    pub fn idempotency_seconds(mut self, seconds: u32) -> Result<Self, String> {
315        if !(IDMP_DURATION_MIN..=IDMP_DURATION_MAX).contains(&seconds) {
316            return Err(format!(
317                "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX} seconds, got: {seconds}"
318            ));
319        }
320        self.idmp_duration = Some(seconds);
321        Ok(self)
322    }
323
324    /// Set or update the IDMP-MAXSIZE parameter
325    ///
326    /// # Errors
327    /// Returns an error if size is not in the valid range `1..=10000`
328    /// (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`])
329    pub fn idempotency_maxsize(mut self, size: u16) -> Result<Self, String> {
330        if !(IDMP_MAXSIZE_MIN..=IDMP_MAXSIZE_MAX).contains(&size) {
331            return Err(format!(
332                "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX} entries, got: {size}"
333            ));
334        }
335        self.idmp_maxsize = Some(size);
336        Ok(self)
337    }
338}
339
340impl ToRedisArgs for StreamConfigOptions {
341    fn write_redis_args<W>(&self, out: &mut W)
342    where
343        W: ?Sized + RedisWrite,
344    {
345        if let Some(duration) = self.idmp_duration {
346            out.write_arg(b"IDMP-DURATION");
347            out.write_arg(duration.to_string().as_bytes());
348        }
349        if let Some(maxsize) = self.idmp_maxsize {
350            out.write_arg(b"IDMP-MAXSIZE");
351            out.write_arg(maxsize.to_string().as_bytes());
352        }
353    }
354}
355
356/// Builder options for [`xadd_options`] command
357///
358/// [`xadd_options`]: ../trait.Commands.html#method.xadd_options
359///
360#[derive(Default, Debug)]
361pub struct StreamAddOptions {
362    nomkstream: bool,
363    trim: Option<StreamTrimStrategy>,
364    deletion_policy: Option<StreamDeletionPolicy>,
365    idempotency: Option<StreamIdempotencyMode>,
366}
367
368impl StreamAddOptions {
369    /// Set the NOMKSTREAM flag on which prevents creating a stream for the XADD operation
370    pub fn nomkstream(mut self) -> Self {
371        self.nomkstream = true;
372        self
373    }
374
375    /// Enable trimming when adding using the given trim strategy
376    pub fn trim(mut self, trim: StreamTrimStrategy) -> Self {
377        self.trim = Some(trim);
378        self
379    }
380
381    /// Set the deletion policy for the XADD operation
382    pub fn set_deletion_policy(mut self, deletion_policy: StreamDeletionPolicy) -> Self {
383        self.deletion_policy = Some(deletion_policy);
384        self
385    }
386
387    /// Enable idempotent message production with manual mode (IDMP)
388    ///
389    /// Manual mode requires both producer ID and idempotent ID to be provided.
390    /// The producer ID uniquely identifies the message producer.
391    /// The idempotent ID uniquely identifies this specific message.
392    ///
393    /// # Example
394    /// ```no_run
395    /// use redis::{Commands, streams::StreamAddOptions};
396    /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap();
397    /// # let mut con = client.get_connection().unwrap();
398    ///
399    /// let opts = StreamAddOptions::default()
400    ///     .idmp("producer-1", "iid-1");
401    /// let _: Option<String> = con.xadd_options(
402    ///     "key",
403    ///     "*",
404    ///     &[("field", "value")],
405    ///     &opts
406    /// ).unwrap();
407    /// ```
408    pub fn idmp(
409        mut self,
410        producer_id: impl Into<String>,
411        idempotent_id: impl Into<String>,
412    ) -> Self {
413        self.idempotency = Some(StreamIdempotencyMode::Manual {
414            producer_id: producer_id.into(),
415            idempotent_id: idempotent_id.into(),
416        });
417        self
418    }
419
420    /// Enable idempotent message production with automatic mode (IDMPAUTO)
421    ///
422    /// Automatic mode requires only a producer ID to be provided.
423    /// Redis automatically generates the idempotent ID based on the message content.
424    ///
425    /// # Example
426    /// ```no_run
427    /// use redis::{Commands, streams::StreamAddOptions};
428    /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap();
429    /// # let mut con = client.get_connection().unwrap();
430    ///
431    /// let opts = StreamAddOptions::default()
432    ///     .idmpauto("producer-1");
433    /// let _: Option<String> = con.xadd_options(
434    ///     "key",
435    ///     "*",
436    ///     &[("field", "value")],
437    ///     &opts
438    /// ).unwrap();
439    /// ```
440    pub fn idmpauto(mut self, producer_id: impl Into<String>) -> Self {
441        self.idempotency = Some(StreamIdempotencyMode::Automatic {
442            producer_id: producer_id.into(),
443        });
444        self
445    }
446}
447
448impl ToRedisArgs for StreamAddOptions {
449    fn write_redis_args<W>(&self, out: &mut W)
450    where
451        W: ?Sized + RedisWrite,
452    {
453        if self.nomkstream {
454            out.write_arg(b"NOMKSTREAM");
455        }
456        if let Some(deletion_policy) = self.deletion_policy.as_ref() {
457            deletion_policy.write_redis_args(out);
458        }
459        if let Some(idempotency) = self.idempotency.as_ref() {
460            idempotency.write_redis_args(out);
461        }
462        if let Some(strategy) = self.trim.as_ref() {
463            strategy.write_redis_args(out);
464        }
465    }
466}
467
468/// Builder options for [`xautoclaim_options`] command.
469///
470/// [`xautoclaim_options`]: ../trait.Commands.html#method.xautoclaim_options
471///
472#[derive(Default, Debug)]
473pub struct StreamAutoClaimOptions {
474    count: Option<usize>,
475    justid: bool,
476}
477
478impl StreamAutoClaimOptions {
479    /// Sets the maximum number of elements to claim per stream.
480    pub fn count(mut self, n: usize) -> Self {
481        self.count = Some(n);
482        self
483    }
484
485    /// Set `JUSTID` cmd arg to true. Be advised: the response
486    /// type changes with this option.
487    pub fn with_justid(mut self) -> Self {
488        self.justid = true;
489        self
490    }
491}
492
493impl ToRedisArgs for StreamAutoClaimOptions {
494    fn write_redis_args<W>(&self, out: &mut W)
495    where
496        W: ?Sized + RedisWrite,
497    {
498        if let Some(ref count) = self.count {
499            out.write_arg(b"COUNT");
500            out.write_arg(format!("{count}").as_bytes());
501        }
502        if self.justid {
503            out.write_arg(b"JUSTID");
504        }
505    }
506}
507
508/// Builder options for [`xclaim_options`] command.
509///
510/// [`xclaim_options`]: ../trait.Commands.html#method.xclaim_options
511///
512#[derive(Default, Debug)]
513pub struct StreamClaimOptions {
514    /// Set `IDLE <milliseconds>` cmd arg.
515    idle: Option<usize>,
516    /// Set `TIME <Unix epoch milliseconds>` cmd arg.
517    time: Option<usize>,
518    /// Set `RETRYCOUNT <count>` cmd arg.
519    retry: Option<usize>,
520    /// Set `FORCE` cmd arg.
521    force: bool,
522    /// Set `JUSTID` cmd arg. Be advised: the response
523    /// type changes with this option.
524    justid: bool,
525    /// Set `LASTID <lastid>` cmd arg.
526    lastid: Option<String>,
527}
528
529impl StreamClaimOptions {
530    /// Set `IDLE <milliseconds>` cmd arg.
531    pub fn idle(mut self, ms: usize) -> Self {
532        self.idle = Some(ms);
533        self
534    }
535
536    /// Set `TIME <Unix epoch milliseconds>` cmd arg.
537    pub fn time(mut self, ms_time: usize) -> Self {
538        self.time = Some(ms_time);
539        self
540    }
541
542    /// Set `RETRYCOUNT <count>` cmd arg.
543    pub fn retry(mut self, count: usize) -> Self {
544        self.retry = Some(count);
545        self
546    }
547
548    /// Set `FORCE` cmd arg to true.
549    pub fn with_force(mut self) -> Self {
550        self.force = true;
551        self
552    }
553
554    /// Set `JUSTID` cmd arg to true. Be advised: the response
555    /// type changes with this option.
556    pub fn with_justid(mut self) -> Self {
557        self.justid = true;
558        self
559    }
560
561    /// Set `LASTID <lastid>` cmd arg.
562    pub fn with_lastid(mut self, lastid: impl Into<String>) -> Self {
563        self.lastid = Some(lastid.into());
564        self
565    }
566}
567
568impl ToRedisArgs for StreamClaimOptions {
569    fn write_redis_args<W>(&self, out: &mut W)
570    where
571        W: ?Sized + RedisWrite,
572    {
573        if let Some(ref ms) = self.idle {
574            out.write_arg(b"IDLE");
575            out.write_arg(format!("{ms}").as_bytes());
576        }
577        if let Some(ref ms_time) = self.time {
578            out.write_arg(b"TIME");
579            out.write_arg(format!("{ms_time}").as_bytes());
580        }
581        if let Some(ref count) = self.retry {
582            out.write_arg(b"RETRYCOUNT");
583            out.write_arg(format!("{count}").as_bytes());
584        }
585        if self.force {
586            out.write_arg(b"FORCE");
587        }
588        if self.justid {
589            out.write_arg(b"JUSTID");
590        }
591        if let Some(ref lastid) = self.lastid {
592            out.write_arg(b"LASTID");
593            lastid.write_redis_args(out);
594        }
595    }
596}
597
598/// Argument to `StreamReadOptions`
599/// Represents the Redis `GROUP <groupname> <consumername>` cmd arg.
600/// This option will toggle the cmd from `XREAD` to `XREADGROUP`
601type SRGroup = Option<(Vec<Vec<u8>>, Vec<Vec<u8>>)>;
602/// Builder options for [`xread_options`] command.
603///
604/// [`xread_options`]: ../trait.Commands.html#method.xread_options
605///
606#[derive(Default, Debug)]
607pub struct StreamReadOptions {
608    /// Set the `BLOCK <milliseconds>` cmd arg.
609    block: Option<usize>,
610    /// Set the `COUNT <count>` cmd arg.
611    count: Option<usize>,
612    /// Set the `NOACK` cmd arg.
613    noack: Option<bool>,
614    /// Set the `GROUP <groupname> <consumername>` cmd arg.
615    /// This option will toggle the cmd from XREAD to XREADGROUP.
616    group: SRGroup,
617    /// Set the `CLAIM <min-idle-time>` cmd arg.
618    /// The `<min-idle-time>` is specified in milliseconds.
619    claim: Option<usize>,
620}
621
622impl StreamReadOptions {
623    /// Indicates whether the command is participating in a group
624    /// and generating ACKs
625    pub fn read_only(&self) -> bool {
626        self.group.is_none()
627    }
628
629    /// Sets the command so that it avoids adding the message
630    /// to the PEL in cases where reliability is not a requirement
631    /// and the occasional message loss is acceptable.
632    pub fn noack(mut self) -> Self {
633        self.noack = Some(true);
634        self
635    }
636
637    /// Sets the block time in milliseconds.
638    pub fn block(mut self, ms: usize) -> Self {
639        self.block = Some(ms);
640        self
641    }
642
643    /// Sets the maximum number of elements to return per stream.
644    pub fn count(mut self, n: usize) -> Self {
645        self.count = Some(n);
646        self
647    }
648
649    /// Sets the name of a consumer group associated to the stream.
650    pub fn group<GN: ToRedisArgs, CN: ToRedisArgs>(
651        mut self,
652        group_name: GN,
653        consumer_name: CN,
654    ) -> Self {
655        self.group = Some((
656            ToRedisArgs::to_redis_args(&group_name),
657            ToRedisArgs::to_redis_args(&consumer_name),
658        ));
659        self
660    }
661
662    /// Set the minimum idle time for the CLAIM parameter.
663    pub fn claim(mut self, min_idle_time: usize) -> Self {
664        self.claim = Some(min_idle_time);
665        self
666    }
667}
668
669impl ToRedisArgs for StreamReadOptions {
670    fn write_redis_args<W>(&self, out: &mut W)
671    where
672        W: ?Sized + RedisWrite,
673    {
674        if let Some(ref group) = self.group {
675            out.write_arg(b"GROUP");
676            for i in &group.0 {
677                out.write_arg(i);
678            }
679            for i in &group.1 {
680                out.write_arg(i);
681            }
682        }
683
684        if let Some(ref ms) = self.block {
685            out.write_arg(b"BLOCK");
686            out.write_arg(format!("{ms}").as_bytes());
687        }
688
689        if let Some(ref n) = self.count {
690            out.write_arg(b"COUNT");
691            out.write_arg(format!("{n}").as_bytes());
692        }
693
694        if self.group.is_some() {
695            // noack is only available w/ xreadgroup
696            if self.noack == Some(true) {
697                out.write_arg(b"NOACK");
698            }
699            // claim is only available w/ xreadgroup
700            if let Some(ref min_idle_time) = self.claim {
701                out.write_arg(b"CLAIM");
702                out.write_arg(format!("{min_idle_time}").as_bytes());
703            }
704        }
705    }
706}
707
708/// Reply type used with the [`xautoclaim_options`] command.
709///
710/// [`xautoclaim_options`]: ../trait.Commands.html#method.xautoclaim_options
711///
712#[derive(Default, Debug, Clone)]
713pub struct StreamAutoClaimReply {
714    /// The next stream id to use as the start argument for the next xautoclaim
715    pub next_stream_id: String,
716    /// The entries claimed for the consumer. When JUSTID is enabled the map in each entry is blank
717    pub claimed: Vec<StreamId>,
718    /// The list of stream ids that were removed due to no longer being in the stream
719    pub deleted_ids: Vec<String>,
720    /// If set, this means that the reply contained invalid nil entries, that were skipped during parsing.
721    ///
722    /// This should only happen when using Redis 6, see <https://github.com/redis-rs/redis-rs/issues/1798>
723    pub invalid_entries: bool,
724}
725
726/// Reply type used with [`xread`] or [`xread_options`] commands.
727///
728/// [`xread`]: ../trait.Commands.html#method.xread
729/// [`xread_options`]: ../trait.Commands.html#method.xread_options
730///
731#[derive(Default, Debug, Clone)]
732pub struct StreamReadReply {
733    /// Complex data structure containing a payload for each key in this array
734    pub keys: Vec<StreamKey>,
735}
736
737/// Reply type used with [`xrange`], [`xrange_count`], [`xrange_all`], [`xrevrange`], [`xrevrange_count`], [`xrevrange_all`] commands.
738///
739/// Represents stream entries matching a given range of `id`'s.
740///
741/// [`xrange`]: ../trait.Commands.html#method.xrange
742/// [`xrange_count`]: ../trait.Commands.html#method.xrange_count
743/// [`xrange_all`]: ../trait.Commands.html#method.xrange_all
744/// [`xrevrange`]: ../trait.Commands.html#method.xrevrange
745/// [`xrevrange_count`]: ../trait.Commands.html#method.xrevrange_count
746/// [`xrevrange_all`]: ../trait.Commands.html#method.xrevrange_all
747///
748#[derive(Default, Debug, Clone)]
749pub struct StreamRangeReply {
750    /// Complex data structure containing a payload for each ID in this array
751    pub ids: Vec<StreamId>,
752}
753
754/// Reply type used with [`xclaim`] command.
755///
756/// Represents that ownership of the specified messages was changed.
757///
758/// [`xclaim`]: ../trait.Commands.html#method.xclaim
759///
760#[derive(Default, Debug, Clone)]
761pub struct StreamClaimReply {
762    /// Complex data structure containing a payload for each ID in this array
763    pub ids: Vec<StreamId>,
764}
765
766/// Reply type used with [`xpending`] command.
767///
768/// Data returned here were fetched from the stream without
769/// having been acknowledged.
770///
771/// [`xpending`]: ../trait.Commands.html#method.xpending
772///
773#[derive(Debug, Clone, Default)]
774#[non_exhaustive]
775pub enum StreamPendingReply {
776    /// The stream is empty.
777    #[default]
778    Empty,
779    /// Data with payload exists in the stream.
780    Data(StreamPendingData),
781}
782
783impl StreamPendingReply {
784    /// Returns how many records are in the reply.
785    pub fn count(&self) -> usize {
786        match self {
787            StreamPendingReply::Empty => 0,
788            StreamPendingReply::Data(x) => x.count,
789        }
790    }
791}
792
793/// Inner reply type when an [`xpending`] command has data.
794///
795/// [`xpending`]: ../trait.Commands.html#method.xpending
796#[derive(Default, Debug, Clone)]
797pub struct StreamPendingData {
798    /// Limit on the number of messages to return per call.
799    pub count: usize,
800    /// ID for the first pending record.
801    pub start_id: String,
802    /// ID for the final pending record.
803    pub end_id: String,
804    /// Every consumer in the consumer group with at
805    /// least one pending message,
806    /// and the number of pending messages it has.
807    pub consumers: Vec<StreamInfoConsumer>,
808}
809
810/// Reply type used with [`xpending_count`] and
811/// [`xpending_consumer_count`] commands.
812///
813/// Data returned here have been fetched from the stream without
814/// any acknowledgement.
815///
816/// [`xpending_count`]: ../trait.Commands.html#method.xpending_count
817/// [`xpending_consumer_count`]: ../trait.Commands.html#method.xpending_consumer_count
818///
819#[derive(Default, Debug, Clone)]
820pub struct StreamPendingCountReply {
821    /// An array of structs containing information about
822    /// message IDs yet to be acknowledged by various consumers,
823    /// time since last ack, and total number of acks by that consumer.
824    pub ids: Vec<StreamPendingId>,
825}
826
827/// Reply type used with [`xinfo_stream`] command, containing
828/// general information about the stream stored at the specified key.
829///
830/// The very first and last IDs in the stream are shown,
831/// in order to give some sense about what is the stream content.
832///
833/// **Note:** For Redis 8.6+ idempotency tracking fields, use [`StreamInfoStreamReplyWithIdempotency`]
834/// via the [`xinfo_stream_with_idempotency`] command instead.
835///
836/// [`xinfo_stream`]: ../trait.Commands.html#method.xinfo_stream
837/// [`xinfo_stream_with_idempotency`]: ../trait.Commands.html#method.xinfo_stream_with_idempotency
838///
839#[derive(Default, Debug, Clone)]
840pub struct StreamInfoStreamReply {
841    /// The last generated ID that may not be the same as the last
842    /// entry ID in case some entry was deleted.
843    pub last_generated_id: String,
844    /// Details about the radix tree representing the stream mostly
845    /// useful for optimization and debugging tasks.
846    pub radix_tree_keys: usize,
847    /// The number of consumer groups associated with the stream.
848    pub groups: usize,
849    /// Number of elements of the stream.
850    pub length: usize,
851    /// The very first entry in the stream.
852    pub first_entry: StreamId,
853    /// The very last entry in the stream.
854    pub last_entry: StreamId,
855}
856
857// TODO: Remove this type and extend StreamInfoStreamReply when creating the next major release.
858/// Reply type used with [`xinfo_stream_with_idempotency`] command (Redis 8.6+).
859///
860/// This type composes [`StreamInfoStreamReply`] with additional idempotency tracking fields
861/// introduced in Redis 8.6.
862///
863/// The base stream information is accessible via the `base` field, while idempotency
864/// fields are directly available as top-level fields.
865///
866/// [`xinfo_stream_with_idempotency`]: ../trait.Commands.html#method.xinfo_stream_with_idempotency
867///
868/// # Example
869/// ```no_run
870/// use redis::{Commands, streams::StreamInfoStreamReplyWithIdempotency};
871/// # let client = redis::Client::open("redis://127.0.0.1/").unwrap();
872/// # let mut con = client.get_connection().unwrap();
873///
874/// let info: StreamInfoStreamReplyWithIdempotency = con.xinfo_stream_with_idempotency("stream").unwrap();
875///
876/// // Access base stream info
877/// println!("Stream length: {}", info.base.length);
878/// println!("Last ID: {}", info.base.last_generated_id);
879///
880/// // Access idempotency tracking (Redis 8.6+)
881/// println!("Producers tracked: {}", info.pids_tracked);
882/// println!("Idempotent IDs tracked: {}", info.iids_tracked);
883/// println!("Duplicates prevented: {}", info.iids_duplicates);
884/// ```
885#[derive(Default, Debug, Clone)]
886#[non_exhaustive]
887pub struct StreamInfoStreamReplyWithIdempotency {
888    /// Base stream information
889    pub base: StreamInfoStreamReply,
890    /// The duration in seconds that idempotent IDs are retained in the stream's IDMP map
891    pub idmp_duration: u32,
892    /// The maximum number of idempotent IDs kept for each producer in the stream's IDMP map
893    pub idmp_maxsize: u16,
894    /// The number of unique producer IDs currently being tracked
895    pub pids_tracked: usize,
896    /// The total number of idempotent IDs currently stored across all producers
897    pub iids_tracked: usize,
898    /// The total count of idempotent IDs that have been added to the stream during its lifetime
899    pub iids_added: usize,
900    /// The total count of duplicate messages that were detected and prevented by IDMP
901    pub iids_duplicates: usize,
902}
903
904/// Reply type used with [`xinfo_consumer`] command, an array of every
905/// consumer in a specific consumer group.
906///
907/// [`xinfo_consumer`]: ../trait.Commands.html#method.xinfo_consumer
908///
909#[derive(Default, Debug, Clone)]
910pub struct StreamInfoConsumersReply {
911    /// An array of every consumer in a specific consumer group.
912    pub consumers: Vec<StreamInfoConsumer>,
913}
914
915/// Reply type used with [`xinfo_groups`] command.
916///
917/// This output represents all the consumer groups associated with
918/// the stream.
919///
920/// [`xinfo_groups`]: ../trait.Commands.html#method.xinfo_groups
921///
922#[derive(Default, Debug, Clone)]
923pub struct StreamInfoGroupsReply {
924    /// All the consumer groups associated with the stream.
925    pub groups: Vec<StreamInfoGroup>,
926}
927
928/// A consumer parsed from [`xinfo_consumers`] command.
929///
930/// [`xinfo_consumers`]: ../trait.Commands.html#method.xinfo_consumers
931///
932#[derive(Default, Debug, Clone)]
933pub struct StreamInfoConsumer {
934    /// Name of the consumer group.
935    pub name: String,
936    /// Number of pending messages for this specific consumer.
937    pub pending: usize,
938    /// This consumer's idle time in milliseconds.
939    pub idle: usize,
940}
941
942/// A group parsed from [`xinfo_groups`] command.
943///
944/// [`xinfo_groups`]: ../trait.Commands.html#method.xinfo_groups
945///
946#[derive(Default, Debug, Clone)]
947pub struct StreamInfoGroup {
948    /// The group name.
949    pub name: String,
950    /// Number of consumers known in the group.
951    pub consumers: usize,
952    /// Number of pending messages (delivered but not yet acknowledged) in the group.
953    pub pending: usize,
954    /// Last ID delivered to this group.
955    pub last_delivered_id: String,
956    /// The logical "read counter" of the last entry delivered to group's consumers
957    /// (or `None` if the server does not provide the value).
958    pub entries_read: Option<usize>,
959    /// The number of entries in the stream that are still waiting to be delivered to the
960    /// group's consumers, or a `None` when that number can't be determined.
961    pub lag: Option<usize>,
962}
963
964/// Represents a pending message parsed from [`xpending`] methods.
965///
966/// [`xpending`]: ../trait.Commands.html#method.xpending
967#[derive(Default, Debug, Clone)]
968pub struct StreamPendingId {
969    /// The ID of the message.
970    pub id: String,
971    /// The name of the consumer that fetched the message and has
972    /// still to acknowledge it. We call it the current owner
973    /// of the message.
974    pub consumer: String,
975    /// The number of milliseconds that elapsed since the
976    /// last time this message was delivered to this consumer.
977    pub last_delivered_ms: usize,
978    /// The number of times this message was delivered.
979    pub times_delivered: usize,
980}
981
982/// Represents a stream `key` and its `id`'s parsed from `xread` methods.
983#[derive(Default, Debug, Clone)]
984pub struct StreamKey {
985    /// The stream `key`.
986    pub key: String,
987    /// The parsed stream `id`'s.
988    pub ids: Vec<StreamId>,
989}
990
991/// Represents a stream `id` and its field/values as a `HashMap`
992/// Also contains optional PEL information if the message was fetched with XREADGROUP with a `claim` option
993#[derive(Default, Debug, Clone, PartialEq)]
994pub struct StreamId {
995    /// The stream `id` (entry ID) of this particular message.
996    pub id: String,
997    /// All fields in this message, associated with their respective values.
998    pub map: HashMap<String, Value>,
999    /// The number of milliseconds that elapsed since the last time this entry was delivered to a consumer.
1000    pub milliseconds_elapsed_from_delivery: Option<usize>,
1001    /// The number of times this entry was delivered.
1002    pub delivered_count: Option<usize>,
1003}
1004
1005impl StreamId {
1006    /// Converts a `Value::Array` into a `StreamId`.
1007    fn from_array_value(v: Value) -> Result<Self, ParsingError> {
1008        let mut stream_id = StreamId::default();
1009        if let Value::Array(mut values) = v {
1010            if let Some(v) = values.first_mut() {
1011                stream_id.id = from_redis_value(std::mem::take(v))?;
1012            }
1013            if let Some(v) = values.first_mut() {
1014                stream_id.map = from_redis_value(std::mem::take(v))?;
1015            }
1016        }
1017
1018        Ok(stream_id)
1019    }
1020
1021    /// Fetches value of a given field and converts it to the specified
1022    /// type.
1023    pub fn get<T: FromRedisValue>(&self, key: &str) -> Option<T> {
1024        match self.map.get(key) {
1025            Some(x) => from_redis_value_ref(x).ok(),
1026            None => None,
1027        }
1028    }
1029
1030    /// Does the message contain a particular field?
1031    pub fn contains_key(&self, key: &str) -> bool {
1032        self.map.contains_key(key)
1033    }
1034
1035    /// Returns how many field/value pairs exist in this message.
1036    pub fn len(&self) -> usize {
1037        self.map.len()
1038    }
1039
1040    /// Returns true if there are no field/value pairs in this message.
1041    pub fn is_empty(&self) -> bool {
1042        self.len() == 0
1043    }
1044}
1045
1046type SACRows = Vec<HashMap<String, HashMap<String, Value>>>;
1047
1048impl FromRedisValue for StreamAutoClaimReply {
1049    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1050        let Value::Array(mut items) = v else {
1051            invalid_type_error!("Not a array response", v);
1052        };
1053
1054        if items.len() > 3 || items.len() < 2 {
1055            invalid_type_error!("Incorrect number of items", &items);
1056        }
1057
1058        let deleted_ids = if items.len() == 3 {
1059            from_redis_value(items.pop().unwrap())?
1060        } else {
1061            Vec::new()
1062        };
1063        // safe, because we've checked for length beforehand
1064        let claimed = items.pop().unwrap();
1065        let next_stream_id = from_redis_value(items.pop().unwrap())?;
1066
1067        let Value::Array(arr) = &claimed else {
1068            invalid_type_error!("Incorrect type", claimed)
1069        };
1070        let Some(entry) = arr.iter().find(|val| !matches!(val, Value::Nil)) else {
1071            return Ok(Self {
1072                next_stream_id,
1073                claimed: Vec::new(),
1074                deleted_ids,
1075                invalid_entries: !arr.is_empty(),
1076            });
1077        };
1078        let (claimed, invalid_entries) = match entry {
1079            Value::BulkString(_) => {
1080                // JUSTID response
1081                let claimed_count = arr.len();
1082                let ids: Vec<Option<String>> = from_redis_value(claimed)?;
1083
1084                let claimed: Vec<_> = ids
1085                    .into_iter()
1086                    .filter_map(|id| {
1087                        id.map(|id| StreamId {
1088                            id,
1089                            ..Default::default()
1090                        })
1091                    })
1092                    .collect();
1093                // This means that some nil entries were filtered
1094                let invalid_entries = claimed.len() < claimed_count;
1095                (claimed, invalid_entries)
1096            }
1097            Value::Array(_) => {
1098                // full response
1099                let claimed_count = arr.len();
1100                let rows: SACRows = from_redis_value(claimed)?;
1101
1102                let claimed: Vec<_> = rows
1103                    .into_iter()
1104                    .flat_map(|row| {
1105                        row.into_iter().map(|(id, map)| StreamId {
1106                            id,
1107                            map,
1108                            milliseconds_elapsed_from_delivery: None,
1109                            delivered_count: None,
1110                        })
1111                    })
1112                    .collect();
1113                // This means that some nil entries were filtered
1114                let invalid_entries = claimed.len() < claimed_count;
1115                (claimed, invalid_entries)
1116            }
1117            _ => invalid_type_error!("Incorrect type", claimed),
1118        };
1119
1120        Ok(Self {
1121            next_stream_id,
1122            claimed,
1123            deleted_ids,
1124            invalid_entries,
1125        })
1126    }
1127}
1128
1129type SRRows = Vec<HashMap<String, Vec<HashMap<String, HashMap<String, Value>>>>>;
1130type SRClaimRows =
1131    Vec<HashMap<String, Vec<(String, HashMap<String, Value>, Option<usize>, Option<usize>)>>>;
1132
1133impl FromRedisValue for StreamReadReply {
1134    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1135        // Try to parse as the standard format first
1136        if let Ok(rows) = from_redis_value::<SRRows>(v.clone()) {
1137            return Ok(Self::from_standard_rows(rows));
1138        }
1139
1140        // If that fails, try to parse as XREADGROUP with CLAIM format
1141        // Format: [[stream_name, [[id, [field, value, ...], ms_elapsed, delivery_count], ...]]]
1142        if let Ok(rows) = from_redis_value::<SRClaimRows>(v.clone()) {
1143            return Ok(Self::from_claim_rows(rows));
1144        }
1145
1146        invalid_type_error!("Could not parse StreamReadReply in any known format", v)
1147    }
1148}
1149
1150impl StreamReadReply {
1151    fn from_standard_rows(rows: SRRows) -> Self {
1152        let keys = rows
1153            .into_iter()
1154            .flat_map(|row| {
1155                row.into_iter().map(|(key, entries)| StreamKey {
1156                    key,
1157                    ids: entries
1158                        .into_iter()
1159                        .flat_map(|id_row| {
1160                            id_row.into_iter().map(|(id, map)| StreamId {
1161                                id,
1162                                map,
1163                                milliseconds_elapsed_from_delivery: None,
1164                                delivered_count: None,
1165                            })
1166                        })
1167                        .collect(),
1168                })
1169            })
1170            .collect();
1171        StreamReadReply { keys }
1172    }
1173
1174    fn from_claim_rows(rows: SRClaimRows) -> Self {
1175        let keys = rows
1176            .into_iter()
1177            .flat_map(|row| {
1178                row.into_iter().map(|(key, entries)| StreamKey {
1179                    key,
1180                    ids: entries
1181                        .into_iter()
1182                        .map(
1183                            |(id, map, milliseconds_elapsed_from_delivery, delivered_count)| {
1184                                StreamId {
1185                                    id,
1186                                    map,
1187                                    milliseconds_elapsed_from_delivery,
1188                                    delivered_count,
1189                                }
1190                            },
1191                        )
1192                        .collect(),
1193                })
1194            })
1195            .collect();
1196        StreamReadReply { keys }
1197    }
1198}
1199
1200impl FromRedisValue for StreamRangeReply {
1201    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1202        let rows: Vec<HashMap<String, HashMap<String, Value>>> = from_redis_value(v)?;
1203        let ids: Vec<StreamId> = rows
1204            .into_iter()
1205            .flat_map(|row| {
1206                row.into_iter().map(|(id, map)| StreamId {
1207                    id,
1208                    map,
1209                    milliseconds_elapsed_from_delivery: None,
1210                    delivered_count: None,
1211                })
1212            })
1213            .collect();
1214        Ok(StreamRangeReply { ids })
1215    }
1216}
1217
1218impl FromRedisValue for StreamClaimReply {
1219    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1220        let rows: Vec<HashMap<String, HashMap<String, Value>>> = from_redis_value(v)?;
1221        let ids: Vec<StreamId> = rows
1222            .into_iter()
1223            .flat_map(|row| {
1224                row.into_iter().map(|(id, map)| StreamId {
1225                    id,
1226                    map,
1227                    milliseconds_elapsed_from_delivery: None,
1228                    delivered_count: None,
1229                })
1230            })
1231            .collect();
1232        Ok(StreamClaimReply { ids })
1233    }
1234}
1235
1236type SPRInner = (
1237    usize,
1238    Option<String>,
1239    Option<String>,
1240    Vec<Option<(String, String)>>,
1241);
1242impl FromRedisValue for StreamPendingReply {
1243    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1244        let (count, start, end, consumer_data): SPRInner = from_redis_value(v)?;
1245
1246        if count == 0 {
1247            Ok(StreamPendingReply::Empty)
1248        } else {
1249            let mut result = StreamPendingData::default();
1250
1251            let start_id = start.ok_or_else(|| {
1252                ParsingError::from(arcstr::literal!(
1253                    "IllegalState: Non-zero pending expects start id"
1254                ))
1255            })?;
1256
1257            let end_id = end.ok_or_else(|| {
1258                ParsingError::from(arcstr::literal!(
1259                    "IllegalState: Non-zero pending expects end id"
1260                ))
1261            })?;
1262
1263            result.count = count;
1264            result.start_id = start_id;
1265            result.end_id = end_id;
1266
1267            result.consumers = consumer_data
1268                .into_iter()
1269                .flatten()
1270                .map(|(name, pending)| StreamInfoConsumer {
1271                    name,
1272                    pending: pending.parse().unwrap_or_default(),
1273                    ..Default::default()
1274                })
1275                .collect();
1276
1277            Ok(StreamPendingReply::Data(result))
1278        }
1279    }
1280}
1281
1282impl FromRedisValue for StreamPendingCountReply {
1283    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1284        let mut reply = StreamPendingCountReply::default();
1285        match v {
1286            Value::Array(outer_tuple) => {
1287                for outer in outer_tuple {
1288                    match outer {
1289                        Value::Array(inner_tuple) => match &inner_tuple[..] {
1290                            [
1291                                Value::BulkString(id_bytes),
1292                                Value::BulkString(consumer_bytes),
1293                                Value::Int(last_delivered_ms_u64),
1294                                Value::Int(times_delivered_u64),
1295                            ] => {
1296                                let id = String::from_utf8(id_bytes.to_vec())?;
1297                                let consumer = String::from_utf8(consumer_bytes.to_vec())?;
1298                                let last_delivered_ms = *last_delivered_ms_u64 as usize;
1299                                let times_delivered = *times_delivered_u64 as usize;
1300                                reply.ids.push(StreamPendingId {
1301                                    id,
1302                                    consumer,
1303                                    last_delivered_ms,
1304                                    times_delivered,
1305                                });
1306                            }
1307                            _ => fail!(ParsingError::from(arcstr::literal!(
1308                                "Cannot parse redis data (3)"
1309                            ))),
1310                        },
1311                        _ => fail!(ParsingError::from(arcstr::literal!(
1312                            "Cannot parse redis data (2)"
1313                        ))),
1314                    }
1315                }
1316            }
1317            _ => fail!(ParsingError::from(arcstr::literal!(
1318                "Cannot parse redis data (1)"
1319            ))),
1320        };
1321        Ok(reply)
1322    }
1323}
1324
1325impl FromRedisValue for StreamInfoStreamReply {
1326    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1327        let mut map: HashMap<String, Value> = from_redis_value(v)?;
1328        let mut reply = StreamInfoStreamReply::default();
1329        if let Some(v) = map.remove("last-generated-id") {
1330            reply.last_generated_id = from_redis_value(v)?;
1331        }
1332        if let Some(v) = map.remove("radix-tree-nodes") {
1333            reply.radix_tree_keys = from_redis_value(v)?;
1334        }
1335        if let Some(v) = map.remove("groups") {
1336            reply.groups = from_redis_value(v)?;
1337        }
1338        if let Some(v) = map.remove("length") {
1339            reply.length = from_redis_value(v)?;
1340        }
1341        if let Some(v) = map.remove("first-entry") {
1342            reply.first_entry = StreamId::from_array_value(v)?;
1343        }
1344        if let Some(v) = map.remove("last-entry") {
1345            reply.last_entry = StreamId::from_array_value(v)?;
1346        }
1347        Ok(reply)
1348    }
1349}
1350
1351impl FromRedisValue for StreamInfoStreamReplyWithIdempotency {
1352    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1353        let mut map: HashMap<String, Value> = from_redis_value(v)?;
1354
1355        // Parse base fields into the composed StreamInfoStreamReply
1356        let mut base = StreamInfoStreamReply::default();
1357        if let Some(v) = map.remove("last-generated-id") {
1358            base.last_generated_id = from_redis_value(v)?;
1359        }
1360        if let Some(v) = map.remove("radix-tree-nodes") {
1361            base.radix_tree_keys = from_redis_value(v)?;
1362        }
1363        if let Some(v) = map.remove("groups") {
1364            base.groups = from_redis_value(v)?;
1365        }
1366        if let Some(v) = map.remove("length") {
1367            base.length = from_redis_value(v)?;
1368        }
1369        if let Some(v) = map.remove("first-entry") {
1370            base.first_entry = StreamId::from_array_value(v)?;
1371        }
1372        if let Some(v) = map.remove("last-entry") {
1373            base.last_entry = StreamId::from_array_value(v)?;
1374        }
1375
1376        // Parse idempotency fields
1377        let mut reply = StreamInfoStreamReplyWithIdempotency {
1378            base,
1379            ..Default::default()
1380        };
1381
1382        if let Some(v) = map.remove("idmp-duration") {
1383            reply.idmp_duration = from_redis_value(v)?;
1384        }
1385        if let Some(v) = map.remove("idmp-maxsize") {
1386            reply.idmp_maxsize = from_redis_value(v)?;
1387        }
1388        if let Some(v) = map.remove("pids-tracked") {
1389            reply.pids_tracked = from_redis_value(v)?;
1390        }
1391        if let Some(v) = map.remove("iids-tracked") {
1392            reply.iids_tracked = from_redis_value(v)?;
1393        }
1394        if let Some(v) = map.remove("iids-added") {
1395            reply.iids_added = from_redis_value(v)?;
1396        }
1397        if let Some(v) = map.remove("iids-duplicates") {
1398            reply.iids_duplicates = from_redis_value(v)?;
1399        }
1400
1401        Ok(reply)
1402    }
1403}
1404
1405impl FromRedisValue for StreamInfoConsumersReply {
1406    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1407        let consumers: Vec<HashMap<String, Value>> = from_redis_value(v)?;
1408        let mut reply = StreamInfoConsumersReply::default();
1409        for mut map in consumers {
1410            let mut c = StreamInfoConsumer::default();
1411            if let Some(v) = map.remove("name") {
1412                c.name = from_redis_value(v)?;
1413            }
1414            if let Some(v) = map.remove("pending") {
1415                c.pending = from_redis_value(v)?;
1416            }
1417            if let Some(v) = map.remove("idle") {
1418                c.idle = from_redis_value(v)?;
1419            }
1420            reply.consumers.push(c);
1421        }
1422
1423        Ok(reply)
1424    }
1425}
1426
1427impl FromRedisValue for StreamInfoGroupsReply {
1428    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1429        let groups: Vec<HashMap<String, Value>> = from_redis_value(v)?;
1430        let mut reply = StreamInfoGroupsReply::default();
1431        for mut map in groups {
1432            let mut g = StreamInfoGroup::default();
1433            if let Some(v) = map.remove("name") {
1434                g.name = from_redis_value(v)?;
1435            }
1436            if let Some(v) = map.remove("pending") {
1437                g.pending = from_redis_value(v)?;
1438            }
1439            if let Some(v) = map.remove("consumers") {
1440                g.consumers = from_redis_value(v)?;
1441            }
1442            if let Some(v) = map.remove("last-delivered-id") {
1443                g.last_delivered_id = from_redis_value(v)?;
1444            }
1445            if let Some(v) = map.remove("entries-read") {
1446                g.entries_read = if let Value::Nil = v {
1447                    None
1448                } else {
1449                    Some(from_redis_value(v)?)
1450                };
1451            }
1452            if let Some(v) = map.remove("lag") {
1453                g.lag = if let Value::Nil = v {
1454                    None
1455                } else {
1456                    Some(from_redis_value(v)?)
1457                };
1458            }
1459            reply.groups.push(g);
1460        }
1461        Ok(reply)
1462    }
1463}
1464
1465/// Deletion policy for stream entries.
1466#[derive(Debug, Clone, Default)]
1467#[non_exhaustive]
1468pub enum StreamDeletionPolicy {
1469    /// Preserve existing references to the deleted entries in all consumer groups' PEL.
1470    #[default]
1471    KeepRef,
1472    /// Delete the entry from the stream and from all the consumer groups' PELs.
1473    DelRef,
1474    /// Delete the entry from the stream and from all the consumer groups' PELs, but only if the entry is acknowledged by all the groups.
1475    Acked,
1476}
1477
1478impl ToRedisArgs for StreamDeletionPolicy {
1479    fn write_redis_args<W>(&self, out: &mut W)
1480    where
1481        W: ?Sized + RedisWrite,
1482    {
1483        match self {
1484            StreamDeletionPolicy::KeepRef => out.write_arg(b"KEEPREF"),
1485            StreamDeletionPolicy::DelRef => out.write_arg(b"DELREF"),
1486            StreamDeletionPolicy::Acked => out.write_arg(b"ACKED"),
1487        }
1488    }
1489}
1490impl ToSingleRedisArg for StreamDeletionPolicy {}
1491
1492/// Status codes returned by the `XDELEX` command
1493#[cfg(feature = "streams")]
1494#[cfg_attr(docsrs, doc(cfg(feature = "streams")))]
1495#[derive(Debug, PartialEq, Eq)]
1496#[non_exhaustive]
1497pub enum XDelExStatusCode {
1498    /// No entry with the given id exists in the stream
1499    IdNotFound = -1,
1500    /// The entry was deleted from the stream
1501    Deleted = 1,
1502    /// The entry was not deleted because it has either not been delivered to any consumer
1503    /// or still has references in the consumer groups' Pending Entries List (PEL)
1504    NotDeletedUnacknowledgedOrStillReferenced = 2,
1505}
1506
1507#[cfg(feature = "streams")]
1508impl FromRedisValue for XDelExStatusCode {
1509    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1510        match v {
1511            Value::Int(code) => match code {
1512                -1 => Ok(XDelExStatusCode::IdNotFound),
1513                1 => Ok(XDelExStatusCode::Deleted),
1514                2 => Ok(XDelExStatusCode::NotDeletedUnacknowledgedOrStillReferenced),
1515                _ => Err(format!("Invalid XDelExStatusCode status code: {code}").into()),
1516            },
1517            _ => Err(arcstr::literal!("Response type not XAckDelStatusCode compatible").into()),
1518        }
1519    }
1520}
1521
1522/// Status codes returned by the `XACKDEL` command
1523#[cfg(feature = "streams")]
1524#[cfg_attr(docsrs, doc(cfg(feature = "streams")))]
1525#[derive(Debug, PartialEq, Eq)]
1526#[non_exhaustive]
1527pub enum XAckDelStatusCode {
1528    /// No entry with the given id exists in the stream
1529    IdNotFound = -1,
1530    /// The entry was acknowledged and deleted from the stream
1531    AcknowledgedAndDeleted = 1,
1532    /// The entry was acknowledged but not deleted because it has references in the consumer groups' Pending Entries List (PEL)
1533    AcknowledgedNotDeletedStillReferenced = 2,
1534}
1535
1536#[cfg(feature = "streams")]
1537impl FromRedisValue for XAckDelStatusCode {
1538    fn from_redis_value(v: Value) -> Result<Self, ParsingError> {
1539        match v {
1540            Value::Int(code) => match code {
1541                -1 => Ok(XAckDelStatusCode::IdNotFound),
1542                1 => Ok(XAckDelStatusCode::AcknowledgedAndDeleted),
1543                2 => Ok(XAckDelStatusCode::AcknowledgedNotDeletedStillReferenced),
1544                _ => Err(arcstr::literal!("Invalid XAckDelStatusCode status code: {code}").into()),
1545            },
1546            _ => Err(arcstr::literal!("Response type not XAckDelStatusCode compatible").into()),
1547        }
1548    }
1549}
1550
1551#[cfg(test)]
1552mod tests {
1553    use super::*;
1554
1555    fn assert_command_eq(object: impl ToRedisArgs, expected: &[u8]) {
1556        let mut out: Vec<Vec<u8>> = Vec::new();
1557
1558        object.write_redis_args(&mut out);
1559
1560        let mut cmd: Vec<u8> = Vec::new();
1561
1562        out.iter_mut().for_each(|item| {
1563            cmd.append(item);
1564            cmd.push(b' ');
1565        });
1566
1567        cmd.pop();
1568
1569        assert_eq!(cmd, expected);
1570    }
1571
1572    mod stream_auto_claim_reply {
1573        use super::*;
1574        use crate::Value;
1575
1576        #[test]
1577        fn short_response() {
1578            let value = Value::Array(vec![Value::BulkString("1713465536578-0".into())]);
1579
1580            StreamAutoClaimReply::from_redis_value(value).unwrap_err();
1581        }
1582
1583        #[test]
1584        fn parses_none_claimed_response() {
1585            let value = Value::Array(vec![
1586                Value::BulkString("0-0".into()),
1587                Value::Array(vec![]),
1588                Value::Array(vec![]),
1589            ]);
1590
1591            let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap();
1592
1593            assert_eq!(reply.next_stream_id.as_str(), "0-0");
1594            assert_eq!(reply.claimed.len(), 0);
1595            assert_eq!(reply.deleted_ids.len(), 0);
1596        }
1597
1598        #[test]
1599        fn parses_response() {
1600            let value = Value::Array(vec![
1601                Value::BulkString("1713465536578-0".into()),
1602                Value::Array(vec![
1603                    Value::Array(vec![
1604                        Value::BulkString("1713465533411-0".into()),
1605                        // Both RESP2 and RESP3 expose this map as an array of key/values
1606                        Value::Array(vec![
1607                            Value::BulkString("name".into()),
1608                            Value::BulkString("test".into()),
1609                            Value::BulkString("other".into()),
1610                            Value::BulkString("whaterver".into()),
1611                        ]),
1612                    ]),
1613                    Value::Array(vec![
1614                        Value::BulkString("1713465536069-0".into()),
1615                        Value::Array(vec![
1616                            Value::BulkString("name".into()),
1617                            Value::BulkString("another test".into()),
1618                            Value::BulkString("other".into()),
1619                            Value::BulkString("something".into()),
1620                        ]),
1621                    ]),
1622                ]),
1623                Value::Array(vec![Value::BulkString("123456789-0".into())]),
1624            ]);
1625
1626            let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap();
1627
1628            assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0");
1629            assert_eq!(reply.claimed.len(), 2);
1630            assert_eq!(reply.claimed[0].id.as_str(), "1713465533411-0");
1631            assert!(
1632                matches!(reply.claimed[0].map.get("name"), Some(Value::BulkString(v)) if v == "test".as_bytes())
1633            );
1634            assert_eq!(reply.claimed[1].id.as_str(), "1713465536069-0");
1635            assert_eq!(reply.deleted_ids.len(), 1);
1636            assert!(reply.deleted_ids.contains(&"123456789-0".to_string()))
1637        }
1638
1639        #[test]
1640        fn parses_v6_response() {
1641            let value = Value::Array(vec![
1642                Value::BulkString("1713465536578-0".into()),
1643                Value::Array(vec![
1644                    Value::Array(vec![
1645                        Value::BulkString("1713465533411-0".into()),
1646                        Value::Array(vec![
1647                            Value::BulkString("name".into()),
1648                            Value::BulkString("test".into()),
1649                            Value::BulkString("other".into()),
1650                            Value::BulkString("whaterver".into()),
1651                        ]),
1652                    ]),
1653                    Value::Array(vec![
1654                        Value::BulkString("1713465536069-0".into()),
1655                        Value::Array(vec![
1656                            Value::BulkString("name".into()),
1657                            Value::BulkString("another test".into()),
1658                            Value::BulkString("other".into()),
1659                            Value::BulkString("something".into()),
1660                        ]),
1661                    ]),
1662                ]),
1663                // V6 and lower lack the deleted_ids array
1664            ]);
1665
1666            let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap();
1667
1668            assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0");
1669            assert_eq!(reply.claimed.len(), 2);
1670            let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect();
1671            assert!(ids.contains(&"1713465533411-0"));
1672            assert!(ids.contains(&"1713465536069-0"));
1673            assert_eq!(reply.deleted_ids.len(), 0);
1674        }
1675
1676        #[test]
1677        fn parses_justid_response() {
1678            let value = Value::Array(vec![
1679                Value::BulkString("1713465536578-0".into()),
1680                Value::Array(vec![
1681                    Value::BulkString("1713465533411-0".into()),
1682                    Value::BulkString("1713465536069-0".into()),
1683                ]),
1684                Value::Array(vec![Value::BulkString("123456789-0".into())]),
1685            ]);
1686
1687            let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap();
1688
1689            assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0");
1690            assert_eq!(reply.claimed.len(), 2);
1691            let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect();
1692            assert!(ids.contains(&"1713465533411-0"));
1693            assert!(ids.contains(&"1713465536069-0"));
1694            assert_eq!(reply.deleted_ids.len(), 1);
1695            assert!(reply.deleted_ids.contains(&"123456789-0".to_string()))
1696        }
1697
1698        #[test]
1699        fn parses_v6_justid_response() {
1700            let value = Value::Array(vec![
1701                Value::BulkString("1713465536578-0".into()),
1702                Value::Array(vec![
1703                    Value::BulkString("1713465533411-0".into()),
1704                    Value::BulkString("1713465536069-0".into()),
1705                ]),
1706                // V6 and lower lack the deleted_ids array
1707            ]);
1708
1709            let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap();
1710
1711            assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0");
1712            assert_eq!(reply.claimed.len(), 2);
1713            let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect();
1714            assert!(ids.contains(&"1713465533411-0"));
1715            assert!(ids.contains(&"1713465536069-0"));
1716            assert_eq!(reply.deleted_ids.len(), 0);
1717        }
1718    }
1719
1720    mod stream_trim_options {
1721        use super::*;
1722
1723        #[test]
1724        fn maxlen_trim() {
1725            let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10);
1726
1727            assert_command_eq(options, b"MAXLEN ~ 10");
1728        }
1729
1730        #[test]
1731        fn maxlen_exact_trim() {
1732            let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Exact, 10);
1733
1734            assert_command_eq(options, b"MAXLEN = 10");
1735        }
1736
1737        #[test]
1738        fn maxlen_trim_limit() {
1739            let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10).limit(5);
1740
1741            assert_command_eq(options, b"MAXLEN ~ 10 LIMIT 5");
1742        }
1743        #[test]
1744        fn minid_trim_limit() {
1745            let options = StreamTrimOptions::minid(StreamTrimmingMode::Exact, "123456-7").limit(5);
1746
1747            assert_command_eq(options, b"MINID = 123456-7 LIMIT 5");
1748        }
1749    }
1750
1751    mod stream_add_options {
1752        use super::*;
1753
1754        #[test]
1755        fn the_default() {
1756            let options = StreamAddOptions::default();
1757
1758            assert_command_eq(options, b"");
1759        }
1760
1761        #[test]
1762        fn with_maxlen_trim() {
1763            let options = StreamAddOptions::default()
1764                .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 10));
1765
1766            assert_command_eq(options, b"MAXLEN = 10");
1767        }
1768
1769        #[test]
1770        fn with_nomkstream() {
1771            let options = StreamAddOptions::default().nomkstream();
1772
1773            assert_command_eq(options, b"NOMKSTREAM");
1774        }
1775
1776        #[test]
1777        fn with_nomkstream_and_maxlen_trim() {
1778            let options = StreamAddOptions::default()
1779                .nomkstream()
1780                .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 10));
1781
1782            assert_command_eq(options, b"NOMKSTREAM MAXLEN = 10");
1783        }
1784
1785        #[test]
1786        fn with_idmp_manual_mode() {
1787            let options = StreamAddOptions::default().idmp("producer-1", "iid-1");
1788
1789            assert_command_eq(options, b"IDMP producer-1 iid-1");
1790        }
1791
1792        #[test]
1793        fn with_idmpauto_automatic_mode() {
1794            let options = StreamAddOptions::default().idmpauto("producer-1");
1795
1796            assert_command_eq(options, b"IDMPAUTO producer-1");
1797        }
1798
1799        #[test]
1800        fn with_nomkstream_and_idmp() {
1801            let options = StreamAddOptions::default()
1802                .nomkstream()
1803                .idmp("producer-1", "iid-1");
1804
1805            assert_command_eq(options, b"NOMKSTREAM IDMP producer-1 iid-1");
1806        }
1807
1808        #[test]
1809        fn with_trim_and_idmp() {
1810            let options = StreamAddOptions::default()
1811                .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 100))
1812                .idmp("producer-1", "iid-1");
1813
1814            assert_command_eq(options, b"IDMP producer-1 iid-1 MAXLEN = 100");
1815        }
1816
1817        #[test]
1818        fn with_all_options_and_idmp() {
1819            let options = StreamAddOptions::default()
1820                .nomkstream()
1821                .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Approx, 100))
1822                .idmp("producer-1", "iid-1")
1823                .set_deletion_policy(StreamDeletionPolicy::KeepRef);
1824
1825            assert_command_eq(
1826                options,
1827                b"NOMKSTREAM KEEPREF IDMP producer-1 iid-1 MAXLEN ~ 100",
1828            );
1829        }
1830
1831        #[test]
1832        fn with_all_options_and_idmpauto() {
1833            let options = StreamAddOptions::default()
1834                .nomkstream()
1835                .trim(StreamTrimStrategy::minid(
1836                    StreamTrimmingMode::Exact,
1837                    "123456-0",
1838                ))
1839                .idmpauto("producer-2")
1840                .set_deletion_policy(StreamDeletionPolicy::DelRef);
1841
1842            assert_command_eq(
1843                options,
1844                b"NOMKSTREAM DELREF IDMPAUTO producer-2 MINID = 123456-0",
1845            );
1846        }
1847    }
1848
1849    mod stream_config_options {
1850        use super::*;
1851
1852        const IDMP_CUSTOM_DURATION: u32 = 300;
1853        const IDMP_CUSTOM_MAXSIZE: u16 = 1000;
1854
1855        #[test]
1856        fn with_idempotency_seconds_only() {
1857            let options =
1858                StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION).unwrap();
1859            assert_command_eq(
1860                options,
1861                format!("IDMP-DURATION {IDMP_CUSTOM_DURATION}").as_bytes(),
1862            );
1863        }
1864
1865        #[test]
1866        fn with_idempotency_maxsize_only() {
1867            let options =
1868                StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE).unwrap();
1869            assert_command_eq(
1870                options,
1871                format!("IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}").as_bytes(),
1872            );
1873        }
1874
1875        #[test]
1876        fn with_both_options_starting_with_idempotency_seconds() {
1877            let options = StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION)
1878                .unwrap()
1879                .idempotency_maxsize(IDMP_CUSTOM_MAXSIZE)
1880                .unwrap();
1881            assert_command_eq(
1882                options,
1883                format!("IDMP-DURATION {IDMP_CUSTOM_DURATION} IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}")
1884                    .as_bytes(),
1885            );
1886        }
1887
1888        #[test]
1889        fn with_both_options_starting_with_idempotency_maxsize() {
1890            let options = StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE)
1891                .unwrap()
1892                .idempotency_seconds(IDMP_CUSTOM_DURATION)
1893                .unwrap();
1894            assert_command_eq(
1895                options,
1896                format!("IDMP-DURATION {IDMP_CUSTOM_DURATION} IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}")
1897                    .as_bytes(),
1898            );
1899        }
1900
1901        #[test]
1902        fn with_max_values() {
1903            let options = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MAX)
1904                .unwrap()
1905                .idempotency_maxsize(IDMP_MAXSIZE_MAX)
1906                .unwrap();
1907            assert_command_eq(
1908                options,
1909                format!("IDMP-DURATION {IDMP_DURATION_MAX} IDMP-MAXSIZE {IDMP_MAXSIZE_MAX}")
1910                    .as_bytes(),
1911            );
1912        }
1913
1914        #[test]
1915        fn with_min_values() {
1916            let options = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MIN)
1917                .unwrap()
1918                .idempotency_maxsize(IDMP_MAXSIZE_MIN)
1919                .unwrap();
1920            assert_command_eq(
1921                options,
1922                format!("IDMP-DURATION {IDMP_DURATION_MIN} IDMP-MAXSIZE {IDMP_MAXSIZE_MIN}")
1923                    .as_bytes(),
1924            );
1925        }
1926
1927        #[test]
1928        fn error_idempotency_seconds_too_low() {
1929            let result = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MIN - 1);
1930            assert!(result.is_err());
1931            assert!(result.unwrap_err().contains(&format!(
1932                "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}"
1933            )));
1934        }
1935
1936        #[test]
1937        fn error_idempotency_seconds_too_high() {
1938            let result = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MAX + 1);
1939            assert!(result.is_err());
1940            assert!(result.unwrap_err().contains(&format!(
1941                "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}"
1942            )));
1943        }
1944
1945        #[test]
1946        fn error_idempotency_maxsize_too_low() {
1947            let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_MAXSIZE_MIN - 1);
1948            assert!(result.is_err());
1949            assert!(result.unwrap_err().contains(&format!(
1950                "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}"
1951            )));
1952        }
1953
1954        #[test]
1955        fn error_idempotency_maxsize_too_high() {
1956            let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_MAXSIZE_MAX + 1);
1957            assert!(result.is_err());
1958            assert!(result.unwrap_err().contains(&format!(
1959                "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}"
1960            )));
1961        }
1962
1963        #[test]
1964        fn error_setter_idempotency_seconds_too_low() {
1965            let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE)
1966                .unwrap()
1967                .idempotency_seconds(IDMP_DURATION_MIN - 1);
1968            assert!(result.is_err());
1969            assert!(result.unwrap_err().contains(&format!(
1970                "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}"
1971            )));
1972        }
1973
1974        #[test]
1975        fn error_setter_idempotency_maxsize_too_high() {
1976            let result = StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION)
1977                .unwrap()
1978                .idempotency_maxsize(IDMP_MAXSIZE_MAX + 1);
1979            assert!(result.is_err());
1980            assert!(result.unwrap_err().contains(&format!(
1981                "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}"
1982            )));
1983        }
1984    }
1985}