use serde::{Deserialize, Serialize};
use crate::error::{ApiError, ValidationDetails};
pub const DEFAULT_PAGE_LIMIT: u32 = 50;
pub const MAX_PAGE_LIMIT: u32 = 100;
pub const MAX_CURSOR_BYTES: usize = 512;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Cursor(String);
impl Cursor {
pub fn new(cursor: impl Into<String>) -> Self {
Self(cursor.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PageRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<Cursor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
impl PageRequest {
pub fn first() -> Self {
Self::default()
}
pub fn after(cursor: Cursor) -> Self {
Self { cursor: Some(cursor), limit: None }
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn validate(&self) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if self.cursor.as_ref().is_some_and(|c| c.as_str().len() > MAX_CURSOR_BYTES) {
details.add("cursor", format!("is longer than {MAX_CURSOR_BYTES} bytes"));
}
details.into_result()
}
pub fn limit_or_default(&self) -> u32 {
self.limit.map_or(DEFAULT_PAGE_LIMIT, |limit| limit.clamp(1, MAX_PAGE_LIMIT))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Page<T> {
pub items: Vec<T>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<Cursor>,
}
impl<T> Page<T> {
pub fn new(items: Vec<T>, next_cursor: Option<Cursor>) -> Self {
Self { items, next_cursor }
}
pub fn is_last(&self) -> bool {
self.next_cursor.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limits_are_clamped() {
assert_eq!(PageRequest::first().limit_or_default(), DEFAULT_PAGE_LIMIT);
assert_eq!(PageRequest::first().with_limit(0).limit_or_default(), 1);
assert_eq!(PageRequest::first().with_limit(10_000).limit_or_default(), MAX_PAGE_LIMIT);
assert_eq!(PageRequest::after(Cursor::new("c")).with_limit(7).limit_or_default(), 7);
assert!(PageRequest::after(Cursor::new("c".repeat(MAX_CURSOR_BYTES))).validate().is_ok());
assert!(PageRequest::after(Cursor::new("c".repeat(MAX_CURSOR_BYTES + 1))).validate().is_err());
}
#[test]
fn page_json() {
let page = Page::new(vec![1, 2], Some(Cursor::new("abc")));
assert_eq!(serde_json::to_string(&page).ok().as_deref(), Some(r#"{"items":[1,2],"next_cursor":"abc"}"#));
let last: Page<i32> = serde_json::from_str(r#"{"items":[]}"#).unwrap_or_else(|_| Page::new(vec![9], None));
assert!(last.is_last() && last.items.is_empty());
let null_cursor: Option<Page<i32>> = serde_json::from_str(r#"{"items":[3],"next_cursor":null}"#).ok();
assert_eq!(null_cursor, Some(Page::new(vec![3], None)));
assert_eq!(serde_json::to_string(&PageRequest::first()).ok().as_deref(), Some("{}"));
}
}