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