code_analyze_mcp/
pagination.rs1use base64::engine::general_purpose::STANDARD;
7use base64::{DecodeError, engine::Engine};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11pub const DEFAULT_PAGE_SIZE: usize = 100;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum PaginationMode {
17 Default,
19 Callers,
21 Callees,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CursorData {
28 pub mode: PaginationMode,
30 pub offset: usize,
32}
33
34#[derive(Debug, Error)]
35pub enum PaginationError {
36 #[error("Invalid cursor: {0}")]
37 InvalidCursor(String),
38}
39
40impl From<DecodeError> for PaginationError {
41 fn from(err: DecodeError) -> Self {
42 PaginationError::InvalidCursor(format!("Base64 decode error: {}", err))
43 }
44}
45
46impl From<serde_json::Error> for PaginationError {
47 fn from(err: serde_json::Error) -> Self {
48 PaginationError::InvalidCursor(format!("JSON parse error: {}", err))
49 }
50}
51
52pub fn encode_cursor(data: &CursorData) -> Result<String, PaginationError> {
58 let json = serde_json::to_string(data)?;
59 Ok(STANDARD.encode(json))
60}
61
62pub fn decode_cursor(cursor: &str) -> Result<CursorData, PaginationError> {
68 let decoded = STANDARD.decode(cursor)?;
69 let json_str = String::from_utf8(decoded)
70 .map_err(|e| PaginationError::InvalidCursor(format!("UTF-8 decode error: {}", e)))?;
71 Ok(serde_json::from_str(&json_str)?)
72}
73
74#[derive(Debug, Clone)]
75pub struct PaginationResult<T> {
76 pub items: Vec<T>,
77 pub next_cursor: Option<String>,
78 pub total: usize,
79}
80
81pub fn paginate_slice<T: Clone>(
90 items: &[T],
91 offset: usize,
92 page_size: usize,
93 mode: PaginationMode,
94) -> Result<PaginationResult<T>, PaginationError> {
95 let total = items.len();
96
97 if offset >= total {
98 return Ok(PaginationResult {
99 items: vec![],
100 next_cursor: None,
101 total,
102 });
103 }
104
105 let end = std::cmp::min(offset + page_size, total);
106 let page_items = items[offset..end].to_vec();
107
108 let next_cursor = if end < total {
109 let cursor_data = CursorData { mode, offset: end };
110 Some(encode_cursor(&cursor_data)?)
111 } else {
112 None
113 };
114
115 Ok(PaginationResult {
116 items: page_items,
117 next_cursor,
118 total,
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_cursor_encode_decode_roundtrip() {
128 let original = CursorData {
129 mode: PaginationMode::Default,
130 offset: 42,
131 };
132
133 let encoded = encode_cursor(&original).expect("encode failed");
134 let decoded = decode_cursor(&encoded).expect("decode failed");
135
136 assert_eq!(decoded.mode, original.mode);
137 assert_eq!(decoded.offset, original.offset);
138 }
139
140 #[test]
141 fn test_pagination_mode_wire_format() {
142 let cursor_data = CursorData {
143 mode: PaginationMode::Callers,
144 offset: 0,
145 };
146
147 let encoded = encode_cursor(&cursor_data).expect("encode failed");
148 let decoded = decode_cursor(&encoded).expect("decode failed");
149
150 assert_eq!(decoded.mode, PaginationMode::Callers);
151
152 let json_str = serde_json::to_string(&cursor_data).expect("serialize failed");
153 assert!(
154 json_str.contains("\"mode\":\"callers\""),
155 "expected lowercase 'callers' in JSON, got: {}",
156 json_str
157 );
158 }
159
160 #[test]
161 fn test_paginate_slice_middle_page() {
162 let items: Vec<i32> = (0..250).collect();
163 let result =
164 paginate_slice(&items, 100, 100, PaginationMode::Default).expect("paginate failed");
165
166 assert_eq!(result.items.len(), 100);
167 assert_eq!(result.items[0], 100);
168 assert_eq!(result.items[99], 199);
169 assert!(result.next_cursor.is_some());
170 assert_eq!(result.total, 250);
171 }
172
173 #[test]
174 fn test_paginate_slice_empty_and_beyond() {
175 let empty: Vec<i32> = vec![];
176 let result =
177 paginate_slice(&empty, 0, 100, PaginationMode::Default).expect("paginate failed");
178 assert_eq!(result.items.len(), 0);
179 assert!(result.next_cursor.is_none());
180 assert_eq!(result.total, 0);
181
182 let items: Vec<i32> = (0..50).collect();
183 let result =
184 paginate_slice(&items, 100, 100, PaginationMode::Default).expect("paginate failed");
185 assert_eq!(result.items.len(), 0);
186 assert!(result.next_cursor.is_none());
187 assert_eq!(result.total, 50);
188 }
189
190 #[test]
191 fn test_paginate_slice_exact_boundary() {
192 let items: Vec<i32> = (0..200).collect();
193 let result =
194 paginate_slice(&items, 100, 100, PaginationMode::Default).expect("paginate failed");
195
196 assert_eq!(result.items.len(), 100);
197 assert_eq!(result.items[0], 100);
198 assert!(result.next_cursor.is_none());
199 assert_eq!(result.total, 200);
200 }
201
202 #[test]
203 fn test_invalid_cursor_error() {
204 let result = decode_cursor("not-valid-base64!!!");
205 assert!(result.is_err());
206 match result {
207 Err(PaginationError::InvalidCursor(msg)) => {
208 assert!(msg.contains("Base64") || msg.contains("decode"));
209 }
210 _ => panic!("Expected InvalidCursor error"),
211 }
212 }
213}