Skip to main content

lago_core/
id.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use ulid::Ulid;
4
5macro_rules! typed_id {
6    ($name:ident) => {
7        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8        #[serde(transparent)]
9        pub struct $name(String);
10
11        impl $name {
12            pub fn new() -> Self {
13                Self(Ulid::new().to_string())
14            }
15
16            pub fn from_string(s: impl Into<String>) -> Self {
17                Self(s.into())
18            }
19
20            pub fn as_str(&self) -> &str {
21                &self.0
22            }
23        }
24
25        impl Default for $name {
26            fn default() -> Self {
27                Self::new()
28            }
29        }
30
31        impl fmt::Display for $name {
32            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33                write!(f, "{}", self.0)
34            }
35        }
36
37        impl From<String> for $name {
38            fn from(s: String) -> Self {
39                Self(s)
40            }
41        }
42
43        impl From<&str> for $name {
44            fn from(s: &str) -> Self {
45                Self(s.to_string())
46            }
47        }
48
49        impl AsRef<str> for $name {
50            fn as_ref(&self) -> &str {
51                &self.0
52            }
53        }
54    };
55}
56
57typed_id!(EventId);
58typed_id!(SessionId);
59typed_id!(BranchId);
60typed_id!(RunId);
61typed_id!(SnapshotId);
62typed_id!(ApprovalId);
63typed_id!(MemoryId);
64
65/// Content-addressed blob hash (SHA-256 hex)
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(transparent)]
68pub struct BlobHash(String);
69
70impl BlobHash {
71    pub fn from_hex(hex: impl Into<String>) -> Self {
72        Self(hex.into())
73    }
74
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78}
79
80impl fmt::Display for BlobHash {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86impl From<String> for BlobHash {
87    fn from(s: String) -> Self {
88        Self(s)
89    }
90}
91
92impl AsRef<str> for BlobHash {
93    fn as_ref(&self) -> &str {
94        &self.0
95    }
96}
97
98/// Monotonic sequence number within a branch
99pub type SeqNo = u64;
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn event_id_new_is_unique() {
107        let a = EventId::new();
108        let b = EventId::new();
109        assert_ne!(a, b);
110    }
111
112    #[test]
113    fn session_id_from_string() {
114        let id = SessionId::from_string("test-session");
115        assert_eq!(id.as_str(), "test-session");
116        assert_eq!(id.to_string(), "test-session");
117    }
118
119    #[test]
120    fn branch_id_from_str_trait() {
121        let id: BranchId = "main".into();
122        assert_eq!(id.as_str(), "main");
123    }
124
125    #[test]
126    fn typed_id_display() {
127        let id = RunId::from_string("run-42");
128        assert_eq!(format!("{id}"), "run-42");
129    }
130
131    #[test]
132    fn typed_id_default_generates_ulid() {
133        let id = EventId::default();
134        // ULIDs are 26 characters in Crockford Base32
135        assert_eq!(id.as_str().len(), 26);
136    }
137
138    #[test]
139    fn typed_id_as_ref_str() {
140        let id = SessionId::from_string("hello");
141        let s: &str = id.as_ref();
142        assert_eq!(s, "hello");
143    }
144
145    #[test]
146    fn typed_id_from_string_owned() {
147        let id = BranchId::from(String::from("dev"));
148        assert_eq!(id.as_str(), "dev");
149    }
150
151    #[test]
152    fn typed_id_serde_roundtrip() {
153        let id = EventId::from_string("01HXYZ");
154        let json = serde_json::to_string(&id).unwrap();
155        assert_eq!(json, "\"01HXYZ\"");
156        let back: EventId = serde_json::from_str(&json).unwrap();
157        assert_eq!(id, back);
158    }
159
160    #[test]
161    fn typed_id_hash_equality() {
162        use std::collections::HashSet;
163        let id = SessionId::from_string("same");
164        let id2 = SessionId::from_string("same");
165        let mut set = HashSet::new();
166        set.insert(id.clone());
167        assert!(set.contains(&id2));
168    }
169
170    #[test]
171    fn blob_hash_from_hex_and_display() {
172        let hash = BlobHash::from_hex("abcdef0123456789");
173        assert_eq!(hash.as_str(), "abcdef0123456789");
174        assert_eq!(format!("{hash}"), "abcdef0123456789");
175    }
176
177    #[test]
178    fn blob_hash_serde_roundtrip() {
179        let hash = BlobHash::from_hex("deadbeef");
180        let json = serde_json::to_string(&hash).unwrap();
181        assert_eq!(json, "\"deadbeef\"");
182        let back: BlobHash = serde_json::from_str(&json).unwrap();
183        assert_eq!(hash, back);
184    }
185
186    #[test]
187    fn blob_hash_from_string_trait() {
188        let hash = BlobHash::from(String::from("cafebabe"));
189        assert_eq!(hash.as_str(), "cafebabe");
190    }
191
192    #[test]
193    fn blob_hash_as_ref() {
194        let hash = BlobHash::from_hex("abc");
195        let s: &str = hash.as_ref();
196        assert_eq!(s, "abc");
197    }
198}