use std::{
collections::VecDeque,
future::Future,
pin::Pin,
task::{Context, Poll}
};
use futures_core::Stream;
use crate::apis::Error;
pub const fn paginate<T, E, F, Fut>(page_size: i32, fetch: F) -> PageStream<T, F, Fut>
where
F: Fn(i32, i32) -> Fut + Unpin,
Fut: Future<Output = Result<Vec<T>, Error<E>>>
{
PageStream {
fetch,
page_size,
offset: 0,
buffer: VecDeque::new(),
in_flight: None,
done: false
}
}
pub struct PageStream<T, F, Fut> {
fetch: F,
page_size: i32,
offset: i32,
buffer: VecDeque<T>,
in_flight: Option<Pin<Box<Fut>>>,
done: bool
}
impl<T, E, F, Fut> Stream for PageStream<T, F, Fut>
where
T: Unpin,
F: Fn(i32, i32) -> Fut + Unpin,
Fut: Future<Output = Result<Vec<T>, Error<E>>>
{
type Item = Result<T, Error<E>>;
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let stream = self.get_mut();
loop {
if let Some(item) = stream.buffer.pop_front() {
return Poll::Ready(Some(Ok(item)));
}
if stream.done {
return Poll::Ready(None);
}
let in_flight = stream
.in_flight
.get_or_insert_with(|| Box::pin((stream.fetch)(stream.page_size, stream.offset)));
match in_flight.as_mut().poll(context) {
Poll::Pending => return Poll::Pending,
Poll::Ready(result) => {
stream.in_flight = None;
match result {
Err(error) => {
stream.done = true;
return Poll::Ready(Some(Err(error)));
}
Ok(page) => {
let received = i32::try_from(page.len()).unwrap_or(i32::MAX);
stream.offset = stream.offset.saturating_add(received);
if received < stream.page_size {
stream.done = true;
}
stream.buffer.extend(page);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use futures_util::{StreamExt, TryStreamExt};
use super::paginate;
use crate::apis::{Error, ResponseContent};
fn not_found() -> Error<()> {
Error::ResponseError(ResponseContent {
status: reqwest::StatusCode::NOT_FOUND,
content: String::new(),
entity: None
})
}
#[tokio::test]
async fn yields_every_item_across_pages_in_order() {
let requests = Mutex::new(Vec::new());
let items: Vec<i32> = paginate(3, |limit, offset| {
requests.lock().expect("lock").push((limit, offset));
let page: Vec<i32> = (offset..(offset + limit).min(8)).collect();
async move { Ok::<_, Error<()>>(page) }
})
.try_collect()
.await
.expect("all pages succeed");
assert_eq!(items, (0..8).collect::<Vec<i32>>());
assert_eq!(
*requests.lock().expect("lock"),
vec![(3, 0), (3, 3), (3, 6)]
);
}
#[tokio::test]
async fn a_full_final_page_triggers_one_trailing_empty_request() {
let requests = Mutex::new(0u32);
let items: Vec<u32> = paginate(2, |_, offset| {
*requests.lock().expect("lock") += 1;
let page: Vec<u32> = if offset < 4 { vec![1, 2] } else { Vec::new() };
async move { Ok::<_, Error<()>>(page) }
})
.try_collect()
.await
.expect("all pages succeed");
assert_eq!(items.len(), 4);
assert_eq!(*requests.lock().expect("lock"), 3);
}
#[tokio::test]
async fn an_empty_collection_finishes_immediately() {
let items: Vec<u32> = paginate(10, |_, _| async { Ok::<Vec<u32>, Error<()>>(Vec::new()) })
.try_collect()
.await
.expect("empty collection succeeds");
assert!(items.is_empty());
}
#[tokio::test]
async fn an_error_is_yielded_once_and_ends_the_stream() {
let mut stream = paginate(2, |_, offset| async move {
if offset == 0 {
Ok::<Vec<u32>, Error<()>>(vec![1, 2])
} else {
Err(not_found())
}
});
assert_eq!(
stream
.next()
.await
.expect("first item")
.expect("first page item"),
1
);
assert_eq!(
stream
.next()
.await
.expect("second item")
.expect("first page item"),
2
);
assert!(
stream
.next()
.await
.expect("third poll yields the error")
.is_err()
);
assert!(stream.next().await.is_none());
}
}