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 WorkflowDefinitionId
205);
206define_id!(
207 ActionIntentId
209);
210define_id!(
211 WorkflowDraftId
213);
214define_id!(
215 WorkflowStepId
217);
218define_id!(
219 NodeId
221);
222define_id!(
223 SpaceId
225);
226define_id!(
227 NotificationId
229);
230define_id!(
231 NotificationAttemptId
233);
234define_id!(
235 DeadLetterId
237);
238define_id!(
239 NewsItemId
241);
242define_id!(
243 NewsSourceBindingId
245);
246define_id!(
247 NewsIngestRunId
249);
250define_id!(
251 BacktestDefinitionId
253);
254define_id!(
255 BacktestRunId
257);
258define_id!(
259 UsageReservationId
261);
262define_id!(
263 ActionCandidateId
265);
266define_id!(
267 RevisionId
269);
270define_id!(
271 AssetId
273);
274define_id!(
275 ReleaseId
277);
278define_id!(
279 TranslationRequestId
281);
282define_id!(
283 InviteId
285);
286define_id!(
287 DocsCollaborationSessionId
289);
290define_id!(
291 CommandId
293);
294define_id!(
295 MigrationId
297);
298define_id!(
299 MigrationRecordId
301);
302
303define_id!(
304 SessionSnapshotId
306);
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn ids_are_distinct_types_with_string_behaviour() {
314 let session = SessionId::try_from("s1").unwrap();
315 assert_eq!(session, "s1");
316 assert_eq!(session.to_string(), "s1");
317 assert_eq!(serde_json::to_string(&session).unwrap(), "\"s1\"");
318 let parsed: SessionId = serde_json::from_str("\"s2\"").unwrap();
319 assert_eq!(parsed.as_str(), "s2");
320 assert_eq!(format!("{session:?}"), "SessionId(\"s1\")");
321 assert_eq!(SessionId::parse(" "), Err(EmptyId("SessionId")));
322 assert!("".parse::<RunId>().is_err());
323 let mut set = std::collections::BTreeSet::new();
324 set.insert(RunId::try_from(String::from("r")).unwrap());
325 assert!(set.contains("r"));
326 }
327
328 #[test]
329 fn every_constructor_rejects_empty_and_whitespace() {
330 for blank in ["", " ", "\t\n"] {
331 assert_eq!(TenantId::try_from(blank), Err(EmptyId("TenantId")));
332 assert_eq!(
333 TenantId::try_from(blank.to_owned()),
334 Err(EmptyId("TenantId"))
335 );
336 assert_eq!(
337 TenantId::try_from(&blank.to_owned()),
338 Err(EmptyId("TenantId"))
339 );
340 assert_eq!(blank.parse::<TenantId>(), Err(EmptyId("TenantId")));
341 let json = serde_json::to_string(blank).unwrap();
342 let rejected = serde_json::from_str::<TenantId>(&json).unwrap_err();
343 assert!(
344 rejected.to_string().contains("TenantId must not be empty"),
345 "{rejected}"
346 );
347 #[derive(serde::Deserialize)]
348 struct Envelope {
349 #[allow(dead_code)]
350 tenant_id: TenantId,
351 }
352 assert!(
353 serde_json::from_str::<Envelope>(&format!("{{\"tenant_id\":{json}}}")).is_err()
354 );
355 }
356 }
357}
358
359define_id! { BranchId
361}
362
363define_id! {
364 ProviderAttemptId
366}
367
368define_id! {
369 MeteringCorrectionId
371}
372
373define_id! {
374 ProfileDraftId
376}
377
378define_id! {
379 MemoryId
381}
382
383define_id! {
384 WorkflowSourceId
386}
387define_id! {
388 WorkflowResourceId
390}