1use serde::{Deserialize, Serialize};
39
40pub const DEFAULT_PAGE_SIZE: u32 = 20;
42
43pub const MAX_PAGE_SIZE: u32 = 1000;
45
46pub const MIN_PAGE_SIZE: u32 = 1;
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum PaginationStrategy {
53 Offset {
55 offset: u64,
57 limit: u32,
59 },
60 Cursor {
62 cursor: Option<String>,
64 limit: u32,
66 direction: CursorDirection,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum CursorDirection {
75 Forward,
77 Backward,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct PaginationRequest {
84 pub strategy: PaginationStrategy,
86}
87
88impl PaginationRequest {
89 pub fn offset(offset: u64, limit: u32) -> Self {
97 let limit = Self::validate_limit(limit);
98 Self {
99 strategy: PaginationStrategy::Offset { offset, limit },
100 }
101 }
102
103 pub fn cursor(cursor: Option<String>, limit: u32, direction: CursorDirection) -> Self {
111 let limit = Self::validate_limit(limit);
112 Self {
113 strategy: PaginationStrategy::Cursor {
114 cursor,
115 limit,
116 direction,
117 },
118 }
119 }
120
121 pub fn limit(&self) -> u32 {
123 match &self.strategy {
124 PaginationStrategy::Offset { limit, .. } => *limit,
125 PaginationStrategy::Cursor { limit, .. } => *limit,
126 }
127 }
128
129 fn validate_limit(limit: u32) -> u32 {
131 limit.clamp(MIN_PAGE_SIZE, MAX_PAGE_SIZE)
132 }
133}
134
135impl Default for PaginationRequest {
136 fn default() -> Self {
137 Self::offset(0, DEFAULT_PAGE_SIZE)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct PaginationResponse<T> {
144 pub items: Vec<T>,
146 pub total: Option<u64>,
148 pub page: Option<u64>,
150 pub page_size: u32,
152 pub has_more: bool,
154 pub next_cursor: Option<String>,
156 pub prev_cursor: Option<String>,
158}
159
160impl<T> PaginationResponse<T> {
161 pub fn offset(items: Vec<T>, total: u64, page: u64, page_size: u32) -> Self {
163 let has_more = (page + 1) * (page_size as u64) < total;
164 Self {
165 items,
166 total: Some(total),
167 page: Some(page),
168 page_size,
169 has_more,
170 next_cursor: None,
171 prev_cursor: None,
172 }
173 }
174
175 pub fn cursor(
177 items: Vec<T>,
178 page_size: u32,
179 next_cursor: Option<String>,
180 prev_cursor: Option<String>,
181 ) -> Self {
182 let has_more = next_cursor.is_some();
183 Self {
184 items,
185 total: None,
186 page: None,
187 page_size,
188 has_more,
189 next_cursor,
190 prev_cursor,
191 }
192 }
193
194 pub fn map<U, F>(self, f: F) -> PaginationResponse<U>
196 where
197 F: FnMut(T) -> U,
198 {
199 PaginationResponse {
200 items: self.items.into_iter().map(f).collect(),
201 total: self.total,
202 page: self.page,
203 page_size: self.page_size,
204 has_more: self.has_more,
205 next_cursor: self.next_cursor,
206 prev_cursor: self.prev_cursor,
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213pub struct PageInfo {
214 pub page: u64,
216 pub page_size: u32,
218 pub total_items: u64,
220 pub total_pages: u64,
222 pub has_next: bool,
224 pub has_prev: bool,
226}
227
228impl PageInfo {
229 pub fn from_total(total: u64, request: &PaginationRequest) -> Self {
242 match &request.strategy {
243 PaginationStrategy::Offset { offset, limit } => {
244 let page_size = *limit;
245 let page = offset / page_size as u64;
246 let total_pages = total.div_ceil(page_size as u64);
247 let has_next = page + 1 < total_pages;
248 let has_prev = page > 0;
249
250 Self {
251 page,
252 page_size,
253 total_items: total,
254 total_pages,
255 has_next,
256 has_prev,
257 }
258 }
259 PaginationStrategy::Cursor { .. } => {
260 Self {
262 page: 0,
263 page_size: request.limit(),
264 total_items: total,
265 total_pages: 1,
266 has_next: false,
267 has_prev: false,
268 }
269 }
270 }
271 }
272}
273
274pub struct PaginationBuilder {
276 request: PaginationRequest,
277}
278
279impl PaginationBuilder {
280 pub fn new(request: PaginationRequest) -> Self {
282 Self { request }
283 }
284
285 pub fn limit(&self) -> u32 {
287 self.request.limit()
288 }
289
290 pub fn offset(&self) -> Option<u64> {
292 match &self.request.strategy {
293 PaginationStrategy::Offset { offset, .. } => Some(*offset),
294 PaginationStrategy::Cursor { .. } => None,
295 }
296 }
297
298 pub fn limit_clause(&self) -> String {
300 format!("LIMIT {}", self.limit())
301 }
302
303 pub fn offset_clause(&self) -> String {
305 if let Some(offset) = self.offset() {
306 format!("OFFSET {offset}")
307 } else {
308 String::new()
309 }
310 }
311
312 pub fn build_sql(&self) -> String {
322 let mut sql = self.limit_clause();
323 let offset = self.offset_clause();
324 if !offset.is_empty() {
325 sql.push(' ');
326 sql.push_str(&offset);
327 }
328 sql
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_pagination_request_offset() {
338 let request = PaginationRequest::offset(20, 10);
339 assert_eq!(request.limit(), 10);
340 match request.strategy {
341 PaginationStrategy::Offset { offset, limit } => {
342 assert_eq!(offset, 20);
343 assert_eq!(limit, 10);
344 }
345 _ => panic!("Expected Offset strategy"),
346 }
347 }
348
349 #[test]
350 fn test_pagination_request_cursor() {
351 let request =
352 PaginationRequest::cursor(Some("cursor123".to_string()), 15, CursorDirection::Forward);
353 assert_eq!(request.limit(), 15);
354 match request.strategy {
355 PaginationStrategy::Cursor {
356 cursor,
357 limit,
358 direction,
359 } => {
360 assert_eq!(cursor, Some("cursor123".to_string()));
361 assert_eq!(limit, 15);
362 assert_eq!(direction, CursorDirection::Forward);
363 }
364 _ => panic!("Expected Cursor strategy"),
365 }
366 }
367
368 #[test]
369 fn test_pagination_request_default() {
370 let request = PaginationRequest::default();
371 assert_eq!(request.limit(), DEFAULT_PAGE_SIZE);
372 }
373
374 #[test]
375 fn test_pagination_limit_validation() {
376 let request = PaginationRequest::offset(0, 5000);
378 assert_eq!(request.limit(), MAX_PAGE_SIZE);
379
380 let request = PaginationRequest::offset(0, 0);
382 assert_eq!(request.limit(), MIN_PAGE_SIZE);
383
384 let request = PaginationRequest::offset(0, 50);
386 assert_eq!(request.limit(), 50);
387 }
388
389 #[test]
390 fn test_pagination_response_offset() {
391 let items = vec![1, 2, 3, 4, 5];
392 let response = PaginationResponse::offset(items, 100, 2, 20);
393
394 assert_eq!(response.items.len(), 5);
395 assert_eq!(response.total, Some(100));
396 assert_eq!(response.page, Some(2));
397 assert_eq!(response.page_size, 20);
398 assert!(response.has_more);
399 }
400
401 #[test]
402 fn test_pagination_response_cursor() {
403 let items = vec!["a", "b", "c"];
404 let response = PaginationResponse::cursor(
405 items,
406 20,
407 Some("next_cursor".to_string()),
408 Some("prev_cursor".to_string()),
409 );
410
411 assert_eq!(response.items.len(), 3);
412 assert_eq!(response.total, None);
413 assert!(response.has_more);
414 assert_eq!(response.next_cursor, Some("next_cursor".to_string()));
415 assert_eq!(response.prev_cursor, Some("prev_cursor".to_string()));
416 }
417
418 #[test]
419 fn test_pagination_response_map() {
420 let items = vec![1, 2, 3];
421 let response = PaginationResponse::offset(items, 10, 0, 20);
422 let mapped = response.map(|x| x * 2);
423
424 assert_eq!(mapped.items, vec![2, 4, 6]);
425 assert_eq!(mapped.total, Some(10));
426 }
427
428 #[test]
429 fn test_page_info_from_total() {
430 let request = PaginationRequest::offset(40, 20);
431 let info = PageInfo::from_total(100, &request);
432
433 assert_eq!(info.page, 2);
434 assert_eq!(info.page_size, 20);
435 assert_eq!(info.total_items, 100);
436 assert_eq!(info.total_pages, 5);
437 assert!(info.has_next);
438 assert!(info.has_prev);
439 }
440
441 #[test]
442 fn test_page_info_first_page() {
443 let request = PaginationRequest::offset(0, 20);
444 let info = PageInfo::from_total(100, &request);
445
446 assert_eq!(info.page, 0);
447 assert!(!info.has_prev);
448 assert!(info.has_next);
449 }
450
451 #[test]
452 fn test_page_info_last_page() {
453 let request = PaginationRequest::offset(80, 20);
454 let info = PageInfo::from_total(100, &request);
455
456 assert_eq!(info.page, 4);
457 assert!(info.has_prev);
458 assert!(!info.has_next);
459 }
460
461 #[test]
462 fn test_pagination_builder() {
463 let request = PaginationRequest::offset(40, 20);
464 let builder = PaginationBuilder::new(request);
465
466 assert_eq!(builder.limit(), 20);
467 assert_eq!(builder.offset(), Some(40));
468 assert_eq!(builder.limit_clause(), "LIMIT 20");
469 assert_eq!(builder.offset_clause(), "OFFSET 40");
470 assert_eq!(builder.build_sql(), "LIMIT 20 OFFSET 40");
471 }
472
473 #[test]
474 fn test_pagination_builder_no_offset() {
475 let request = PaginationRequest::offset(0, 10);
476 let builder = PaginationBuilder::new(request);
477
478 assert_eq!(builder.build_sql(), "LIMIT 10 OFFSET 0");
479 }
480
481 #[test]
482 fn test_pagination_builder_cursor() {
483 let request =
484 PaginationRequest::cursor(Some("abc".to_string()), 25, CursorDirection::Forward);
485 let builder = PaginationBuilder::new(request);
486
487 assert_eq!(builder.limit(), 25);
488 assert_eq!(builder.offset(), None);
489 assert_eq!(builder.build_sql(), "LIMIT 25");
490 }
491}