1use crate::error::Result;
6use std::future::Future;
7use std::pin::Pin;
8
9#[derive(Debug, Clone)]
11pub struct EdgePage<T, C> {
12 pub nodes: Vec<T>,
14 pub next_cursor: Option<C>,
16}
17
18pub 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
33pub type BoxFutureResult<'a, R> = Pin<Box<dyn Future<Output = Result<R>> + 'a>>;
35
36pub type BoxFetch<'a, C, R> = Box<dyn FnMut(Option<C>) -> BoxFutureResult<'a, R> + 'a>;
38
39pub type BoxExtract<'a, T, C, R> = Box<dyn FnMut(R) -> Result<EdgePage<T, C>> + 'a>;
41
42pub 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 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 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 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}