1use serde::Deserialize;
20use serde_json::Value;
21
22use crate::error::Result;
23use crate::transport::WireRequest;
24
25pub const CURSOR_PARAM: &str = "cursor";
27
28pub const LIMIT_PARAM: &str = "limit";
30
31#[derive(Debug, Clone, Deserialize)]
33pub struct Page<T> {
34 #[serde(default = "Vec::new")]
36 pub items: Vec<T>,
37 #[serde(default)]
39 pub next_cursor: Option<String>,
40}
41
42impl<T> Page<T> {
43 #[must_use]
48 pub fn next(&self) -> Option<&str> {
49 self.next_cursor
50 .as_deref()
51 .filter(|cursor| !cursor.is_empty())
52 }
53}
54
55impl<T> Default for Page<T> {
56 fn default() -> Self {
57 Self {
58 items: Vec::new(),
59 next_cursor: None,
60 }
61 }
62}
63
64#[must_use]
70pub fn more_pages(document: &Value) -> bool {
71 document
72 .get("next_cursor")
73 .and_then(Value::as_str)
74 .is_some_and(|cursor| !cursor.is_empty())
75}
76
77pub fn set_cursor(request: &mut WireRequest<'_>, cursor: Option<&str>) {
79 request.query.retain(|(name, _)| name != CURSOR_PARAM);
80 if let Some(cursor) = cursor {
81 request
82 .query
83 .push((CURSOR_PARAM.to_owned(), cursor.to_owned()));
84 }
85}
86
87pub async fn walk<T, F, Fut>(mut fetch: F) -> Result<Vec<T>>
95where
96 F: FnMut(Option<String>) -> Fut,
97 Fut: Future<Output = Result<Page<T>>>,
98{
99 let mut items = Vec::new();
100 let mut cursor: Option<String> = None;
101 let mut seen: Vec<String> = Vec::new();
102
103 loop {
104 let mut page = fetch(cursor.clone()).await?;
105 items.append(&mut page.items);
106 match page.next() {
107 Some(next) if !seen.iter().any(|prior| prior == next) => {
108 seen.push(next.to_owned());
109 cursor = Some(next.to_owned());
110 }
111 _ => return Ok(items),
112 }
113 }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
118mod tests {
119 use super::*;
120 use std::cell::RefCell;
121
122 fn parse(body: &str) -> Page<i32> {
124 serde_json::from_str(body).unwrap()
125 }
126
127 #[test]
128 fn an_empty_cursor_is_the_end_of_the_listing_not_a_fourth_page() {
129 for raw in [
131 r#"{"items":[]}"#,
132 r#"{"items":[],"next_cursor":null}"#,
133 r#"{"items":[],"next_cursor":""}"#,
134 ] {
135 let page: Page<Value> = serde_json::from_str(raw).unwrap();
136 assert_eq!(page.next(), None, "{raw}");
137 }
138 let page: Page<Value> = serde_json::from_str(r#"{"items":[],"next_cursor":"c1"}"#).unwrap();
139 assert_eq!(page.next(), Some("c1"));
140 }
141
142 #[test]
143 fn a_raw_document_reads_the_same_way_as_a_decoded_page() {
144 assert!(!more_pages(&serde_json::json!({"items": []})));
147 assert!(!more_pages(
148 &serde_json::json!({"items": [], "next_cursor": null})
149 ));
150 assert!(!more_pages(
151 &serde_json::json!({"items": [], "next_cursor": ""})
152 ));
153 assert!(more_pages(
154 &serde_json::json!({"items": [], "next_cursor": "c1"})
155 ));
156 }
157
158 #[tokio::test]
159 async fn a_walk_follows_cursors_to_the_end() {
160 let pages = [
161 (None, r#"{"items":[1,2],"next_cursor":"c1"}"#),
162 (Some("c1"), r#"{"items":[3],"next_cursor":"c2"}"#),
163 (Some("c2"), r#"{"items":[4],"next_cursor":null}"#),
164 ];
165 let asked: RefCell<Vec<Option<String>>> = RefCell::new(Vec::new());
166
167 let fetch = |cursor: Option<String>| {
168 asked.borrow_mut().push(cursor.clone());
169 let page = pages
170 .iter()
171 .find(|(want, _)| want.map(ToOwned::to_owned) == cursor)
172 .map(|(_, body)| parse(body))
173 .unwrap();
174 std::future::ready(Ok(page))
175 };
176 let items: Vec<i32> = walk(fetch).await.unwrap();
177
178 assert_eq!(items, vec![1, 2, 3, 4]);
179 assert_eq!(
180 asked.into_inner(),
181 vec![None, Some("c1".to_owned()), Some("c2".to_owned())],
182 );
183 }
184
185 #[tokio::test]
186 async fn a_repeated_cursor_stops_the_walk_rather_than_hanging() {
187 let calls = RefCell::new(0_usize);
191 let fetch = |_| {
192 *calls.borrow_mut() += 1;
193 std::future::ready(Ok(parse(r#"{"items":[1],"next_cursor":"stuck"}"#)))
194 };
195 let items: Vec<i32> = walk(fetch).await.unwrap();
196
197 assert_eq!(*calls.borrow(), 2, "the repeat must end the walk");
198 assert_eq!(items, vec![1, 1]);
199 }
200
201 #[test]
202 fn setting_a_cursor_replaces_rather_than_appends() {
203 let mut request = WireRequest {
206 method: "GET",
207 path: "/v1/sessions",
208 query: vec![
209 ("limit".to_owned(), "25".to_owned()),
210 (CURSOR_PARAM.to_owned(), "old".to_owned()),
211 ],
212 ..Default::default()
213 };
214 set_cursor(&mut request, Some("new"));
215 assert_eq!(
216 request.query,
217 vec![
218 ("limit".to_owned(), "25".to_owned()),
219 (CURSOR_PARAM.to_owned(), "new".to_owned()),
220 ],
221 );
222 set_cursor(&mut request, None);
223 assert_eq!(request.query, vec![("limit".to_owned(), "25".to_owned())]);
224 }
225}