1use std::borrow::Borrow;
11use std::fmt;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16#[error("{0} must not be empty")]
17pub struct EmptyId(pub &'static str);
18
19macro_rules! define_id {
20 ($(#[$doc:meta])* $name:ident) => {
21 $(#[$doc])*
22 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
23 #[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
24 #[serde(try_from = "String", into = "String")]
25 pub struct $name(String);
26
27 impl $name {
28 pub fn parse(value: impl Into<String>) -> Result<Self, EmptyId> {
31 let value = value.into();
32 if value.trim().is_empty() {
33 return Err(EmptyId(stringify!($name)));
34 }
35 Ok(Self(value))
36 }
37
38 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42
43 pub fn into_string(self) -> String {
45 self.0
46 }
47 }
48
49 impl fmt::Debug for $name {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}({:?})", stringify!($name), self.0)
52 }
53 }
54
55 impl fmt::Display for $name {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(&self.0)
58 }
59 }
60
61 impl FromStr for $name {
62 type Err = EmptyId;
63 fn from_str(value: &str) -> Result<Self, EmptyId> {
64 Self::parse(value)
65 }
66 }
67
68 impl TryFrom<String> for $name {
69 type Error = EmptyId;
70 fn try_from(value: String) -> Result<Self, EmptyId> {
71 Self::parse(value)
72 }
73 }
74
75 impl TryFrom<&str> for $name {
76 type Error = EmptyId;
77 fn try_from(value: &str) -> Result<Self, EmptyId> {
78 Self::parse(value)
79 }
80 }
81
82 impl TryFrom<&String> for $name {
83 type Error = EmptyId;
84 fn try_from(value: &String) -> Result<Self, EmptyId> {
85 Self::parse(value.as_str())
86 }
87 }
88
89 impl From<&$name> for $name {
90 fn from(value: &$name) -> Self {
91 value.clone()
92 }
93 }
94
95 impl From<$name> for String {
96 fn from(value: $name) -> Self {
97 value.0
98 }
99 }
100
101 impl AsRef<str> for $name {
102 fn as_ref(&self) -> &str {
103 &self.0
104 }
105 }
106
107 impl Borrow<str> for $name {
108 fn borrow(&self) -> &str {
109 &self.0
110 }
111 }
112
113 impl std::ops::Deref for $name {
114 type Target = str;
115 fn deref(&self) -> &str {
116 &self.0
117 }
118 }
119
120 impl PartialEq<str> for $name {
121 fn eq(&self, other: &str) -> bool {
122 self.0 == other
123 }
124 }
125
126 impl PartialEq<&str> for $name {
127 fn eq(&self, other: &&str) -> bool {
128 self.0 == *other
129 }
130 }
131
132 impl PartialEq<String> for $name {
133 fn eq(&self, other: &String) -> bool {
134 &self.0 == other
135 }
136 }
137
138 impl PartialEq<$name> for String {
139 fn eq(&self, other: &$name) -> bool {
140 self == &other.0
141 }
142 }
143
144 impl PartialEq<$name> for &str {
145 fn eq(&self, other: &$name) -> bool {
146 *self == other.0
147 }
148 }
149 };
150}
151
152define_id!(
153 TenantId
155);
156define_id!(
157 SubjectId
159);
160define_id!(
161 RequestId
163);
164define_id!(
165 SessionId
167);
168define_id!(
169 RunId
171);
172define_id!(
173 InputId
175);
176define_id!(
177 ToolCallId
179);
180define_id!(
181 InteractionId
183);
184define_id!(
185 ProfileRevisionId
187);
188define_id!(
189 InstanceId
191);
192define_id!(
193 ActionIntentId
195);
196define_id!(
197 NodeId
199);
200define_id!(
201 SpaceId
203);
204define_id!(
205 RevisionId
207);
208define_id!(
209 AssetId
211);
212define_id!(
213 ReleaseId
215);
216define_id!(
217 InviteId
219);
220define_id!(
221 CommandId
223);
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn ids_are_distinct_types_with_string_behaviour() {
231 let session = SessionId::try_from("s1").unwrap();
232 assert_eq!(session, "s1");
233 assert_eq!(session.to_string(), "s1");
234 assert_eq!(serde_json::to_string(&session).unwrap(), "\"s1\"");
235 let parsed: SessionId = serde_json::from_str("\"s2\"").unwrap();
236 assert_eq!(parsed.as_str(), "s2");
237 assert_eq!(format!("{session:?}"), "SessionId(\"s1\")");
238 assert_eq!(SessionId::parse(" "), Err(EmptyId("SessionId")));
239 assert!("".parse::<RunId>().is_err());
240 let mut set = std::collections::BTreeSet::new();
241 set.insert(RunId::try_from(String::from("r")).unwrap());
242 assert!(set.contains("r"));
243 }
244
245 #[test]
246 fn every_constructor_rejects_empty_and_whitespace() {
247 for blank in ["", " ", "\t\n"] {
248 assert_eq!(TenantId::try_from(blank), Err(EmptyId("TenantId")));
249 assert_eq!(
250 TenantId::try_from(blank.to_owned()),
251 Err(EmptyId("TenantId"))
252 );
253 assert_eq!(
254 TenantId::try_from(&blank.to_owned()),
255 Err(EmptyId("TenantId"))
256 );
257 assert_eq!(blank.parse::<TenantId>(), Err(EmptyId("TenantId")));
258 let json = serde_json::to_string(blank).unwrap();
259 let rejected = serde_json::from_str::<TenantId>(&json).unwrap_err();
260 assert!(
261 rejected.to_string().contains("TenantId must not be empty"),
262 "{rejected}"
263 );
264 #[derive(serde::Deserialize)]
265 struct Envelope {
266 #[allow(dead_code)]
267 tenant_id: TenantId,
268 }
269 assert!(
270 serde_json::from_str::<Envelope>(&format!("{{\"tenant_id\":{json}}}")).is_err()
271 );
272 }
273 }
274}