167 lines
5.0 KiB
Rust
167 lines
5.0 KiB
Rust
/// Pagination utilities for query results and graph traversal.
|
|||
|
|
///
|
||
|
|
/// Enables efficient paginated retrieval of large result sets without
|
||
|
|
/// loading everything into memory.
|
||
|
|
///
|
||
|
|
/// # Example
|
||
|
|
/// ```
|
||
|
|
/// let params = PaginationParams { limit: 50, page: 1 };
|
||
|
|
/// let (offset, limit) = params.calculate_offset_limit();
|
||
|
|
/// // SELECT ... OFFSET 0 LIMIT 50
|
||
|
|
/// ```
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
const DEFAULT_LIMIT: usize = 50;
|
||
|
|
const MAX_LIMIT: usize = 100;
|
||
|
|
const MIN_LIMIT: usize = 1;
|
||
|
|
|
||
|
|
/// Pagination parameters extracted from request.
|
||
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||
|
|
pub struct PaginationParams {
|
||
|
|
/// Results per page (1-100, default 50)
|
||
|
|
pub limit: Option<usize>,
|
||
|
|
|
||
|
|
/// Page number (1-indexed, default 1)
|
||
|
|
pub page: Option<usize>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PaginationParams {
|
||
|
|
/// Create pagination params with defaults.
|
||
|
|
pub fn new(limit: Option<usize>, page: Option<usize>) -> Result<Self, String> {
|
||
|
|
let limit = limit.unwrap_or(DEFAULT_LIMIT);
|
||
|
|
let page = page.unwrap_or(1);
|
||
|
|
|
||
|
|
// Validate
|
||
|
|
if limit < MIN_LIMIT {
|
||
|
|
return Err(format!("limit must be >= {}", MIN_LIMIT));
|
||
|
|
}
|
||
|
|
if limit > MAX_LIMIT {
|
||
|
|
return Err(format!("limit must be <= {}", MAX_LIMIT));
|
||
|
|
}
|
||
|
|
if page < 1 {
|
||
|
|
return Err("page must be >= 1".to_string());
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Self {
|
||
|
|
limit: Some(limit),
|
||
|
|
page: Some(page),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Calculate SQL OFFSET and LIMIT for database query.
|
||
|
|
pub fn calculate_offset_limit(&self) -> (usize, usize) {
|
||
|
|
let limit = self.limit.unwrap_or(DEFAULT_LIMIT);
|
||
|
|
let page = self.page.unwrap_or(1);
|
||
|
|
let offset = (page - 1) * limit;
|
||
|
|
(offset, limit)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Calculate total pages given result count.
|
||
|
|
pub fn calculate_total_pages(&self, total_results: usize) -> usize {
|
||
|
|
let limit = self.limit.unwrap_or(DEFAULT_LIMIT);
|
||
|
|
(total_results + limit - 1) / limit
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if there's a next page.
|
||
|
|
pub fn has_next(&self, total_results: usize) -> bool {
|
||
|
|
let page = self.page.unwrap_or(1);
|
||
|
|
let total_pages = self.calculate_total_pages(total_results);
|
||
|
|
page < total_pages
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if there's a previous page.
|
||
|
|
pub fn has_prev(&self) -> bool {
|
||
|
|
let page = self.page.unwrap_or(1);
|
||
|
|
page > 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Pagination metadata in response.
|
||
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
|
|
pub struct PaginationMeta {
|
||
|
|
pub page: usize,
|
||
|
|
pub limit: usize,
|
||
|
|
pub total_results: usize,
|
||
|
|
pub total_pages: usize,
|
||
|
|
pub has_next: bool,
|
||
|
|
pub has_prev: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PaginationMeta {
|
||
|
|
/// Create pagination metadata from params and total count.
|
||
|
|
pub fn new(params: &PaginationParams, total_results: usize) -> Self {
|
||
|
|
let page = params.page.unwrap_or(1);
|
||
|
|
let limit = params.limit.unwrap_or(DEFAULT_LIMIT);
|
||
|
|
let total_pages = params.calculate_total_pages(total_results);
|
||
|
|
let has_next = params.has_next(total_results);
|
||
|
|
let has_prev = params.has_prev();
|
||
|
|
|
||
|
|
Self {
|
||
|
|
page,
|
||
|
|
limit,
|
||
|
|
total_results,
|
||
|
|
total_pages,
|
||
|
|
has_next,
|
||
|
|
has_prev,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_defaults() {
|
||
|
|
let params = PaginationParams::new(None, None).unwrap();
|
||
|
|
let (offset, limit) = params.calculate_offset_limit();
|
||
|
|
assert_eq!(offset, 0);
|
||
|
|
assert_eq!(limit, 50);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_page_2() {
|
||
|
|
let params = PaginationParams::new(Some(50), Some(2)).unwrap();
|
||
|
|
let (offset, limit) = params.calculate_offset_limit();
|
||
|
|
assert_eq!(offset, 50);
|
||
|
|
assert_eq!(limit, 50);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_total_pages() {
|
||
|
|
let params = PaginationParams::new(Some(50), Some(1)).unwrap();
|
||
|
|
assert_eq!(params.calculate_total_pages(127), 3);
|
||
|
|
assert_eq!(params.calculate_total_pages(100), 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_has_next() {
|
||
|
|
let params = PaginationParams::new(Some(50), Some(1)).unwrap();
|
||
|
|
assert!(params.has_next(127));
|
||
|
|
|
||
|
|
let params = PaginationParams::new(Some(50), Some(3)).unwrap();
|
||
|
|
assert!(!params.has_next(127));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_validation() {
|
||
|
|
assert!(PaginationParams::new(Some(150), Some(1)).is_err()); // > MAX_LIMIT
|
||
|
|
assert!(PaginationParams::new(Some(0), Some(1)).is_err()); // < MIN_LIMIT
|
||
|
|
assert!(PaginationParams::new(Some(50), Some(0)).is_err()); // page < 1
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_pagination_meta() {
|
||
|
|
let params = PaginationParams::new(Some(50), Some(1)).unwrap();
|
||
|
|
let meta = PaginationMeta::new(¶ms, 127);
|
||
|
|
|
||
|
|
assert_eq!(meta.page, 1);
|
||
|
|
assert_eq!(meta.limit, 50);
|
||
|
|
assert_eq!(meta.total_results, 127);
|
||
|
|
assert_eq!(meta.total_pages, 3);
|
||
|
|
assert!(meta.has_next);
|
||
|
|
assert!(!meta.has_prev);
|
||
|
|
}
|
||
|
|
}
|