use std::collections::VecDeque;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::Stream;
use futures_util::StreamExt as _;
use crate::Error;
pub struct Pages<T> {
stream: Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>,
}
struct State<R, F, T> {
request: Option<R>,
buffered: VecDeque<T>,
fetch: F,
set_page_token: fn(&mut R, String),
}
impl<T: Send + 'static> Pages<T> {
pub fn new<R, F, Fut>(request: R, fetch: F, set_page_token: fn(&mut R, String)) -> Self
where
R: Clone + Send + 'static,
F: Fn(R) -> Fut + Send + 'static,
Fut: Future<Output = Result<(Vec<T>, String), Error>> + Send + 'static,
{
let state = State {
request: Some(request),
buffered: VecDeque::new(),
fetch,
set_page_token,
};
let stream = futures_util::stream::unfold(state, |mut state| async move {
loop {
if let Some(item) = state.buffered.pop_front() {
return Some((Ok(item), state));
}
let mut request = state.request.take()?;
match (state.fetch)(request.clone()).await {
Ok((items, next_page_token)) => {
state.buffered.extend(items);
if !next_page_token.is_empty() {
(state.set_page_token)(&mut request, next_page_token);
state.request = Some(request);
}
}
Err(err) => return Some((Err(err), state)),
}
}
});
Self {
stream: Box::pin(stream),
}
}
#[allow(clippy::should_implement_trait)] pub async fn next(&mut self) -> Option<Result<T, Error>> {
self.stream.next().await
}
}
impl<T> Stream for Pages<T> {
type Item = Result<T, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.stream.as_mut().poll_next(cx)
}
}
impl<T> fmt::Debug for Pages<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pages").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ApiError;
#[tokio::test]
async fn pages_are_followed_until_the_token_is_empty() {
let mut pages = Pages::new(
String::new(),
|token: String| async move {
match token.as_str() {
"" => Ok((vec![1, 2], "p2".to_owned())),
"p2" => Ok((vec![], "p3".to_owned())),
"p3" => Ok((vec![3], String::new())),
other => panic!("token {other}"),
}
},
|request, token| *request = token,
);
let mut got = Vec::new();
while let Some(item) = pages.next().await {
got.push(item.unwrap());
}
assert_eq!(got, [1, 2, 3]);
}
#[tokio::test]
async fn a_failed_page_ends_the_stream_with_its_error() {
let mut pages = Pages::new(
String::new(),
|token: String| async move {
match token.as_str() {
"" => Ok((vec![1], "p2".to_owned())),
_ => Err(Error::from(ApiError::from_response(400, b""))),
}
},
|request, token| *request = token,
);
assert_eq!(pages.next().await.unwrap().unwrap(), 1);
assert!(pages.next().await.unwrap().is_err());
assert!(pages.next().await.is_none());
}
}