1use crate::builder::Value;
4
5use super::encoding::{base64_decode, base64_encode, escape_json, split_json_pairs, unescape_json};
6
7const MAX_CURSOR_SIZE: usize = 4 * 1024;
10
11const MAX_CURSOR_FIELDS: usize = 16;
15
16#[derive(Debug, Clone, PartialEq)]
33#[non_exhaustive]
34#[must_use = "cursor must be encoded with .encode() or used with a query builder"]
35pub struct Cursor {
36 pub fields: Vec<(String, Value)>,
38}
39
40impl Cursor {
41 #[must_use]
43 pub const fn new() -> Self {
44 Self { fields: Vec::new() }
45 }
46
47 pub fn field(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
49 self.fields.push((name.into(), value.into()));
50 self
51 }
52
53 pub fn int(self, name: impl Into<String>, value: i64) -> Self {
55 self.field(name, Value::Int(value))
56 }
57
58 pub fn string(self, name: impl Into<String>, value: impl Into<String>) -> Self {
60 self.field(name, Value::String(value.into()))
61 }
62
63 #[must_use]
67 pub fn encode(&self) -> String {
68 let json = self.to_json();
69 base64_encode(&json)
70 }
71
72 pub fn decode(encoded: &str) -> Result<Self, CursorError> {
76 if encoded.len() > MAX_CURSOR_SIZE {
78 return Err(CursorError::TooLarge);
79 }
80 let json = base64_decode(encoded).map_err(|()| CursorError::InvalidBase64)?;
81 Self::from_json(&json)
82 }
83
84 fn to_json(&self) -> String {
86 let mut parts = Vec::new();
87 for (name, value) in &self.fields {
88 let val_str = match value {
89 Value::Null => "null".to_string(),
90 Value::Bool(b) => b.to_string(),
91 Value::Int(i) => i.to_string(),
92 Value::Float(f) => f.to_string(),
93 Value::String(s) => format!("\"{}\"", escape_json(s)),
94 Value::Array(_) => continue, };
96 parts.push(format!("\"{name}\":{val_str}"));
97 }
98 format!("{{{}}}", parts.join(","))
99 }
100
101 fn from_json(json: &str) -> Result<Self, CursorError> {
103 let mut cursor = Self::new();
104 let json = json.trim();
105
106 if !json.starts_with('{') || !json.ends_with('}') {
107 return Err(CursorError::InvalidFormat);
108 }
109
110 let inner = &json[1..json.len() - 1];
111 if inner.is_empty() {
112 return Ok(cursor);
113 }
114
115 for pair in split_json_pairs(inner) {
117 let pair = pair.trim();
118 if pair.is_empty() {
119 continue;
120 }
121
122 let colon_idx = pair.find(':').ok_or(CursorError::InvalidFormat)?;
123 let key = pair[..colon_idx].trim();
124 let value = pair[colon_idx + 1..].trim();
125
126 if !key.starts_with('"') || !key.ends_with('"') {
128 return Err(CursorError::InvalidFormat);
129 }
130 let key = &key[1..key.len() - 1];
131
132 let parsed_value = if value == "null" {
134 Value::Null
135 } else if value == "true" {
136 Value::Bool(true)
137 } else if value == "false" {
138 Value::Bool(false)
139 } else if value.starts_with('"') && value.ends_with('"') {
140 Value::String(unescape_json(&value[1..value.len() - 1]))
141 } else if value.contains('.') {
142 value
143 .parse::<f64>()
144 .map(Value::Float)
145 .map_err(|_| CursorError::InvalidFormat)?
146 } else {
147 value
148 .parse::<i64>()
149 .map(Value::Int)
150 .map_err(|_| CursorError::InvalidFormat)?
151 };
152
153 cursor.fields.push((key.to_string(), parsed_value));
154
155 if cursor.fields.len() > MAX_CURSOR_FIELDS {
157 return Err(CursorError::TooManyFields);
158 }
159 }
160
161 Ok(cursor)
162 }
163}
164
165impl Default for Cursor {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173#[non_exhaustive]
174pub enum CursorError {
175 InvalidBase64,
177 InvalidFormat,
179 TooLarge,
181 TooManyFields,
183}
184
185impl std::fmt::Display for CursorError {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Self::InvalidBase64 => write!(f, "invalid base64 encoding in cursor"),
189 Self::InvalidFormat => write!(f, "invalid cursor format (expected JSON object)"),
190 Self::TooLarge => write!(
191 f,
192 "cursor exceeds maximum size ({}KB limit)",
193 MAX_CURSOR_SIZE / 1024
194 ),
195 Self::TooManyFields => {
196 write!(f, "cursor has too many fields (max {MAX_CURSOR_FIELDS})")
197 },
198 }
199 }
200}
201
202impl std::error::Error for CursorError {}
203
204impl CursorError {
205 #[inline]
209 #[must_use]
210 pub const fn is_format_error(&self) -> bool {
211 matches!(self, Self::InvalidBase64 | Self::InvalidFormat)
212 }
213
214 #[inline]
218 #[must_use]
219 pub const fn is_limit_error(&self) -> bool {
220 matches!(self, Self::TooLarge | Self::TooManyFields)
221 }
222}
223
224pub trait IntoCursor {
246 fn into_cursor(self) -> Option<Cursor>;
249}
250
251impl IntoCursor for Cursor {
252 fn into_cursor(self) -> Option<Cursor> {
253 if self.fields.is_empty() {
255 None
256 } else {
257 Some(self)
258 }
259 }
260}
261
262impl IntoCursor for &str {
263 fn into_cursor(self) -> Option<Cursor> {
264 if self.is_empty() || self.len() > MAX_CURSOR_SIZE {
265 return None;
266 }
267 Cursor::decode(self).ok()
268 }
269}
270
271impl IntoCursor for String {
272 fn into_cursor(self) -> Option<Cursor> {
273 self.as_str().into_cursor()
274 }
275}
276
277impl IntoCursor for &String {
278 fn into_cursor(self) -> Option<Cursor> {
279 self.as_str().into_cursor()
280 }
281}
282
283impl<T: IntoCursor> IntoCursor for Option<T> {
284 fn into_cursor(self) -> Option<Cursor> {
285 self.and_then(IntoCursor::into_cursor)
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::pagination::encoding::base64_encode;
293
294 #[test]
295 fn test_cursor_encode_decode() {
296 let cursor = Cursor::new().int("id", 100).string("name", "Alice");
297
298 let encoded = cursor.encode();
299 let decoded = Cursor::decode(&encoded).unwrap();
300
301 assert_eq!(cursor.fields, decoded.fields);
302 }
303
304 #[test]
305 fn test_cursor_empty() {
306 let cursor = Cursor::new();
307 let encoded = cursor.encode();
308 let decoded = Cursor::decode(&encoded).unwrap();
309 assert!(decoded.fields.is_empty());
310 }
311
312 #[test]
313 fn test_cursor_with_special_chars() {
314 let cursor = Cursor::new().string("name", "Hello \"World\"");
315
316 let encoded = cursor.encode();
317 let decoded = Cursor::decode(&encoded).unwrap();
318
319 assert_eq!(cursor.fields, decoded.fields);
320 }
321
322 #[test]
323 fn test_cursor_with_float() {
324 let cursor = Cursor::new().field("score", 1.234f64);
325
326 let encoded = cursor.encode();
327 let decoded = Cursor::decode(&encoded).unwrap();
328
329 assert_eq!(decoded.fields.len(), 1);
330 let Value::Float(f) = &decoded.fields[0].1 else {
331 panic!("expected Value::Float, got {:?}", decoded.fields[0].1)
332 };
333 assert!((f - 1.234).abs() < 0.001);
334 }
335
336 #[test]
337 fn test_cursor_invalid_base64() {
338 let result = Cursor::decode("not valid base64!!!");
339 assert!(matches!(result, Err(CursorError::InvalidBase64)));
340 }
341
342 #[test]
343 fn test_cursor_too_large() {
344 let oversized = "a".repeat(5 * 1024);
346 let result = Cursor::decode(&oversized);
347 assert!(matches!(result, Err(CursorError::TooLarge)));
348
349 let cursor: Option<Cursor> = oversized.as_str().into_cursor();
351 assert!(cursor.is_none());
352 }
353
354 #[test]
355 fn test_cursor_too_many_fields() {
356 let mut fields = Vec::new();
358 for i in 0..20 {
359 fields.push(format!("\"f{i}\":1"));
360 }
361 let json = format!("{{{}}}", fields.join(","));
362 let encoded = base64_encode(&json);
363
364 let result = Cursor::decode(&encoded);
365 assert!(matches!(result, Err(CursorError::TooManyFields)));
366
367 let cursor: Option<Cursor> = encoded.as_str().into_cursor();
369 assert!(cursor.is_none());
370 }
371
372 #[test]
373 fn test_cursor_exactly_at_max_fields() {
374 let mut fields = Vec::new();
376 for i in 0..16 {
377 fields.push(format!("\"f{i}\":1"));
378 }
379 let json = format!("{{{}}}", fields.join(","));
380 let encoded = base64_encode(&json);
381
382 let result = Cursor::decode(&encoded);
383 assert!(
384 result.is_ok(),
385 "Cursor with exactly 16 fields should succeed"
386 );
387 assert_eq!(result.unwrap().fields.len(), 16);
388 }
389
390 #[test]
391 fn test_cursor_one_under_max_fields() {
392 let mut fields = Vec::new();
394 for i in 0..15 {
395 fields.push(format!("\"f{i}\":1"));
396 }
397 let json = format!("{{{}}}", fields.join(","));
398 let encoded = base64_encode(&json);
399
400 let result = Cursor::decode(&encoded);
401 assert!(result.is_ok(), "Cursor with 15 fields should succeed");
402 assert_eq!(result.unwrap().fields.len(), 15);
403 }
404
405 #[test]
406 fn test_cursor_one_over_max_fields() {
407 let mut fields = Vec::new();
409 for i in 0..17 {
410 fields.push(format!("\"f{i}\":1"));
411 }
412 let json = format!("{{{}}}", fields.join(","));
413 let encoded = base64_encode(&json);
414
415 let result = Cursor::decode(&encoded);
416 assert!(matches!(result, Err(CursorError::TooManyFields)));
417 }
418
419 #[test]
420 fn test_cursor_near_max_size() {
421 let long_value = "x".repeat(200);
425 let cursor = Cursor::new()
426 .string("f1", &long_value)
427 .string("f2", &long_value)
428 .string("f3", &long_value)
429 .string("f4", &long_value);
430
431 let encoded = cursor.encode();
432 assert!(encoded.len() < 4096, "Cursor should be under 4KB limit");
433
434 let decoded = Cursor::decode(&encoded);
436 assert!(decoded.is_ok());
437 }
438
439 #[test]
440 fn test_cursor_exactly_at_max_size_boundary() {
441 let oversized = "a".repeat(4097);
444 let result = Cursor::decode(&oversized);
445 assert!(matches!(result, Err(CursorError::TooLarge)));
446
447 let at_limit = "a".repeat(4096);
449 let result = Cursor::decode(&at_limit);
450 assert!(!matches!(result, Err(CursorError::TooLarge)));
452 }
453
454 #[test]
455 fn test_into_cursor_boundary_behavior() {
456 let cursor: Option<Cursor> = "".into_cursor();
458 assert!(cursor.is_none(), "Empty string should return None");
459
460 let oversized = "a".repeat(4097);
462 let cursor: Option<Cursor> = oversized.as_str().into_cursor();
463 assert!(cursor.is_none(), "Oversized cursor should return None");
464 }
465
466 #[test]
467 fn test_cursor_with_various_value_types() {
468 let cursor = Cursor::new()
470 .int("int_field", 42)
471 .string("str_field", "hello")
472 .field("float_field", 1.234f64)
473 .field("bool_field", true);
474
475 let encoded = cursor.encode();
476 let decoded = Cursor::decode(&encoded).unwrap();
477
478 assert_eq!(decoded.fields.len(), 4);
479
480 assert!(matches!(
482 decoded.fields.iter().find(|(k, _)| k == "int_field"),
483 Some((_, Value::Int(42)))
484 ));
485 assert!(matches!(
486 decoded.fields.iter().find(|(k, _)| k == "str_field"),
487 Some((_, Value::String(s))) if s == "hello"
488 ));
489 }
490
491 #[test]
492 fn test_cursor_with_special_json_characters() {
493 let cursor = Cursor::new()
495 .string("quotes", "say \"hello\"")
496 .string("backslash", "path\\to\\file")
497 .string("newline", "line1\nline2");
498
499 let encoded = cursor.encode();
500 let decoded = Cursor::decode(&encoded).unwrap();
501
502 assert_eq!(decoded.fields.len(), 3);
503 }
504
505 #[test]
506 fn test_cursor_from_helper() {
507 use super::super::PageInfo;
508
509 #[derive(Debug)]
510 struct User {
511 id: i64,
512 }
513
514 let user = User { id: 42 };
515 let cursor = PageInfo::cursor_from(Some(&user), |u| Cursor::new().int("id", u.id));
516
517 assert!(cursor.is_some());
518 let decoded = Cursor::decode(&cursor.unwrap()).unwrap();
519 assert_eq!(decoded.fields[0], ("id".to_string(), Value::Int(42)));
520 }
521}