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