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