1use axum::extract::FromRequestParts;
2use http::request::Parts;
3use serde::{Deserialize, Serialize};
4
5use crate::error::Error;
6
7#[derive(Debug, Clone)]
14pub struct PaginationConfig {
15 pub default_per_page: i64,
17 pub max_per_page: i64,
19}
20
21impl Default for PaginationConfig {
22 fn default() -> Self {
23 Self {
24 default_per_page: 20,
25 max_per_page: 100,
26 }
27 }
28}
29
30#[derive(Debug, Serialize)]
37pub struct Page<T: Serialize> {
38 pub items: Vec<T>,
40 pub total: i64,
42 pub page: i64,
44 pub per_page: i64,
46 pub total_pages: i64,
48 pub has_next: bool,
50 pub has_prev: bool,
52}
53
54impl<T: Serialize> Page<T> {
55 pub fn new(items: Vec<T>, total: i64, page: i64, per_page: i64) -> Self {
57 let total_pages = if total == 0 || per_page == 0 {
58 0
59 } else {
60 (total + per_page - 1) / per_page
61 };
62 Self {
63 items,
64 total,
65 page,
66 per_page,
67 total_pages,
68 has_next: page < total_pages,
69 has_prev: page > 1,
70 }
71 }
72}
73
74#[derive(Debug, Serialize)]
80pub struct CursorPage<T: Serialize> {
81 pub items: Vec<T>,
83 pub next_cursor: Option<String>,
85 pub has_more: bool,
87 pub per_page: i64,
89}
90
91impl<T: Serialize> CursorPage<T> {
92 pub fn new(items: Vec<T>, next_cursor: Option<String>, per_page: i64) -> Self {
95 Self {
96 has_more: next_cursor.is_some(),
97 items,
98 next_cursor,
99 per_page,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Deserialize)]
113pub struct PageRequest {
114 #[serde(default = "one")]
116 pub page: i64,
117 #[serde(default)]
119 pub per_page: i64,
120}
121
122impl PageRequest {
123 pub fn clamp(&mut self, config: &PaginationConfig) {
125 if self.page < 1 {
126 self.page = 1;
127 }
128 if self.per_page < 1 {
129 self.per_page = config.default_per_page;
130 }
131 if self.per_page > config.max_per_page {
132 self.per_page = config.max_per_page;
133 }
134 }
135
136 pub fn offset(&self) -> i64 {
138 (self.page - 1) * self.per_page
139 }
140}
141
142#[derive(Debug, Clone, Deserialize)]
147pub struct CursorRequest {
148 #[serde(default)]
150 pub after: Option<String>,
151 #[serde(default)]
153 pub per_page: i64,
154}
155
156impl CursorRequest {
157 pub fn clamp(&mut self, config: &PaginationConfig) {
159 if self.per_page < 1 {
160 self.per_page = config.default_per_page;
161 }
162 if self.per_page > config.max_per_page {
163 self.per_page = config.max_per_page;
164 }
165 }
166}
167
168fn one() -> i64 {
169 1
170}
171
172fn resolve_config(parts: &Parts) -> PaginationConfig {
173 parts
174 .extensions
175 .get::<PaginationConfig>()
176 .cloned()
177 .unwrap_or_default()
178}
179
180impl<S: Send + Sync> FromRequestParts<S> for PageRequest {
181 type Rejection = Error;
182
183 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
184 let config = resolve_config(parts);
185 let axum::extract::Query(mut req) =
186 axum::extract::Query::<PageRequest>::from_request_parts(parts, state)
187 .await
188 .map_err(|e| Error::bad_request(format!("invalid pagination params: {e}")))?;
189 req.clamp(&config);
190 Ok(req)
191 }
192}
193
194impl<S: Send + Sync> FromRequestParts<S> for CursorRequest {
195 type Rejection = Error;
196
197 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
198 let config = resolve_config(parts);
199 let axum::extract::Query(mut req) =
200 axum::extract::Query::<CursorRequest>::from_request_parts(parts, state)
201 .await
202 .map_err(|e| Error::bad_request(format!("invalid pagination params: {e}")))?;
203 req.clamp(&config);
204 Ok(req)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn config() -> PaginationConfig {
213 PaginationConfig {
214 default_per_page: 20,
215 max_per_page: 100,
216 }
217 }
218
219 #[test]
220 fn page_request_defaults() {
221 let mut req: PageRequest = serde_urlencoded::from_str("").unwrap();
222 req.clamp(&config());
223 assert_eq!(req.page, 1);
224 assert_eq!(req.per_page, 20);
225 }
226
227 #[test]
228 fn page_request_zero_page_becomes_one() {
229 let mut req = PageRequest {
230 page: 0,
231 per_page: 10,
232 };
233 req.clamp(&config());
234 assert_eq!(req.page, 1);
235 }
236
237 #[test]
238 fn page_request_per_page_zero_uses_default() {
239 let mut req = PageRequest {
240 page: 1,
241 per_page: 0,
242 };
243 req.clamp(&config());
244 assert_eq!(req.per_page, 20);
245 }
246
247 #[test]
248 fn page_request_per_page_over_max_clamped() {
249 let mut req = PageRequest {
250 page: 1,
251 per_page: 999,
252 };
253 req.clamp(&config());
254 assert_eq!(req.per_page, 100);
255 }
256
257 #[test]
258 fn page_request_valid_values_unchanged() {
259 let mut req = PageRequest {
260 page: 3,
261 per_page: 50,
262 };
263 req.clamp(&config());
264 assert_eq!(req.page, 3);
265 assert_eq!(req.per_page, 50);
266 }
267
268 #[test]
269 fn page_request_offset_calculation() {
270 let req = PageRequest {
271 page: 3,
272 per_page: 10,
273 };
274 assert_eq!(req.offset(), 20);
275 }
276
277 #[test]
278 fn page_request_offset_first_page() {
279 let req = PageRequest {
280 page: 1,
281 per_page: 10,
282 };
283 assert_eq!(req.offset(), 0);
284 }
285
286 #[test]
287 fn cursor_request_defaults() {
288 let mut req: CursorRequest = serde_urlencoded::from_str("").unwrap();
289 req.clamp(&config());
290 assert!(req.after.is_none());
291 assert_eq!(req.per_page, 20);
292 }
293
294 #[test]
295 fn cursor_request_per_page_over_max_clamped() {
296 let mut req = CursorRequest {
297 after: None,
298 per_page: 500,
299 };
300 req.clamp(&config());
301 assert_eq!(req.per_page, 100);
302 }
303
304 #[test]
305 fn cursor_request_per_page_zero_becomes_default() {
306 let mut req = CursorRequest {
307 after: Some("abc".into()),
308 per_page: 0,
309 };
310 req.clamp(&config());
311 assert_eq!(req.per_page, 20);
312 assert_eq!(req.after.as_deref(), Some("abc"));
313 }
314
315 #[test]
316 fn page_new_calculates_fields() {
317 let page: Page<String> = Page::new(vec!["a".into(), "b".into()], 5, 2, 2);
318 assert_eq!(page.total_pages, 3);
319 assert!(page.has_next);
320 assert!(page.has_prev);
321 }
322
323 #[test]
324 fn page_new_first_page() {
325 let page: Page<String> = Page::new(vec!["a".into(), "b".into()], 10, 1, 2);
326 assert_eq!(page.total_pages, 5);
327 assert!(page.has_next);
328 assert!(!page.has_prev);
329 }
330
331 #[test]
332 fn page_new_last_page() {
333 let page: Page<String> = Page::new(vec!["e".into()], 5, 3, 2);
334 assert_eq!(page.total_pages, 3);
335 assert!(!page.has_next);
336 assert!(page.has_prev);
337 }
338
339 #[test]
340 fn page_new_empty() {
341 let page: Page<String> = Page::new(vec![], 0, 1, 20);
342 assert_eq!(page.total_pages, 0);
343 assert!(!page.has_next);
344 assert!(!page.has_prev);
345 }
346
347 #[test]
348 fn cursor_page_with_more() {
349 let page: CursorPage<String> =
350 CursorPage::new(vec!["a".into(), "b".into()], Some("id_b".into()), 2);
351 assert!(page.has_more);
352 assert_eq!(page.next_cursor.as_deref(), Some("id_b"));
353 assert_eq!(page.per_page, 2);
354 }
355
356 #[test]
357 fn cursor_page_last() {
358 let page: CursorPage<String> = CursorPage::new(vec!["a".into()], None, 20);
359 assert!(!page.has_more);
360 assert!(page.next_cursor.is_none());
361 }
362
363 #[test]
364 fn page_serializes_to_json() {
365 let page: Page<i32> = Page::new(vec![1, 2, 3], 10, 1, 3);
366 let json = serde_json::to_value(&page).unwrap();
367 assert_eq!(json["items"], serde_json::json!([1, 2, 3]));
368 assert_eq!(json["total"], 10);
369 assert_eq!(json["page"], 1);
370 assert_eq!(json["per_page"], 3);
371 assert_eq!(json["total_pages"], 4);
372 assert_eq!(json["has_next"], true);
373 assert_eq!(json["has_prev"], false);
374 }
375
376 #[test]
377 fn page_request_deserializes_from_query_string() {
378 let req: PageRequest = serde_urlencoded::from_str("page=2&per_page=30").unwrap();
379 assert_eq!(req.page, 2);
380 assert_eq!(req.per_page, 30);
381 }
382
383 #[test]
384 fn cursor_request_deserializes_from_query_string() {
385 let req: CursorRequest = serde_urlencoded::from_str("after=01ABC&per_page=10").unwrap();
386 assert_eq!(req.after.as_deref(), Some("01ABC"));
387 assert_eq!(req.per_page, 10);
388 }
389
390 #[test]
391 fn cursor_request_deserializes_without_after() {
392 let req: CursorRequest = serde_urlencoded::from_str("per_page=10").unwrap();
393 assert!(req.after.is_none());
394 }
395}