ultrafast_mcp_core/utils/
pagination.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Cursor-based pagination for MCP list operations
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct Cursor {
7    /// Opaque cursor value
8    pub value: String,
9    /// Optional metadata for the cursor
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub metadata: Option<HashMap<String, serde_json::Value>>,
12}
13
14impl Cursor {
15    /// Create a new cursor with a value
16    pub fn new(value: impl Into<String>) -> Self {
17        Self {
18            value: value.into(),
19            metadata: None,
20        }
21    }
22
23    /// Create a cursor with metadata
24    pub fn with_metadata(
25        value: impl Into<String>,
26        metadata: HashMap<String, serde_json::Value>,
27    ) -> Self {
28        Self {
29            value: value.into(),
30            metadata: Some(metadata),
31        }
32    }
33
34    /// Get the cursor value
35    pub fn value(&self) -> &str {
36        &self.value
37    }
38
39    /// Get cursor metadata
40    pub fn metadata(&self) -> Option<&HashMap<String, serde_json::Value>> {
41        self.metadata.as_ref()
42    }
43}
44
45/// Pagination parameters for list requests
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct PaginationParams {
48    /// Maximum number of items to return
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub limit: Option<u32>,
51    /// Cursor for pagination
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub cursor: Option<Cursor>,
54}
55
56impl PaginationParams {
57    /// Create new pagination parameters
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Set the limit
63    pub fn with_limit(mut self, limit: u32) -> Self {
64        self.limit = Some(limit);
65        self
66    }
67
68    /// Set the cursor
69    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
70        self.cursor = Some(cursor);
71        self
72    }
73
74    /// Get the effective limit, using a default if not set
75    pub fn effective_limit(&self, default: u32) -> u32 {
76        self.limit.unwrap_or(default)
77    }
78}
79
80/// Pagination information in responses
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PaginationInfo {
83    /// Total number of items (if known)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub total: Option<u64>,
86    /// Cursor for the next page
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub next_cursor: Option<Cursor>,
89    /// Whether there are more items
90    pub has_more: bool,
91}
92
93impl PaginationInfo {
94    /// Create pagination info indicating no more items
95    pub fn no_more() -> Self {
96        Self {
97            total: None,
98            next_cursor: None,
99            has_more: false,
100        }
101    }
102
103    /// Create pagination info with a next cursor
104    pub fn with_next(cursor: Cursor) -> Self {
105        Self {
106            total: None,
107            next_cursor: Some(cursor),
108            has_more: true,
109        }
110    }
111
112    /// Create pagination info with total count
113    pub fn with_total(total: u64, next_cursor: Option<Cursor>) -> Self {
114        Self {
115            total: Some(total),
116            next_cursor: next_cursor.clone(),
117            has_more: next_cursor.is_some(),
118        }
119    }
120}
121
122/// A paginated list response
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PaginatedList<T> {
125    /// The items in this page
126    pub items: Vec<T>,
127    /// Pagination information
128    #[serde(flatten)]
129    pub pagination: PaginationInfo,
130}
131
132impl<T> PaginatedList<T> {
133    /// Create a new paginated list
134    pub fn new(items: Vec<T>, pagination: PaginationInfo) -> Self {
135        Self { items, pagination }
136    }
137
138    /// Create a complete list (no pagination)
139    pub fn complete(items: Vec<T>) -> Self {
140        Self {
141            items,
142            pagination: PaginationInfo::no_more(),
143        }
144    }
145
146    /// Create a paginated list with next cursor
147    pub fn with_next(items: Vec<T>, next_cursor: Cursor) -> Self {
148        Self {
149            items,
150            pagination: PaginationInfo::with_next(next_cursor),
151        }
152    }
153
154    /// Get the number of items in this page
155    pub fn len(&self) -> usize {
156        self.items.len()
157    }
158
159    /// Check if this page is empty
160    pub fn is_empty(&self) -> bool {
161        self.items.is_empty()
162    }
163
164    /// Check if there are more pages
165    pub fn has_more(&self) -> bool {
166        self.pagination.has_more
167    }
168
169    /// Get the next cursor if available
170    pub fn next_cursor(&self) -> Option<&Cursor> {
171        self.pagination.next_cursor.as_ref()
172    }
173}
174
175impl<T> IntoIterator for PaginatedList<T> {
176    type Item = T;
177    type IntoIter = std::vec::IntoIter<T>;
178
179    fn into_iter(self) -> Self::IntoIter {
180        self.items.into_iter()
181    }
182}
183
184impl<T> AsRef<[T]> for PaginatedList<T> {
185    fn as_ref(&self) -> &[T] {
186        &self.items
187    }
188}
189
190/// Helper for building paginated responses
191pub struct PaginationBuilder<T> {
192    items: Vec<T>,
193    limit: Option<u32>,
194    total: Option<u64>,
195}
196
197impl<T> PaginationBuilder<T> {
198    /// Create a new pagination builder
199    pub fn new(items: Vec<T>) -> Self {
200        Self {
201            items,
202            limit: None,
203            total: None,
204        }
205    }
206
207    /// Set the page limit
208    pub fn with_limit(mut self, limit: u32) -> Self {
209        self.limit = Some(limit);
210        self
211    }
212
213    /// Set the total count
214    pub fn with_total(mut self, total: u64) -> Self {
215        self.total = Some(total);
216        self
217    }
218
219    /// Build the paginated list
220    pub fn build<F>(self, next_cursor_fn: F) -> PaginatedList<T>
221    where
222        F: FnOnce(&[T]) -> Option<Cursor>,
223    {
224        let has_more = if let Some(limit) = self.limit {
225            self.items.len() > limit as usize
226        } else {
227            false
228        };
229
230        let items = if let Some(limit) = self.limit {
231            self.items.into_iter().take(limit as usize).collect()
232        } else {
233            self.items
234        };
235
236        let next_cursor = if has_more {
237            next_cursor_fn(&items)
238        } else {
239            None
240        };
241
242        let pagination = if let Some(total) = self.total {
243            PaginationInfo::with_total(total, next_cursor)
244        } else if let Some(cursor) = next_cursor {
245            PaginationInfo::with_next(cursor)
246        } else {
247            PaginationInfo::no_more()
248        };
249
250        PaginatedList::new(items, pagination)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_cursor_creation() {
260        let cursor = Cursor::new("abc123");
261        assert_eq!(cursor.value(), "abc123");
262        assert!(cursor.metadata().is_none());
263
264        let mut metadata = HashMap::new();
265        metadata.insert(
266            "key".to_string(),
267            serde_json::Value::String("value".to_string()),
268        );
269        let cursor = Cursor::with_metadata("def456", metadata.clone());
270        assert_eq!(cursor.value(), "def456");
271        assert_eq!(cursor.metadata(), Some(&metadata));
272    }
273
274    #[test]
275    fn test_pagination_params() {
276        let params = PaginationParams::new()
277            .with_limit(10)
278            .with_cursor(Cursor::new("cursor123"));
279
280        assert_eq!(params.limit, Some(10));
281        assert_eq!(params.cursor.as_ref().unwrap().value(), "cursor123");
282        assert_eq!(params.effective_limit(50), 10);
283
284        let params = PaginationParams::new();
285        assert_eq!(params.effective_limit(50), 50);
286    }
287
288    #[test]
289    fn test_paginated_list() {
290        let items = vec![1, 2, 3, 4, 5];
291        let list = PaginatedList::complete(items.clone());
292
293        assert_eq!(list.len(), 5);
294        assert!(!list.has_more());
295        assert!(list.next_cursor().is_none());
296
297        let list = PaginatedList::with_next(items, Cursor::new("next"));
298        assert!(list.has_more());
299        assert_eq!(list.next_cursor().unwrap().value(), "next");
300    }
301
302    #[test]
303    fn test_pagination_builder() {
304        let items = vec![1, 2, 3, 4, 5];
305        let list = PaginationBuilder::new(items).with_limit(3).build(|items| {
306            if !items.is_empty() {
307                Some(Cursor::new(format!("after_{}", items.last().unwrap())))
308            } else {
309                None
310            }
311        });
312
313        assert_eq!(list.len(), 3);
314        assert!(list.has_more());
315        assert_eq!(list.next_cursor().unwrap().value(), "after_3");
316    }
317}