1use crate::core::{
73 error::{RedisError, RedisResult},
74 value::RespValue,
75};
76use std::collections::HashMap;
77use std::time::Duration;
78
79#[derive(Debug, Clone)]
81pub struct StreamEntry {
82 pub id: String,
84 pub fields: HashMap<String, String>,
86}
87
88impl StreamEntry {
89 pub fn new(id: String, fields: HashMap<String, String>) -> Self {
91 Self { id, fields }
92 }
93
94 #[must_use]
96 pub fn get_field(&self, field: &str) -> Option<&String> {
97 self.fields.get(field)
98 }
99
100 #[must_use]
102 pub fn has_field(&self, field: &str) -> bool {
103 self.fields.contains_key(field)
104 }
105
106 #[must_use]
118 pub fn timestamp(&self) -> Option<u64> {
119 self.id.split('-').next()?.parse().ok()
120 }
121
122 #[must_use]
134 pub fn sequence(&self) -> Option<u64> {
135 self.id.split('-').nth(1)?.parse().ok()
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct StreamInfo {
142 pub length: u64,
144 pub groups: u64,
146 pub first_entry: Option<String>,
148 pub last_entry: Option<String>,
150 pub last_generated_id: String,
152}
153
154#[derive(Debug, Clone)]
156pub struct ConsumerGroupInfo {
157 pub name: String,
159 pub consumers: u64,
161 pub pending: u64,
163 pub last_delivered_id: String,
165}
166
167#[derive(Debug, Clone)]
169pub struct ConsumerInfo {
170 pub name: String,
172 pub pending: u64,
174 pub idle: u64,
176}
177
178#[derive(Debug, Clone)]
180pub struct PendingMessage {
181 pub id: String,
183 pub consumer: String,
185 pub idle_time: u64,
187 pub delivery_count: u64,
189}
190
191#[derive(Debug, Clone)]
193pub struct StreamRange {
194 pub start: String,
196 pub end: String,
198 pub count: Option<u64>,
200}
201
202impl StreamRange {
203 pub fn new(start: impl Into<String>, end: impl Into<String>) -> Self {
205 Self {
206 start: start.into(),
207 end: end.into(),
208 count: None,
209 }
210 }
211
212 pub fn with_count(mut self, count: u64) -> Self {
214 self.count = Some(count);
215 self
216 }
217
218 pub fn all() -> Self {
220 Self::new("-", "+")
221 }
222
223 pub fn from(start: impl Into<String>) -> Self {
225 Self::new(start, "+")
226 }
227
228 pub fn to(end: impl Into<String>) -> Self {
230 Self::new("-", end)
231 }
232}
233
234#[derive(Debug, Clone)]
236pub struct ReadOptions {
237 pub count: Option<u64>,
239 pub block: Option<Duration>,
241}
242
243impl ReadOptions {
244 #[must_use]
246 pub fn new() -> Self {
247 Self {
248 count: None,
249 block: None,
250 }
251 }
252
253 pub fn with_count(mut self, count: u64) -> Self {
255 self.count = Some(count);
256 self
257 }
258
259 pub fn with_block(mut self, timeout: Duration) -> Self {
261 self.block = Some(timeout);
262 self
263 }
264
265 pub fn blocking(timeout: Duration) -> Self {
267 Self::new().with_block(timeout)
268 }
269
270 pub fn non_blocking(count: u64) -> Self {
272 Self::new().with_count(count)
273 }
274}
275
276impl Default for ReadOptions {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282pub fn parse_stream_entries(response: RespValue) -> RedisResult<Vec<StreamEntry>> {
284 match response {
285 RespValue::Array(items) => {
286 let mut entries = Vec::new();
287
288 for item in items {
289 match item {
290 RespValue::Array(entry_data) if entry_data.len() == 2 => {
291 let id = entry_data[0].as_string()?;
292
293 match &entry_data[1] {
294 RespValue::Array(field_values) => {
295 let mut fields = HashMap::new();
296
297 for chunk in field_values.chunks(2) {
299 if chunk.len() == 2 {
300 let field = chunk[0].as_string()?;
301 let value = chunk[1].as_string()?;
302 fields.insert(field, value);
303 }
304 }
305
306 entries.push(StreamEntry::new(id, fields));
307 }
308 _ => {
309 return Err(RedisError::Type(format!(
310 "Invalid stream entry field format: {:?}",
311 entry_data[1]
312 )))
313 }
314 }
315 }
316 _ => {
317 return Err(RedisError::Type(format!(
318 "Invalid stream entry format: {:?}",
319 item
320 )))
321 }
322 }
323 }
324
325 Ok(entries)
326 }
327 _ => Err(RedisError::Type(format!(
328 "Expected array for stream entries, got: {:?}",
329 response
330 ))),
331 }
332}
333
334pub fn parse_xread_response(response: RespValue) -> RedisResult<HashMap<String, Vec<StreamEntry>>> {
336 match response {
337 RespValue::Array(streams) => {
338 let mut result = HashMap::new();
339
340 for stream in streams {
341 match stream {
342 RespValue::Array(stream_data) if stream_data.len() == 2 => {
343 let stream_name = stream_data[0].as_string()?;
344 let entries = parse_stream_entries(stream_data[1].clone())?;
345 result.insert(stream_name, entries);
346 }
347 _ => {
348 return Err(RedisError::Type(format!(
349 "Invalid XREAD response format: {:?}",
350 stream
351 )))
352 }
353 }
354 }
355
356 Ok(result)
357 }
358 RespValue::Null => Ok(HashMap::new()), _ => Err(RedisError::Type(format!(
360 "Expected array or null for XREAD response, got: {:?}",
361 response
362 ))),
363 }
364}
365
366pub fn parse_stream_info(response: RespValue) -> RedisResult<StreamInfo> {
368 match response {
369 RespValue::Array(items) => {
370 let mut length = 0;
371 let mut groups = 0;
372 let mut first_entry = None;
373 let mut last_entry = None;
374 let mut last_generated_id = String::new();
375
376 for chunk in items.chunks(2) {
378 if chunk.len() == 2 {
379 let key = chunk[0].as_string()?;
380 match key.as_str() {
381 "length" => length = chunk[1].as_int()? as u64,
382 "groups" => groups = chunk[1].as_int()? as u64,
383 "first-entry" => {
384 first_entry = parse_stream_info_entry_id(&chunk[1])?;
385 }
386 "last-entry" => {
387 last_entry = parse_stream_info_entry_id(&chunk[1])?;
388 }
389 "last-generated-id" => last_generated_id = chunk[1].as_string()?,
390 _ => {} }
392 }
393 }
394
395 Ok(StreamInfo {
396 length,
397 groups,
398 first_entry,
399 last_entry,
400 last_generated_id,
401 })
402 }
403 _ => Err(RedisError::Type(format!(
404 "Expected array for stream info, got: {:?}",
405 response
406 ))),
407 }
408}
409
410fn parse_stream_info_entry_id(value: &RespValue) -> RedisResult<Option<String>> {
411 match value {
412 RespValue::Array(entry) => entry.first().map(RespValue::as_string).transpose(),
413 _ => Ok(None),
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_stream_entry_creation() {
423 let mut fields = HashMap::new();
424 fields.insert("user".to_string(), "alice".to_string());
425 fields.insert("action".to_string(), "login".to_string());
426
427 let entry = StreamEntry::new("1234567890123-0".to_string(), fields.clone());
428
429 assert_eq!(entry.id, "1234567890123-0");
430 assert_eq!(entry.fields, fields);
431 assert_eq!(entry.get_field("user"), Some(&"alice".to_string()));
432 assert!(entry.has_field("action"));
433 assert!(!entry.has_field("nonexistent"));
434 }
435
436 #[test]
437 fn test_stream_entry_timestamp_parsing() {
438 let entry = StreamEntry::new("1234567890123-5".to_string(), HashMap::new());
439
440 assert_eq!(entry.timestamp(), Some(1_234_567_890_123));
441 assert_eq!(entry.sequence(), Some(5));
442 }
443
444 #[test]
445 fn test_stream_entry_invalid_id() {
446 let entry = StreamEntry::new("invalid-id".to_string(), HashMap::new());
447
448 assert_eq!(entry.timestamp(), None);
449 assert_eq!(entry.sequence(), None);
450 }
451
452 #[test]
453 fn test_stream_range_creation() {
454 let range = StreamRange::new("1000", "2000").with_count(10);
455
456 assert_eq!(range.start, "1000");
457 assert_eq!(range.end, "2000");
458 assert_eq!(range.count, Some(10));
459 }
460
461 #[test]
462 fn test_stream_range_presets() {
463 let all = StreamRange::all();
464 assert_eq!(all.start, "-");
465 assert_eq!(all.end, "+");
466
467 let from = StreamRange::from("1000");
468 assert_eq!(from.start, "1000");
469 assert_eq!(from.end, "+");
470
471 let to = StreamRange::to("2000");
472 assert_eq!(to.start, "-");
473 assert_eq!(to.end, "2000");
474 }
475
476 #[test]
477 fn test_read_options() {
478 let options = ReadOptions::new()
479 .with_count(5)
480 .with_block(Duration::from_secs(1));
481
482 assert_eq!(options.count, Some(5));
483 assert_eq!(options.block, Some(Duration::from_secs(1)));
484
485 let blocking = ReadOptions::blocking(Duration::from_millis(500));
486 assert_eq!(blocking.block, Some(Duration::from_millis(500)));
487
488 let non_blocking = ReadOptions::non_blocking(10);
489 assert_eq!(non_blocking.count, Some(10));
490 assert_eq!(non_blocking.block, None);
491 }
492
493 #[test]
494 fn test_parse_stream_entries() {
495 let response = RespValue::Array(vec![
496 RespValue::Array(vec![
497 RespValue::from("1234567890123-0"),
498 RespValue::Array(vec![
499 RespValue::from("user"),
500 RespValue::from("alice"),
501 RespValue::from("action"),
502 RespValue::from("login"),
503 ]),
504 ]),
505 RespValue::Array(vec![
506 RespValue::from("1234567890124-0"),
507 RespValue::Array(vec![
508 RespValue::from("user"),
509 RespValue::from("bob"),
510 RespValue::from("action"),
511 RespValue::from("logout"),
512 ]),
513 ]),
514 ]);
515
516 let entries = parse_stream_entries(response).unwrap();
517 assert_eq!(entries.len(), 2);
518
519 assert_eq!(entries[0].id, "1234567890123-0");
520 assert_eq!(entries[0].get_field("user"), Some(&"alice".to_string()));
521 assert_eq!(entries[0].get_field("action"), Some(&"login".to_string()));
522
523 assert_eq!(entries[1].id, "1234567890124-0");
524 assert_eq!(entries[1].get_field("user"), Some(&"bob".to_string()));
525 assert_eq!(entries[1].get_field("action"), Some(&"logout".to_string()));
526 }
527
528 #[test]
529 fn test_parse_xread_response() {
530 let response = RespValue::Array(vec![RespValue::Array(vec![
531 RespValue::from("stream1"),
532 RespValue::Array(vec![RespValue::Array(vec![
533 RespValue::from("1234567890123-0"),
534 RespValue::Array(vec![RespValue::from("field1"), RespValue::from("value1")]),
535 ])]),
536 ])]);
537
538 let result = parse_xread_response(response).unwrap();
539 assert_eq!(result.len(), 1);
540 assert!(result.contains_key("stream1"));
541
542 let entries = &result["stream1"];
543 assert_eq!(entries.len(), 1);
544 assert_eq!(entries[0].id, "1234567890123-0");
545 assert_eq!(entries[0].get_field("field1"), Some(&"value1".to_string()));
546 }
547
548 #[test]
549 fn test_parse_xread_response_null() {
550 let response = RespValue::Null;
551 let result = parse_xread_response(response).unwrap();
552 assert!(result.is_empty());
553 }
554}