use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cursor(String);
impl Cursor {
pub fn new(value: String) -> Self {
Self(value)
}
pub fn encode(value: &str) -> Self {
let encoded = general_purpose::STANDARD.encode(value.as_bytes());
Self(encoded)
}
pub fn decode(&self) -> Result<String, String> {
general_purpose::STANDARD
.decode(&self.0)
.map_err(|e| format!("Failed to decode cursor: {}", e))
.and_then(|bytes| {
String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in cursor: {}", e))
})
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Cursor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Cursor {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Direction {
Forward,
Backward,
}
#[derive(Debug, Clone)]
pub struct CursorPagination {
pub cursor: Option<Cursor>,
pub limit: u64,
pub direction: Direction,
}
impl Default for CursorPagination {
fn default() -> Self {
Self {
cursor: None,
limit: 20,
direction: Direction::Forward,
}
}
}
impl CursorPagination {
pub fn new(cursor: Option<Cursor>, limit: u64, direction: Direction) -> Self {
Self {
cursor,
limit,
direction,
}
}
pub fn forward(limit: u64) -> Self {
Self {
cursor: None,
limit,
direction: Direction::Forward,
}
}
pub fn after(cursor: Cursor, limit: u64) -> Self {
Self {
cursor: Some(cursor),
limit,
direction: Direction::Forward,
}
}
pub fn before(cursor: Cursor, limit: u64) -> Self {
Self {
cursor: Some(cursor),
limit,
direction: Direction::Backward,
}
}
pub fn sql_limit(&self) -> i64 {
(self.limit + 1) as i64
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Edge<T> {
pub cursor: Cursor,
pub node: T,
}
impl<T> Edge<T> {
pub fn new(cursor: Cursor, node: T) -> Self {
Self { cursor, node }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PageInfo {
pub has_next_page: bool,
pub has_previous_page: bool,
pub start_cursor: Option<Cursor>,
pub end_cursor: Option<Cursor>,
}
impl PageInfo {
pub fn new(
has_next_page: bool,
has_previous_page: bool,
start_cursor: Option<Cursor>,
end_cursor: Option<Cursor>,
) -> Self {
Self {
has_next_page,
has_previous_page,
start_cursor,
end_cursor,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Connection<T> {
pub edges: Vec<Edge<T>>,
pub page_info: PageInfo,
pub total_count: Option<u64>,
}
impl<T> Connection<T> {
pub fn new(
mut edges: Vec<Edge<T>>,
pagination: &CursorPagination,
has_previous: bool,
total_count: Option<u64>,
) -> Self {
let has_next = edges.len() > pagination.limit as usize;
if has_next {
edges.pop();
}
let start_cursor = edges.first().map(|e| e.cursor.clone());
let end_cursor = edges.last().map(|e| e.cursor.clone());
let page_info = PageInfo::new(has_next, has_previous, start_cursor, end_cursor);
Self {
edges,
page_info,
total_count,
}
}
pub fn nodes(&self) -> Vec<&T> {
self.edges.iter().map(|e| &e.node).collect()
}
pub fn into_nodes(self) -> Vec<T> {
self.edges.into_iter().map(|e| e.node).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorBuilder {
fields: Vec<String>,
}
impl CursorBuilder {
pub fn new() -> Self {
Self { fields: Vec::new() }
}
pub fn add_field<T: ToString>(mut self, value: T) -> Self {
self.fields.push(value.to_string());
self
}
pub fn build(self) -> Cursor {
let value = self.fields.join("|");
Cursor::encode(&value)
}
pub fn parse(cursor: &Cursor) -> Result<Vec<String>, String> {
let decoded = cursor.decode()?;
Ok(decoded.split('|').map(|s| s.to_string()).collect())
}
}
impl Default for CursorBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_encode_decode() {
let cursor = Cursor::encode("test-id-123");
let decoded = cursor.decode().unwrap();
assert_eq!(decoded, "test-id-123");
}
#[test]
fn test_cursor_display() {
let cursor = Cursor::new("abc123".to_string());
assert_eq!(format!("{}", cursor), "abc123");
}
#[test]
fn test_cursor_pagination_default() {
let pagination = CursorPagination::default();
assert_eq!(pagination.cursor, None);
assert_eq!(pagination.limit, 20);
assert_eq!(pagination.direction, Direction::Forward);
}
#[test]
fn test_cursor_pagination_forward() {
let pagination = CursorPagination::forward(10);
assert_eq!(pagination.cursor, None);
assert_eq!(pagination.limit, 10);
assert_eq!(pagination.direction, Direction::Forward);
}
#[test]
fn test_cursor_pagination_after() {
let cursor = Cursor::new("abc".to_string());
let pagination = CursorPagination::after(cursor.clone(), 15);
assert_eq!(pagination.cursor, Some(cursor));
assert_eq!(pagination.limit, 15);
assert_eq!(pagination.direction, Direction::Forward);
}
#[test]
fn test_cursor_pagination_before() {
let cursor = Cursor::new("xyz".to_string());
let pagination = CursorPagination::before(cursor.clone(), 10);
assert_eq!(pagination.cursor, Some(cursor));
assert_eq!(pagination.limit, 10);
assert_eq!(pagination.direction, Direction::Backward);
}
#[test]
fn test_cursor_pagination_sql_limit() {
let pagination = CursorPagination::forward(20);
assert_eq!(pagination.sql_limit(), 21); }
#[test]
fn test_edge_creation() {
let cursor = Cursor::new("cursor1".to_string());
let edge = Edge::new(cursor.clone(), "data");
assert_eq!(edge.cursor, cursor);
assert_eq!(edge.node, "data");
}
#[test]
fn test_page_info_creation() {
let cursor1 = Cursor::new("c1".to_string());
let cursor2 = Cursor::new("c2".to_string());
let page_info = PageInfo::new(true, false, Some(cursor1.clone()), Some(cursor2.clone()));
assert!(page_info.has_next_page);
assert!(!page_info.has_previous_page);
assert_eq!(page_info.start_cursor, Some(cursor1));
assert_eq!(page_info.end_cursor, Some(cursor2));
}
#[test]
fn test_connection_creation() {
let edges = vec![
Edge::new(Cursor::new("c1".to_string()), 1),
Edge::new(Cursor::new("c2".to_string()), 2),
Edge::new(Cursor::new("c3".to_string()), 3),
];
let pagination = CursorPagination::forward(2);
let connection = Connection::new(edges, &pagination, false, Some(10));
assert_eq!(connection.edges.len(), 2); assert!(connection.page_info.has_next_page);
assert!(!connection.page_info.has_previous_page);
assert_eq!(connection.total_count, Some(10));
}
#[test]
fn test_connection_nodes() {
let edges = vec![
Edge::new(Cursor::new("c1".to_string()), "a"),
Edge::new(Cursor::new("c2".to_string()), "b"),
];
let pagination = CursorPagination::forward(5);
let connection = Connection::new(edges, &pagination, false, None);
let nodes = connection.nodes();
assert_eq!(nodes, vec![&"a", &"b"]);
}
#[test]
fn test_cursor_builder() {
let cursor = CursorBuilder::new()
.add_field("field1")
.add_field(123)
.add_field("field3")
.build();
let fields = CursorBuilder::parse(&cursor).unwrap();
assert_eq!(fields, vec!["field1", "123", "field3"]);
}
#[test]
fn test_cursor_builder_empty() {
let cursor = CursorBuilder::new().build();
let fields = CursorBuilder::parse(&cursor).unwrap();
assert_eq!(fields, vec![""]);
}
}