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 rkyv and base64-encoded for URL safety.
106#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
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/// Trait for types that can be used as pagination cursors.
129///
130/// Implementors define how to serialize their position data to/from
131/// base64-encoded cursor values.
132pub trait Cursor {
133    /// The cursor data type (e.g., `EventCursor`)
134    type T;
135
136    /// Extracts cursor data from this item.
137    fn serialize(&self) -> Self::T;
138    /// Serializes cursor data to a base64 [`Value`].
139    fn serialize_cursor(&self) -> Result<Value, CursorError>;
140    /// Deserializes cursor data from a base64 [`Value`].
141    fn deserialize_cursor(value: &Value) -> Result<Self::T, CursorError>;
142}
143
144#[derive(Debug, Error)]
145pub enum CursorError {
146    #[error("base64 decode: {0}")]
147    Base64Decode(#[from] base64::DecodeError),
148
149    #[error("rkyv: {0}")]
150    Rkyv(String),
151}
152
153/// Pagination arguments for querying events.
154///
155/// Supports both forward (first/after) and backward (last/before) pagination.
156///
157/// # Example
158///
159/// ```rust,ignore
160/// // Forward: first 20 items
161/// let args = Args::forward(20, None);
162///
163/// // Forward: next 20 items after cursor
164/// let args = Args::forward(20, Some(end_cursor));
165///
166/// // Backward: last 20 items before cursor
167/// let args = Args::backward(20, Some(start_cursor));
168/// ```
169#[derive(Default, Serialize, Deserialize, Clone)]
170pub struct Args {
171    /// Number of items for forward pagination
172    pub first: Option<u16>,
173    /// Cursor to start after (forward pagination)
174    pub after: Option<Value>,
175    /// Number of items for backward pagination
176    pub last: Option<u16>,
177    /// Cursor to end before (backward pagination)
178    pub before: Option<Value>,
179}
180
181impl Args {
182    pub fn forward(first: u16, after: Option<Value>) -> Self {
183        Self {
184            first: Some(first),
185            after,
186            last: None,
187            before: None,
188        }
189    }
190
191    pub fn backward(last: u16, before: Option<Value>) -> Self {
192        Self {
193            first: None,
194            after: None,
195            last: Some(last),
196            before,
197        }
198    }
199
200    pub fn is_backward(&self) -> bool {
201        (self.last.is_some() || self.before.is_some())
202            && self.first.is_none()
203            && self.after.is_none()
204    }
205
206    pub fn get_info(&self) -> (u16, Option<Value>) {
207        if self.is_backward() {
208            (self.last.unwrap_or(40), self.before.clone())
209        } else {
210            (self.first.unwrap_or(40), self.after.clone())
211        }
212    }
213
214    pub fn limit(self, v: u16) -> Self {
215        if self.is_backward() {
216            Args::backward(self.last.unwrap_or(v).min(v), self.before)
217        } else {
218            Args::forward(self.first.unwrap_or(v).min(v), self.after)
219        }
220    }
221}
222
223#[derive(Debug, Error)]
224pub enum ReadError {
225    #[error("{0}")]
226    Unknown(#[from] anyhow::Error),
227
228    #[error("cursor: {0}")]
229    Cursor(#[from] CursorError),
230}
231
232/// In-memory pagination executor.
233///
234/// `Reader` performs cursor-based pagination on an in-memory vector of items.
235/// It's useful for testing or when data is already loaded.
236///
237/// # Example
238///
239/// ```rust,ignore
240/// let events = vec![event1, event2, event3];
241///
242/// let result = Reader::new(events)
243///     .forward(2, None)
244///     .execute()?;
245///
246/// assert_eq!(result.edges.len(), 2);
247/// assert!(result.page_info.has_next_page);
248/// ```
249pub struct Reader<T> {
250    data: Vec<T>,
251    args: Args,
252    order: Order,
253}
254
255impl<T> Reader<T>
256where
257    T: Cursor + Clone,
258    T: Send + Unpin,
259    T: Bind<T = T>,
260{
261    pub fn new(data: Vec<T>) -> Self {
262        Self {
263            data,
264            args: Args::default(),
265            order: Order::Asc,
266        }
267    }
268
269    pub fn order(&mut self, order: Order) -> &mut Self {
270        self.order = order;
271
272        self
273    }
274
275    pub fn desc(&mut self) -> &mut Self {
276        self.order(Order::Desc)
277    }
278
279    pub fn args(&mut self, args: Args) -> &mut Self {
280        self.args = args;
281
282        self
283    }
284
285    pub fn backward(&mut self, last: u16, before: Option<Value>) -> &mut Self {
286        self.args(Args {
287            last: Some(last),
288            before,
289            ..Default::default()
290        })
291    }
292
293    pub fn forward(&mut self, first: u16, after: Option<Value>) -> &mut Self {
294        self.args(Args {
295            first: Some(first),
296            after,
297            ..Default::default()
298        })
299    }
300
301    pub fn execute(&self) -> Result<ReadResult<T>, ReadError> {
302        let is_order_desc = matches!(
303            (&self.order, self.args.is_backward()),
304            (Order::Asc, true) | (Order::Desc, false)
305        );
306
307        let mut data = self.data.clone().into_iter().collect::<Vec<_>>();
308        T::sort_by(&mut data, is_order_desc);
309        let (limit, cursor) = self.args.get_info();
310
311        if let Some(cursor) = cursor.as_ref() {
312            let cursor = T::deserialize_cursor(cursor)?;
313            T::retain(&mut data, cursor, is_order_desc);
314        }
315
316        let data_len = data.len();
317        data = data.into_iter().take((limit + 1).into()).collect();
318
319        let has_more = data_len > data.len();
320        if has_more {
321            data.pop();
322        }
323
324        let mut edges = data
325            .into_iter()
326            .map(|node| Edge {
327                cursor: node
328                    .serialize_cursor()
329                    .expect("Error while serialize_cursor in assert_read_result"),
330                node,
331            })
332            .collect::<Vec<_>>();
333
334        if self.args.is_backward() {
335            edges = edges.into_iter().rev().collect();
336        }
337
338        let page_info = if self.args.is_backward() {
339            PageInfo {
340                has_previous_page: has_more,
341                start_cursor: edges.first().map(|e| e.cursor.to_owned()),
342                ..Default::default()
343            }
344        } else {
345            PageInfo {
346                has_next_page: has_more,
347                end_cursor: edges.last().map(|e| e.cursor.to_owned()),
348                ..Default::default()
349            }
350        };
351
352        Ok(ReadResult { edges, page_info })
353    }
354}
355
356impl<T> Deref for Reader<T> {
357    type Target = Vec<T>;
358
359    fn deref(&self) -> &Self::Target {
360        &self.data
361    }
362}
363
364impl<T> DerefMut for Reader<T> {
365    fn deref_mut(&mut self) -> &mut Self::Target {
366        &mut self.data
367    }
368}
369
370/// Trait for sorting and filtering data for pagination.
371///
372/// Implementors define how to sort items and filter by cursor position.
373pub trait Bind {
374    /// The item type being paginated
375    type T: Cursor + Clone;
376
377    /// Sorts items in ascending or descending order.
378    fn sort_by(data: &mut Vec<Self::T>, is_order_desc: bool);
379    /// Retains only items after/before the cursor position.
380    fn retain(
381        data: &mut Vec<Self::T>,
382        cursor: <<Self as Bind>::T as Cursor>::T,
383        is_order_desc: bool,
384    );
385}