systemprompt_models/notifications/
cursor.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct KeysetCursor {
17 pub at: DateTime<Utc>,
18 pub id: Uuid,
19}
20
21#[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");