1use base64::{engine::general_purpose, Engine as _};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Cursor(String);
15
16impl Cursor {
17 pub fn new(value: String) -> Self {
19 Self(value)
20 }
21
22 pub fn encode(value: &str) -> Self {
24 let encoded = general_purpose::STANDARD.encode(value.as_bytes());
25 Self(encoded)
26 }
27
28 pub fn decode(&self) -> Result<String, String> {
30 general_purpose::STANDARD
31 .decode(&self.0)
32 .map_err(|e| format!("Failed to decode cursor: {}", e))
33 .and_then(|bytes| {
34 String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in cursor: {}", e))
35 })
36 }
37
38 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42}
43
44impl fmt::Display for Cursor {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "{}", self.0)
47 }
48}
49
50impl From<String> for Cursor {
51 fn from(s: String) -> Self {
52 Self(s)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub enum Direction {
59 Forward,
61 Backward,
63}
64
65#[derive(Debug, Clone)]
67pub struct CursorPagination {
68 pub cursor: Option<Cursor>,
70 pub limit: u64,
72 pub direction: Direction,
74}
75
76impl Default for CursorPagination {
77 fn default() -> Self {
78 Self {
79 cursor: None,
80 limit: 20,
81 direction: Direction::Forward,
82 }
83 }
84}
85
86impl CursorPagination {
87 pub fn new(cursor: Option<Cursor>, limit: u64, direction: Direction) -> Self {
89 Self {
90 cursor,
91 limit,
92 direction,
93 }
94 }
95
96 pub fn forward(limit: u64) -> Self {
98 Self {
99 cursor: None,
100 limit,
101 direction: Direction::Forward,
102 }
103 }
104
105 pub fn after(cursor: Cursor, limit: u64) -> Self {
107 Self {
108 cursor: Some(cursor),
109 limit,
110 direction: Direction::Forward,
111 }
112 }
113
114 pub fn before(cursor: Cursor, limit: u64) -> Self {
116 Self {
117 cursor: Some(cursor),
118 limit,
119 direction: Direction::Backward,
120 }
121 }
122
123 pub fn sql_limit(&self) -> i64 {
125 (self.limit + 1) as i64
127 }
128}
129
130#[derive(Debug, Clone, Serialize)]
132pub struct Edge<T> {
133 pub cursor: Cursor,
135 pub node: T,
137}
138
139impl<T> Edge<T> {
140 pub fn new(cursor: Cursor, node: T) -> Self {
142 Self { cursor, node }
143 }
144}
145
146#[derive(Debug, Clone, Serialize)]
148pub struct PageInfo {
149 pub has_next_page: bool,
151 pub has_previous_page: bool,
153 pub start_cursor: Option<Cursor>,
155 pub end_cursor: Option<Cursor>,
157}
158
159impl PageInfo {
160 pub fn new(
162 has_next_page: bool,
163 has_previous_page: bool,
164 start_cursor: Option<Cursor>,
165 end_cursor: Option<Cursor>,
166 ) -> Self {
167 Self {
168 has_next_page,
169 has_previous_page,
170 start_cursor,
171 end_cursor,
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize)]
178pub struct Connection<T> {
179 pub edges: Vec<Edge<T>>,
181 pub page_info: PageInfo,
183 pub total_count: Option<u64>,
185}
186
187impl<T> Connection<T> {
188 pub fn new(
190 mut edges: Vec<Edge<T>>,
191 pagination: &CursorPagination,
192 has_previous: bool,
193 total_count: Option<u64>,
194 ) -> Self {
195 let has_next = edges.len() > pagination.limit as usize;
196
197 if has_next {
199 edges.pop();
200 }
201
202 let start_cursor = edges.first().map(|e| e.cursor.clone());
203 let end_cursor = edges.last().map(|e| e.cursor.clone());
204
205 let page_info = PageInfo::new(has_next, has_previous, start_cursor, end_cursor);
206
207 Self {
208 edges,
209 page_info,
210 total_count,
211 }
212 }
213
214 pub fn nodes(&self) -> Vec<&T> {
216 self.edges.iter().map(|e| &e.node).collect()
217 }
218
219 pub fn into_nodes(self) -> Vec<T> {
221 self.edges.into_iter().map(|e| e.node).collect()
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct CursorBuilder {
228 fields: Vec<String>,
229}
230
231impl CursorBuilder {
232 pub fn new() -> Self {
234 Self { fields: Vec::new() }
235 }
236
237 pub fn add_field<T: ToString>(mut self, value: T) -> Self {
239 self.fields.push(value.to_string());
240 self
241 }
242
243 pub fn build(self) -> Cursor {
245 let value = self.fields.join("|");
246 Cursor::encode(&value)
247 }
248
249 pub fn parse(cursor: &Cursor) -> Result<Vec<String>, String> {
251 let decoded = cursor.decode()?;
252 Ok(decoded.split('|').map(|s| s.to_string()).collect())
253 }
254}
255
256impl Default for CursorBuilder {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_cursor_encode_decode() {
268 let cursor = Cursor::encode("test-id-123");
269 let decoded = cursor.decode().unwrap();
270 assert_eq!(decoded, "test-id-123");
271 }
272
273 #[test]
274 fn test_cursor_display() {
275 let cursor = Cursor::new("abc123".to_string());
276 assert_eq!(format!("{}", cursor), "abc123");
277 }
278
279 #[test]
280 fn test_cursor_pagination_default() {
281 let pagination = CursorPagination::default();
282 assert_eq!(pagination.cursor, None);
283 assert_eq!(pagination.limit, 20);
284 assert_eq!(pagination.direction, Direction::Forward);
285 }
286
287 #[test]
288 fn test_cursor_pagination_forward() {
289 let pagination = CursorPagination::forward(10);
290 assert_eq!(pagination.cursor, None);
291 assert_eq!(pagination.limit, 10);
292 assert_eq!(pagination.direction, Direction::Forward);
293 }
294
295 #[test]
296 fn test_cursor_pagination_after() {
297 let cursor = Cursor::new("abc".to_string());
298 let pagination = CursorPagination::after(cursor.clone(), 15);
299 assert_eq!(pagination.cursor, Some(cursor));
300 assert_eq!(pagination.limit, 15);
301 assert_eq!(pagination.direction, Direction::Forward);
302 }
303
304 #[test]
305 fn test_cursor_pagination_before() {
306 let cursor = Cursor::new("xyz".to_string());
307 let pagination = CursorPagination::before(cursor.clone(), 10);
308 assert_eq!(pagination.cursor, Some(cursor));
309 assert_eq!(pagination.limit, 10);
310 assert_eq!(pagination.direction, Direction::Backward);
311 }
312
313 #[test]
314 fn test_cursor_pagination_sql_limit() {
315 let pagination = CursorPagination::forward(20);
316 assert_eq!(pagination.sql_limit(), 21); }
318
319 #[test]
320 fn test_edge_creation() {
321 let cursor = Cursor::new("cursor1".to_string());
322 let edge = Edge::new(cursor.clone(), "data");
323 assert_eq!(edge.cursor, cursor);
324 assert_eq!(edge.node, "data");
325 }
326
327 #[test]
328 fn test_page_info_creation() {
329 let cursor1 = Cursor::new("c1".to_string());
330 let cursor2 = Cursor::new("c2".to_string());
331 let page_info = PageInfo::new(true, false, Some(cursor1.clone()), Some(cursor2.clone()));
332
333 assert!(page_info.has_next_page);
334 assert!(!page_info.has_previous_page);
335 assert_eq!(page_info.start_cursor, Some(cursor1));
336 assert_eq!(page_info.end_cursor, Some(cursor2));
337 }
338
339 #[test]
340 fn test_connection_creation() {
341 let edges = vec![
342 Edge::new(Cursor::new("c1".to_string()), 1),
343 Edge::new(Cursor::new("c2".to_string()), 2),
344 Edge::new(Cursor::new("c3".to_string()), 3),
345 ];
346
347 let pagination = CursorPagination::forward(2);
348 let connection = Connection::new(edges, &pagination, false, Some(10));
349
350 assert_eq!(connection.edges.len(), 2); assert!(connection.page_info.has_next_page);
352 assert!(!connection.page_info.has_previous_page);
353 assert_eq!(connection.total_count, Some(10));
354 }
355
356 #[test]
357 fn test_connection_nodes() {
358 let edges = vec![
359 Edge::new(Cursor::new("c1".to_string()), "a"),
360 Edge::new(Cursor::new("c2".to_string()), "b"),
361 ];
362
363 let pagination = CursorPagination::forward(5);
364 let connection = Connection::new(edges, &pagination, false, None);
365
366 let nodes = connection.nodes();
367 assert_eq!(nodes, vec![&"a", &"b"]);
368 }
369
370 #[test]
371 fn test_cursor_builder() {
372 let cursor = CursorBuilder::new()
373 .add_field("field1")
374 .add_field(123)
375 .add_field("field3")
376 .build();
377
378 let fields = CursorBuilder::parse(&cursor).unwrap();
379 assert_eq!(fields, vec!["field1", "123", "field3"]);
380 }
381
382 #[test]
383 fn test_cursor_builder_empty() {
384 let cursor = CursorBuilder::new().build();
385 let fields = CursorBuilder::parse(&cursor).unwrap();
386 assert_eq!(fields, vec![""]);
387 }
388}