use std::fmt; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; pub const DEFAULT_PAGE_LIMIT: u64 = 50; pub const MAX_PAGE_LIMIT: u64 = 100; pub const MAX_OFFSET: u64 = 5000; #[derive(Debug, Clone, Copy, Default, Deserialize, IntoParams, ToSchema)] #[serde(deny_unknown_fields)] pub struct PageQuery { pub limit: Option, pub offset: Option, } impl PageQuery { pub fn resolve(&self, default: u64, max: u64) -> Result<(u64, u64), PageError> { resolve_bounds(self.limit, self.offset, default, max) } pub fn bounds(&self) -> Result<(u64, u64), PageError> { self.resolve(DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT) } } pub fn page_bounds( limit: Option, offset: Option, default: u64, max: u64, ) -> Result<(u64, u64), PageError> { resolve_bounds(limit, offset, default, max) } fn resolve_bounds( limit: Option, offset: Option, default: u64, max: u64, ) -> Result<(u64, u64), PageError> { let limit = limit.unwrap_or(default); if limit == 0 || limit > max { return Err(PageError(format!("limit must be between 1 and {max}"))); } let offset = offset.unwrap_or(0); if offset > MAX_OFFSET { return Err(PageError(format!("offset must not exceed {MAX_OFFSET}"))); } Ok((limit, offset)) } #[derive(Debug, Clone)] pub struct PageError(String); impl PageError { pub fn message(&self) -> &str { &self.0 } } impl fmt::Display for PageError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } impl std::error::Error for PageError {} #[derive(Debug, Clone, Serialize, ToSchema)] pub struct Page { pub items: Vec, pub total: u64, pub limit: u64, pub offset: u64, } impl Page { pub fn new(items: Vec, total: u64, limit: u64, offset: u64) -> Self { Self { items, total, limit, offset, } } } pub fn paginate(items: Vec, page: &PageQuery) -> Result, PageError> { let (limit, offset) = page.bounds()?; let total = items.len() as u64; let items = items .into_iter() .skip(offset as usize) .take(limit as usize) .collect(); Ok(Page { items, total, limit, offset, }) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum SortOrder { Asc, Desc, } impl SortOrder { pub fn is_desc(self) -> bool { matches!(self, Self::Desc) } }