use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Paginated<T> {
pub data: Vec<T>,
pub total: u64,
pub current_page: u64,
pub per_page: u64,
}
impl<T> Paginated<T> {
pub fn new(data: Vec<T>, total: u64, page: u64, per_page: u64) -> Self {
Self { data, total, current_page: page.max(1), per_page: per_page.max(1) }
}
pub fn empty(page: u64, per_page: u64) -> Self {
Self::new(Vec::new(), 0, page, per_page)
}
pub fn last_page(&self) -> u64 {
self.total.div_ceil(self.per_page).max(1)
}
pub fn from(&self) -> Option<u64> {
if self.data.is_empty() {
return None;
}
Some((self.current_page - 1) * self.per_page + 1)
}
pub fn to(&self) -> Option<u64> {
let from = self.from()?;
Some(from + self.data.len() as u64 - 1)
}
pub fn has_more_pages(&self) -> bool {
self.current_page < self.last_page()
}
pub fn next_page(&self) -> Option<u64> {
self.has_more_pages().then(|| self.current_page + 1)
}
pub fn previous_page(&self) -> Option<u64> {
(self.current_page > 1).then(|| self.current_page - 1)
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn offset(&self) -> u64 {
(self.current_page - 1) * self.per_page
}
pub fn map<U>(self, transform: impl FnMut(T) -> U) -> Paginated<U> {
Paginated {
data: self.data.into_iter().map(transform).collect(),
total: self.total,
current_page: self.current_page,
per_page: self.per_page,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn computes_the_page_window() {
let page = Paginated::new(vec![1, 2, 3], 25, 2, 10);
assert_eq!(page.last_page(), 3);
assert_eq!(page.offset(), 10);
assert_eq!(page.from(), Some(11));
assert_eq!(page.to(), Some(13));
assert_eq!(page.next_page(), Some(3));
assert_eq!(page.previous_page(), Some(1));
}
#[test]
fn the_last_page_rounds_up() {
assert_eq!(Paginated::new(vec![1], 21, 1, 10).last_page(), 3);
assert_eq!(Paginated::new(vec![1], 20, 1, 10).last_page(), 2);
assert_eq!(Paginated::new(vec![1], 1, 1, 10).last_page(), 1);
}
#[test]
fn an_empty_result_still_has_a_first_page() {
let page: Paginated<i32> = Paginated::empty(1, 10);
assert_eq!(page.last_page(), 1);
assert_eq!(page.from(), None);
assert_eq!(page.to(), None);
assert!(!page.has_more_pages());
assert!(page.is_empty());
}
#[test]
fn page_and_size_are_clamped_to_sane_values() {
let page = Paginated::new(vec![1], 5, 0, 0);
assert_eq!(page.current_page, 1);
assert_eq!(page.per_page, 1);
assert_eq!(page.last_page(), 5);
}
#[test]
fn the_final_page_reports_a_short_window() {
let page = Paginated::new(vec![1, 2, 3], 23, 3, 10);
assert_eq!(page.from(), Some(21));
assert_eq!(page.to(), Some(23));
assert!(!page.has_more_pages());
assert_eq!(page.next_page(), None);
}
#[test]
fn map_keeps_the_metadata() {
let page = Paginated::new(vec![1, 2], 20, 2, 5);
let mapped = page.map(|n| n.to_string());
assert_eq!(mapped.data, vec!["1", "2"]);
assert_eq!(mapped.total, 20);
assert_eq!(mapped.current_page, 2);
assert_eq!(mapped.per_page, 5);
}
#[test]
fn serialises_for_an_api_response() {
let page = Paginated::new(vec!["a"], 3, 1, 2);
let json = serde_json::to_value(&page).unwrap();
assert_eq!(json["data"][0], "a");
assert_eq!(json["total"], 3);
assert_eq!(json["current_page"], 1);
assert_eq!(json["per_page"], 2);
}
}