1use std::ops::Deref;
2
3use crate::http::Method;
4use serde_json::Value;
5use url::Url;
6
7use crate::client::{Client, Response};
8use crate::error::Error;
9use crate::observability::OperationInfo;
10use crate::operation::Operation;
11use crate::route::Route;
12use crate::security::is_same_origin;
13
14#[derive(Debug, Clone)]
19pub struct Page<T> {
20 value: T,
21 next_url: Option<Url>,
22 next_cursor: Option<String>,
23 total_count: Option<u64>,
24 info: OperationInfo,
27 route: Option<&'static Route>,
30}
31
32impl<T> Page<T> {
33 pub(crate) fn new(
34 value: T,
35 response: &Response,
36 info: OperationInfo,
37 route: Option<&'static Route>,
38 ) -> Page<T> {
39 let next_url = response
40 .headers
41 .get("link")
42 .and_then(|value| value.to_str().ok())
43 .and_then(next_link)
44 .and_then(|target| response.url.join(&target).ok());
45 let next_cursor = next_url.as_ref().and_then(|url| {
46 url.query_pairs()
47 .find(|(name, _)| name == "page")
48 .map(|(_, value)| value.into_owned())
49 });
50 let total_count = response
51 .headers
52 .get("x-total-count")
53 .and_then(|value| value.to_str().ok())
54 .and_then(|value| value.trim().parse().ok());
55 Page {
56 value,
57 next_url,
58 next_cursor,
59 total_count,
60 info,
61 route,
62 }
63 }
64
65 pub(crate) fn info(&self) -> &OperationInfo {
66 &self.info
67 }
68
69 pub(crate) fn route(&self) -> Option<&'static Route> {
70 self.route
71 }
72
73 pub fn into_inner(self) -> T {
75 self.value
76 }
77
78 pub fn value(&self) -> &T {
80 &self.value
81 }
82
83 pub fn next_page(&self) -> Option<&str> {
85 self.next_cursor.as_deref()
86 }
87
88 pub fn next_url(&self) -> Option<&Url> {
90 self.next_url.as_ref()
91 }
92
93 pub fn has_next(&self) -> bool {
95 self.next_url.is_some()
96 }
97
98 pub fn total_count(&self) -> Option<u64> {
100 self.total_count
101 }
102
103 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Page<U> {
106 Page {
107 value: f(self.value),
108 next_url: self.next_url,
109 next_cursor: self.next_cursor,
110 total_count: self.total_count,
111 info: self.info,
112 route: self.route,
113 }
114 }
115}
116
117impl<T> Deref for Page<T> {
118 type Target = T;
119
120 fn deref(&self) -> &T {
121 &self.value
122 }
123}
124
125impl Client {
126 pub async fn get_all(&self, path: &str) -> Result<Vec<Value>, Error> {
133 self.get_all_with_limit(path, 0).await
134 }
135
136 pub async fn get_all_with_limit(&self, path: &str, limit: usize) -> Result<Vec<Value>, Error> {
139 self.within_limit(Box::pin(async move {
140 let mut operation = self.raw(Method::GET, path)?;
141 let started_at = self.url_for(&operation)?;
142 let mut collected: Vec<Value> = Vec::new();
143 let mut pages = 0;
144
145 loop {
146 let response = self.execute(operation).await?;
147 collected.extend(response.json::<Vec<Value>>()?);
148 pages += 1;
149
150 if limit > 0 && collected.len() >= limit {
151 collected.truncate(limit);
152 break;
153 }
154 match next_page_url(&response, &started_at)? {
155 Some(next) if pages < self.max_pages() => {
156 operation = Operation::at(Method::GET, next);
157 }
158 Some(_) => return Err(Error::pagination_capped(self.max_pages())),
159 None => break,
160 }
161 }
162 Ok(collected)
163 }))
164 .await
165 }
166
167 pub async fn follow_pagination(
175 &self,
176 first: &Response,
177 first_page_count: usize,
178 limit: usize,
179 ) -> Result<Vec<Value>, Error> {
180 self.within_limit(Box::pin(async move {
181 if limit > 0 && first_page_count >= limit {
182 return Ok(Vec::new());
183 }
184
185 let started_at = first.url.clone();
186 let mut next = next_page_url(first, &started_at)?;
187 let mut collected: Vec<Value> = Vec::new();
188 let mut count = first_page_count;
189 let mut pages = 1;
190
191 while let Some(url) = next {
192 if pages >= self.max_pages() {
193 return Err(Error::pagination_capped(self.max_pages()));
194 }
195 let response = self.execute(Operation::at(Method::GET, url)).await?;
196 let items: Vec<Value> = response.json()?;
197 count += items.len();
198 collected.extend(items);
199 pages += 1;
200
201 if limit > 0 && count >= limit {
202 collected.truncate(collected.len().saturating_sub(count - limit));
203 break;
204 }
205 next = next_page_url(&response, &started_at)?;
206 }
207 Ok(collected)
208 }))
209 .await
210 }
211}
212
213fn next_page_url(response: &Response, started_at: &Url) -> Result<Option<Url>, Error> {
218 match response.header("link").and_then(next_link) {
219 None => Ok(None),
220 Some(target) => {
221 let next = response.url.join(&target)?;
222 if is_same_origin(&next, started_at) {
223 Ok(Some(next))
224 } else {
225 Err(Error::usage(format!(
226 "pagination Link header points to a different origin: {next}"
227 )))
228 }
229 }
230 }
231}
232
233pub fn next_link(header: &str) -> Option<String> {
237 let mut remaining = header;
238 while let Some(start) = remaining.find('<') {
239 let after_start = &remaining[start + 1..];
240 let end = after_start.find('>')?;
241 let target = &after_start[..end];
242 let rest = &after_start[end + 1..];
243 let params_end = rest.find('<').unwrap_or(rest.len());
244 if link_is_next(&rest[..params_end]) {
245 return Some(target.to_string());
246 }
247 remaining = &rest[params_end..];
248 }
249 None
250}
251
252fn link_is_next(params: &str) -> bool {
253 params.split(';').any(|param| {
254 let mut parts = param.splitn(2, '=');
255 let name = parts.next().unwrap_or_default().trim();
256 let value = parts.next().unwrap_or_default().trim().trim_matches('"');
257 name.eq_ignore_ascii_case("rel")
258 && value
259 .split_whitespace()
260 .any(|rel| rel.eq_ignore_ascii_case("next"))
261 })
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn finds_the_next_link_among_others() {
270 let header = r#"<https://app.hey.com/imbox.json?page=a,b>; rel="prev", <https://app.hey.com/imbox.json?page=c>; rel="next""#;
271 assert_eq!(
272 next_link(header).as_deref(),
273 Some("https://app.hey.com/imbox.json?page=c")
274 );
275 }
276
277 #[test]
278 fn matches_rel_sets_and_case() {
279 assert_eq!(
280 next_link(r#"</x?page=2>; REL="prev next""#).as_deref(),
281 Some("/x?page=2")
282 );
283 assert_eq!(next_link(r#"</x?page=2>; rel="last""#), None);
284 assert_eq!(next_link(""), None);
285 }
286}