Files
khadhroony-bot3/ks-store/src/contracts/pagination.rs

177 lines
5.8 KiB
Rust

// file: ks-store/src/contracts/pagination.rs
// version: 5
//! Backend-neutral pagination contracts for repository operations.
/// Default page size for repository list operations.
pub const DEFAULT_PAGE_SIZE: u16 = 100;
/// Maximum page size for repository list operations.
pub const MAX_PAGE_SIZE: u16 = 500;
/// Maximum serialized opaque cursor length accepted by repository list operations.
pub const MAX_PAGE_CURSOR_LENGTH: usize = 512;
/// Sort direction for repository list operations.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum SortDirection {
/// Sort values in ascending order.
Asc,
/// Sort values in descending order.
Desc,
}
/// Stable cursor request contract for repository list operations.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PageRequest {
/// Maximum number of rows to return.
pub limit: u16,
/// Optional opaque cursor returned by the previous page.
pub cursor: std::option::Option<std::string::String>,
}
impl crate::PageRequest {
/// Builds a page request after validating the limit and opaque cursor bounds.
pub fn new(
limit: u16,
cursor: std::option::Option<std::string::String>,
) -> ks_core::Result<Self> {
if limit == 0 {
return std::result::Result::Err(ks_core::Error::db(
"page limit must be greater than zero",
));
}
if limit > crate::MAX_PAGE_SIZE {
return std::result::Result::Err(ks_core::Error::db(
"page limit exceeds maximum page size",
));
}
let cursor_value = cursor.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
return std::option::Option::None;
}
return std::option::Option::Some(trimmed.to_string());
});
if cursor_value
.as_deref()
.is_some_and(|value| return value.len() > crate::MAX_PAGE_CURSOR_LENGTH)
{
return std::result::Result::Err(ks_core::Error::db(
"page cursor exceeds maximum encoded length",
));
}
return std::result::Result::Ok(Self { limit, cursor: cursor_value });
}
/// Builds the default first page request.
pub fn first_page() -> Self {
return Self {
limit: crate::DEFAULT_PAGE_SIZE,
cursor: std::option::Option::None,
};
}
/// Builds a request continuing after an opaque cursor returned by a previous page.
pub fn after(
limit: u16,
cursor: impl std::convert::Into<std::string::String>,
) -> ks_core::Result<Self> {
return Self::new(limit, std::option::Option::Some(cursor.into()));
}
}
/// One bounded page of rows and its continuation cursor.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct PageSlice<T> {
/// Rows returned for this page.
pub rows: std::vec::Vec<T>,
/// Opaque cursor for the next page when additional rows exist.
pub next_cursor: std::option::Option<std::string::String>,
}
impl<T> crate::PageSlice<T> {
/// Builds one page from already bounded rows and an optional continuation cursor.
pub fn new(
rows: std::vec::Vec<T>,
next_cursor: std::option::Option<std::string::String>,
) -> Self {
return Self { rows, next_cursor };
}
/// Returns true when this page has no continuation cursor.
pub fn is_last_page(&self) -> bool {
return self.next_cursor.is_none();
}
}
/// One bounded page with a refreshed total row count for the same filter.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct CountedPageSlice<T> {
/// Rows returned for this page.
pub rows: std::vec::Vec<T>,
/// Opaque cursor for the next page when additional rows exist.
pub next_cursor: std::option::Option<std::string::String>,
/// Total number of rows matching the filter at query time.
pub total_rows: u64,
}
impl<T> crate::CountedPageSlice<T> {
/// Builds one counted page.
pub fn new(
rows: std::vec::Vec<T>,
next_cursor: std::option::Option<std::string::String>,
total_rows: u64,
) -> Self {
return Self { rows, next_cursor, total_rows };
}
/// Returns true when this page has no continuation cursor.
pub fn is_last_page(&self) -> bool {
return self.next_cursor.is_none();
}
}
#[cfg(test)]
mod tests {
#[test]
fn page_request_rejects_zero_limit() {
let result = crate::PageRequest::new(0, std::option::Option::None);
assert!(result.is_err());
}
#[test]
fn page_request_rejects_limit_above_maximum() {
let result = crate::PageRequest::new(crate::MAX_PAGE_SIZE + 1, std::option::Option::None);
assert!(result.is_err());
}
#[test]
fn page_request_rejects_oversized_cursor() {
let result = crate::PageRequest::after(10, "x".repeat(crate::MAX_PAGE_CURSOR_LENGTH + 1));
assert!(result.is_err());
}
#[test]
fn first_page_uses_default_limit_without_cursor() {
let request = crate::PageRequest::first_page();
assert_eq!(request.limit, crate::DEFAULT_PAGE_SIZE);
assert!(request.cursor.is_none());
}
#[test]
fn page_slice_reports_last_page_from_cursor_presence() {
let page = crate::PageSlice::new(vec![1_u32], std::option::Option::None);
assert!(page.is_last_page());
}
#[test]
fn counted_page_preserves_total_rows_and_cursor_state() {
let page = crate::CountedPageSlice::new(
vec![1_u32, 2_u32],
std::option::Option::Some("cursor".to_string()),
501,
);
assert_eq!(page.total_rows, 501);
assert!(!page.is_last_page());
}
}