Skip to main content

agentic_evolve_core/query/
pagination.rs

1//! Cursor-based pagination for token-efficient result delivery.
2
3use serde::{Deserialize, Serialize};
4
5/// A page of results using cursor-based pagination.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct CursorPage<T> {
8    /// The items in this page.
9    pub items: Vec<T>,
10    /// Cursor for the next page, if any.
11    pub next_cursor: Option<String>,
12    /// Whether more results are available beyond this page.
13    pub has_more: bool,
14    /// Total number of items across all pages (if known).
15    pub total: Option<usize>,
16}
17
18impl<T: Clone> CursorPage<T> {
19    /// Create a page from a full slice, given a cursor (starting index) and limit.
20    ///
21    /// The cursor is a stringified index. If `None`, starts from 0.
22    pub fn from_slice(data: &[T], cursor: Option<&str>, limit: usize) -> Self {
23        let start = cursor.and_then(|c| c.parse::<usize>().ok()).unwrap_or(0);
24
25        let clamped_start = start.min(data.len());
26        let end = (clamped_start + limit).min(data.len());
27        let items = data[clamped_start..end].to_vec();
28        let has_more = end < data.len();
29        let next_cursor = if has_more {
30            Some(end.to_string())
31        } else {
32            None
33        };
34
35        Self {
36            items,
37            next_cursor,
38            has_more,
39            total: Some(data.len()),
40        }
41    }
42
43    /// Number of items in this page.
44    pub fn count(&self) -> usize {
45        self.items.len()
46    }
47
48    /// Whether this page is empty.
49    pub fn is_empty(&self) -> bool {
50        self.items.is_empty()
51    }
52
53    /// Map items to another type.
54    pub fn map<U: Clone, F: Fn(&T) -> U>(&self, f: F) -> CursorPage<U> {
55        CursorPage {
56            items: self.items.iter().map(f).collect(),
57            next_cursor: self.next_cursor.clone(),
58            has_more: self.has_more,
59            total: self.total,
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn from_slice_first_page() {
70        let data: Vec<i32> = (0..10).collect();
71        let page = CursorPage::from_slice(&data, None, 3);
72        assert_eq!(page.items, vec![0, 1, 2]);
73        assert!(page.has_more);
74        assert_eq!(page.next_cursor, Some("3".to_string()));
75        assert_eq!(page.total, Some(10));
76    }
77
78    #[test]
79    fn from_slice_second_page() {
80        let data: Vec<i32> = (0..10).collect();
81        let page = CursorPage::from_slice(&data, Some("3"), 3);
82        assert_eq!(page.items, vec![3, 4, 5]);
83        assert!(page.has_more);
84        assert_eq!(page.next_cursor, Some("6".to_string()));
85    }
86
87    #[test]
88    fn from_slice_last_page() {
89        let data: Vec<i32> = (0..10).collect();
90        let page = CursorPage::from_slice(&data, Some("8"), 5);
91        assert_eq!(page.items, vec![8, 9]);
92        assert!(!page.has_more);
93        assert_eq!(page.next_cursor, None);
94    }
95
96    #[test]
97    fn from_slice_exact_fit() {
98        let data = vec![1, 2, 3];
99        let page = CursorPage::from_slice(&data, None, 3);
100        assert_eq!(page.items, vec![1, 2, 3]);
101        assert!(!page.has_more);
102    }
103
104    #[test]
105    fn from_slice_empty() {
106        let data: Vec<i32> = vec![];
107        let page = CursorPage::from_slice(&data, None, 10);
108        assert!(page.is_empty());
109        assert!(!page.has_more);
110    }
111
112    #[test]
113    fn from_slice_cursor_beyond_end() {
114        let data = vec![1, 2, 3];
115        let page = CursorPage::from_slice(&data, Some("100"), 5);
116        assert!(page.is_empty());
117        assert!(!page.has_more);
118    }
119
120    #[test]
121    fn map_transforms_items() {
122        let data = vec![1, 2, 3];
123        let page = CursorPage::from_slice(&data, None, 10);
124        let mapped = page.map(|x| x * 10);
125        assert_eq!(mapped.items, vec![10, 20, 30]);
126    }
127
128    #[test]
129    fn count_and_is_empty() {
130        let data = vec![1, 2];
131        let page = CursorPage::from_slice(&data, None, 10);
132        assert_eq!(page.count(), 2);
133        assert!(!page.is_empty());
134    }
135}