use std::fmt;
use serde::Serialize;
use crate::orm::queryset::QuerySet;
use crate::orm::{HydrateRelated, Model};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaginationError {
InvalidPage {
requested: i64,
num_pages: i64,
},
Db(String),
}
pub type PageError = PaginationError;
impl fmt::Display for PaginationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PaginationError::InvalidPage {
requested,
num_pages,
} => write!(
f,
"invalid page number {requested}: valid pages are 1..={num_pages}"
),
PaginationError::Db(msg) => write!(f, "pagination query failed: {msg}"),
}
}
}
impl std::error::Error for PaginationError {}
impl From<sqlx::Error> for PaginationError {
fn from(e: sqlx::Error) -> Self {
PaginationError::Db(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct Paginator<T> {
queryset: QuerySet<T>,
per_page: usize,
}
impl<T> Paginator<T>
where
T: Model
+ Clone
+ for<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow>
+ for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
+ HydrateRelated,
{
pub fn new(queryset: QuerySet<T>, per_page: usize) -> Self {
Self {
queryset,
per_page: per_page.max(1),
}
}
pub fn per_page(&self) -> usize {
self.per_page
}
pub async fn count(&self) -> Result<i64, PaginationError> {
Ok(self.queryset.clone().count().await?)
}
pub async fn num_pages(&self) -> Result<i64, PaginationError> {
let count = self.count().await?;
Ok(num_pages_for(count, self.per_page))
}
pub async fn page(&self, number: i64) -> Result<Page<T>, PaginationError> {
let count = self.count().await?;
let num_pages = num_pages_for(count, self.per_page);
if number < 1 || number > num_pages {
return Err(PaginationError::InvalidPage {
requested: number,
num_pages,
});
}
self.build_page(number, count, num_pages).await
}
pub async fn page_clamped(&self, number: i64) -> Result<Page<T>, PaginationError> {
let count = self.count().await?;
let num_pages = num_pages_for(count, self.per_page);
let number = number.clamp(1, num_pages);
self.build_page(number, count, num_pages).await
}
async fn build_page(
&self,
number: i64,
count: i64,
num_pages: i64,
) -> Result<Page<T>, PaginationError> {
let per_page = self.per_page as u64;
let offset = (number - 1) as u64 * per_page;
let object_list = self
.queryset
.clone()
.limit(per_page)
.offset(offset)
.fetch()
.await?;
Ok(Page {
object_list,
number,
per_page: self.per_page,
total_count: count,
num_pages,
})
}
}
fn num_pages_for(count: i64, per_page: usize) -> i64 {
if count <= 0 {
return 1;
}
let per_page = per_page.max(1) as i64;
(count + per_page - 1) / per_page
}
#[derive(Debug, Clone)]
pub struct Page<T> {
pub object_list: Vec<T>,
pub number: i64,
pub per_page: usize,
pub total_count: i64,
pub num_pages: i64,
}
impl<T> Page<T> {
pub fn has_next(&self) -> bool {
self.number < self.num_pages
}
pub fn has_previous(&self) -> bool {
self.number > 1
}
pub fn has_other_pages(&self) -> bool {
self.has_next() || self.has_previous()
}
pub fn next_page_number(&self) -> Option<i64> {
self.has_next().then(|| self.number + 1)
}
pub fn previous_page_number(&self) -> Option<i64> {
self.has_previous().then(|| self.number - 1)
}
pub fn start_index(&self) -> i64 {
if self.total_count == 0 {
return 0;
}
(self.number - 1) * self.per_page as i64 + 1
}
pub fn end_index(&self) -> i64 {
if self.total_count == 0 {
return 0;
}
(self.number * self.per_page as i64).min(self.total_count)
}
pub fn elided_page_range(&self, on_each_side: i64, on_ends: i64) -> Vec<PageItem> {
elided_range(self.number, self.num_pages, on_each_side, on_ends)
}
pub fn context(&self) -> PageContext {
self.context_with(3, 1)
}
pub fn context_with(&self, on_each_side: i64, on_ends: i64) -> PageContext {
PageContext {
number: self.number,
num_pages: self.num_pages,
total_count: self.total_count,
per_page: self.per_page,
has_next: self.has_next(),
has_previous: self.has_previous(),
next_page_number: self.next_page_number(),
previous_page_number: self.previous_page_number(),
start_index: self.start_index(),
end_index: self.end_index(),
page_range: self
.elided_page_range(on_each_side, on_ends)
.into_iter()
.map(PageItemContext::from)
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageItem {
Number(i64),
Ellipsis,
}
fn elided_range(current: i64, num_pages: i64, on_each_side: i64, on_ends: i64) -> Vec<PageItem> {
let on_each_side = on_each_side.max(0);
let on_ends = on_ends.max(0);
if num_pages <= (on_each_side + on_ends) * 2 + 1 {
return (1..=num_pages).map(PageItem::Number).collect();
}
let mut items = Vec::new();
let left_window_start = current - on_each_side;
if left_window_start > on_ends + 1 {
for p in 1..=on_ends {
items.push(PageItem::Number(p));
}
if left_window_start > on_ends + 2 {
items.push(PageItem::Ellipsis);
} else {
items.push(PageItem::Number(on_ends + 1));
}
} else {
for p in 1..left_window_start.max(1) {
items.push(PageItem::Number(p));
}
}
let window_start = left_window_start.max(1);
let window_end = (current + on_each_side).min(num_pages);
for p in window_start..=window_end {
items.push(PageItem::Number(p));
}
let right_window_end = current + on_each_side;
if right_window_end < num_pages - on_ends {
if right_window_end < num_pages - on_ends - 1 {
items.push(PageItem::Ellipsis);
} else {
items.push(PageItem::Number(num_pages - on_ends));
}
for p in (num_pages - on_ends + 1)..=num_pages {
items.push(PageItem::Number(p));
}
} else {
for p in (right_window_end + 1)..=num_pages {
items.push(PageItem::Number(p));
}
}
items
}
#[derive(Debug, Clone, Serialize)]
pub struct PageContext {
pub number: i64,
pub num_pages: i64,
pub total_count: i64,
pub per_page: usize,
pub has_next: bool,
pub has_previous: bool,
pub next_page_number: Option<i64>,
pub previous_page_number: Option<i64>,
pub start_index: i64,
pub end_index: i64,
pub page_range: Vec<PageItemContext>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PageItemContext {
pub n: Option<i64>,
pub ellipsis: bool,
}
impl From<PageItem> for PageItemContext {
fn from(item: PageItem) -> Self {
match item {
PageItem::Number(n) => PageItemContext {
n: Some(n),
ellipsis: false,
},
PageItem::Ellipsis => PageItemContext {
n: None,
ellipsis: true,
},
}
}
}
pub fn querystring_with(current_query: &str, key: &str, value: &str) -> String {
let mut pairs: Vec<(String, String)> = Vec::new();
let mut replaced = false;
for pair in current_query.trim_start_matches('?').split('&') {
if pair.is_empty() {
continue;
}
let (k, v) = match pair.split_once('=') {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (pair.to_string(), String::new()),
};
if k == key {
pairs.push((k, encode_component(value)));
replaced = true;
} else {
pairs.push((k, v));
}
}
if !replaced {
pairs.push((key.to_string(), encode_component(value)));
}
pairs
.into_iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&")
}
fn encode_component(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
' ' => out.push_str("%20"),
'&' => out.push_str("%26"),
'=' => out.push_str("%3D"),
'#' => out.push_str("%23"),
'?' => out.push_str("%3F"),
'%' => out.push_str("%25"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn nums(items: &[PageItem]) -> Vec<String> {
items
.iter()
.map(|i| match i {
PageItem::Number(n) => n.to_string(),
PageItem::Ellipsis => "…".to_string(),
})
.collect()
}
#[test]
fn num_pages_math() {
assert_eq!(num_pages_for(23, 10), 3);
assert_eq!(num_pages_for(20, 10), 2);
assert_eq!(num_pages_for(21, 10), 3);
assert_eq!(num_pages_for(0, 10), 1);
assert_eq!(num_pages_for(-5, 10), 1);
assert_eq!(num_pages_for(5, 0), 5);
}
fn page_of(number: i64, per_page: usize, total: i64, num_pages: i64) -> Page<()> {
Page {
object_list: Vec::new(),
number,
per_page,
total_count: total,
num_pages,
}
}
#[test]
fn start_end_index_semantics() {
let p1 = page_of(1, 10, 23, 3);
assert_eq!(p1.start_index(), 1);
assert_eq!(p1.end_index(), 10);
assert!(!p1.has_previous());
assert!(p1.has_next());
assert_eq!(p1.next_page_number(), Some(2));
assert_eq!(p1.previous_page_number(), None);
let p3 = page_of(3, 10, 23, 3);
assert_eq!(p3.start_index(), 21);
assert_eq!(p3.end_index(), 23);
assert!(p3.has_previous());
assert!(!p3.has_next());
assert_eq!(p3.next_page_number(), None);
assert_eq!(p3.previous_page_number(), Some(2));
}
#[test]
fn empty_page_indices_are_zero() {
let p = page_of(1, 10, 0, 1);
assert_eq!(p.start_index(), 0);
assert_eq!(p.end_index(), 0);
assert!(!p.has_other_pages());
}
#[test]
fn elided_range_shows_all_when_small() {
let r = elided_range(2, 5, 2, 1);
assert_eq!(nums(&r), vec!["1", "2", "3", "4", "5"]);
}
#[test]
fn elided_range_windows_middle_with_both_ellipses() {
let r = elided_range(6, 20, 2, 1);
assert_eq!(
nums(&r),
vec!["1", "…", "4", "5", "6", "7", "8", "…", "20"]
);
assert!(r.contains(&PageItem::Ellipsis));
}
#[test]
fn elided_range_near_start_only_right_ellipsis() {
let r = elided_range(2, 20, 2, 1);
assert_eq!(nums(&r), vec!["1", "2", "3", "4", "…", "20"]);
}
#[test]
fn elided_range_near_end_only_left_ellipsis() {
let r = elided_range(19, 20, 2, 1);
assert_eq!(nums(&r), vec!["1", "…", "17", "18", "19", "20"]);
}
#[test]
fn querystring_replaces_page_preserving_others() {
assert_eq!(
querystring_with("page=1&sort=name", "page", "3"),
"page=3&sort=name"
);
assert_eq!(querystring_with("sort=name", "page", "2"), "sort=name&page=2");
assert_eq!(querystring_with("", "page", "5"), "page=5");
assert_eq!(querystring_with("?page=1", "page", "2"), "page=2");
assert_eq!(
querystring_with("page=1", "sort", "first name"),
"page=1&sort=first%20name"
);
}
}