Skip to main content

timeweb_rs/
pagination.rs

1//! Offset pagination as a [`Stream`] over the generated list operations.
2
3use 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
14/// Streams every item of a paginated collection.
15///
16/// The closure receives `(limit, offset)` for each page and returns the
17/// page's items; the stream yields them one by one, requesting the next
18/// page on demand and finishing when a page comes back shorter than
19/// `page_size`. The generated list operations take `Option<i32>` for both
20/// parameters, so a closure usually just forwards them:
21///
22/// ```no_run
23/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
24/// use futures_util::TryStreamExt;
25/// use timeweb_rs::{apis::servers_api, paginate};
26///
27/// let config = timeweb_rs::authenticated("your-jwt-token");
28/// let mut servers = paginate(100, |limit, offset| {
29///     let config = &config;
30///     async move {
31///         let page = servers_api::get_servers(config, Some(limit), Some(offset)).await?;
32///         Ok(page.servers)
33///     }
34/// });
35/// while let Some(server) = servers.try_next().await? {
36///     println!("{}", server.name);
37/// }
38/// # Ok(())
39/// # }
40/// ```
41pub 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
56/// Stream created by [`paginate`]; yields `Result<T, Error<E>>` items.
57pub 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}