use crate::ValkeyResult;
use crate::cmd::Cmd;
use crate::commands::core::CommandDispatch;
use crate::value::FromValkeyValue;
#[cfg(feature = "sync")]
use crate::commands::core::SyncCommandDispatch;
#[derive(Debug)]
struct PageSpec {
prefix: Vec<Vec<u8>>,
suffix: Vec<Vec<u8>>,
}
impl PageSpec {
fn to_cmd(&self, cursor: u64) -> Cmd {
let mut cmd = Cmd::new();
for a in &self.prefix {
cmd.arg(&a[..]);
}
cmd.arg(cursor);
for a in &self.suffix {
cmd.arg(&a[..]);
}
cmd
}
}
pub struct ScanIter<'a, C: ?Sized, RV> {
con: &'a C,
spec: PageSpec,
cursor: u64,
batch: std::vec::IntoIter<RV>,
}
impl<'a, C: CommandDispatch, RV: FromValkeyValue> ScanIter<'a, C, RV> {
pub(crate) async fn new(
con: &'a C,
prefix: Vec<Vec<u8>>,
suffix: Vec<Vec<u8>>,
) -> ValkeyResult<ScanIter<'a, C, RV>> {
let spec = PageSpec { prefix, suffix };
let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(
con.glide_dispatch_command(spec.to_cmd(0)).await?,
)?;
Ok(ScanIter {
con,
spec,
cursor,
batch: batch.into_iter(),
})
}
pub async fn next_item(&mut self) -> Option<ValkeyResult<RV>> {
loop {
if let Some(v) = self.batch.next() {
return Some(Ok(v));
}
if self.cursor == 0 {
return None;
}
match self.fetch_page().await {
Ok(()) => {}
Err(e) => {
self.cursor = 0;
return Some(Err(e));
}
}
}
}
async fn fetch_page(&mut self) -> ValkeyResult<()> {
let reply = self
.con
.glide_dispatch_command(self.spec.to_cmd(self.cursor))
.await?;
let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(reply)?;
self.cursor = cursor;
self.batch = batch.into_iter();
Ok(())
}
}
#[cfg(feature = "sync")]
pub struct SyncScanIter<'a, C: ?Sized, RV> {
con: &'a C,
spec: PageSpec,
cursor: u64,
batch: std::vec::IntoIter<RV>,
}
#[cfg(feature = "sync")]
impl<'a, C: SyncCommandDispatch, RV: FromValkeyValue> SyncScanIter<'a, C, RV> {
pub(crate) fn new(
con: &'a C,
prefix: Vec<Vec<u8>>,
suffix: Vec<Vec<u8>>,
) -> ValkeyResult<SyncScanIter<'a, C, RV>> {
let spec = PageSpec { prefix, suffix };
let (cursor, batch): (u64, Vec<RV>) =
FromValkeyValue::from_owned_valkey_value(con.glide_dispatch_command(spec.to_cmd(0))?)?;
Ok(SyncScanIter {
con,
spec,
cursor,
batch: batch.into_iter(),
})
}
fn fetch_page(&mut self) -> ValkeyResult<()> {
let reply = self
.con
.glide_dispatch_command(self.spec.to_cmd(self.cursor))?;
let (cursor, batch): (u64, Vec<RV>) = FromValkeyValue::from_owned_valkey_value(reply)?;
self.cursor = cursor;
self.batch = batch.into_iter();
Ok(())
}
}
#[cfg(feature = "sync")]
impl<C: SyncCommandDispatch, RV: FromValkeyValue> Iterator for SyncScanIter<'_, C, RV> {
type Item = ValkeyResult<RV>;
fn next(&mut self) -> Option<ValkeyResult<RV>> {
loop {
if let Some(v) = self.batch.next() {
return Some(Ok(v));
}
if self.cursor == 0 {
return None;
}
match self.fetch_page() {
Ok(()) => {}
Err(e) => {
self.cursor = 0;
return Some(Err(e));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::GlideError;
use crate::value::ValkeyValue;
use crate::{ValkeyFuture, ValkeyResult};
use bytes::Bytes;
use std::collections::VecDeque;
use std::sync::Mutex;
struct MockConnection {
replies: Mutex<VecDeque<ValkeyResult<ValkeyValue>>>,
}
impl MockConnection {
fn new(replies: Vec<ValkeyResult<ValkeyValue>>) -> Self {
MockConnection {
replies: Mutex::new(replies.into()),
}
}
fn pop(&self) -> ValkeyResult<ValkeyValue> {
self.replies
.lock()
.unwrap()
.pop_front()
.expect("scan iterator requested more pages than the test queued")
}
fn remaining(&self) -> usize {
self.replies.lock().unwrap().len()
}
}
#[sealed::sealed]
impl crate::commands::core::CommandDispatch for MockConnection {
fn glide_dispatch_command<'a>(&'a self, _cmd: Cmd) -> ValkeyFuture<'a, ValkeyValue> {
Box::pin(async move { self.pop() })
}
}
#[cfg(feature = "sync")]
#[sealed::sealed]
impl crate::commands::core::SyncCommandDispatch for MockConnection {
fn glide_dispatch_command(&self, _cmd: Cmd) -> ValkeyResult<ValkeyValue> {
self.pop()
}
}
fn page(cursor: &str, items: &[&str]) -> ValkeyValue {
ValkeyValue::Array(vec![
ValkeyValue::BulkString(Bytes::copy_from_slice(cursor.as_bytes())),
ValkeyValue::Array(
items
.iter()
.map(|s| ValkeyValue::BulkString(Bytes::copy_from_slice(s.as_bytes())))
.collect(),
),
])
}
fn error() -> GlideError {
GlideError::Request("simulated scan failure".into())
}
async fn scan_iter(con: &MockConnection) -> ValkeyResult<ScanIter<'_, MockConnection, String>> {
ScanIter::new(con, vec![b"SCAN".to_vec()], Vec::new()).await
}
async fn get_items(iter: &mut ScanIter<'_, MockConnection, String>) -> Vec<String> {
let mut items = Vec::new();
while let Some(item) = iter.next_item().await {
items.push(item.expect("unexpected error during scan"));
}
items
}
#[tokio::test]
async fn async_success_empty_page() {
let con = MockConnection::new(vec![Ok(page("4", &[])), Ok(page("0", &[]))]);
let mut iter = scan_iter(&con).await.unwrap();
assert_eq!(get_items(&mut iter).await, Vec::<String>::new());
assert_eq!(con.remaining(), 0);
}
#[tokio::test]
async fn async_success_one_page() {
let con = MockConnection::new(vec![Ok(page("0", &["a", "b"]))]);
let mut iter = scan_iter(&con).await.unwrap();
assert_eq!(get_items(&mut iter).await, ["a", "b"]);
assert_eq!(con.remaining(), 0);
}
#[tokio::test]
async fn async_success_multi_page() {
let con = MockConnection::new(vec![Ok(page("6", &["a", "b"])), Ok(page("0", &["c"]))]);
let mut iter = scan_iter(&con).await.unwrap();
assert_eq!(get_items(&mut iter).await, ["a", "b", "c"]);
assert_eq!(con.remaining(), 0);
}
#[tokio::test]
async fn async_error_new() {
let con = MockConnection::new(vec![Err(error())]);
assert!(scan_iter(&con).await.is_err());
}
#[tokio::test]
async fn async_error_scan() {
let con = MockConnection::new(vec![Ok(page("5", &["a"])), Err(error())]);
let mut iter = scan_iter(&con).await.unwrap();
assert_eq!(iter.next_item().await.unwrap().unwrap(), "a");
assert!(iter.next_item().await.expect("expected an error").is_err());
assert!(iter.next_item().await.is_none());
}
#[cfg(feature = "sync")]
fn sync_scan_iter(
con: &MockConnection,
) -> ValkeyResult<SyncScanIter<'_, MockConnection, String>> {
SyncScanIter::new(con, vec![b"SCAN".to_vec()], Vec::new())
}
#[cfg(feature = "sync")]
fn sync_get_items(iter: SyncScanIter<'_, MockConnection, String>) -> Vec<String> {
iter.collect::<ValkeyResult<Vec<String>>>()
.expect("unexpected error during scan")
}
#[cfg(feature = "sync")]
#[test]
fn sync_success_empty_page() {
let con = MockConnection::new(vec![Ok(page("4", &[])), Ok(page("0", &[]))]);
let items = sync_get_items(sync_scan_iter(&con).unwrap());
assert_eq!(items, Vec::<String>::new());
assert_eq!(con.remaining(), 0);
}
#[cfg(feature = "sync")]
#[test]
fn sync_success_one_page() {
let con = MockConnection::new(vec![Ok(page("0", &["a", "b"]))]);
let items = sync_get_items(sync_scan_iter(&con).unwrap());
assert_eq!(items, ["a", "b"]);
assert_eq!(con.remaining(), 0);
}
#[cfg(feature = "sync")]
#[test]
fn sync_success_multi_page() {
let con = MockConnection::new(vec![Ok(page("6", &["a", "b"])), Ok(page("0", &["c"]))]);
let items = sync_get_items(sync_scan_iter(&con).unwrap());
assert_eq!(items, ["a", "b", "c"]);
assert_eq!(con.remaining(), 0);
}
#[cfg(feature = "sync")]
#[test]
fn sync_error_new() {
let con = MockConnection::new(vec![Err(error())]);
assert!(sync_scan_iter(&con).is_err());
}
#[cfg(feature = "sync")]
#[test]
fn sync_error_scan() {
let con = MockConnection::new(vec![Ok(page("5", &["a"])), Err(error())]);
let mut iter = sync_scan_iter(&con).unwrap();
assert_eq!(iter.next().unwrap().unwrap(), "a");
assert!(iter.next().expect("expected an error").is_err());
assert!(iter.next().is_none());
}
}