Skip to main content

af_context/
ids.rs

1//! Branded identifiers shared by every Factory crate.
2//!
3//! Each id is a distinct newtype over a non-empty string so a `SessionId` can
4//! never be passed where a `RunId` is expected. Serialization, `Display`,
5//! ordering and hashing behave like the underlying string; store adapters bind
6//! [`as_str`](TenantId::as_str) and wire adapters convert with `TryFrom<String>`
7//! / [`FromStr`], which reject empty or whitespace-only input. UUID-backed
8//! stores may use infallible `From<uuid::Uuid>`; there is no `Default`.
9
10use std::borrow::Borrow;
11use std::fmt;
12use std::str::FromStr;
13
14/// The value given to an id constructor was empty or whitespace.
15#[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            /// Wraps a non-empty string; the only constructor, also used by
29            /// `TryFrom`, `FromStr` and `Deserialize`.
30            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            /// Borrows the identifier as a string slice.
39            pub fn as_str(&self) -> &str {
40                &self.0
41            }
42
43            /// Consumes the identifier and returns the owned string.
44            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    /// Tenant that owns every durable record.
160    TenantId
161);
162define_id!(
163    /// Authenticated subject acting inside a tenant.
164    SubjectId
165);
166define_id!(
167    /// Transport request identity used for tracing and idempotency.
168    RequestId
169);
170define_id!(
171    /// Append-only Agent Session identity.
172    SessionId
173);
174define_id!(
175    /// One Run inside a Session.
176    RunId
177);
178define_id!(
179    /// One queued input inside a Session inbox.
180    InputId
181);
182define_id!(
183    /// Correlates a tool call with its result.
184    ToolCallId
185);
186define_id!(
187    /// One interaction (approval or question) awaiting resolution.
188    InteractionId
189);
190define_id!(
191    /// Stable identity of an installable capability definition.
192    CapabilityId
193);
194define_id!(
195    /// Immutable Agent Profile revision pinned by a Session.
196    ProfileRevisionId
197);
198define_id!(
199    /// Durable workflow instance identity.
200    InstanceId
201);
202define_id!(
203    /// Durable workflow action intent identity.
204    ActionIntentId
205);
206define_id!(
207    /// Docs tree node (folder or document) identity.
208    NodeId
209);
210define_id!(
211    /// Docs Space identity.
212    SpaceId
213);
214define_id!(
215    /// Durable notification identity.
216    NotificationId
217);
218define_id!(
219    /// Append-only notification delivery-attempt event identity.
220    NotificationAttemptId
221);
222define_id!(
223    /// Durable dead-letter identity exposed to operators.
224    DeadLetterId
225);
226define_id!(
227    /// Stable identity of one normalized content item.
228    NewsItemId
229);
230define_id!(
231    /// Tenant-owned binding between a content source and Docs.
232    NewsSourceBindingId
233);
234define_id!(
235    /// Durable content-ingestion run identity.
236    NewsIngestRunId
237);
238define_id!(
239    /// Durable deterministic replay definition identity.
240    BacktestDefinitionId
241);
242define_id!(
243    /// Durable deterministic replay run identity.
244    BacktestRunId
245);
246define_id!(
247    /// Product-owned usage reservation identity.
248    UsageReservationId
249);
250define_id!(
251    /// Product action awaiting authorization.
252    ActionCandidateId
253);
254define_id!(
255    /// Immutable Docs revision identity.
256    RevisionId
257);
258define_id!(
259    /// Docs asset identity.
260    AssetId
261);
262define_id!(
263    /// Immutable Docs release identity.
264    ReleaseId
265);
266define_id!(
267    /// Docs invitation identity.
268    InviteId
269);
270define_id!(
271    /// Idempotency identity for a durable command.
272    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}