use std::convert::Infallible;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Page(pub u32);
impl<S: Send + Sync> FromRequestParts<S> for Page {
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Infallible> {
let page = parts
.uri
.query()
.and_then(|query| {
form_urlencoded::parse(query.as_bytes())
.find(|(key, _)| key == "page")
.and_then(|(_, value)| value.parse().ok())
})
.unwrap_or(1u32)
.max(1);
Ok(Self(page))
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct Paginated<T> {
pub items: Vec<T>,
pub page: u32,
pub per_page: u32,
pub total: u64,
pub last_page: u32,
pub from: u64,
pub to: u64,
pub has_prev: bool,
pub has_next: bool,
pub pages: Vec<Option<u32>>,
}
impl<T> Paginated<T> {
pub fn new(items: Vec<T>, page: u32, per_page: u32, total: u64) -> Self {
let last_page = total.div_ceil(u64::from(per_page)).max(1) as u32;
let from = if items.is_empty() {
0
} else {
u64::from(page - 1) * u64::from(per_page) + 1
};
let to = if items.is_empty() {
0
} else {
from + items.len() as u64 - 1
};
Self {
items,
page,
per_page,
total,
last_page,
from,
to,
has_prev: page > 1,
has_next: page < last_page,
pages: window(page, last_page),
}
}
pub fn map<U>(self, f: impl FnMut(T) -> U) -> Paginated<U> {
Paginated {
items: self.items.into_iter().map(f).collect(),
page: self.page,
per_page: self.per_page,
total: self.total,
last_page: self.last_page,
from: self.from,
to: self.to,
has_prev: self.has_prev,
has_next: self.has_next,
pages: self.pages,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct SimplePage<T> {
pub items: Vec<T>,
pub page: u32,
pub per_page: u32,
pub has_prev: bool,
pub has_next: bool,
}
impl<T> SimplePage<T> {
pub fn map<U>(self, f: impl FnMut(T) -> U) -> SimplePage<U> {
SimplePage {
items: self.items.into_iter().map(f).collect(),
page: self.page,
per_page: self.per_page,
has_prev: self.has_prev,
has_next: self.has_next,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CursorPage<T> {
pub items: Vec<T>,
pub per_page: u32,
pub next_cursor: Option<String>,
}
impl<T> CursorPage<T> {
pub fn map<U>(self, f: impl FnMut(T) -> U) -> CursorPage<U> {
CursorPage {
items: self.items.into_iter().map(f).collect(),
per_page: self.per_page,
next_cursor: self.next_cursor,
}
}
}
fn window(page: u32, last: u32) -> Vec<Option<u32>> {
let mut pages = Vec::new();
let mut previous = 0;
for n in 1..=last {
if n == 1 || n == last || n.abs_diff(page) <= 2 {
if n > previous + 1 {
pages.push(None);
}
pages.push(Some(n));
previous = n;
}
}
pages
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_pages_and_positions() {
let p = Paginated::new(vec![11, 12, 13], 2, 10, 23);
assert_eq!((p.last_page, p.from, p.to), (3, 11, 13));
assert!(p.has_prev && p.has_next);
let empty = Paginated::<i32>::new(vec![], 1, 10, 0);
assert_eq!((empty.last_page, empty.from, empty.to), (1, 0, 0));
assert!(!empty.has_prev && !empty.has_next);
}
#[test]
fn windows_page_links() {
assert_eq!(window(1, 3), vec![Some(1), Some(2), Some(3)]);
assert_eq!(
window(10, 20),
vec![
Some(1),
None,
Some(8),
Some(9),
Some(10),
Some(11),
Some(12),
None,
Some(20)
]
);
assert_eq!(
window(2, 9),
vec![Some(1), Some(2), Some(3), Some(4), None, Some(9)]
);
}
#[test]
fn map_keeps_the_page_information() {
let page = Paginated {
items: vec![1, 2],
page: 2,
per_page: 2,
total: 5,
last_page: 3,
from: 3,
to: 4,
has_prev: true,
has_next: true,
pages: window(2, 3),
}
.map(|n| n * 10);
assert_eq!(page.items, [10, 20]);
assert_eq!((page.page, page.total, page.last_page), (2, 5, 3));
assert!(page.has_prev && page.has_next);
let simple = SimplePage {
items: vec!["a"],
page: 1,
per_page: 10,
has_prev: false,
has_next: true,
}
.map(str::to_uppercase);
assert_eq!(simple.items, ["A"]);
assert!(!simple.has_prev && simple.has_next);
let cursor = CursorPage {
items: vec![1],
per_page: 1,
next_cursor: Some("abc".into()),
}
.map(|n| n + 1);
assert_eq!(cursor.items, [2]);
assert_eq!(cursor.next_cursor.as_deref(), Some("abc"));
}
}