use base64::{Engine as _, engine::general_purpose};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::ops::{Deref, DerefMut};
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Cursor(pub usize);
impl Serialize for Cursor {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let json = serde_json::to_vec(&self.0).map_err(serde::ser::Error::custom)?;
let encoded = general_purpose::STANDARD.encode(json);
serializer.serialize_str(&encoded)
}
}
impl<'de> Deserialize<'de> for Cursor {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let encoded = String::deserialize(deserializer)?;
let decoded = general_purpose::STANDARD
.decode(&encoded)
.map_err(serde::de::Error::custom)?;
let index: usize = serde_json::from_slice(&decoded).map_err(serde::de::Error::custom)?;
Ok(Cursor(index))
}
}
impl Deref for Cursor {
type Target = usize;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Cursor {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug)]
pub struct Page<'a, T> {
pub items: &'a [T],
pub next_cursor: Option<Cursor>,
}
pub trait Pagination<T> {
fn paginate(&self, cursor: Option<Cursor>, page_size: usize) -> Page<'_, T>;
}
impl<T> Pagination<T> for Vec<T> {
#[inline]
fn paginate(&self, cursor: Option<Cursor>, page_size: usize) -> Page<'_, T> {
self.as_slice().paginate(cursor, page_size)
}
}
impl<T> Pagination<T> for [T] {
#[inline]
fn paginate(&self, cursor: Option<Cursor>, page_size: usize) -> Page<'_, T> {
let start = *cursor.unwrap_or_default();
let end = usize::min(start + page_size, self.len());
let items = &self[start..end];
let next_cursor = if end < self.len() {
Some(Cursor(end))
} else {
None
};
Page { items, next_cursor }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_serializes_cursor() {
let cursor = Cursor(42);
let json = serde_json::to_string(&cursor).unwrap();
assert!(json.starts_with("\"") && json.ends_with("\""));
let base64_str = json.trim_matches('"');
let decoded = general_purpose::STANDARD.decode(base64_str).unwrap();
let index: usize = serde_json::from_slice(&decoded).unwrap();
assert_eq!(index, 42);
}
#[test]
fn it_deserializes_cursor() {
let cursor = Cursor(123456);
let json = serde_json::to_string(&cursor).unwrap();
let parsed: Cursor = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cursor);
}
#[test]
fn it_does_roundtrip() {
for i in [0, 1, 42, 9999, usize::MAX / 2] {
let original = Cursor(i);
let json = serde_json::to_string(&original).unwrap();
let decoded: Cursor = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, original);
}
}
#[test]
fn it_returns_invalid_base64() {
let result: Result<Cursor, _> = serde_json::from_str("\"not_base64\"");
assert!(result.is_err());
}
#[test]
fn it_returns_invalid_json_inside_base64() {
let invalid = general_purpose::STANDARD.encode(b"not_json");
let json = format!("\"{invalid}\"");
let result: Result<Cursor, _> = serde_json::from_str(&json);
assert!(result.is_err());
}
#[test]
fn it_paginates_over_vec() {
let data = vec![1, 2, 3, 4, 5];
let mut cursor = None;
let mut collected = vec![];
loop {
let page = data.paginate(cursor, 2);
collected.extend_from_slice(page.items);
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
assert_eq!(collected, data);
}
}