Skip to main content

laterite_core/
pagination.rs

1//! Pagination primitives shared by admin lists and API endpoints.
2
3use serde::{Deserialize, Serialize};
4
5pub const MAX_PER_PAGE: u32 = 200;
6pub const DEFAULT_PER_PAGE: u32 = 25;
7
8/// An incoming page request (1-based).
9#[derive(Debug, Clone, Copy, Deserialize)]
10pub struct PageRequest {
11    #[serde(default = "default_page")]
12    pub page: u32,
13    #[serde(default = "default_per_page")]
14    pub per_page: u32,
15}
16
17fn default_page() -> u32 {
18    1
19}
20
21fn default_per_page() -> u32 {
22    DEFAULT_PER_PAGE
23}
24
25impl Default for PageRequest {
26    fn default() -> Self {
27        Self {
28            page: 1,
29            per_page: DEFAULT_PER_PAGE,
30        }
31    }
32}
33
34impl PageRequest {
35    /// Clamps page and page size into sane bounds.
36    pub fn clamped(self) -> Self {
37        Self {
38            page: self.page.max(1),
39            per_page: self.per_page.clamp(1, MAX_PER_PAGE),
40        }
41    }
42
43    pub fn limit(&self) -> i64 {
44        i64::from(self.clamped().per_page)
45    }
46
47    pub fn offset(&self) -> i64 {
48        let clamped = self.clamped();
49        i64::from(clamped.page - 1) * i64::from(clamped.per_page)
50    }
51}
52
53/// One page of results with totals for rendering pagers.
54#[derive(Debug, Clone, Serialize)]
55pub struct Page<T> {
56    pub items: Vec<T>,
57    pub page: u32,
58    pub per_page: u32,
59    pub total: u64,
60}
61
62impl<T> Page<T> {
63    pub fn total_pages(&self) -> u32 {
64        if self.per_page == 0 {
65            return 0;
66        }
67        let pages = self.total.div_ceil(u64::from(self.per_page));
68        u32::try_from(pages).unwrap_or(u32::MAX)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn clamps_out_of_range_requests() {
78        let request = PageRequest {
79            page: 0,
80            per_page: 10_000,
81        };
82        let clamped = request.clamped();
83        assert_eq!(clamped.page, 1);
84        assert_eq!(clamped.per_page, MAX_PER_PAGE);
85    }
86
87    #[test]
88    fn computes_limit_and_offset() {
89        let request = PageRequest {
90            page: 3,
91            per_page: 25,
92        };
93        assert_eq!(request.limit(), 25);
94        assert_eq!(request.offset(), 50);
95    }
96
97    #[test]
98    fn computes_total_pages() {
99        let page = Page::<u8> {
100            items: vec![],
101            page: 1,
102            per_page: 25,
103            total: 51,
104        };
105        assert_eq!(page.total_pages(), 3);
106    }
107}