use serde::{Deserialize, Serialize};
pub const MAX_PER_PAGE: u32 = 100;
pub const DEFAULT_PER_PAGE: u32 = 20;
#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub struct PageParams {
#[serde(default)]
page: Option<u32>,
#[serde(default)]
per_page: Option<u32>,
}
impl PageParams {
#[inline]
pub fn page(&self) -> u32 {
self.page.unwrap_or(1).max(1)
}
#[inline]
pub fn per_page(&self) -> u32 {
self.per_page
.unwrap_or(DEFAULT_PER_PAGE)
.clamp(1, MAX_PER_PAGE)
}
#[inline]
pub fn limit(&self) -> u32 {
self.per_page()
}
#[inline]
pub fn offset(&self) -> u64 {
(self.page() as u64 - 1).saturating_mul(self.per_page() as u64)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Page<T> {
pub items: Vec<T>,
pub page: u32,
pub per_page: u32,
pub total: u64,
pub total_pages: u64,
pub has_next: bool,
pub has_prev: bool,
}
impl<T> Page<T> {
pub fn new(items: Vec<T>, params: PageParams, total: u64) -> Self {
let page = params.page();
let per_page = params.per_page();
let total_pages = total.div_ceil(per_page as u64).max(1);
Self {
items,
page,
per_page,
total,
total_pages,
has_next: (page as u64) < total_pages,
has_prev: page > 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn params(page: Option<u32>, per_page: Option<u32>) -> PageParams {
PageParams { page, per_page }
}
#[test]
fn clamps_and_defaults() {
let p = params(None, None);
assert_eq!(p.page(), 1);
assert_eq!(p.per_page(), DEFAULT_PER_PAGE);
assert_eq!(p.offset(), 0);
let p = params(Some(0), Some(0));
assert_eq!(p.page(), 1, "page floors at 1");
assert_eq!(p.per_page(), 1, "per_page floors at 1");
let p = params(Some(3), Some(10_000));
assert_eq!(p.per_page(), MAX_PER_PAGE, "per_page capped");
assert_eq!(p.offset(), 2 * MAX_PER_PAGE as u64);
}
#[test]
fn deserializes_from_query() {
let p: PageParams = serde_urlencoded::from_str("page=2&per_page=50").unwrap();
assert_eq!(p.page(), 2);
assert_eq!(p.per_page(), 50);
assert_eq!(p.offset(), 50);
let empty: PageParams = serde_urlencoded::from_str("").unwrap();
assert_eq!(empty.page(), 1);
}
#[test]
fn envelope_metadata() {
let page = Page::new(vec![1, 2, 3], params(Some(2), Some(3)), 7);
assert_eq!(page.total_pages, 3);
assert!(page.has_next);
assert!(page.has_prev);
let last = Page::new(vec![7], params(Some(3), Some(3)), 7);
assert!(!last.has_next);
assert!(last.has_prev);
let empty = Page::new(Vec::<i32>::new(), params(Some(1), Some(20)), 0);
assert_eq!(empty.total_pages, 1, "always at least one page");
assert!(!empty.has_next);
assert!(!empty.has_prev);
}
}