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. There is no
8//! infallible constructor and 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<$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    /// Tenant that owns every durable record.
154    TenantId
155);
156define_id!(
157    /// Authenticated subject acting inside a tenant.
158    SubjectId
159);
160define_id!(
161    /// Transport request identity used for tracing and idempotency.
162    RequestId
163);
164define_id!(
165    /// Append-only Agent Session identity.
166    SessionId
167);
168define_id!(
169    /// One Run inside a Session.
170    RunId
171);
172define_id!(
173    /// One queued input inside a Session inbox.
174    InputId
175);
176define_id!(
177    /// Correlates a tool call with its result.
178    ToolCallId
179);
180define_id!(
181    /// One interaction (approval or question) awaiting resolution.
182    InteractionId
183);
184define_id!(
185    /// Immutable Agent Profile revision pinned by a Session.
186    ProfileRevisionId
187);
188define_id!(
189    /// Durable workflow instance identity.
190    InstanceId
191);
192define_id!(
193    /// Durable workflow action intent identity.
194    ActionIntentId
195);
196define_id!(
197    /// Docs tree node (folder or document) identity.
198    NodeId
199);
200define_id!(
201    /// Docs Space identity.
202    SpaceId
203);
204define_id!(
205    /// Immutable Docs revision identity.
206    RevisionId
207);
208define_id!(
209    /// Docs asset identity.
210    AssetId
211);
212define_id!(
213    /// Immutable Docs release identity.
214    ReleaseId
215);
216define_id!(
217    /// Docs invitation identity.
218    InviteId
219);
220define_id!(
221    /// Idempotency identity for a durable command.
222    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}