1use serde::{Deserialize, Serialize};
7use std::fmt;
8use ulid::Ulid;
9
10macro_rules! define_id {
12 ($name:ident, $prefix:expr) => {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14 #[serde(transparent)]
15 pub struct $name(Ulid);
16
17 impl $name {
18 pub fn new() -> Self {
20 Self(Ulid::new())
21 }
22
23 pub fn from_ulid(ulid: Ulid) -> Self {
25 Self(ulid)
26 }
27
28 pub fn parse(s: &str) -> Result<Self, IdParseError> {
30 let s = s.strip_prefix($prefix).unwrap_or(s);
31 let s = s.strip_prefix('_').unwrap_or(s);
32 let ulid = Ulid::from_string(s).map_err(|_| IdParseError::InvalidFormat)?;
33 Ok(Self(ulid))
34 }
35
36 pub fn inner(&self) -> Ulid {
38 self.0
39 }
40
41 pub fn timestamp(&self) -> u64 {
43 self.0.timestamp_ms()
44 }
45
46 pub fn to_prefixed_string(&self) -> String {
48 format!("{}_{}", $prefix, self.0)
49 }
50 }
51
52 impl Default for $name {
53 fn default() -> Self {
54 Self::new()
55 }
56 }
57
58 impl fmt::Display for $name {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "{}_{}", $prefix, self.0)
61 }
62 }
63
64 impl std::str::FromStr for $name {
65 type Err = IdParseError;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 Self::parse(s)
69 }
70 }
71 };
72}
73
74#[derive(Debug, Clone, thiserror::Error)]
76pub enum IdParseError {
77 #[error("invalid ID format")]
78 InvalidFormat,
79}
80
81define_id!(TenantId, "ten");
83define_id!(WorkspaceId, "wks");
84define_id!(ProjectId, "prj");
85define_id!(AgentId, "agt");
86define_id!(AgentVersionId, "agv");
87define_id!(ToolId, "tol");
88define_id!(ToolVersionId, "tov");
89define_id!(RunId, "run");
90define_id!(StepId, "stp");
91define_id!(PolicyRuleId, "pol");
92define_id!(PolicyDecisionId, "pdc");
93define_id!(ApprovalId, "apr");
94define_id!(AuditEventId, "aud");
95define_id!(ApiKeyId, "key");
96define_id!(ArtifactId, "art");
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::collections::HashSet;
102
103 #[test]
107 fn test_run_id_generation() {
108 let id = RunId::new();
109 let s = id.to_string();
110 assert!(s.starts_with("run_"));
111 assert_eq!(s.len(), 30); }
113
114 #[test]
118 fn test_step_id_generation() {
119 let id = StepId::new();
120 let s = id.to_string();
121 assert!(s.starts_with("stp_"));
122 assert_eq!(s.len(), 30);
123 }
124
125 #[test]
129 fn test_agent_id_generation() {
130 let id = AgentId::new();
131 let s = id.to_string();
132 assert!(s.starts_with("agt_"));
133 assert_eq!(s.len(), 30);
134 }
135
136 #[test]
140 fn test_id_parsing_valid() {
141 let id = RunId::new();
142 let s = id.to_string();
143 let parsed = RunId::parse(&s).unwrap();
144 assert_eq!(id, parsed);
145 }
146
147 #[test]
148 fn test_run_id_parse_without_prefix() {
149 let id = RunId::new();
150 let ulid_str = id.inner().to_string();
151 let parsed = RunId::parse(&ulid_str).unwrap();
152 assert_eq!(id, parsed);
153 }
154
155 #[test]
159 fn test_id_parsing_different_prefix_fails() {
160 let id = RunId::new();
162 let s = id.to_string();
163 let wrong_prefix = s.replace("run_", "stp_");
165 let result = RunId::parse(&wrong_prefix);
167 assert!(result.is_err());
168 }
169
170 #[test]
171 fn test_id_parsing_with_correct_prefix_works() {
172 let id = RunId::new();
174 let prefixed = id.to_prefixed_string();
175 let result = RunId::parse(&prefixed);
176 assert!(result.is_ok());
177 assert_eq!(result.unwrap(), id);
178 }
179
180 #[test]
184 fn test_id_parsing_invalid_ulid() {
185 let result = RunId::parse("run_INVALID_ULID_STRING!!!");
186 assert!(result.is_err());
187 match result {
188 Err(IdParseError::InvalidFormat) => {}
189 _ => panic!("Expected InvalidFormat error"),
190 }
191 }
192
193 #[test]
194 fn test_id_parsing_empty_string() {
195 let result = RunId::parse("");
196 assert!(result.is_err());
197 }
198
199 #[test]
200 fn test_id_parsing_too_short() {
201 let result = RunId::parse("run_ABC");
202 assert!(result.is_err());
203 }
204
205 #[test]
209 fn test_id_uniqueness() {
210 let mut ids = HashSet::new();
211 for _ in 0..1000 {
212 let id = RunId::new();
213 assert!(ids.insert(id), "Duplicate ID generated");
214 }
215 assert_eq!(ids.len(), 1000);
216 }
217
218 #[test]
222 fn test_id_ordering() {
223 let id1 = RunId::new();
224 std::thread::sleep(std::time::Duration::from_millis(2));
225 let id2 = RunId::new();
226
227 assert!(id2.timestamp() >= id1.timestamp());
229
230 assert!(id2.to_string() > id1.to_string());
232 }
233
234 #[test]
238 fn test_id_serialization() {
239 let id = RunId::new();
240
241 let json = serde_json::to_string(&id).unwrap();
243
244 assert!(json.starts_with('"'));
246 assert!(json.ends_with('"'));
247
248 let parsed: RunId = serde_json::from_str(&json).unwrap();
250 assert_eq!(id, parsed);
251 }
252
253 #[test]
254 fn test_id_serialization_in_struct() {
255 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
256 struct TestStruct {
257 id: RunId,
258 name: String,
259 }
260
261 let original = TestStruct {
262 id: RunId::new(),
263 name: "test".to_string(),
264 };
265
266 let json = serde_json::to_string(&original).unwrap();
267 let parsed: TestStruct = serde_json::from_str(&json).unwrap();
268 assert_eq!(original, parsed);
269 }
270
271 #[test]
275 fn test_id_equality() {
276 let id1 = RunId::new();
277 let id2 = RunId::parse(&id1.to_string()).unwrap();
278 assert_eq!(id1, id2);
279 }
280
281 #[test]
282 fn test_id_hash_consistency() {
283 use std::collections::hash_map::DefaultHasher;
284 use std::hash::{Hash, Hasher};
285
286 let id1 = RunId::new();
287 let id2 = RunId::parse(&id1.to_string()).unwrap();
288
289 let hash1 = {
290 let mut h = DefaultHasher::new();
291 id1.hash(&mut h);
292 h.finish()
293 };
294 let hash2 = {
295 let mut h = DefaultHasher::new();
296 id2.hash(&mut h);
297 h.finish()
298 };
299 assert_eq!(hash1, hash2);
300 }
301
302 #[test]
306 fn test_all_id_types_have_correct_prefix() {
307 assert!(TenantId::new().to_string().starts_with("ten_"));
308 assert!(WorkspaceId::new().to_string().starts_with("wks_"));
309 assert!(ProjectId::new().to_string().starts_with("prj_"));
310 assert!(AgentVersionId::new().to_string().starts_with("agv_"));
311 assert!(ToolId::new().to_string().starts_with("tol_"));
312 assert!(ToolVersionId::new().to_string().starts_with("tov_"));
313 assert!(PolicyRuleId::new().to_string().starts_with("pol_"));
314 assert!(PolicyDecisionId::new().to_string().starts_with("pdc_"));
315 assert!(ApprovalId::new().to_string().starts_with("apr_"));
316 assert!(AuditEventId::new().to_string().starts_with("aud_"));
317 assert!(ApiKeyId::new().to_string().starts_with("key_"));
318 assert!(ArtifactId::new().to_string().starts_with("art_"));
319 }
320
321 #[test]
322 fn test_id_from_ulid() {
323 let ulid = Ulid::new();
324 let id = RunId::from_ulid(ulid);
325 assert_eq!(id.inner(), ulid);
326 }
327
328 #[test]
329 fn test_id_default() {
330 let id: RunId = Default::default();
331 assert!(id.to_string().starts_with("run_"));
332 }
333
334 #[test]
335 fn test_id_from_str_trait() {
336 let id = RunId::new();
337 let s = id.to_string();
338 let parsed: RunId = s.parse().unwrap();
339 assert_eq!(id, parsed);
340 }
341
342 #[test]
343 fn test_id_display_and_to_prefixed_string() {
344 let id = RunId::new();
345 assert_eq!(id.to_string(), id.to_prefixed_string());
347 }
348
349 #[test]
350 fn test_id_timestamp_extraction() {
351 let before = std::time::SystemTime::now()
352 .duration_since(std::time::UNIX_EPOCH)
353 .unwrap()
354 .as_millis() as u64;
355
356 let id = RunId::new();
357
358 let after = std::time::SystemTime::now()
359 .duration_since(std::time::UNIX_EPOCH)
360 .unwrap()
361 .as_millis() as u64;
362
363 assert!(id.timestamp() >= before);
364 assert!(id.timestamp() <= after);
365 }
366}