Skip to main content

paginator_utils/
response.rs

1use crate::cursor::{Cursor, CursorDirection, KeysetPlan};
2use crate::params::PaginationParams;
3use serde::{Deserialize, Serialize};
4
5#[derive(Serialize, Deserialize, Debug)]
6pub struct PaginatorResponse<T> {
7    pub data: Vec<T>,
8    pub meta: PaginatorResponseMeta,
9}
10
11impl<T: Serialize> PaginatorResponse<T> {
12    /// Attach `next_cursor`/`prev_cursor` keyed on `field` to an offset page, so a
13    /// client can switch from page numbers to keyset pagination.
14    ///
15    /// This is the usual way to hand out the first cursor: serve page 1 with
16    /// offset pagination, call `with_cursors("id")`, and let the client follow
17    /// `next_cursor` from there. Cursors already present on the response are kept.
18    /// A cursor is only attached when the corresponding side has more rows
19    /// (`has_next`/`has_prev`), the page is not empty, and `field` can be read
20    /// from the serialized boundary row.
21    pub fn with_cursors(mut self, field: &str) -> Self {
22        if self.meta.has_next && self.meta.next_cursor.is_none() {
23            self.meta.next_cursor = self
24                .data
25                .last()
26                .and_then(|row| Cursor::from_row(field, row, CursorDirection::After).ok())
27                .and_then(|c| c.encode().ok());
28        }
29        if self.meta.has_prev && self.meta.prev_cursor.is_none() {
30            self.meta.prev_cursor = self
31                .data
32                .first()
33                .and_then(|row| Cursor::from_row(field, row, CursorDirection::Before).ok())
34                .and_then(|c| c.encode().ok());
35        }
36        self
37    }
38}
39
40#[derive(Serialize, Deserialize, Debug)]
41pub struct PaginatorResponseMeta {
42    pub page: u32,
43    pub per_page: u32,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub total: Option<u32>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub total_pages: Option<u32>,
48    pub has_next: bool,
49    pub has_prev: bool,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub next_cursor: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub prev_cursor: Option<String>,
54}
55
56fn total_pages_for(total: u32, per_page: u32) -> u32 {
57    (total as f32 / per_page as f32).ceil() as u32
58}
59
60impl PaginatorResponseMeta {
61    pub fn new(page: u32, per_page: u32, total: u32) -> Self {
62        let total_pages = total_pages_for(total, per_page);
63        Self {
64            page,
65            per_page,
66            total: Some(total),
67            total_pages: Some(total_pages),
68            has_next: page < total_pages,
69            has_prev: page > 1,
70            next_cursor: None,
71            prev_cursor: None,
72        }
73    }
74
75    pub fn new_without_total(page: u32, per_page: u32, has_next: bool) -> Self {
76        Self {
77            page,
78            per_page,
79            total: None,
80            total_pages: None,
81            has_next,
82            has_prev: page > 1,
83            next_cursor: None,
84            prev_cursor: None,
85        }
86    }
87
88    pub fn new_with_cursors(
89        page: u32,
90        per_page: u32,
91        total: Option<u32>,
92        has_next: bool,
93        next_cursor: Option<String>,
94        prev_cursor: Option<String>,
95    ) -> Self {
96        let total_pages = total.map(|t| total_pages_for(t, per_page));
97        Self {
98            page,
99            per_page,
100            total,
101            total_pages,
102            has_next,
103            has_prev: page > 1 || prev_cursor.is_some(),
104            next_cursor,
105            prev_cursor,
106        }
107    }
108
109    /// Build the metadata for a keyset page and normalize its rows.
110    ///
111    /// `rows` must be what the cursor query returned with `LIMIT per_page + 1`, in
112    /// query order (see [`KeysetPlan::query_sort`]). The overflow row, if any, is
113    /// dropped and a `Before` page is flipped back into the caller's sort order.
114    ///
115    /// `has_next`/`has_prev` describe the side of the page that was probed: an
116    /// `After` page knows whether more rows follow and always reports rows behind
117    /// it, and a `Before` page the reverse. `next_cursor`/`prev_cursor` are derived
118    /// from the last and first row and are only set when that side has more rows,
119    /// the page is not empty, and the cursor field can be read from the serialized
120    /// row (see [`Cursor::value_from_row`]).
121    pub fn from_cursor_page<T: Serialize>(
122        rows: &mut Vec<T>,
123        params: &PaginationParams,
124        plan: &KeysetPlan<'_>,
125        total: Option<u32>,
126    ) -> Self {
127        let per_page = params.per_page as usize;
128        let has_more = rows.len() > per_page;
129        rows.truncate(per_page);
130        if plan.reverse_rows() {
131            rows.reverse();
132        }
133
134        let (has_next, has_prev) = match plan.cursor.direction {
135            CursorDirection::After => (has_more, true),
136            CursorDirection::Before => (true, has_more),
137        };
138
139        let encode = |row: &T, direction: CursorDirection| {
140            plan.cursor
141                .at_row(row, direction)
142                .ok()
143                .and_then(|c| c.encode().ok())
144        };
145        let next_cursor = if has_next {
146            rows.last()
147                .and_then(|row| encode(row, CursorDirection::After))
148        } else {
149            None
150        };
151        let prev_cursor = if has_prev {
152            rows.first()
153                .and_then(|row| encode(row, CursorDirection::Before))
154        } else {
155            None
156        };
157
158        Self {
159            page: params.page,
160            per_page: params.per_page,
161            total,
162            total_pages: total.map(|t| total_pages_for(t, params.per_page)),
163            has_next,
164            has_prev,
165            next_cursor,
166            prev_cursor,
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::cursor::{Cursor, CursorValue};
175    use crate::params::SortDirection;
176
177    #[derive(Serialize, Debug, PartialEq, Clone)]
178    struct Row {
179        id: i64,
180    }
181
182    fn rows(ids: &[i64]) -> Vec<Row> {
183        ids.iter().map(|&id| Row { id }).collect()
184    }
185
186    fn ids(rows: &[Row]) -> Vec<i64> {
187        rows.iter().map(|r| r.id).collect()
188    }
189
190    fn decoded(encoded: &Option<String>) -> Option<(i64, CursorDirection)> {
191        encoded.as_ref().map(|e| {
192            let c = Cursor::decode(e).unwrap();
193            match c.value {
194                CursorValue::Int(i) => (i, c.direction),
195                other => panic!("unexpected value {other:?}"),
196            }
197        })
198    }
199
200    fn params(cursor: Cursor, per_page: u32, page: u32) -> PaginationParams {
201        PaginationParams {
202            page,
203            per_page,
204            cursor: Some(cursor),
205            ..Default::default()
206        }
207    }
208
209    #[test]
210    fn after_page_with_more_rows() {
211        let cursor = Cursor::new("id".into(), CursorValue::Int(3), CursorDirection::After);
212        let p = params(cursor, 3, 1);
213        let plan = p.keyset_plan().unwrap().unwrap();
214        // Query returned per_page + 1 rows in ascending order.
215        let mut data = rows(&[4, 5, 6, 7]);
216        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, Some(10));
217
218        assert_eq!(ids(&data), vec![4, 5, 6]);
219        assert!(meta.has_next);
220        assert!(meta.has_prev);
221        assert_eq!(
222            decoded(&meta.next_cursor),
223            Some((6, CursorDirection::After))
224        );
225        assert_eq!(
226            decoded(&meta.prev_cursor),
227            Some((4, CursorDirection::Before))
228        );
229        assert_eq!(meta.total, Some(10));
230        assert_eq!(meta.total_pages, Some(4));
231        assert_eq!(meta.page, 1);
232    }
233
234    #[test]
235    fn after_page_at_the_end() {
236        let cursor = Cursor::new("id".into(), CursorValue::Int(7), CursorDirection::After);
237        let p = params(cursor, 3, 1);
238        let plan = p.keyset_plan().unwrap().unwrap();
239        let mut data = rows(&[8, 9]);
240        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
241
242        assert_eq!(ids(&data), vec![8, 9]);
243        assert!(!meta.has_next);
244        assert!(meta.has_prev);
245        assert!(meta.next_cursor.is_none());
246        assert_eq!(
247            decoded(&meta.prev_cursor),
248            Some((8, CursorDirection::Before))
249        );
250        assert!(meta.total.is_none());
251        assert!(meta.total_pages.is_none());
252    }
253
254    #[test]
255    fn empty_after_page_has_no_cursors() {
256        let cursor = Cursor::new("id".into(), CursorValue::Int(99), CursorDirection::After);
257        let p = params(cursor, 3, 1);
258        let plan = p.keyset_plan().unwrap().unwrap();
259        let mut data: Vec<Row> = vec![];
260        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
261        assert!(!meta.has_next);
262        assert!(meta.has_prev);
263        assert!(meta.next_cursor.is_none());
264        assert!(meta.prev_cursor.is_none());
265    }
266
267    #[test]
268    fn before_page_is_flipped_back_into_sort_order() {
269        let cursor = Cursor::new("id".into(), CursorValue::Int(7), CursorDirection::Before);
270        let p = params(cursor, 3, 1);
271        let plan = p.keyset_plan().unwrap().unwrap();
272        assert_eq!(plan.query_sort, SortDirection::Desc);
273        // Query ran `id < 7 ORDER BY id DESC LIMIT 4`.
274        let mut data = rows(&[6, 5, 4, 3]);
275        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
276
277        assert_eq!(ids(&data), vec![4, 5, 6]);
278        assert!(meta.has_next);
279        assert!(meta.has_prev);
280        assert_eq!(
281            decoded(&meta.next_cursor),
282            Some((6, CursorDirection::After))
283        );
284        assert_eq!(
285            decoded(&meta.prev_cursor),
286            Some((4, CursorDirection::Before))
287        );
288    }
289
290    #[test]
291    fn before_page_at_the_start() {
292        let cursor = Cursor::new("id".into(), CursorValue::Int(3), CursorDirection::Before);
293        let p = params(cursor, 3, 1);
294        let plan = p.keyset_plan().unwrap().unwrap();
295        let mut data = rows(&[2, 1]);
296        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
297
298        assert_eq!(ids(&data), vec![1, 2]);
299        assert!(meta.has_next);
300        assert!(!meta.has_prev);
301        assert_eq!(
302            decoded(&meta.next_cursor),
303            Some((2, CursorDirection::After))
304        );
305        assert!(meta.prev_cursor.is_none());
306    }
307
308    #[test]
309    fn before_page_with_descending_sort() {
310        let cursor = Cursor::new("id".into(), CursorValue::Int(4), CursorDirection::Before);
311        let mut p = params(cursor, 2, 1);
312        p.sort_direction = Some(SortDirection::Desc);
313        let plan = p.keyset_plan().unwrap().unwrap();
314        assert_eq!(plan.query_sort, SortDirection::Asc);
315        // Caller sorts DESC, so "before 4" are the larger ids; query ran ASC.
316        let mut data = rows(&[5, 6, 7]);
317        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
318
319        assert_eq!(ids(&data), vec![6, 5]);
320        assert!(meta.has_prev);
321        assert_eq!(
322            decoded(&meta.prev_cursor),
323            Some((6, CursorDirection::Before))
324        );
325        assert_eq!(
326            decoded(&meta.next_cursor),
327            Some((5, CursorDirection::After))
328        );
329    }
330
331    #[test]
332    fn relative_page_is_echoed() {
333        let cursor = Cursor::new("id".into(), CursorValue::Int(0), CursorDirection::After);
334        let p = params(cursor, 2, 3);
335        let plan = p.keyset_plan().unwrap().unwrap();
336        let mut data = rows(&[5, 6, 7]);
337        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
338        assert_eq!(meta.page, 3);
339        assert_eq!(ids(&data), vec![5, 6]);
340        assert!(meta.has_next && meta.has_prev);
341    }
342
343    #[test]
344    fn missing_field_leaves_cursors_unset() {
345        #[derive(Serialize)]
346        struct NoId {
347            name: String,
348        }
349        let cursor = Cursor::new("id".into(), CursorValue::Int(0), CursorDirection::After);
350        let p = params(cursor, 1, 1);
351        let plan = p.keyset_plan().unwrap().unwrap();
352        let mut data = vec![NoId { name: "a".into() }, NoId { name: "b".into() }];
353        let meta = PaginatorResponseMeta::from_cursor_page(&mut data, &p, &plan, None);
354        assert_eq!(data.len(), 1);
355        assert!(meta.has_next);
356        assert!(meta.next_cursor.is_none());
357        assert!(meta.prev_cursor.is_none());
358    }
359
360    #[test]
361    fn with_cursors_bootstraps_from_an_offset_page() {
362        let response = PaginatorResponse {
363            data: rows(&[1, 2, 3]),
364            meta: PaginatorResponseMeta::new(1, 3, 10),
365        }
366        .with_cursors("id");
367        assert_eq!(
368            decoded(&response.meta.next_cursor),
369            Some((3, CursorDirection::After))
370        );
371        assert!(response.meta.prev_cursor.is_none(), "page 1 has no prev");
372
373        let response = PaginatorResponse {
374            data: rows(&[4, 5, 6]),
375            meta: PaginatorResponseMeta::new(2, 3, 10),
376        }
377        .with_cursors("id");
378        assert_eq!(
379            decoded(&response.meta.next_cursor),
380            Some((6, CursorDirection::After))
381        );
382        assert_eq!(
383            decoded(&response.meta.prev_cursor),
384            Some((4, CursorDirection::Before))
385        );
386
387        let response = PaginatorResponse {
388            data: rows(&[10]),
389            meta: PaginatorResponseMeta::new(4, 3, 10),
390        }
391        .with_cursors("id");
392        assert!(response.meta.next_cursor.is_none(), "last page has no next");
393        assert_eq!(
394            decoded(&response.meta.prev_cursor),
395            Some((10, CursorDirection::Before))
396        );
397    }
398
399    #[test]
400    fn with_cursors_keeps_existing_cursors() {
401        let mut meta = PaginatorResponseMeta::new(2, 1, 3);
402        meta.next_cursor = Some("keep".into());
403        let response = PaginatorResponse {
404            data: rows(&[2]),
405            meta,
406        }
407        .with_cursors("id");
408        assert_eq!(response.meta.next_cursor.as_deref(), Some("keep"));
409        assert!(response.meta.prev_cursor.is_some());
410    }
411}