1use serde::{Deserialize, Serialize};
7use std::fmt;
8use thiserror::Error;
9
10const MAX_ACTOR_ID_BYTES: usize = 256;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
18#[serde(deny_unknown_fields)]
19pub struct ActorRef {
20 pub kind: ActorKind,
22 pub id: ActorId,
24}
25
26impl ActorRef {
27 pub fn user(id: ActorId) -> Self {
29 Self {
30 kind: ActorKind::User,
31 id,
32 }
33 }
34
35 pub fn service(id: ActorId) -> Self {
37 Self {
38 kind: ActorKind::Service,
39 id,
40 }
41 }
42
43 pub fn system(id: ActorId) -> Self {
45 Self {
46 kind: ActorKind::System,
47 id,
48 }
49 }
50
51 pub fn loonfs_system() -> Self {
53 Self::system(ActorId::parse("loonfs").expect("`loonfs` should be a valid actor id"))
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
60#[serde(rename_all = "snake_case")]
61pub enum ActorKind {
62 User,
64 Service,
68 System,
72}
73
74impl ActorKind {
75 pub fn as_str(self) -> &'static str {
77 match self {
78 Self::User => "user",
79 Self::Service => "service",
80 Self::System => "system",
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
91pub struct ActorId(String);
92
93impl ActorId {
94 pub fn parse(value: impl AsRef<str>) -> Result<Self, ActorIdValidationError> {
96 let value = value.as_ref();
97 validate_actor_id(value)?;
98 Ok(Self(value.to_owned()))
99 }
100
101 pub fn as_str(&self) -> &str {
103 &self.0
104 }
105}
106
107impl TryFrom<&str> for ActorId {
108 type Error = ActorIdValidationError;
109
110 fn try_from(value: &str) -> Result<Self, Self::Error> {
111 Self::parse(value)
112 }
113}
114
115impl TryFrom<String> for ActorId {
116 type Error = ActorIdValidationError;
117
118 fn try_from(value: String) -> Result<Self, Self::Error> {
119 Self::parse(value)
120 }
121}
122
123impl std::str::FromStr for ActorId {
124 type Err = ActorIdValidationError;
125
126 fn from_str(value: &str) -> Result<Self, Self::Err> {
127 Self::parse(value)
128 }
129}
130
131impl AsRef<str> for ActorId {
132 fn as_ref(&self) -> &str {
133 self.as_str()
134 }
135}
136
137impl std::borrow::Borrow<str> for ActorId {
138 fn borrow(&self) -> &str {
139 self.as_str()
140 }
141}
142
143impl fmt::Display for ActorId {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.write_str(&self.0)
146 }
147}
148
149#[cfg(feature = "openapi")]
150impl utoipa::PartialSchema for ActorId {
151 #[allow(
152 deprecated,
153 reason = "the published schema uses the requested singular example field"
154 )]
155 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
156 utoipa::openapi::schema::Object::builder()
157 .schema_type(utoipa::openapi::schema::Type::String)
158 .description(Some(
159 "Opaque hosting-platform actor id: non-empty, at most 256 UTF-8 bytes, without leading or trailing whitespace or control characters.",
160 ))
161 .example(Some(serde_json::json!("usr_8f3c")))
162 .into()
163 }
164}
165
166#[cfg(feature = "openapi")]
167impl utoipa::ToSchema for ActorId {}
168
169impl Serialize for ActorId {
170 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
171 where
172 S: serde::Serializer,
173 {
174 serializer.serialize_str(&self.0)
175 }
176}
177
178impl<'de> Deserialize<'de> for ActorId {
179 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
180 where
181 D: serde::Deserializer<'de>,
182 {
183 let value = String::deserialize(deserializer)?;
184 Self::parse(value).map_err(serde::de::Error::custom)
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Error)]
190#[error("invalid actor_id {value:?}: {reason}")]
191pub struct ActorIdValidationError {
192 value: String,
193 reason: String,
194}
195
196impl ActorIdValidationError {
197 pub fn value(&self) -> &str {
199 &self.value
200 }
201
202 pub fn reason(&self) -> &str {
204 &self.reason
205 }
206}
207
208fn validate_actor_id(value: &str) -> Result<(), ActorIdValidationError> {
209 if value.is_empty() {
210 return Err(actor_id_error(value, "must not be empty"));
211 }
212 if value.len() > MAX_ACTOR_ID_BYTES {
213 return Err(actor_id_error(value, "must be 256 bytes or fewer"));
214 }
215 if value.trim() != value {
216 return Err(actor_id_error(
217 value,
218 "must not have leading or trailing whitespace",
219 ));
220 }
221 if value.chars().any(char::is_control) {
222 return Err(actor_id_error(value, "must not contain control characters"));
223 }
224 Ok(())
225}
226
227fn actor_id_error(value: &str, reason: &str) -> ActorIdValidationError {
228 ActorIdValidationError {
229 value: value.to_owned(),
230 reason: reason.to_owned(),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::{ActorId, ActorKind, ActorRef};
237
238 #[test]
239 fn actor_kind_serializes_as_snake_case_strings() {
240 for (kind, json) in [
241 (ActorKind::User, r#""user""#),
242 (ActorKind::Service, r#""service""#),
243 (ActorKind::System, r#""system""#),
244 ] {
245 assert_eq!(serde_json::to_string(&kind).expect("serialize kind"), json);
246 assert_eq!(
247 serde_json::from_str::<ActorKind>(json).expect("deserialize kind"),
248 kind
249 );
250 }
251 }
252
253 #[test]
254 fn actor_ref_has_the_exact_wire_shape() {
255 let json = r#"{"kind":"user","id":"usr_8f3c"}"#;
256 let actor = ActorRef::user(ActorId::parse("usr_8f3c").expect("valid actor id"));
257
258 assert_eq!(
259 serde_json::to_string(&actor).expect("serialize actor"),
260 json
261 );
262 assert_eq!(
263 serde_json::from_str::<ActorRef>(json).expect("deserialize actor"),
264 actor
265 );
266 }
267
268 #[test]
269 fn actor_id_rejects_invalid_values_with_stable_reasons() {
270 let too_long = "x".repeat(257);
271 for (value, reason) in [
272 ("", "must not be empty"),
273 (&too_long, "must be 256 bytes or fewer"),
274 (" actor", "must not have leading or trailing whitespace"),
275 ("actor ", "must not have leading or trailing whitespace"),
276 ("actor\nid", "must not contain control characters"),
277 ("actor\0id", "must not contain control characters"),
278 ("actor\u{7f}id", "must not contain control characters"),
279 ] {
280 let error = ActorId::parse(value).expect_err("invalid actor id");
281 assert_eq!(error.value(), value);
282 assert_eq!(error.reason(), reason);
283 }
284 }
285
286 #[test]
287 fn actor_id_error_escapes_hostile_input() {
288 let error = ActorId::parse("actor\nid").expect_err("control character");
289
290 assert_eq!(
291 error.to_string(),
292 r#"invalid actor_id "actor\nid": must not contain control characters"#
293 );
294 }
295
296 #[test]
297 fn actor_id_accepts_external_syntax_and_round_trips() {
298 let exactly_256_bytes = "x".repeat(256);
299 for value in [
300 "auth0|64abc",
301 "AAD:uPn@Example",
302 "123e4567-e89b-12d3-a456-426614174000",
303 &exactly_256_bytes,
304 ] {
305 let parsed = ActorId::parse(value).expect("valid external actor id");
306 assert_eq!(parsed.as_str(), value);
307 assert_eq!(parsed.to_string(), value);
308 assert_eq!(ActorId::try_from(value).expect("try_from actor id"), parsed);
309 assert_eq!(value.parse::<ActorId>().expect("from_str actor id"), parsed);
310
311 let json = serde_json::to_string(&parsed).expect("serialize actor id");
312 assert_eq!(
313 serde_json::from_str::<ActorId>(&json).expect("deserialize actor id"),
314 parsed
315 );
316 }
317 }
318
319 #[test]
320 fn actor_id_utf8_limit_counts_bytes_not_characters() {
321 let exactly_256_bytes = "é".repeat(128);
322 let too_long = format!("{exactly_256_bytes}a");
323
324 ActorId::parse(&exactly_256_bytes).expect("256-byte unicode actor id");
325 assert_eq!(
326 ActorId::parse(&too_long)
327 .expect_err("257-byte unicode actor id")
328 .reason(),
329 "must be 256 bytes or fewer"
330 );
331 }
332
333 #[test]
334 fn actor_ref_rejects_unknown_kind_and_fields() {
335 assert!(serde_json::from_str::<ActorRef>(r#"{"kind":"robot","id":"x"}"#).is_err());
336 assert!(
337 serde_json::from_str::<ActorRef>(r#"{"kind":"user","id":"x","name":"Ada"}"#).is_err()
338 );
339 }
340
341 #[test]
342 fn actor_ref_convenience_constructors_select_the_kind() {
343 let id = ActorId::parse("actor").expect("valid actor id");
344
345 assert_eq!(ActorRef::user(id.clone()).kind, ActorKind::User);
346 assert_eq!(ActorRef::service(id.clone()).kind, ActorKind::Service);
347 assert_eq!(ActorRef::system(id).kind, ActorKind::System);
348 assert_eq!(
349 ActorRef::loonfs_system(),
350 ActorRef::system(ActorId::parse("loonfs").expect("valid bootstrap actor id"))
351 );
352 }
353
354 #[cfg(feature = "openapi")]
355 #[test]
356 fn actor_vocabulary_registers_openapi_schemas() {
357 #[derive(utoipa::OpenApi)]
358 #[openapi(components(schemas(ActorRef, ActorKind, ActorId)))]
359 struct ActorVocabularyOpenApi;
360
361 let document = serde_json::to_value(<ActorVocabularyOpenApi as utoipa::OpenApi>::openapi())
362 .expect("serialize actor vocabulary OpenAPI document");
363 let schemas = document
364 .pointer("/components/schemas")
365 .and_then(serde_json::Value::as_object)
366 .expect("actor vocabulary schemas");
367
368 for name in ["ActorRef", "ActorKind", "ActorId"] {
369 assert!(schemas.contains_key(name), "missing `{name}` schema");
370 }
371 }
372}