use serde::{Deserialize, Serialize};
use std::fmt;
use ulid::Ulid;
macro_rules! define_id {
($name:ident, $prefix:expr) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(Ulid);
impl $name {
pub fn new() -> Self {
Self(Ulid::new())
}
pub fn from_ulid(ulid: Ulid) -> Self {
Self(ulid)
}
pub fn parse(s: &str) -> Result<Self, IdParseError> {
let s = s.strip_prefix($prefix).unwrap_or(s);
let s = s.strip_prefix('_').unwrap_or(s);
let ulid = Ulid::from_string(s).map_err(|_| IdParseError::InvalidFormat)?;
Ok(Self(ulid))
}
pub fn inner(&self) -> Ulid {
self.0
}
pub fn timestamp(&self) -> u64 {
self.0.timestamp_ms()
}
pub fn to_prefixed_string(&self) -> String {
format!("{}_{}", $prefix, self.0)
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", $prefix, self.0)
}
}
impl std::str::FromStr for $name {
type Err = IdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
};
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum IdParseError {
#[error("invalid ID format")]
InvalidFormat,
}
define_id!(TenantId, "ten");
define_id!(WorkspaceId, "wks");
define_id!(ProjectId, "prj");
define_id!(AgentId, "agt");
define_id!(AgentVersionId, "agv");
define_id!(ToolId, "tol");
define_id!(ToolVersionId, "tov");
define_id!(RunId, "run");
define_id!(StepId, "stp");
define_id!(PolicyRuleId, "pol");
define_id!(PolicyDecisionId, "pdc");
define_id!(ApprovalId, "apr");
define_id!(AuditEventId, "aud");
define_id!(ApiKeyId, "key");
define_id!(ArtifactId, "art");
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_run_id_generation() {
let id = RunId::new();
let s = id.to_string();
assert!(s.starts_with("run_"));
assert_eq!(s.len(), 30); }
#[test]
fn test_step_id_generation() {
let id = StepId::new();
let s = id.to_string();
assert!(s.starts_with("stp_"));
assert_eq!(s.len(), 30);
}
#[test]
fn test_agent_id_generation() {
let id = AgentId::new();
let s = id.to_string();
assert!(s.starts_with("agt_"));
assert_eq!(s.len(), 30);
}
#[test]
fn test_id_parsing_valid() {
let id = RunId::new();
let s = id.to_string();
let parsed = RunId::parse(&s).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_run_id_parse_without_prefix() {
let id = RunId::new();
let ulid_str = id.inner().to_string();
let parsed = RunId::parse(&ulid_str).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_id_parsing_different_prefix_fails() {
let id = RunId::new();
let s = id.to_string();
let wrong_prefix = s.replace("run_", "stp_");
let result = RunId::parse(&wrong_prefix);
assert!(result.is_err());
}
#[test]
fn test_id_parsing_with_correct_prefix_works() {
let id = RunId::new();
let prefixed = id.to_prefixed_string();
let result = RunId::parse(&prefixed);
assert!(result.is_ok());
assert_eq!(result.unwrap(), id);
}
#[test]
fn test_id_parsing_invalid_ulid() {
let result = RunId::parse("run_INVALID_ULID_STRING!!!");
assert!(result.is_err());
match result {
Err(IdParseError::InvalidFormat) => {}
_ => panic!("Expected InvalidFormat error"),
}
}
#[test]
fn test_id_parsing_empty_string() {
let result = RunId::parse("");
assert!(result.is_err());
}
#[test]
fn test_id_parsing_too_short() {
let result = RunId::parse("run_ABC");
assert!(result.is_err());
}
#[test]
fn test_id_uniqueness() {
let mut ids = HashSet::new();
for _ in 0..1000 {
let id = RunId::new();
assert!(ids.insert(id), "Duplicate ID generated");
}
assert_eq!(ids.len(), 1000);
}
#[test]
fn test_id_ordering() {
let id1 = RunId::new();
std::thread::sleep(std::time::Duration::from_millis(2));
let id2 = RunId::new();
assert!(id2.timestamp() >= id1.timestamp());
assert!(id2.to_string() > id1.to_string());
}
#[test]
fn test_id_serialization() {
let id = RunId::new();
let json = serde_json::to_string(&id).unwrap();
assert!(json.starts_with('"'));
assert!(json.ends_with('"'));
let parsed: RunId = serde_json::from_str(&json).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_id_serialization_in_struct() {
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct TestStruct {
id: RunId,
name: String,
}
let original = TestStruct {
id: RunId::new(),
name: "test".to_string(),
};
let json = serde_json::to_string(&original).unwrap();
let parsed: TestStruct = serde_json::from_str(&json).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_id_equality() {
let id1 = RunId::new();
let id2 = RunId::parse(&id1.to_string()).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn test_id_hash_consistency() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let id1 = RunId::new();
let id2 = RunId::parse(&id1.to_string()).unwrap();
let hash1 = {
let mut h = DefaultHasher::new();
id1.hash(&mut h);
h.finish()
};
let hash2 = {
let mut h = DefaultHasher::new();
id2.hash(&mut h);
h.finish()
};
assert_eq!(hash1, hash2);
}
#[test]
fn test_all_id_types_have_correct_prefix() {
assert!(TenantId::new().to_string().starts_with("ten_"));
assert!(WorkspaceId::new().to_string().starts_with("wks_"));
assert!(ProjectId::new().to_string().starts_with("prj_"));
assert!(AgentVersionId::new().to_string().starts_with("agv_"));
assert!(ToolId::new().to_string().starts_with("tol_"));
assert!(ToolVersionId::new().to_string().starts_with("tov_"));
assert!(PolicyRuleId::new().to_string().starts_with("pol_"));
assert!(PolicyDecisionId::new().to_string().starts_with("pdc_"));
assert!(ApprovalId::new().to_string().starts_with("apr_"));
assert!(AuditEventId::new().to_string().starts_with("aud_"));
assert!(ApiKeyId::new().to_string().starts_with("key_"));
assert!(ArtifactId::new().to_string().starts_with("art_"));
}
#[test]
fn test_id_from_ulid() {
let ulid = Ulid::new();
let id = RunId::from_ulid(ulid);
assert_eq!(id.inner(), ulid);
}
#[test]
fn test_id_default() {
let id: RunId = Default::default();
assert!(id.to_string().starts_with("run_"));
}
#[test]
fn test_id_from_str_trait() {
let id = RunId::new();
let s = id.to_string();
let parsed: RunId = s.parse().unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_id_display_and_to_prefixed_string() {
let id = RunId::new();
assert_eq!(id.to_string(), id.to_prefixed_string());
}
#[test]
fn test_id_timestamp_extraction() {
let before = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let id = RunId::new();
let after = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
assert!(id.timestamp() >= before);
assert!(id.timestamp() <= after);
}
}