1use std::{
4 collections::VecDeque,
5 future::Future,
6 pin::Pin,
7 task::{Context, Poll}
8};
9
10use futures_core::Stream;
11
12use crate::apis::Error;
13
14pub const fn paginate<T, E, F, Fut>(page_size: i32, fetch: F) -> PageStream<T, F, Fut>
42where
43 F: Fn(i32, i32) -> Fut + Unpin,
44 Fut: Future<Output = Result<Vec<T>, Error<E>>>
45{
46 PageStream {
47 fetch,
48 page_size,
49 offset: 0,
50 buffer: VecDeque::new(),
51 in_flight: None,
52 done: false
53 }
54}
55
56pub struct PageStream<T, F, Fut> {
58 fetch: F,
59 page_size: i32,
60 offset: i32,
61 buffer: VecDeque<T>,
62 in_flight: Option<Pin<Box<Fut>>>,
63 done: bool
64}
65
66impl<T, E, F, Fut> Stream for PageStream<T, F, Fut>
67where
68 T: Unpin,
69 F: Fn(i32, i32) -> Fut + Unpin,
70 Fut: Future<Output = Result<Vec<T>, Error<E>>>
71{
72 type Item = Result<T, Error<E>>;
73
74 fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
75 let stream = self.get_mut();
76 loop {
77 if let Some(item) = stream.buffer.pop_front() {
78 return Poll::Ready(Some(Ok(item)));
79 }
80 if stream.done {
81 return Poll::Ready(None);
82 }
83 let in_flight = stream
84 .in_flight
85 .get_or_insert_with(|| Box::pin((stream.fetch)(stream.page_size, stream.offset)));
86 match in_flight.as_mut().poll(context) {
87 Poll::Pending => return Poll::Pending,
88 Poll::Ready(result) => {
89 stream.in_flight = None;
90 match result {
91 Err(error) => {
92 stream.done = true;
93 return Poll::Ready(Some(Err(error)));
94 }
95 Ok(page) => {
96 let received = i32::try_from(page.len()).unwrap_or(i32::MAX);
97 stream.offset = stream.offset.saturating_add(received);
98 if received < stream.page_size {
99 stream.done = true;
100 }
101 stream.buffer.extend(page);
102 }
103 }
104 }
105 }
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::sync::Mutex;
113
114 use futures_util::{StreamExt, TryStreamExt};
115
116 use super::paginate;
117 use crate::apis::{Error, ResponseContent};
118
119 fn not_found() -> Error<()> {
120 Error::ResponseError(ResponseContent {
121 status: reqwest::StatusCode::NOT_FOUND,
122 content: String::new(),
123 entity: None
124 })
125 }
126
127 #[tokio::test]
128 async fn yields_every_item_across_pages_in_order() {
129 let requests = Mutex::new(Vec::new());
130 let items: Vec<i32> = paginate(3, |limit, offset| {
131 requests.lock().expect("lock").push((limit, offset));
132 let page: Vec<i32> = (offset..(offset + limit).min(8)).collect();
133 async move { Ok::<_, Error<()>>(page) }
134 })
135 .try_collect()
136 .await
137 .expect("all pages succeed");
138
139 assert_eq!(items, (0..8).collect::<Vec<i32>>());
140 assert_eq!(
141 *requests.lock().expect("lock"),
142 vec![(3, 0), (3, 3), (3, 6)]
143 );
144 }
145
146 #[tokio::test]
147 async fn a_full_final_page_triggers_one_trailing_empty_request() {
148 let requests = Mutex::new(0u32);
149 let items: Vec<u32> = paginate(2, |_, offset| {
150 *requests.lock().expect("lock") += 1;
151 let page: Vec<u32> = if offset < 4 { vec![1, 2] } else { Vec::new() };
152 async move { Ok::<_, Error<()>>(page) }
153 })
154 .try_collect()
155 .await
156 .expect("all pages succeed");
157
158 assert_eq!(items.len(), 4);
159 assert_eq!(*requests.lock().expect("lock"), 3);
160 }
161
162 #[tokio::test]
163 async fn an_empty_collection_finishes_immediately() {
164 let items: Vec<u32> = paginate(10, |_, _| async { Ok::<Vec<u32>, Error<()>>(Vec::new()) })
165 .try_collect()
166 .await
167 .expect("empty collection succeeds");
168 assert!(items.is_empty());
169 }
170
171 #[tokio::test]
172 async fn an_error_is_yielded_once_and_ends_the_stream() {
173 let mut stream = paginate(2, |_, offset| async move {
174 if offset == 0 {
175 Ok::<Vec<u32>, Error<()>>(vec![1, 2])
176 } else {
177 Err(not_found())
178 }
179 });
180
181 assert_eq!(
182 stream
183 .next()
184 .await
185 .expect("first item")
186 .expect("first page item"),
187 1
188 );
189 assert_eq!(
190 stream
191 .next()
192 .await
193 .expect("second item")
194 .expect("first page item"),
195 2
196 );
197 assert!(
198 stream
199 .next()
200 .await
201 .expect("third poll yields the error")
202 .is_err()
203 );
204 assert!(stream.next().await.is_none());
205 }
206}