Skip to main content

systemprompt_models/notifications/
cursor.rs

1//! Opaque keyset cursor over the `(timestamp, id)` feed ordering.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::fmt;
7use std::str::FromStr;
8
9use base64::Engine;
10use base64::engine::general_purpose::URL_SAFE_NO_PAD;
11use chrono::{DateTime, SecondsFormat, Utc};
12use uuid::Uuid;
13
14/// A feed position, encoded as base64url of `<rfc3339 micros>|<uuid>`.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct KeysetCursor {
17    pub at: DateTime<Utc>,
18    pub id: Uuid,
19}
20
21/// Why a cursor string did not decode.
22#[derive(Debug, thiserror::Error)]
23pub enum KeysetCursorError {
24    #[error("cursor is not valid base64")]
25    Encoding(#[source] base64::DecodeError),
26    #[error("cursor is not valid UTF-8")]
27    Utf8(#[source] std::string::FromUtf8Error),
28    #[error("cursor has no separator")]
29    Shape,
30    #[error("cursor timestamp is invalid")]
31    Timestamp(#[source] chrono::ParseError),
32    #[error("cursor id is invalid")]
33    Id(#[source] uuid::Error),
34}
35
36impl KeysetCursor {
37    #[must_use]
38    pub const fn new(at: DateTime<Utc>, id: Uuid) -> Self {
39        Self { at, id }
40    }
41}
42
43impl fmt::Display for KeysetCursor {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        let raw = format!(
46            "{}|{}",
47            self.at.to_rfc3339_opts(SecondsFormat::Micros, true),
48            self.id
49        );
50        f.write_str(&URL_SAFE_NO_PAD.encode(raw))
51    }
52}
53
54impl FromStr for KeysetCursor {
55    type Err = KeysetCursorError;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        let bytes = URL_SAFE_NO_PAD
59            .decode(s)
60            .map_err(KeysetCursorError::Encoding)?;
61        let raw = String::from_utf8(bytes).map_err(KeysetCursorError::Utf8)?;
62        let (at, id) = raw.split_once('|').ok_or(KeysetCursorError::Shape)?;
63        let at = DateTime::parse_from_rfc3339(at)
64            .map_err(KeysetCursorError::Timestamp)?
65            .with_timezone(&Utc);
66        let id = Uuid::parse_str(id).map_err(KeysetCursorError::Id)?;
67        Ok(Self { at, id })
68    }
69}
70
71crate::notifications::string_form::string_serde!(KeysetCursor, "KeysetCursor");