arcly_http/web/
pagination.rs1use serde::{Deserialize, Serialize};
21
22pub const MAX_PER_PAGE: u32 = 100;
25pub const DEFAULT_PER_PAGE: u32 = 20;
27
28#[derive(Debug, Clone, Copy, Default, Deserialize)]
33pub struct PageParams {
34 #[serde(default)]
35 page: Option<u32>,
36 #[serde(default)]
37 per_page: Option<u32>,
38}
39
40impl PageParams {
41 #[inline]
43 pub fn page(&self) -> u32 {
44 self.page.unwrap_or(1).max(1)
45 }
46
47 #[inline]
49 pub fn per_page(&self) -> u32 {
50 self.per_page
51 .unwrap_or(DEFAULT_PER_PAGE)
52 .clamp(1, MAX_PER_PAGE)
53 }
54
55 #[inline]
57 pub fn limit(&self) -> u32 {
58 self.per_page()
59 }
60
61 #[inline]
63 pub fn offset(&self) -> u64 {
64 (self.page() as u64 - 1).saturating_mul(self.per_page() as u64)
65 }
66}
67
68#[derive(Debug, Clone, Serialize)]
70pub struct Page<T> {
71 pub items: Vec<T>,
73 pub page: u32,
75 pub per_page: u32,
77 pub total: u64,
79 pub total_pages: u64,
81 pub has_next: bool,
83 pub has_prev: bool,
85}
86
87impl<T> Page<T> {
88 pub fn new(items: Vec<T>, params: PageParams, total: u64) -> Self {
91 let page = params.page();
92 let per_page = params.per_page();
93 let total_pages = total.div_ceil(per_page as u64).max(1);
94 Self {
95 items,
96 page,
97 per_page,
98 total,
99 total_pages,
100 has_next: (page as u64) < total_pages,
101 has_prev: page > 1,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn params(page: Option<u32>, per_page: Option<u32>) -> PageParams {
111 PageParams { page, per_page }
112 }
113
114 #[test]
115 fn clamps_and_defaults() {
116 let p = params(None, None);
117 assert_eq!(p.page(), 1);
118 assert_eq!(p.per_page(), DEFAULT_PER_PAGE);
119 assert_eq!(p.offset(), 0);
120
121 let p = params(Some(0), Some(0));
122 assert_eq!(p.page(), 1, "page floors at 1");
123 assert_eq!(p.per_page(), 1, "per_page floors at 1");
124
125 let p = params(Some(3), Some(10_000));
126 assert_eq!(p.per_page(), MAX_PER_PAGE, "per_page capped");
127 assert_eq!(p.offset(), 2 * MAX_PER_PAGE as u64);
128 }
129
130 #[test]
131 fn deserializes_from_query() {
132 let p: PageParams = serde_urlencoded::from_str("page=2&per_page=50").unwrap();
133 assert_eq!(p.page(), 2);
134 assert_eq!(p.per_page(), 50);
135 assert_eq!(p.offset(), 50);
136
137 let empty: PageParams = serde_urlencoded::from_str("").unwrap();
138 assert_eq!(empty.page(), 1);
139 }
140
141 #[test]
142 fn envelope_metadata() {
143 let page = Page::new(vec![1, 2, 3], params(Some(2), Some(3)), 7);
144 assert_eq!(page.total_pages, 3);
145 assert!(page.has_next);
146 assert!(page.has_prev);
147
148 let last = Page::new(vec![7], params(Some(3), Some(3)), 7);
149 assert!(!last.has_next);
150 assert!(last.has_prev);
151
152 let empty = Page::new(Vec::<i32>::new(), params(Some(1), Some(20)), 0);
153 assert_eq!(empty.total_pages, 1, "always at least one page");
154 assert!(!empty.has_next);
155 assert!(!empty.has_prev);
156 }
157}