evento_core/
cursor.rs

1//! Cursor-based pagination for event queries.
2//!
3//! This module provides GraphQL-style cursor pagination for efficiently querying
4//! large sets of events. It uses keyset pagination for stable, efficient results.
5//!
6//! # Key Types
7//!
8//! - [`Value`] - Base64-encoded cursor string
9//! - [`Args`] - Pagination arguments (first/after, last/before)
10//! - [`ReadResult`] - Paginated result with edges and page info
11//! - [`Reader`] - In-memory pagination executor
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use evento::cursor::{Args, Reader};
17//!
18//! // Forward pagination: first 10 events
19//! let args = Args::forward(10, None);
20//!
21//! // Continue from cursor
22//! let args = Args::forward(10, Some(page_info.end_cursor));
23//!
24//! // Backward pagination: last 10 events before cursor
25//! let args = Args::backward(10, Some(cursor));
26//!
27//! // In-memory pagination
28//! let result = Reader::new(events)
29//!     .forward(10, None)
30//!     .execute()?;
31//! ```
32
33use serde::{Deserialize, Serialize};
34use std::ops::{Deref, DerefMut};
35use thiserror::Error;
36
37/// Sort order for pagination.
38#[derive(Debug, Clone, PartialEq)]
39pub enum Order {
40    /// Ascending order (oldest first)
41    Asc,
42    /// Descending order (newest first)
43    Desc,
44}
45
46/// A paginated item with its cursor.
47///
48/// Each edge contains a node (the actual data) and its cursor
49/// for use in subsequent pagination requests.
50#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
51pub struct Edge<N> {
52    /// Cursor for this item's position
53    pub cursor: Value,
54    /// The actual data item
55    pub node: N,
56}
57
58/// Pagination metadata for a result set.
59#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
60pub struct PageInfo {
61    /// Whether there are more items before the first edge
62    pub has_previous_page: bool,
63    /// Whether there are more items after the last edge
64    pub has_next_page: bool,
65    /// Cursor of the first edge (for backward pagination)
66    pub start_cursor: Option<Value>,
67    /// Cursor of the last edge (for forward pagination)
68    pub end_cursor: Option<Value>,
69}
70
71/// Result of a paginated query.
72///
73/// Contains the requested edges and pagination metadata.
74#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
75pub struct ReadResult<N> {
76    /// The paginated items with their cursors
77    pub edges: Vec<Edge<N>>,
78    /// Pagination metadata
79    pub page_info: PageInfo,
80}
81
82impl<N> ReadResult<N> {
83    pub fn map<B, F>(self, f: F) -> ReadResult<B>
84    where
85        Self: Sized,
86        F: Fn(N) -> B,
87    {
88        ReadResult {
89            page_info: self.page_info,
90            edges: self
91                .edges
92                .into_iter()
93                .map(|e| Edge {
94                    cursor: e.cursor.to_owned(),
95                    node: f(e.node),
96                })
97                .collect(),
98        }
99    }
100}
101
102/// A base64-encoded cursor value for pagination.
103///
104/// Cursors are opaque strings that identify a position in a result set.
105/// They are serialized using bitcode and base64-encoded for URL safety.
106#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
107pub struct Value(pub String);
108
109impl Deref for Value {
110    type Target = String;
111    fn deref(&self) -> &Self::Target {
112        &self.0
113    }
114}
115
116impl From<String> for Value {
117    fn from(value: String) -> Self {
118        Self(value)
119    }
120}
121
122impl AsRef<[u8]> for Value {
123    fn as_ref(&self) -> &[u8] {
124        self.0.as_bytes()
125    }
126}
127
128// Your encoding traits
129pub trait Encode {
130    fn encode(&self) -> Result<Vec<u8>, CursorError>;
131}
132
133pub trait Decode: Sized {
134    fn decode(bytes: &[u8]) -> Result<Self, CursorError>;
135}
136
137// Blanket impl: anything with bitcode gets it for free
138impl<T: bitcode::Encode> Encode for T {
139    fn encode(&self) -> Result<Vec<u8>, CursorError> {
140        Ok(bitcode::encode(self))
141    }
142}
143
144impl<T: bitcode::DecodeOwned> Decode for T {
145    fn decode(bytes: &[u8]) -> Result<Self, CursorError> {
146        bitcode::decode(bytes).map_err(|e| CursorError::Bitcode(e.to_string()))
147    }
148}
149
150/// Trait for types that can be used as pagination cursors.
151///
152/// Implementors define how to serialize their position data to/from
153/// base64-encoded cursor values.
154pub trait Cursor {
155    /// The cursor data type (e.g., `EventCursor`)
156    type T: Encode + Decode;
157
158    /// Extracts cursor data from this item.
159    fn serialize(&self) -> Self::T;
160    /// Serializes cursor data to a base64 [`Value`].
161    fn serialize_cursor(&self) -> Result<Value, CursorError> {
162        use base64::{alphabet, engine::general_purpose, engine::GeneralPurpose, Engine};
163
164        let bytes = self.serialize().encode()?;
165        let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::PAD);
166
167        Ok(Value(engine.encode(&bytes)))
168    }
169    /// Deserializes cursor data from a base64 [`Value`].
170    fn deserialize_cursor(value: &Value) -> Result<Self::T, CursorError> {
171        use base64::{alphabet, engine::general_purpose, engine::GeneralPurpose, Engine};
172
173        let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::PAD);
174        let bytes = engine.decode(value)?;
175
176        Self::T::decode(&bytes)
177    }
178}
179
180#[derive(Debug, Error)]
181pub enum CursorError {
182    #[error("base64 decode: {0}")]
183    Base64Decode(#[from] base64::DecodeError),
184
185    #[error("bitcode: {0}")]
186    Bitcode(String),
187}
188
189/// Pagination arguments for querying events.
190///
191/// Supports both forward (first/after) and backward (last/before) pagination.
192///
193/// # Example
194///
195/// ```rust,ignore
196/// // Forward: first 20 items
197/// let args = Args::forward(20, None);
198///
199/// // Forward: next 20 items after cursor
200/// let args = Args::forward(20, Some(end_cursor));
201///
202/// // Backward: last 20 items before cursor
203/// let args = Args::backward(20, Some(start_cursor));
204/// ```
205#[derive(Default, Serialize, Deserialize, Clone)]
206pub struct Args {
207    /// Number of items for forward pagination
208    pub first: Option<u16>,
209    /// Cursor to start after (forward pagination)
210    pub after: Option<Value>,
211    /// Number of items for backward pagination
212    pub last: Option<u16>,
213    /// Cursor to end before (backward pagination)
214    pub before: Option<Value>,
215}
216
217impl Args {
218    pub fn forward(first: u16, after: Option<Value>) -> Self {
219        Self {
220            first: Some(first),
221            after,
222            last: None,
223            before: None,
224        }
225    }
226
227    pub fn backward(last: u16, before: Option<Value>) -> Self {
228        Self {
229            first: None,
230            after: None,
231            last: Some(last),
232            before,
233        }
234    }
235
236    pub fn is_backward(&self) -> bool {
237        (self.last.is_some() || self.before.is_some())
238            && self.first.is_none()
239            && self.after.is_none()
240    }
241
242    pub fn get_info(&self) -> (u16, Option<Value>) {
243        if self.is_backward() {
244            (self.last.unwrap_or(40), self.before.clone())
245        } else {
246            (self.first.unwrap_or(40), self.after.clone())
247        }
248    }
249
250    pub fn limit(self, v: u16) -> Self {
251        if self.is_backward() {
252            Args::backward(self.last.unwrap_or(v).min(v), self.before)
253        } else {
254            Args::forward(self.first.unwrap_or(v).min(v), self.after)
255        }
256    }
257}
258
259#[derive(Debug, Error)]
260pub enum ReadError {
261    #[error("{0}")]
262    Unknown(#[from] anyhow::Error),
263
264    #[error("cursor: {0}")]
265    Cursor(#[from] CursorError),
266}
267
268/// In-memory pagination executor.
269///
270/// `Reader` performs cursor-based pagination on an in-memory vector of items.
271/// It's useful for testing or when data is already loaded.
272///
273/// # Example
274///
275/// ```rust,ignore
276/// let events = vec![event1, event2, event3];
277///
278/// let result = Reader::new(events)
279///     .forward(2, None)
280///     .execute()?;
281///
282/// assert_eq!(result.edges.len(), 2);
283/// assert!(result.page_info.has_next_page);
284/// ```
285pub struct Reader<T> {
286    data: Vec<T>,
287    args: Args,
288    order: Order,
289}
290
291impl<T> Reader<T>
292where
293    T: Cursor + Clone,
294    T: Send + Unpin,
295    T: Bind<T = T>,
296{
297    pub fn new(data: Vec<T>) -> Self {
298        Self {
299            data,
300            args: Args::default(),
301            order: Order::Asc,
302        }
303    }
304
305    pub fn order(&mut self, order: Order) -> &mut Self {
306        self.order = order;
307
308        self
309    }
310
311    pub fn desc(&mut self) -> &mut Self {
312        self.order(Order::Desc)
313    }
314
315    pub fn args(&mut self, args: Args) -> &mut Self {
316        self.args = args;
317
318        self
319    }
320
321    pub fn backward(&mut self, last: u16, before: Option<Value>) -> &mut Self {
322        self.args(Args {
323            last: Some(last),
324            before,
325            ..Default::default()
326        })
327    }
328
329    pub fn forward(&mut self, first: u16, after: Option<Value>) -> &mut Self {
330        self.args(Args {
331            first: Some(first),
332            after,
333            ..Default::default()
334        })
335    }
336
337    pub fn execute(&self) -> Result<ReadResult<T>, ReadError> {
338        let is_order_desc = matches!(
339            (&self.order, self.args.is_backward()),
340            (Order::Asc, true) | (Order::Desc, false)
341        );
342
343        let mut data = self.data.clone().into_iter().collect::<Vec<_>>();
344        T::sort_by(&mut data, is_order_desc);
345        let (limit, cursor) = self.args.get_info();
346
347        if let Some(cursor) = cursor.as_ref() {
348            let cursor = T::deserialize_cursor(cursor)?;
349            T::retain(&mut data, cursor, is_order_desc);
350        }
351
352        let data_len = data.len();
353        data = data.into_iter().take((limit + 1).into()).collect();
354
355        let has_more = data_len > data.len();
356        if has_more {
357            data.pop();
358        }
359
360        let mut edges = data
361            .into_iter()
362            .map(|node| Edge {
363                cursor: node
364                    .serialize_cursor()
365                    .expect("Error while serialize_cursor in assert_read_result"),
366                node,
367            })
368            .collect::<Vec<_>>();
369
370        if self.args.is_backward() {
371            edges = edges.into_iter().rev().collect();
372        }
373
374        let page_info = if self.args.is_backward() {
375            PageInfo {
376                has_previous_page: has_more,
377                start_cursor: edges.first().map(|e| e.cursor.to_owned()),
378                ..Default::default()
379            }
380        } else {
381            PageInfo {
382                has_next_page: has_more,
383                end_cursor: edges.last().map(|e| e.cursor.to_owned()),
384                ..Default::default()
385            }
386        };
387
388        Ok(ReadResult { edges, page_info })
389    }
390}
391
392impl<T> Deref for Reader<T> {
393    type Target = Vec<T>;
394
395    fn deref(&self) -> &Self::Target {
396        &self.data
397    }
398}
399
400impl<T> DerefMut for Reader<T> {
401    fn deref_mut(&mut self) -> &mut Self::Target {
402        &mut self.data
403    }
404}
405
406/// Trait for sorting and filtering data for pagination.
407///
408/// Implementors define how to sort items and filter by cursor position.
409pub trait Bind {
410    /// The item type being paginated
411    type T: Cursor + Clone;
412
413    /// Sorts items in ascending or descending order.
414    fn sort_by(data: &mut Vec<Self::T>, is_order_desc: bool);
415    /// Retains only items after/before the cursor position.
416    fn retain(
417        data: &mut Vec<Self::T>,
418        cursor: <<Self as Bind>::T as Cursor>::T,
419        is_order_desc: bool,
420    );
421}