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 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 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 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 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 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}