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);
63
64/// Content-addressed blob hash (SHA-256 hex)
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[serde(transparent)]
67pub struct BlobHash(String);
68
69impl BlobHash {
70    pub fn from_hex(hex: impl Into<String>) -> Self {
71        Self(hex.into())
72    }
73
74    pub fn as_str(&self) -> &str {
75        &self.0
76    }
77}
78
79impl fmt::Display for BlobHash {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(f, "{}", self.0)
82    }
83}
84
85impl From<String> for BlobHash {
86    fn from(s: String) -> Self {
87        Self(s)
88    }
89}
90
91impl AsRef<str> for BlobHash {
92    fn as_ref(&self) -> &str {
93        &self.0
94    }
95}
96
97/// Monotonic sequence number within a branch
98pub type SeqNo = u64;
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn event_id_new_is_unique() {
106        let a = EventId::new();
107        let b = EventId::new();
108        assert_ne!(a, b);
109    }
110
111    #[test]
112    fn session_id_from_string() {
113        let id = SessionId::from_string("test-session");
114        assert_eq!(id.as_str(), "test-session");
115        assert_eq!(id.to_string(), "test-session");
116    }
117
118    #[test]
119    fn branch_id_from_str_trait() {
120        let id: BranchId = "main".into();
121        assert_eq!(id.as_str(), "main");
122    }
123
124    #[test]
125    fn typed_id_display() {
126        let id = RunId::from_string("run-42");
127        assert_eq!(format!("{id}"), "run-42");
128    }
129
130    #[test]
131    fn typed_id_default_generates_ulid() {
132        let id = EventId::default();
133        // ULIDs are 26 characters in Crockford Base32
134        assert_eq!(id.as_str().len(), 26);
135    }
136
137    #[test]
138    fn typed_id_as_ref_str() {
139        let id = SessionId::from_string("hello");
140        let s: &str = id.as_ref();
141        assert_eq!(s, "hello");
142    }
143
144    #[test]
145    fn typed_id_from_string_owned() {
146        let id = BranchId::from(String::from("dev"));
147        assert_eq!(id.as_str(), "dev");
148    }
149
150    #[test]
151    fn typed_id_serde_roundtrip() {
152        let id = EventId::from_string("01HXYZ");
153        let json = serde_json::to_string(&id).unwrap();
154        assert_eq!(json, "\"01HXYZ\"");
155        let back: EventId = serde_json::from_str(&json).unwrap();
156        assert_eq!(id, back);
157    }
158
159    #[test]
160    fn typed_id_hash_equality() {
161        use std::collections::HashSet;
162        let id = SessionId::from_string("same");
163        let id2 = SessionId::from_string("same");
164        let mut set = HashSet::new();
165        set.insert(id.clone());
166        assert!(set.contains(&id2));
167    }
168
169    #[test]
170    fn blob_hash_from_hex_and_display() {
171        let hash = BlobHash::from_hex("abcdef0123456789");
172        assert_eq!(hash.as_str(), "abcdef0123456789");
173        assert_eq!(format!("{hash}"), "abcdef0123456789");
174    }
175
176    #[test]
177    fn blob_hash_serde_roundtrip() {
178        let hash = BlobHash::from_hex("deadbeef");
179        let json = serde_json::to_string(&hash).unwrap();
180        assert_eq!(json, "\"deadbeef\"");
181        let back: BlobHash = serde_json::from_str(&json).unwrap();
182        assert_eq!(hash, back);
183    }
184
185    #[test]
186    fn blob_hash_from_string_trait() {
187        let hash = BlobHash::from(String::from("cafebabe"));
188        assert_eq!(hash.as_str(), "cafebabe");
189    }
190
191    #[test]
192    fn blob_hash_as_ref() {
193        let hash = BlobHash::from_hex("abc");
194        let s: &str = hash.as_ref();
195        assert_eq!(s, "abc");
196    }
197}