1use arrayvec::ArrayString;
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4use std::str::FromStr;
5use vitaminc::random::{Generatable, SafeRand};
6
7const ACTOR_ID_BYTE_LEN: usize = 10;
8const ACTOR_ID_ENCODED_LEN: usize = 16;
9const ALPHABET: base32::Alphabet = base32::Alphabet::Rfc4648 { padding: false };
10
11type ActorIdString = ArrayString<ACTOR_ID_ENCODED_LEN>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
15#[serde(rename_all = "lowercase")]
16pub enum ActorKind {
17 App,
18 Agent,
19}
20
21impl ActorKind {
22 pub fn as_str(&self) -> &'static str {
23 match self {
24 Self::App => "app",
25 Self::Agent => "agent",
26 }
27 }
28
29 pub fn parse(s: &str) -> Option<Self> {
30 match s {
31 "app" => Some(Self::App),
32 "agent" => Some(Self::Agent),
33 _ => None,
34 }
35 }
36}
37
38#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
47#[serde(transparent)]
48#[cfg_attr(
49 feature = "server",
50 derive(diesel::expression::AsExpression, diesel::deserialize::FromSqlRow)
51)]
52#[cfg_attr(feature = "server", diesel(sql_type = diesel::sql_types::Text))]
53pub struct ActorId(ActorIdString);
54
55impl ActorId {
56 pub fn generate() -> Result<Self, vitaminc::random::RandomError> {
58 let mut rng = SafeRand::from_entropy()?;
59 let buf: [u8; ACTOR_ID_BYTE_LEN] = Generatable::random(&mut rng)?;
60 let encoded = base32::encode(ALPHABET, &buf);
61 let mut id = ActorIdString::new();
62 id.push_str(&encoded);
63 Ok(Self(id))
64 }
65
66 pub fn as_str(&self) -> &str {
67 self.0.as_str()
68 }
69}
70
71impl Display for ActorId {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "{}", self.0)
74 }
75}
76
77impl TryFrom<&str> for ActorId {
78 type Error = InvalidActorId;
79
80 fn try_from(value: &str) -> Result<Self, Self::Error> {
81 if is_valid_actor_id(value) {
82 let mut id = ActorIdString::new();
83 id.push_str(value);
84 Ok(Self(id))
85 } else {
86 Err(InvalidActorId(value.to_string()))
87 }
88 }
89}
90
91impl TryFrom<String> for ActorId {
92 type Error = InvalidActorId;
93
94 fn try_from(value: String) -> Result<Self, Self::Error> {
95 Self::try_from(value.as_str())
96 }
97}
98
99impl FromStr for ActorId {
100 type Err = InvalidActorId;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::try_from(s)
104 }
105}
106
107impl PartialEq<&str> for ActorId {
108 fn eq(&self, other: &&str) -> bool {
109 self.0.as_str() == *other
110 }
111}
112
113#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
121pub struct ActorIdentifier {
122 kind: ActorKind,
123 id: ActorId,
124}
125
126impl ActorIdentifier {
127 pub fn new(kind: ActorKind, id: ActorId) -> Self {
129 Self { kind, id }
130 }
131
132 pub fn generate(kind: ActorKind) -> Result<Self, vitaminc::random::RandomError> {
134 let id = ActorId::generate()?;
135 Ok(Self { kind, id })
136 }
137
138 pub fn kind(&self) -> ActorKind {
140 self.kind
141 }
142
143 pub fn id(&self) -> ActorId {
145 self.id
146 }
147}
148
149impl Display for ActorIdentifier {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 write!(f, "{}-{}", self.kind.as_str(), self.id)
152 }
153}
154
155impl TryFrom<&str> for ActorIdentifier {
156 type Error = InvalidActorId;
157
158 fn try_from(value: &str) -> Result<Self, Self::Error> {
159 let (prefix, id_str) = value
160 .split_once('-')
161 .ok_or_else(|| InvalidActorId(value.to_string()))?;
162
163 let kind = ActorKind::parse(prefix).ok_or_else(|| InvalidActorId(value.to_string()))?;
164 let id = ActorId::try_from(id_str)?;
165
166 Ok(Self { kind, id })
167 }
168}
169
170impl FromStr for ActorIdentifier {
171 type Err = InvalidActorId;
172
173 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 Self::try_from(s)
175 }
176}
177
178impl Serialize for ActorIdentifier {
179 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
180 where
181 S: serde::Serializer,
182 {
183 serializer.serialize_str(&self.to_string())
184 }
185}
186
187impl<'de> Deserialize<'de> for ActorIdentifier {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: serde::Deserializer<'de>,
191 {
192 let s = String::deserialize(deserializer)?;
193 Self::try_from(s.as_str()).map_err(serde::de::Error::custom)
194 }
195}
196
197fn is_valid_actor_id(id: &str) -> bool {
198 base32::decode(ALPHABET, id)
199 .map(|bytes| bytes.len() == ACTOR_ID_BYTE_LEN)
200 .unwrap_or(false)
201}
202
203#[derive(Debug, thiserror::Error)]
204#[error("Invalid actor ID: {0}")]
205pub struct InvalidActorId(String);
206
207#[cfg(feature = "server")]
208mod sql_types {
209 use super::ActorId;
210 use diesel::{
211 backend::Backend,
212 deserialize::{self, FromSql},
213 serialize::{self, Output, ToSql},
214 sql_types::Text,
215 };
216
217 impl<DB> ToSql<Text, DB> for ActorId
218 where
219 DB: Backend,
220 str: ToSql<Text, DB>,
221 {
222 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
223 self.0.to_sql(out)
224 }
225 }
226
227 impl<DB> FromSql<Text, DB> for ActorId
228 where
229 DB: Backend,
230 String: FromSql<Text, DB>,
231 {
232 fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
233 let raw = String::from_sql(bytes)?;
234 let actor_id = ActorId::try_from(raw)?;
235 Ok(actor_id)
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use serde_json::json;
244
245 mod actor_id {
246 use super::*;
247
248 #[test]
249 fn generate_produces_valid_id() {
250 let id = ActorId::generate().unwrap();
251 assert_eq!(id.as_str().len(), 16, "base32 ID should be 16 chars");
252 }
253
254 #[test]
255 fn round_trips_through_serde() {
256 let id = ActorId::generate().unwrap();
257 let json = serde_json::to_value(id).unwrap();
258 let parsed: ActorId = serde_json::from_value(json).unwrap();
259 assert_eq!(parsed, id);
260 }
261
262 #[test]
263 fn from_str_round_trips() {
264 let id = ActorId::generate().unwrap();
265 let s = id.to_string();
266 let parsed: ActorId = s.parse().unwrap();
267 assert_eq!(parsed, id);
268 }
269
270 #[test]
271 fn rejects_invalid_base32() {
272 assert!(ActorId::try_from("!!!INVALID!!!").is_err());
273 }
274
275 #[test]
276 fn rejects_wrong_length() {
277 assert!(ActorId::try_from("AAAA").is_err());
278 }
279 }
280
281 mod identifier_app {
282 use super::*;
283
284 #[test]
285 fn generate_produces_valid_identifier() {
286 let ident = ActorIdentifier::generate(ActorKind::App).unwrap();
287 assert_eq!(ident.kind(), ActorKind::App, "kind should be App");
288 assert_eq!(
289 ident.id().as_str().len(),
290 16,
291 "base32 ID should be 16 chars"
292 );
293 }
294
295 #[test]
296 fn serializes_with_prefix() {
297 let ident = ActorIdentifier::generate(ActorKind::App).unwrap();
298 let serialized = serde_json::to_value(ident).unwrap();
299 let s = serialized.as_str().unwrap();
300 assert!(s.starts_with("app-"), "should start with 'app-', got: {s}");
301 assert_eq!(s.len(), 20, "app-<16 chars> = 20 chars, got: {s}");
302 }
303
304 #[test]
305 fn round_trips_through_serde() {
306 let ident = ActorIdentifier::generate(ActorKind::App).unwrap();
307 let json = serde_json::to_value(ident).unwrap();
308 let parsed: ActorIdentifier = serde_json::from_value(json).unwrap();
309 assert_eq!(parsed, ident, "should round-trip through serde");
310 }
311 }
312
313 mod identifier_agent {
314 use super::*;
315
316 #[test]
317 fn generate_produces_valid_identifier() {
318 let ident = ActorIdentifier::generate(ActorKind::Agent).unwrap();
319 assert_eq!(ident.kind(), ActorKind::Agent, "kind should be Agent");
320 assert_eq!(
321 ident.id().as_str().len(),
322 16,
323 "base32 ID should be 16 chars"
324 );
325 }
326
327 #[test]
328 fn serializes_with_prefix() {
329 let ident = ActorIdentifier::generate(ActorKind::Agent).unwrap();
330 let serialized = serde_json::to_value(ident).unwrap();
331 let s = serialized.as_str().unwrap();
332 assert!(
333 s.starts_with("agent-"),
334 "should start with 'agent-', got: {s}"
335 );
336 assert_eq!(s.len(), 22, "agent-<16 chars> = 22 chars, got: {s}");
337 }
338
339 #[test]
340 fn round_trips_through_serde() {
341 let ident = ActorIdentifier::generate(ActorKind::Agent).unwrap();
342 let json = serde_json::to_value(ident).unwrap();
343 let parsed: ActorIdentifier = serde_json::from_value(json).unwrap();
344 assert_eq!(parsed, ident, "should round-trip through serde");
345 }
346 }
347
348 mod identifier_invalid {
349 use super::*;
350
351 #[test]
352 fn rejects_unknown_prefix() {
353 let json = json!("unknown-JBSWY3DPEHPK3PXP");
354 let result: Result<ActorIdentifier, _> = serde_json::from_value(json);
355 assert!(result.is_err(), "should reject unknown actor kind");
356 }
357
358 #[test]
359 fn rejects_missing_delimiter() {
360 let json = json!("appJBSWY3DPEHPK3PXP");
361 let result: Result<ActorIdentifier, _> = serde_json::from_value(json);
362 assert!(result.is_err(), "should reject missing delimiter");
363 }
364
365 #[test]
366 fn rejects_invalid_base32() {
367 let json = json!("app-!!!INVALID!!!");
368 let result: Result<ActorIdentifier, _> = serde_json::from_value(json);
369 assert!(result.is_err(), "should reject invalid base32");
370 }
371
372 #[test]
373 fn rejects_wrong_length() {
374 let json = json!("app-AAAA");
375 let result: Result<ActorIdentifier, _> = serde_json::from_value(json);
376 assert!(result.is_err(), "should reject wrong-length ID");
377 }
378
379 #[test]
380 fn rejects_empty_string() {
381 let json = json!("");
382 let result: Result<ActorIdentifier, _> = serde_json::from_value(json);
383 assert!(result.is_err(), "should reject empty string");
384 }
385 }
386
387 #[test]
388 fn from_str_round_trips() {
389 let ident = ActorIdentifier::generate(ActorKind::App).unwrap();
390 let s = ident.to_string();
391 let parsed: ActorIdentifier = s.parse().unwrap();
392 assert_eq!(parsed, ident);
393 }
394
395 #[test]
396 fn from_str_rejects_invalid() {
397 assert!("not-valid".parse::<ActorIdentifier>().is_err());
398 }
399
400 #[test]
401 fn display_matches_serialize() {
402 let ident = ActorIdentifier::generate(ActorKind::App).unwrap();
403 let display = ident.to_string();
404 let serialized = serde_json::to_value(ident).unwrap();
405 assert_eq!(
406 display,
407 serialized.as_str().unwrap(),
408 "Display and Serialize should produce the same string"
409 );
410 }
411
412 #[test]
413 fn new_constructs_from_parts() {
414 let id = ActorId::generate().unwrap();
415 let ident = ActorIdentifier::new(ActorKind::App, id);
416 assert_eq!(ident.kind(), ActorKind::App);
417 assert_eq!(ident.id(), id);
418 }
419}