origincloud_core/
pages.rs1use std::collections::VecDeque;
2use std::fmt;
3use std::future::Future;
4use std::pin::Pin;
5use std::task::{Context, Poll};
6
7use futures_core::Stream;
8use futures_util::StreamExt as _;
9
10use crate::Error;
11
12pub struct Pages<T> {
27 stream: Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>,
28}
29
30struct State<R, F, T> {
31 request: Option<R>,
33 buffered: VecDeque<T>,
34 fetch: F,
35 set_page_token: fn(&mut R, String),
36}
37
38impl<T: Send + 'static> Pages<T> {
39 pub fn new<R, F, Fut>(request: R, fetch: F, set_page_token: fn(&mut R, String)) -> Self
44 where
45 R: Clone + Send + 'static,
46 F: Fn(R) -> Fut + Send + 'static,
47 Fut: Future<Output = Result<(Vec<T>, String), Error>> + Send + 'static,
48 {
49 let state = State {
50 request: Some(request),
51 buffered: VecDeque::new(),
52 fetch,
53 set_page_token,
54 };
55 let stream = futures_util::stream::unfold(state, |mut state| async move {
56 loop {
57 if let Some(item) = state.buffered.pop_front() {
58 return Some((Ok(item), state));
59 }
60 let mut request = state.request.take()?;
61 match (state.fetch)(request.clone()).await {
62 Ok((items, next_page_token)) => {
63 state.buffered.extend(items);
64 if !next_page_token.is_empty() {
65 (state.set_page_token)(&mut request, next_page_token);
66 state.request = Some(request);
67 }
68 }
69 Err(err) => return Some((Err(err), state)),
70 }
71 }
72 });
73 Self {
74 stream: Box::pin(stream),
75 }
76 }
77
78 #[allow(clippy::should_implement_trait)] pub async fn next(&mut self) -> Option<Result<T, Error>> {
82 self.stream.next().await
83 }
84}
85
86impl<T> Stream for Pages<T> {
87 type Item = Result<T, Error>;
88
89 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
90 self.stream.as_mut().poll_next(cx)
91 }
92}
93
94impl<T> fmt::Debug for Pages<T> {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("Pages").finish_non_exhaustive()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::error::ApiError;
104
105 #[tokio::test]
106 async fn pages_are_followed_until_the_token_is_empty() {
107 let mut pages = Pages::new(
108 String::new(),
109 |token: String| async move {
110 match token.as_str() {
111 "" => Ok((vec![1, 2], "p2".to_owned())),
112 "p2" => Ok((vec![], "p3".to_owned())),
113 "p3" => Ok((vec![3], String::new())),
114 other => panic!("token {other}"),
115 }
116 },
117 |request, token| *request = token,
118 );
119 let mut got = Vec::new();
120 while let Some(item) = pages.next().await {
121 got.push(item.unwrap());
122 }
123 assert_eq!(got, [1, 2, 3]);
124 }
125
126 #[tokio::test]
127 async fn a_failed_page_ends_the_stream_with_its_error() {
128 let mut pages = Pages::new(
129 String::new(),
130 |token: String| async move {
131 match token.as_str() {
132 "" => Ok((vec![1], "p2".to_owned())),
133 _ => Err(Error::from(ApiError::from_response(400, b""))),
134 }
135 },
136 |request, token| *request = token,
137 );
138 assert_eq!(pages.next().await.unwrap().unwrap(), 1);
139 assert!(pages.next().await.unwrap().is_err());
140 assert!(pages.next().await.is_none());
141 }
142}