Coverage Report

Created: 2026-11-12 07:20

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