use serde::{Deserialize, Serialize};
use crate::error::DataError;
use super::{Cursor, MAX_PAGE_SIZE, SortKey};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysetPosition {
pub values: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysetPageRequest {
pub sort: Vec<SortKey>,
pub after: Option<Cursor>,
pub limit: u32,
}
impl KeysetPageRequest {
pub fn new(sort: Vec<SortKey>, limit: u32) -> Result<Self, DataError> {
if sort.is_empty() {
return Err(DataError::InvalidPage(
"keyset pagination requires at least one sort key".into(),
));
}
if limit < 1 {
return Err(DataError::InvalidPage("keyset limit must be >= 1".into()));
}
let limit = limit.min(MAX_PAGE_SIZE);
Ok(KeysetPageRequest { sort, after: None, limit })
}
pub fn decoded_after(&self) -> Result<Option<KeysetPosition>, DataError> {
self.after.as_ref().map(|c| c.decode::<KeysetPosition>()).transpose()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysetPage<T> {
pub items: Vec<T>,
pub start_cursor: Option<Cursor>,
pub end_cursor: Option<Cursor>,
pub has_next_page: bool,
pub has_prev_page: bool,
}
impl<T> KeysetPage<T> {
pub fn from_items<F>(
items: Vec<T>,
encode: F,
has_prev: bool,
has_next: bool,
) -> Result<Self, DataError>
where
F: Fn(&T) -> Vec<serde_json::Value>,
{
let start_cursor = items
.first()
.map(|item| Cursor::encode(&KeysetPosition { values: encode(item) }))
.transpose()?;
let end_cursor = items
.last()
.map(|item| Cursor::encode(&KeysetPosition { values: encode(item) }))
.transpose()?;
Ok(KeysetPage {
items,
start_cursor,
end_cursor,
has_next_page: has_next,
has_prev_page: has_prev,
})
}
pub fn map<U, F: FnMut(T) -> U>(self, f: F) -> KeysetPage<U> {
KeysetPage {
items: self.items.into_iter().map(f).collect(),
start_cursor: self.start_cursor,
end_cursor: self.end_cursor,
has_next_page: self.has_next_page,
has_prev_page: self.has_prev_page,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyset_page_builds_correctly() {
let items = vec![("alice", 100i64), ("bob", 200)];
let page = KeysetPage::from_items(
items,
|(name, ts)| {
vec![
serde_json::Value::String((*name).to_string()),
serde_json::Value::Number(serde_json::Number::from(*ts)),
]
},
false,
true,
)
.unwrap();
assert_eq!(page.items.len(), 2);
assert!(page.start_cursor.is_some());
assert!(page.end_cursor.is_some());
assert!(page.has_next_page);
let pos: KeysetPosition = page.start_cursor.unwrap().decode().unwrap();
assert_eq!(pos.values.len(), 2);
assert_eq!(pos.values[0].as_str().unwrap(), "alice");
}
#[test]
fn keyset_request_validates_sort_nonempty() {
let err = KeysetPageRequest::new(vec![], 10).unwrap_err();
match err {
DataError::InvalidPage(msg) => assert!(msg.contains("sort key")),
other => panic!("expected InvalidPage, got {other:?}"),
}
}
#[test]
fn keyset_request_limit_capped() {
let req = KeysetPageRequest::new(vec![SortKey::asc("id")], 9999).unwrap();
assert_eq!(req.limit, MAX_PAGE_SIZE);
}
}