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<uuid::Uuid> for $name {
96 fn from(value: uuid::Uuid) -> Self {
97 Self(value.to_string())
98 }
99 }
100
101 impl From<$name> for String {
102 fn from(value: $name) -> Self {
103 value.0
104 }
105 }
106
107 impl AsRef<str> for $name {
108 fn as_ref(&self) -> &str {
109 &self.0
110 }
111 }
112
113 impl Borrow<str> for $name {
114 fn borrow(&self) -> &str {
115 &self.0
116 }
117 }
118
119 impl std::ops::Deref for $name {
120 type Target = str;
121 fn deref(&self) -> &str {
122 &self.0
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<&str> for $name {
133 fn eq(&self, other: &&str) -> bool {
134 self.0 == *other
135 }
136 }
137
138 impl PartialEq<String> for $name {
139 fn eq(&self, other: &String) -> bool {
140 &self.0 == other
141 }
142 }
143
144 impl PartialEq<$name> for String {
145 fn eq(&self, other: &$name) -> bool {
146 self == &other.0
147 }
148 }
149
150 impl PartialEq<$name> for &str {
151 fn eq(&self, other: &$name) -> bool {
152 *self == other.0
153 }
154 }
155 };
156}
157
158define_id!(
159 TenantId
161);
162define_id!(
163 SubjectId
165);
166define_id!(
167 RequestId
169);
170define_id!(
171 SessionId
173);
174define_id!(
175 RunId
177);
178define_id!(
179 InputId
181);
182define_id!(
183 ToolCallId
185);
186define_id!(
187 InteractionId
189);
190define_id!(
191 CapabilityId
193);
194define_id!(
195 ProfileRevisionId
197);
198define_id!(
199 InstanceId
201);
202define_id!(
203 ActionIntentId
205);
206define_id!(
207 NodeId
209);
210define_id!(
211 SpaceId
213);
214define_id!(
215 NotificationId
217);
218define_id!(
219 NotificationAttemptId
221);
222define_id!(
223 DeadLetterId
225);
226define_id!(
227 NewsItemId
229);
230define_id!(
231 NewsSourceBindingId
233);
234define_id!(
235 NewsIngestRunId
237);
238define_id!(
239 BacktestDefinitionId
241);
242define_id!(
243 BacktestRunId
245);
246define_id!(
247 UsageReservationId
249);
250define_id!(
251 ActionCandidateId
253);
254define_id!(
255 RevisionId
257);
258define_id!(
259 AssetId
261);
262define_id!(
263 ReleaseId
265);
266define_id!(
267 InviteId
269);
270define_id!(
271 CommandId
273);
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn ids_are_distinct_types_with_string_behaviour() {
281 let session = SessionId::try_from("s1").unwrap();
282 assert_eq!(session, "s1");
283 assert_eq!(session.to_string(), "s1");
284 assert_eq!(serde_json::to_string(&session).unwrap(), "\"s1\"");
285 let parsed: SessionId = serde_json::from_str("\"s2\"").unwrap();
286 assert_eq!(parsed.as_str(), "s2");
287 assert_eq!(format!("{session:?}"), "SessionId(\"s1\")");
288 assert_eq!(SessionId::parse(" "), Err(EmptyId("SessionId")));
289 assert!("".parse::<RunId>().is_err());
290 let mut set = std::collections::BTreeSet::new();
291 set.insert(RunId::try_from(String::from("r")).unwrap());
292 assert!(set.contains("r"));
293 }
294
295 #[test]
296 fn every_constructor_rejects_empty_and_whitespace() {
297 for blank in ["", " ", "\t\n"] {
298 assert_eq!(TenantId::try_from(blank), Err(EmptyId("TenantId")));
299 assert_eq!(
300 TenantId::try_from(blank.to_owned()),
301 Err(EmptyId("TenantId"))
302 );
303 assert_eq!(
304 TenantId::try_from(&blank.to_owned()),
305 Err(EmptyId("TenantId"))
306 );
307 assert_eq!(blank.parse::<TenantId>(), Err(EmptyId("TenantId")));
308 let json = serde_json::to_string(blank).unwrap();
309 let rejected = serde_json::from_str::<TenantId>(&json).unwrap_err();
310 assert!(
311 rejected.to_string().contains("TenantId must not be empty"),
312 "{rejected}"
313 );
314 #[derive(serde::Deserialize)]
315 struct Envelope {
316 #[allow(dead_code)]
317 tenant_id: TenantId,
318 }
319 assert!(
320 serde_json::from_str::<Envelope>(&format!("{{\"tenant_id\":{json}}}")).is_err()
321 );
322 }
323 }
324}