use serde::Serialize;
pub const MAX_PAGE_SIZE: u64 = 100;
pub const DEFAULT_PAGE_SIZE: u64 = 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageRequest {
pub page: u64,
pub size: u64,
}
impl PageRequest {
pub fn from_query<'a>(query: impl IntoIterator<Item = (&'a String, &'a String)>) -> Self {
let mut page = 1;
let mut size = DEFAULT_PAGE_SIZE;
for (k, v) in query {
match k.as_str() {
"page" => {
if let Ok(p) = v.parse::<u64>() {
page = p;
}
}
"size" => {
if let Ok(s) = v.parse::<u64>() {
size = s;
}
}
_ => {}
}
}
Self::new(page, size)
}
pub fn new(page: u64, size: u64) -> Self {
Self {
page: page.max(1),
size: size.clamp(1, MAX_PAGE_SIZE),
}
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Page<T> {
pub items: Vec<T>,
pub total: u64,
pub next: Option<u64>,
}
pub fn paginate<T: Clone>(items: Vec<T>, req: PageRequest) -> Page<T> {
let total = items.len() as u64;
let start = (req.page - 1)
.checked_mul(req.size)
.and_then(|offset| usize::try_from(offset).ok());
let items = match start {
Some(start) if start < items.len() => {
let end = start.saturating_add(req.size as usize).min(items.len());
items[start..end].to_vec()
}
_ => Vec::new(),
};
let next = match req.page.checked_mul(req.size) {
Some(consumed) if consumed < total => Some(req.page + 1),
_ => None,
};
Page { items, total, next }
}
#[cfg(all(test, feature = "paginate"))]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn page_request_defaults() {
let q: HashMap<String, String> = HashMap::new();
let req = PageRequest::from_query(&q);
assert_eq!(req, PageRequest { page: 1, size: 20 });
}
#[test]
fn page_request_clamps_bounds() {
let req = PageRequest::new(0, 0);
assert_eq!(req.page, 1);
assert_eq!(req.size, 1);
let req = PageRequest::new(2, 10_000);
assert_eq!(req.size, MAX_PAGE_SIZE);
}
#[test]
fn page_request_ignores_non_numeric_values() {
let mut q = HashMap::new();
q.insert("page".to_string(), "abc".to_string());
q.insert("size".to_string(), "-5".to_string());
let req = PageRequest::from_query(&q);
assert_eq!(req.page, 1);
assert_eq!(req.size, DEFAULT_PAGE_SIZE);
}
#[test]
fn paginate_slices_middle_page() {
let page = paginate(vec![1, 2, 3, 4, 5], PageRequest::new(2, 2));
assert_eq!(page.items, vec![3, 4]);
assert_eq!(page.total, 5);
assert_eq!(page.next, Some(3));
}
#[test]
fn paginate_last_page_has_null_next() {
let page = paginate(vec![1, 2, 3, 4, 5], PageRequest::new(3, 2));
assert_eq!(page.items, vec![5]);
assert_eq!(page.next, None);
}
#[test]
fn paginate_beyond_end_yields_empty_items() {
let page = paginate(vec![1, 2], PageRequest::new(9, 2));
assert!(page.items.is_empty());
assert_eq!(page.total, 2);
assert_eq!(page.next, None);
}
#[test]
fn paginate_huge_page_does_not_wrap_offset() {
let page = paginate(vec![1, 2, 3], PageRequest::new(u64::MAX, MAX_PAGE_SIZE));
assert!(page.items.is_empty(), "wrapped offset would leak items");
assert_eq!(page.next, None);
}
#[test]
fn paginated_envelope_serializes_contract() {
let page = paginate(
vec!["a".to_string(), "b".to_string()],
PageRequest::new(1, 2),
);
let json = serde_json::to_value(&page).unwrap();
assert_eq!(json["items"], serde_json::json!(["a", "b"]));
assert_eq!(json["total"], 2);
assert_eq!(json["next"], serde_json::Value::Null);
}
}