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    /// Tenant-scoped immutable workflow definition identity.
204    WorkflowDefinitionId
205);
206define_id!(
207    /// Durable workflow action intent identity.
208    ActionIntentId
209);
210define_id!(
211    /// Immutable workflow proposal and decision identity.
212    WorkflowDraftId
213);
214define_id!(
215    /// One durable workflow step fact.
216    WorkflowStepId
217);
218define_id!(
219    /// Docs tree node (folder or document) identity.
220    NodeId
221);
222define_id!(
223    /// Docs Space identity.
224    SpaceId
225);
226define_id!(
227    /// Durable notification identity.
228    NotificationId
229);
230define_id!(
231    /// Append-only notification delivery-attempt event identity.
232    NotificationAttemptId
233);
234define_id!(
235    /// Durable dead-letter identity exposed to operators.
236    DeadLetterId
237);
238define_id!(
239    /// Stable identity of one normalized content item.
240    NewsItemId
241);
242define_id!(
243    /// Tenant-owned binding between a content source and Docs.
244    NewsSourceBindingId
245);
246define_id!(
247    /// Durable content-ingestion run identity.
248    NewsIngestRunId
249);
250define_id!(
251    /// Durable deterministic replay definition identity.
252    BacktestDefinitionId
253);
254define_id!(
255    /// Durable deterministic replay run identity.
256    BacktestRunId
257);
258define_id!(
259    /// Product-owned usage reservation identity.
260    UsageReservationId
261);
262define_id!(
263    /// Product action awaiting authorization.
264    ActionCandidateId
265);
266define_id!(
267    /// Immutable Docs revision identity.
268    RevisionId
269);
270define_id!(
271    /// Docs asset identity.
272    AssetId
273);
274define_id!(
275    /// Immutable Docs release identity.
276    ReleaseId
277);
278define_id!(
279    /// Durable Docs translation request identity.
280    TranslationRequestId
281);
282define_id!(
283    /// Docs invitation identity.
284    InviteId
285);
286define_id!(
287    /// Authenticated live-editing session for one Docs node.
288    DocsCollaborationSessionId
289);
290define_id!(
291    /// Idempotency identity for a durable command.
292    CommandId
293);
294define_id!(
295    /// One controlled consumer migration split into bounded batches.
296    MigrationId
297);
298define_id!(
299    /// Stable source identity of one normalized migration record.
300    MigrationRecordId
301);
302
303define_id!(
304    /// Immutable Agent Session sharing snapshot identity.
305    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! { /// Branch identity within a pinned workflow spec.
360    BranchId
361}
362
363define_id! {
364    /// Durable identity of one provider invocation.
365    ProviderAttemptId
366}
367
368define_id! {
369    /// Idempotency identity of a reported measurement correction.
370    MeteringCorrectionId
371}
372
373define_id! {
374    /// Immutable Profile proposal identity.
375    ProfileDraftId
376}
377
378define_id! {
379    /// Durable subject-owned Memory identity.
380    MemoryId
381}
382
383define_id! {
384    /// Host-defined workflow source identity.
385    WorkflowSourceId
386}
387define_id! {
388    /// Host-defined connection or external resource identity for workflow admission.
389    WorkflowResourceId
390}