1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::error::{ErrorCode, MachiError};
10
11macro_rules! typed_id {
12 ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
13 $(#[$meta])*
14 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15 #[serde(transparent)]
16 pub struct $name(String);
17
18 impl $name {
19 #[must_use]
21 pub fn generate() -> Self {
22 Self(format!("{}_{}", $prefix, Uuid::new_v4().simple()))
23 }
24
25 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30
31 pub fn new(value: impl Into<String>) -> Result<Self, MachiError> {
37 let value = value.into();
38 if value.trim().is_empty() {
39 return Err(MachiError::new(
40 ErrorCode::TypesInvalidId,
41 format!("{} must be non-empty", stringify!($name)),
42 ));
43 }
44 Ok(Self(value))
45 }
46 }
47
48 impl fmt::Display for $name {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.write_str(&self.0)
51 }
52 }
53
54 impl FromStr for $name {
55 type Err = MachiError;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 Self::new(s)
59 }
60 }
61
62 impl AsRef<str> for $name {
63 fn as_ref(&self) -> &str {
64 self.as_str()
65 }
66 }
67 };
68}
69
70typed_id!(
71 AgentId,
73 "agent"
74);
75typed_id!(
76 RunId,
78 "run"
79);
80typed_id!(
81 SessionId,
83 "session"
84);
85typed_id!(
86 ToolCallId,
88 "call"
89);
90typed_id!(
91 WorkflowRunId,
93 "wf"
94);
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn generate_has_prefix() {
102 let id = AgentId::generate();
103 assert!(id.as_str().starts_with("agent_"), "{}", id);
104 }
105
106 #[test]
107 fn rejects_empty() {
108 let err = SessionId::new(" ").expect_err("empty");
109 assert_eq!(err.code(), ErrorCode::TypesInvalidId);
110 }
111}