Skip to main content

infrahub/
pagination.rs

1//! pagination helpers
2//!
3//! generic paginator for connection-style graphql results.
4
5use crate::error::Result;
6use std::future::Future;
7use std::pin::Pin;
8
9/// a single page of connection results
10#[derive(Debug, Clone)]
11pub struct EdgePage<T, C> {
12    /// node payloads for this page
13    pub nodes: Vec<T>,
14    /// next cursor (if any)
15    pub next_cursor: Option<C>,
16}
17
18/// generic paginator for connection-style data
19pub struct Paginator<T, C, R, Fetch, Fut, Extract>
20where
21    C: Clone,
22    Fetch: FnMut(Option<C>) -> Fut,
23    Fut: Future<Output = Result<R>>,
24    Extract: FnMut(R) -> Result<EdgePage<T, C>>,
25{
26    fetch: Fetch,
27    extract: Extract,
28    cursor: Option<C>,
29    done: bool,
30    _phantom: std::marker::PhantomData<(T, R)>,
31}
32
33/// boxed future used by [`DynPaginator`]
34pub type BoxFutureResult<'a, R> = Pin<Box<dyn Future<Output = Result<R>> + 'a>>;
35
36/// boxed fetch callback used by [`DynPaginator`]
37pub type BoxFetch<'a, C, R> = Box<dyn FnMut(Option<C>) -> BoxFutureResult<'a, R> + 'a>;
38
39/// boxed extract callback used by [`DynPaginator`]
40pub type BoxExtract<'a, T, C, R> = Box<dyn FnMut(R) -> Result<EdgePage<T, C>> + 'a>;
41
42/// type-erased paginator for ergonomic API surfaces that cannot expose closure types.
43pub type DynPaginator<'a, T, C, R> =
44    Paginator<T, C, R, BoxFetch<'a, C, R>, BoxFutureResult<'a, R>, BoxExtract<'a, T, C, R>>;
45
46impl<T, C, R, Fetch, Fut, Extract> Paginator<T, C, R, Fetch, Fut, Extract>
47where
48    C: Clone,
49    Fetch: FnMut(Option<C>) -> Fut,
50    Fut: Future<Output = Result<R>>,
51    Extract: FnMut(R) -> Result<EdgePage<T, C>>,
52{
53    /// create a new paginator
54    pub fn new(fetch: Fetch, extract: Extract) -> Self {
55        Self {
56            fetch,
57            extract,
58            cursor: None,
59            done: false,
60            _phantom: std::marker::PhantomData,
61        }
62    }
63
64    /// fetch the next page of results
65    pub async fn next_page(&mut self) -> Result<Option<Vec<T>>> {
66        if self.done {
67            return Ok(None);
68        }
69
70        let response = (self.fetch)(self.cursor.clone()).await?;
71        let page = (self.extract)(response)?;
72        self.cursor = page.next_cursor.clone();
73        if self.cursor.is_none() {
74            self.done = true;
75        }
76
77        Ok(Some(page.nodes))
78    }
79
80    /// fetch all pages and return a single collection
81    pub async fn collect_all(mut self) -> Result<Vec<T>> {
82        let mut items = Vec::new();
83        while let Some(page) = self.next_page().await? {
84            items.extend(page);
85        }
86        Ok(items)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::sync::{Arc, Mutex};
94
95    #[cfg_attr(miri, ignore)]
96    #[tokio::test]
97    async fn test_pagination_collect_all() {
98        let state: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
99        let state_fetch = state.clone();
100
101        let fetch = move |cursor: Option<String>| {
102            let state = state_fetch.clone();
103            async move {
104                let mut count = state.lock().unwrap();
105                *count += 1;
106                if cursor.is_none() {
107                    Ok(EdgePage {
108                        nodes: vec![1, 2],
109                        next_cursor: Some("next".to_string()),
110                    })
111                } else {
112                    Ok(EdgePage {
113                        nodes: vec![3],
114                        next_cursor: None,
115                    })
116                }
117            }
118        };
119
120        let extract = |page: EdgePage<i32, String>| Ok(page);
121
122        let paginator = Paginator::new(fetch, extract);
123        let items = paginator.collect_all().await.unwrap();
124        assert_eq!(items, vec![1, 2, 3]);
125        assert_eq!(*state.lock().unwrap(), 2);
126    }
127
128    #[cfg_attr(miri, ignore)]
129    #[tokio::test]
130    async fn test_next_page_multi_page() {
131        let pages = vec![
132            EdgePage {
133                nodes: vec![1, 2],
134                next_cursor: Some("cur1".to_string()),
135            },
136            EdgePage {
137                nodes: vec![3],
138                next_cursor: Some("cur2".to_string()),
139            },
140            EdgePage {
141                nodes: vec![4, 5],
142                next_cursor: None,
143            },
144        ];
145        let pages = Arc::new(Mutex::new(pages.into_iter()));
146
147        let pages_fetch = pages.clone();
148        let fetch = move |_cursor: Option<String>| {
149            let pages = pages_fetch.clone();
150            async move {
151                let page = pages.lock().unwrap().next().unwrap();
152                Ok(page)
153            }
154        };
155        let extract = |page: EdgePage<i32, String>| Ok(page);
156
157        let mut paginator = Paginator::new(fetch, extract);
158
159        let p1 = paginator.next_page().await.unwrap().unwrap();
160        assert_eq!(p1, vec![1, 2]);
161
162        let p2 = paginator.next_page().await.unwrap().unwrap();
163        assert_eq!(p2, vec![3]);
164
165        let p3 = paginator.next_page().await.unwrap().unwrap();
166        assert_eq!(p3, vec![4, 5]);
167
168        assert!(paginator.next_page().await.unwrap().is_none());
169    }
170
171    #[cfg_attr(miri, ignore)]
172    #[tokio::test]
173    async fn test_next_page_empty_first_page() {
174        let fetch = |_: Option<()>| async {
175            Ok(EdgePage::<i32, ()> {
176                nodes: vec![],
177                next_cursor: None,
178            })
179        };
180        let extract = |page: EdgePage<i32, ()>| Ok(page);
181
182        let mut paginator = Paginator::new(fetch, extract);
183        let page = paginator.next_page().await.unwrap().unwrap();
184        assert!(page.is_empty());
185        assert!(paginator.next_page().await.unwrap().is_none());
186    }
187
188    #[cfg_attr(miri, ignore)]
189    #[tokio::test]
190    async fn test_pagination_next_page_done() {
191        let fetch = |_: Option<()>| async {
192            Ok(EdgePage::<i32, ()> {
193                nodes: vec![42],
194                next_cursor: None,
195            })
196        };
197        let extract = |page: EdgePage<i32, ()>| Ok(page);
198
199        let mut paginator = Paginator::new(fetch, extract);
200        let page = paginator.next_page().await.unwrap();
201        assert_eq!(page.unwrap(), vec![42]);
202        let none = paginator.next_page().await.unwrap();
203        assert!(none.is_none());
204    }
205}