1use std::fmt;
2use std::sync::Arc;
3
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
10pub struct CursorId(Arc<str>);
11
12impl CursorId {
13 pub fn new(value: impl Into<Arc<str>>) -> Result<Self> {
14 let value: Arc<str> = value.into();
15 if value.is_empty() || value.len() > 128 {
16 return Err(Error::Config(format!(
17 "invalid cursor id: {:?}",
18 value.as_ref()
19 )));
20 }
21 if !value
22 .chars()
23 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
24 {
25 return Err(Error::Config(format!(
26 "invalid cursor id: {:?}",
27 value.as_ref()
28 )));
29 }
30 Ok(Self(value))
31 }
32
33 pub fn global() -> Self {
34 Self(Arc::from("global"))
35 }
36
37 pub fn partition(count: u16, id: u16) -> Self {
38 Self(Arc::from(format!("partition:{count}:{id}")))
39 }
40
41 pub fn as_str(&self) -> &str {
42 &self.0
43 }
44
45 pub fn prefixed(&self, prefix: impl AsRef<str>) -> Result<Self> {
46 Self::new(format!("{}:{}", prefix.as_ref(), self.as_str()))
47 }
48}
49
50impl fmt::Display for CursorId {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.write_str(&self.0)
53 }
54}
55
56impl serde::Serialize for CursorId {
57 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
58 s.serialize_str(&self.0)
59 }
60}
61
62impl<'de> serde::Deserialize<'de> for CursorId {
63 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
64 let value = String::deserialize(d)?;
65 CursorId::new(Arc::from(value)).map_err(serde::de::Error::custom)
66 }
67}
68
69#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
70pub struct CursorOrder(Arc<[u8]>);
71
72impl CursorOrder {
73 pub fn min() -> Self {
74 Self(Arc::from(&[][..]))
75 }
76
77 pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Self {
78 Self(bytes.into())
79 }
80
81 pub fn from_u64(value: u64) -> Self {
82 Self(Arc::from(value.to_be_bytes().as_slice()))
83 }
84
85 pub fn from_i64(value: i64) -> Self {
86 let mapped = (value as u64) ^ (1u64 << 63);
87 Self::from_u64(mapped)
88 }
89
90 pub fn as_bytes(&self) -> &[u8] {
91 &self.0
92 }
93}
94
95impl serde::Serialize for CursorOrder {
96 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
97 use base64::Engine;
98 use base64::engine::general_purpose::STANDARD;
99 s.serialize_str(&STANDARD.encode(&self.0))
100 }
101}
102
103impl<'de> serde::Deserialize<'de> for CursorOrder {
104 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
105 use base64::Engine;
106 use base64::engine::general_purpose::STANDARD;
107 let raw = String::deserialize(d)?;
108 let bytes = STANDARD
109 .decode(raw.as_bytes())
110 .map_err(serde::de::Error::custom)?;
111 Ok(Self(Arc::from(bytes.as_slice())))
112 }
113}
114
115#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
116pub struct CursorKind(Arc<str>);
117
118impl CursorKind {
119 pub fn new(value: impl Into<Arc<str>>) -> Result<Self> {
120 let value: Arc<str> = value.into();
121 if value.is_empty() || value.len() > 128 {
122 return Err(Error::Config(format!(
123 "invalid cursor kind: {:?}",
124 value.as_ref()
125 )));
126 }
127 if !value
128 .chars()
129 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
130 {
131 return Err(Error::Config(format!(
132 "invalid cursor kind: {:?}",
133 value.as_ref()
134 )));
135 }
136 Ok(Self(value))
137 }
138
139 pub fn as_str(&self) -> &str {
140 &self.0
141 }
142}
143
144impl fmt::Display for CursorKind {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str(&self.0)
147 }
148}
149
150impl Serialize for CursorKind {
151 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
152 s.serialize_str(&self.0)
153 }
154}
155
156impl<'de> Deserialize<'de> for CursorKind {
157 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
158 let value = String::deserialize(d)?;
159 CursorKind::new(Arc::from(value)).map_err(serde::de::Error::custom)
160 }
161}
162
163#[derive(Debug, Clone, Eq, PartialEq, Hash)]
164pub struct EncodedCursor {
165 id: CursorId,
166 kind: CursorKind,
167 order: CursorOrder,
168 payload: Arc<str>,
169}
170
171#[derive(Serialize, Deserialize)]
172struct EncodedCursorRepr {
173 id: CursorId,
174 kind: CursorKind,
175 order: CursorOrder,
176 payload: String,
177}
178
179impl Serialize for EncodedCursor {
180 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
181 let repr = EncodedCursorRepr {
182 id: self.id.clone(),
183 kind: self.kind.clone(),
184 order: self.order.clone(),
185 payload: self.payload.as_ref().to_owned(),
186 };
187 repr.serialize(s)
188 }
189}
190
191impl<'de> Deserialize<'de> for EncodedCursor {
192 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
193 let repr = EncodedCursorRepr::deserialize(d)?;
194 Ok(Self {
195 id: repr.id,
196 kind: repr.kind,
197 order: repr.order,
198 payload: Arc::from(repr.payload),
199 })
200 }
201}
202
203impl EncodedCursor {
204 pub fn new(
205 id: CursorId,
206 kind: CursorKind,
207 order: CursorOrder,
208 payload: impl Into<Arc<str>>,
209 ) -> Result<Self> {
210 let payload: Arc<str> = payload.into();
211 if payload.is_empty() {
212 return Err(Error::InvalidCursor(
213 "encoded cursor payload must not be empty".to_owned(),
214 ));
215 }
216
217 use base64::Engine;
218 use base64::engine::general_purpose::STANDARD;
219 STANDARD.decode(payload.as_bytes()).map_err(|e| {
220 Error::InvalidCursor(format!("encoded cursor payload is not base64: {e}"))
221 })?;
222
223 Ok(Self {
224 id,
225 kind,
226 order,
227 payload,
228 })
229 }
230
231 pub fn from_bytes(
232 id: CursorId,
233 kind: CursorKind,
234 order: CursorOrder,
235 bytes: impl AsRef<[u8]>,
236 ) -> Self {
237 use base64::Engine;
238 use base64::engine::general_purpose::STANDARD;
239 Self {
240 id,
241 kind,
242 order,
243 payload: Arc::from(STANDARD.encode(bytes.as_ref())),
244 }
245 }
246
247 pub fn from_json<T: Serialize>(
248 id: CursorId,
249 kind: CursorKind,
250 order: CursorOrder,
251 value: &T,
252 ) -> Result<Self> {
253 let bytes = serde_json::to_vec(value)
254 .map_err(|e| Error::Serialization(format!("encode cursor json: {e}")))?;
255 Ok(Self::from_bytes(id, kind, order, bytes))
256 }
257
258 pub fn decode_bytes(&self) -> Result<Vec<u8>> {
259 use base64::Engine;
260 use base64::engine::general_purpose::STANDARD;
261 STANDARD
262 .decode(self.payload.as_bytes())
263 .map_err(|e| Error::InvalidCursor(format!("decode cursor payload: {e}")))
264 }
265
266 pub fn decode_json<T: DeserializeOwned>(&self, expected_kind: &CursorKind) -> Result<T> {
267 if &self.kind != expected_kind {
268 return Err(Error::InvalidCursor(format!(
269 "expected cursor kind `{}`, got `{}`",
270 expected_kind.as_str(),
271 self.kind.as_str()
272 )));
273 }
274 serde_json::from_slice(&self.decode_bytes()?)
275 .map_err(|e| Error::InvalidCursor(format!("decode cursor json: {e}")))
276 }
277
278 pub fn id_ref(&self) -> &CursorId {
279 &self.id
280 }
281
282 pub fn kind(&self) -> &CursorKind {
283 &self.kind
284 }
285
286 pub fn order(&self) -> &CursorOrder {
287 &self.order
288 }
289
290 pub fn payload(&self) -> &str {
291 &self.payload
292 }
293}
294
295impl PartialOrd for EncodedCursor {
296 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
297 Some(self.cmp(other))
298 }
299}
300
301impl Ord for EncodedCursor {
302 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
303 self.id.cmp(&other.id).then(self.order.cmp(&other.order))
304 }
305}
306
307impl Cursor for EncodedCursor {
308 fn id(&self) -> CursorId {
309 self.id.clone()
310 }
311
312 fn order_key(&self) -> CursorOrder {
313 self.order.clone()
314 }
315}
316
317pub trait CursorCodec<C>: Clone + Send + Sync + 'static {
318 fn encode(&self, cursor: &C) -> Result<EncodedCursor>;
319
320 fn decode(&self, cursor: &EncodedCursor) -> Result<C>;
321}
322
323#[derive(Debug)]
324pub struct JsonCursorCodec<C> {
325 kind: CursorKind,
326 _cursor: std::marker::PhantomData<fn() -> C>,
327}
328
329impl<C> Clone for JsonCursorCodec<C> {
330 fn clone(&self) -> Self {
331 Self {
332 kind: self.kind.clone(),
333 _cursor: std::marker::PhantomData,
334 }
335 }
336}
337
338impl<C> JsonCursorCodec<C> {
339 pub fn new(kind: impl Into<Arc<str>>) -> Result<Self> {
340 Ok(Self {
341 kind: CursorKind::new(kind)?,
342 _cursor: std::marker::PhantomData,
343 })
344 }
345
346 pub fn kind(&self) -> &CursorKind {
347 &self.kind
348 }
349}
350
351impl<C> CursorCodec<C> for JsonCursorCodec<C>
352where
353 C: Cursor + Serialize + DeserializeOwned + Send + Sync + 'static,
354{
355 fn encode(&self, cursor: &C) -> Result<EncodedCursor> {
356 EncodedCursor::from_json(cursor.id(), self.kind.clone(), cursor.order_key(), cursor)
357 }
358
359 fn decode(&self, cursor: &EncodedCursor) -> Result<C> {
360 cursor.decode_json(&self.kind)
361 }
362}
363
364#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
365pub struct NoCursor;
366
367pub trait Cursor {
368 fn id(&self) -> CursorId {
369 CursorId::global()
370 }
371
372 fn order_key(&self) -> CursorOrder {
373 CursorOrder::min()
374 }
375}
376
377impl Cursor for NoCursor {}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn global_returns_static_global_id() {
385 assert_eq!(CursorId::global().as_str(), "global");
386 assert_eq!(CursorId::global(), CursorId::global());
387 }
388
389 #[test]
390 fn partition_constructs_stable_format() {
391 let id = CursorId::partition(100, 17);
392 assert_eq!(id.as_str(), "partition:100:17");
393 }
394
395 #[test]
396 fn new_validates_and_stores_value() {
397 let id = CursorId::new("custom.stream").unwrap();
398 assert_eq!(id.as_str(), "custom.stream");
399 }
400
401 #[test]
402 fn new_rejects_empty() {
403 assert!(CursorId::new("").is_err());
404 }
405
406 #[test]
407 fn new_rejects_too_long() {
408 let long = "a".repeat(129);
409 assert!(CursorId::new(long).is_err());
410 }
411
412 #[test]
413 fn new_rejects_invalid_chars() {
414 assert!(CursorId::new("bad space").is_err());
415 assert!(CursorId::new("bad/char").is_err());
416 }
417
418 #[test]
419 fn new_accepts_valid_chars() {
420 assert!(CursorId::new("valid-name_01.v2:tag").is_ok());
421 }
422
423 #[test]
424 fn equality_by_value() {
425 let a = CursorId::new("test").unwrap();
426 let b = CursorId::new("test").unwrap();
427 assert_eq!(a, b);
428 }
429
430 #[test]
431 fn distinct_values_differ() {
432 let a = CursorId::new("a").unwrap();
433 let b = CursorId::new("b").unwrap();
434 assert_ne!(a, b);
435 }
436
437 #[test]
438 fn cursor_id_orders_lexically() {
439 let a = CursorId::new("a").unwrap();
440 let b = CursorId::new("b").unwrap();
441 assert!(a < b);
442 }
443
444 #[test]
445 fn prefixed_adds_stable_prefix() {
446 let id = CursorId::global().prefixed("left").unwrap();
447 assert_eq!(id.as_str(), "left:global");
448 }
449
450 #[test]
451 fn prefixed_rejects_invalid_prefix() {
452 let err = CursorId::global().prefixed("bad prefix").unwrap_err();
453 assert!(err.to_string().contains("invalid cursor id"));
454 }
455
456 #[test]
457 fn cursor_order_from_i64_preserves_signed_ordering() {
458 let lo = CursorOrder::from_i64(i64::MIN);
459 let zero = CursorOrder::from_i64(0);
460 let hi = CursorOrder::from_i64(i64::MAX);
461 assert!(lo < zero);
462 assert!(zero < hi);
463 assert!(CursorOrder::from_i64(-1) < CursorOrder::from_i64(1));
464 assert!(CursorOrder::from_i64(9) < CursorOrder::from_i64(10));
465 }
466
467 #[test]
468 fn cursor_order_from_u64_preserves_ordering() {
469 assert!(CursorOrder::from_u64(9) < CursorOrder::from_u64(10));
470 }
471
472 #[test]
473 fn cursor_order_roundtrips_via_json() {
474 let v = CursorOrder::from_i64(-42);
475 let s = serde_json::to_string(&v).unwrap();
476 let back: CursorOrder = serde_json::from_str(&s).unwrap();
477 assert_eq!(v, back);
478 }
479
480 #[test]
481 fn cursor_order_min_is_smallest() {
482 assert!(CursorOrder::min() < CursorOrder::from_u64(0));
483 }
484
485 #[test]
486 fn cursor_kind_validates() {
487 assert!(CursorKind::new("eventuary.postgres.cursor.v1").is_ok());
488 assert!(CursorKind::new("").is_err());
489 assert!(CursorKind::new("bad kind").is_err());
490 }
491
492 #[test]
493 fn encoded_cursor_roundtrips_json() {
494 #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
495 struct TestCursor {
496 sequence: i64,
497 }
498
499 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
500 let cursor = EncodedCursor::from_json(
501 CursorId::global(),
502 kind.clone(),
503 CursorOrder::from_i64(42),
504 &TestCursor { sequence: 42 },
505 )
506 .unwrap();
507
508 assert_eq!(cursor.id_ref(), &CursorId::global());
509 assert_eq!(cursor.kind(), &kind);
510 assert_eq!(cursor.order(), &CursorOrder::from_i64(42));
511 let decoded: TestCursor = cursor.decode_json(&kind).unwrap();
512 assert_eq!(decoded, TestCursor { sequence: 42 });
513 }
514
515 #[test]
516 fn encoded_cursor_rejects_wrong_kind() {
517 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
518 let other = CursorKind::new("eventuary.other.cursor.v1").unwrap();
519 let cursor =
520 EncodedCursor::from_json(CursorId::global(), kind, CursorOrder::from_i64(0), &42_i64)
521 .unwrap();
522
523 let err = cursor.decode_json::<i64>(&other).unwrap_err();
524 assert!(err.to_string().contains("expected cursor kind"));
525 }
526
527 #[test]
528 fn encoded_cursor_orders_by_id_then_order() {
529 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
530 let a9 = EncodedCursor::from_bytes(
531 CursorId::new("a").unwrap(),
532 kind.clone(),
533 CursorOrder::from_i64(9),
534 b"x",
535 );
536 let a10 = EncodedCursor::from_bytes(
537 CursorId::new("a").unwrap(),
538 kind.clone(),
539 CursorOrder::from_i64(10),
540 b"x",
541 );
542 let b0 = EncodedCursor::from_bytes(
543 CursorId::new("b").unwrap(),
544 kind,
545 CursorOrder::from_i64(0),
546 b"x",
547 );
548
549 assert!(a9 < a10);
550 assert!(a10 < b0);
551 }
552
553 #[test]
554 fn encoded_cursor_new_validates_payload() {
555 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
556 let err = EncodedCursor::new(
557 CursorId::global(),
558 kind.clone(),
559 CursorOrder::from_i64(0),
560 "",
561 )
562 .unwrap_err();
563 assert!(err.to_string().contains("must not be empty"));
564
565 let err = EncodedCursor::new(
566 CursorId::global(),
567 kind.clone(),
568 CursorOrder::from_i64(0),
569 "not-base64-$$",
570 )
571 .unwrap_err();
572 assert!(err.to_string().contains("not base64"));
573
574 let ok =
575 EncodedCursor::from_bytes(CursorId::global(), kind, CursorOrder::from_i64(0), b"x");
576 let rebuilt = EncodedCursor::new(
577 ok.id_ref().clone(),
578 ok.kind().clone(),
579 ok.order().clone(),
580 ok.payload().to_owned(),
581 )
582 .unwrap();
583 assert_eq!(rebuilt, ok);
584 }
585
586 #[test]
587 fn encoded_cursor_implements_cursor_trait() {
588 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
589 let cursor = EncodedCursor::from_bytes(
590 CursorId::new("custom").unwrap(),
591 kind,
592 CursorOrder::from_u64(7),
593 b"p",
594 );
595 assert_eq!(<EncodedCursor as Cursor>::id(&cursor).as_str(), "custom");
596 assert_eq!(
597 <EncodedCursor as Cursor>::order_key(&cursor),
598 CursorOrder::from_u64(7)
599 );
600 }
601
602 #[test]
603 fn json_cursor_codec_roundtrips() {
604 #[derive(
605 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
606 )]
607 struct TestCursor(i64);
608
609 impl Cursor for TestCursor {
610 fn order_key(&self) -> CursorOrder {
611 CursorOrder::from_i64(self.0)
612 }
613 }
614
615 let codec = JsonCursorCodec::<TestCursor>::new("eventuary.test.cursor.v1").unwrap();
616 let encoded = codec.encode(&TestCursor(7)).unwrap();
617 assert_eq!(encoded.id_ref(), &CursorId::global());
618 assert_eq!(encoded.kind(), codec.kind());
619 assert_eq!(encoded.order(), &CursorOrder::from_i64(7));
620
621 let decoded = codec.decode(&encoded).unwrap();
622 assert_eq!(decoded, TestCursor(7));
623 }
624
625 #[test]
626 fn json_cursor_codec_preserves_typed_ordering_under_erasure() {
627 #[derive(
628 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
629 )]
630 struct TestCursor(i64);
631
632 impl Cursor for TestCursor {
633 fn order_key(&self) -> CursorOrder {
634 CursorOrder::from_i64(self.0)
635 }
636 }
637
638 let codec = JsonCursorCodec::<TestCursor>::new("eventuary.test.cursor.v1").unwrap();
639 for (a, b) in [(-5_i64, 0), (0, 1), (9, 10), (i64::MIN, i64::MAX)] {
640 let ea = codec.encode(&TestCursor(a)).unwrap();
641 let eb = codec.encode(&TestCursor(b)).unwrap();
642 assert_eq!(
643 TestCursor(a).cmp(&TestCursor(b)),
644 ea.cmp(&eb),
645 "a={a} b={b}"
646 );
647 }
648 }
649
650 #[test]
651 fn encoded_cursor_roundtrips_via_serde() {
652 let kind = CursorKind::new("eventuary.test.cursor.v1").unwrap();
653 let cursor =
654 EncodedCursor::from_json(CursorId::global(), kind, CursorOrder::from_i64(42), &42_i64)
655 .unwrap();
656
657 let s = serde_json::to_string(&cursor).unwrap();
658 let back: EncodedCursor = serde_json::from_str(&s).unwrap();
659 assert_eq!(cursor, back);
660 }
661
662 #[test]
663 fn cursor_trait_default_is_global() {
664 struct SomeCursor;
665 impl Cursor for SomeCursor {}
666 assert_eq!(SomeCursor.id(), CursorId::global());
667 }
668
669 #[test]
670 fn cursor_trait_named_example() {
671 struct NamedCursor;
672 impl Cursor for NamedCursor {
673 fn id(&self) -> CursorId {
674 CursorId::new("partition:100:17").unwrap()
675 }
676 }
677 assert_eq!(NamedCursor.id(), CursorId::partition(100, 17));
678 }
679
680 #[test]
681 fn cursor_trait_default_order_key_is_min() {
682 struct Probe;
683 impl Cursor for Probe {}
684 assert_eq!(Probe.order_key(), CursorOrder::min());
685 }
686
687 #[test]
688 fn cursor_trait_named_example_supports_custom_order() {
689 struct OrderedCursor(i64);
690 impl Cursor for OrderedCursor {
691 fn order_key(&self) -> CursorOrder {
692 CursorOrder::from_i64(self.0)
693 }
694 }
695 assert!(OrderedCursor(9).order_key() < OrderedCursor(10).order_key());
696 }
697
698 #[test]
699 fn cursor_trait_does_not_require_ord() {
700 struct UnorderedCursor;
701
702 impl Cursor for UnorderedCursor {}
703
704 assert_eq!(UnorderedCursor.id(), CursorId::global());
705 assert_eq!(UnorderedCursor.order_key(), CursorOrder::min());
706 }
707
708 #[test]
709 fn serializes_as_plain_string() {
710 let id = CursorId::global();
711 let v = serde_json::to_value(id).unwrap();
712 assert_eq!(v.as_str(), Some("global"));
713
714 let id = CursorId::partition(4, 1);
715 let v = serde_json::to_value(id).unwrap();
716 assert_eq!(v.as_str(), Some("partition:4:1"));
717 }
718
719 #[test]
720 fn roundtrips_via_json() {
721 let id = CursorId::global();
722 let v = serde_json::to_value(id.clone()).unwrap();
723 let back: CursorId = serde_json::from_value(v).unwrap();
724 assert_eq!(back, id);
725
726 let id = CursorId::partition(4, 2);
727 let v = serde_json::to_value(id.clone()).unwrap();
728 let back: CursorId = serde_json::from_value(v).unwrap();
729 assert_eq!(back, id);
730 }
731
732 #[test]
733 fn display_output_matches_as_str() {
734 let id = CursorId::global();
735 assert_eq!(id.to_string(), "global");
736
737 let id = CursorId::partition(4, 1);
738 assert_eq!(id.to_string(), "partition:4:1");
739 }
740
741 #[test]
742 fn no_cursor_uses_global_cursor_id() {
743 assert_eq!(NoCursor.id(), CursorId::global());
744 }
745}