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