use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
pub const DEFAULT_PAGE_SIZE: usize = 100;
pub const MAX_PAGE_SIZE: usize = 1000;
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct PaginationQuery {
#[serde(rename = "pageToken")]
pub page_token: Option<String>,
#[serde(rename = "pageSize")]
pub page_size: Option<usize>,
}
impl PaginationQuery {
pub fn effective_page_size(&self) -> usize {
self.page_size
.map(|size| size.clamp(1, MAX_PAGE_SIZE))
.unwrap_or(DEFAULT_PAGE_SIZE)
}
pub fn decode_cursor(&self) -> Option<String> {
self.page_token.as_ref().and_then(|token| {
URL_SAFE_NO_PAD
.decode(token)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
})
}
pub fn paginate<T, F>(&self, items: Vec<T>, cursor_fn: F) -> PagedResponse<T>
where
T: Clone + Debug,
F: Fn(&T) -> String,
{
let page_size = self.effective_page_size();
let cursor = self.decode_cursor();
let start_index = match &cursor {
Some(cursor_key) => items
.iter()
.position(|item| cursor_fn(item) > *cursor_key)
.unwrap_or(items.len()),
None => 0,
};
let end_index = (start_index + page_size).min(items.len());
let page_items: Vec<T> = items[start_index..end_index].to_vec();
let next_page_token = if end_index < items.len() {
page_items.last().map(|item| {
let cursor_key = cursor_fn(item);
URL_SAFE_NO_PAD.encode(cursor_key.as_bytes())
})
} else {
None
};
PagedResponse {
items: page_items,
next_page_token,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PagedResponse<T> {
pub items: Vec<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}
#[allow(dead_code)]
impl<T> PagedResponse<T> {
pub fn new(items: Vec<T>, next_page_token: Option<String>) -> Self {
Self {
items,
next_page_token,
}
}
pub fn has_more(&self) -> bool {
self.next_page_token.is_some()
}
pub fn map<U, F>(self, f: F) -> PagedResponse<U>
where
F: FnMut(T) -> U,
{
PagedResponse {
items: self.items.into_iter().map(f).collect(),
next_page_token: self.next_page_token,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub struct ListNamespacesResponse {
pub namespaces: Vec<Vec<String>>,
#[serde(rename = "next-page-token", skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub struct ListTablesResponse {
pub identifiers: Vec<TableIdentifierResponse>,
#[serde(rename = "next-page-token", skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub struct TableIdentifierResponse {
pub namespace: Vec<String>,
pub name: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effective_page_size_default() {
let query = PaginationQuery::default();
assert_eq!(query.effective_page_size(), DEFAULT_PAGE_SIZE);
}
#[test]
fn test_effective_page_size_custom() {
let query = PaginationQuery {
page_token: None,
page_size: Some(50),
};
assert_eq!(query.effective_page_size(), 50);
}
#[test]
fn test_effective_page_size_clamped_max() {
let query = PaginationQuery {
page_token: None,
page_size: Some(10000),
};
assert_eq!(query.effective_page_size(), MAX_PAGE_SIZE);
}
#[test]
fn test_effective_page_size_clamped_min() {
let query = PaginationQuery {
page_token: None,
page_size: Some(0),
};
assert_eq!(query.effective_page_size(), 1);
}
#[test]
fn test_paginate_first_page() {
let items: Vec<i32> = (1..=10).collect();
let query = PaginationQuery {
page_token: None,
page_size: Some(3),
};
let result = query.paginate(items, |item| item.to_string());
assert_eq!(result.items, vec![1, 2, 3]);
assert!(result.next_page_token.is_some());
}
#[test]
fn test_paginate_second_page() {
let items: Vec<i32> = (1..=10).collect();
let query1 = PaginationQuery {
page_token: None,
page_size: Some(3),
};
let result1 = query1.paginate(items.clone(), |item| item.to_string());
let query2 = PaginationQuery {
page_token: result1.next_page_token,
page_size: Some(3),
};
let result2 = query2.paginate(items, |item| item.to_string());
assert_eq!(result2.items, vec![4, 5, 6]);
assert!(result2.next_page_token.is_some());
}
#[test]
fn test_paginate_last_page() {
let items: Vec<i32> = (1..=5).collect();
let query = PaginationQuery {
page_token: None,
page_size: Some(3),
};
let result1 = query.paginate(items.clone(), |item| item.to_string());
assert_eq!(result1.items, vec![1, 2, 3]);
assert!(result1.next_page_token.is_some());
let query2 = PaginationQuery {
page_token: result1.next_page_token,
page_size: Some(3),
};
let result2 = query2.paginate(items, |item| item.to_string());
assert_eq!(result2.items, vec![4, 5]);
assert!(result2.next_page_token.is_none()); }
#[test]
fn test_paginate_empty() {
let items: Vec<i32> = vec![];
let query = PaginationQuery::default();
let result = query.paginate(items, |item| item.to_string());
assert!(result.items.is_empty());
assert!(result.next_page_token.is_none());
}
#[test]
fn test_paginate_exact_page_size() {
let items: Vec<i32> = (1..=3).collect();
let query = PaginationQuery {
page_token: None,
page_size: Some(3),
};
let result = query.paginate(items, |item| item.to_string());
assert_eq!(result.items, vec![1, 2, 3]);
assert!(result.next_page_token.is_none()); }
#[test]
fn test_decode_cursor_valid() {
let token = URL_SAFE_NO_PAD.encode(b"cursor123");
let query = PaginationQuery {
page_token: Some(token),
page_size: None,
};
assert_eq!(query.decode_cursor(), Some("cursor123".to_string()));
}
#[test]
fn test_decode_cursor_invalid() {
let query = PaginationQuery {
page_token: Some("!!!invalid!!!".to_string()),
page_size: None,
};
assert_eq!(query.decode_cursor(), None);
}
#[test]
fn test_paged_response_map() {
let response = PagedResponse {
items: vec![1, 2, 3],
next_page_token: Some("token".to_string()),
};
let mapped = response.map(|x| x * 2);
assert_eq!(mapped.items, vec![2, 4, 6]);
assert_eq!(mapped.next_page_token, Some("token".to_string()));
}
}