#[derive(Debug, Clone)]
pub struct PageParams {
pub limit: usize,
pub cursor: Option<String>,
}
impl PageParams {
pub fn first(limit: usize) -> Self {
Self { limit, cursor: None }
}
pub fn after(cursor: impl Into<String>, limit: usize) -> Self {
Self {
limit,
cursor: Some(cursor.into()),
}
}
}
impl Default for PageParams {
fn default() -> Self {
Self::first(100)
}
}
#[derive(Debug, Clone)]
pub struct Page<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
pub total: Option<usize>,
}
impl<T> Page<T> {
pub fn with_cursor(items: Vec<T>, next_cursor: impl Into<String>, total: Option<usize>) -> Self {
Self {
items,
next_cursor: Some(next_cursor.into()),
total,
}
}
pub fn last(items: Vec<T>, total: Option<usize>) -> Self {
Self {
items,
next_cursor: None,
total,
}
}
pub fn has_next(&self) -> bool {
self.next_cursor.is_some()
}
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> Page<U> {
Page {
items: self.items.into_iter().map(f).collect(),
next_cursor: self.next_cursor,
total: self.total,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_has_no_cursor() {
let p = PageParams::first(50);
assert_eq!(p.limit, 50);
assert!(p.cursor.is_none());
}
#[test]
fn default_is_first_100() {
let p = PageParams::default();
assert_eq!(p.limit, 100);
assert!(p.cursor.is_none());
}
#[test]
fn after_stores_cursor_and_limit() {
let p = PageParams::after("cursor-abc", 25);
assert_eq!(p.limit, 25);
assert_eq!(p.cursor.as_deref(), Some("cursor-abc"));
}
#[test]
fn after_accepts_owned_string_cursor() {
let p = PageParams::after(String::from("xyz"), 10);
assert_eq!(p.cursor.as_deref(), Some("xyz"));
}
#[test]
fn with_cursor_sets_next_cursor() {
let page: Page<i32> = Page::with_cursor(vec![1, 2, 3], "next-cursor", Some(10));
assert_eq!(page.items, vec![1, 2, 3]);
assert_eq!(page.next_cursor.as_deref(), Some("next-cursor"));
assert_eq!(page.total, Some(10));
assert!(page.has_next());
}
#[test]
fn last_has_no_next_cursor() {
let page: Page<&str> = Page::last(vec!["a", "b"], Some(2));
assert_eq!(page.items, vec!["a", "b"]);
assert!(page.next_cursor.is_none());
assert_eq!(page.total, Some(2));
assert!(!page.has_next());
}
#[test]
fn last_without_total() {
let page: Page<u8> = Page::last(vec![], None);
assert!(page.total.is_none());
assert!(!page.has_next());
}
#[test]
fn empty_page_has_no_next() {
let page: Page<i32> = Page::last(vec![], None);
assert!(!page.has_next());
assert!(page.items.is_empty());
}
#[test]
fn map_transforms_items_preserving_cursor_and_total() {
let page: Page<i32> = Page::with_cursor(vec![1, 2, 3], "cur", Some(10));
let mapped = page.map(|n| n.to_string());
assert_eq!(mapped.items, vec!["1", "2", "3"]);
assert_eq!(mapped.next_cursor.as_deref(), Some("cur"));
assert_eq!(mapped.total, Some(10));
}
#[test]
fn map_on_last_page_preserves_no_cursor() {
let page: Page<i32> = Page::last(vec![10, 20], None);
let mapped = page.map(|n| n * 2);
assert_eq!(mapped.items, vec![20, 40]);
assert!(mapped.next_cursor.is_none());
}
#[test]
fn page_clone_is_independent() {
let page: Page<i32> = Page::with_cursor(vec![1], "c", None);
let cloned = page.clone();
assert_eq!(cloned.items, page.items);
assert_eq!(cloned.next_cursor, page.next_cursor);
}
}