1use crate::ids::{string_id, validation_error};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10const MAX_ACTOR_ID_BYTES: usize = 256;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
21#[serde(deny_unknown_fields)]
22pub struct ActorRef {
23 pub kind: ActorKind,
25 pub id: ActorId,
27}
28
29impl ActorRef {
30 pub fn user(id: ActorId) -> Self {
32 Self {
33 kind: ActorKind::User,
34 id,
35 }
36 }
37
38 pub fn service(id: ActorId) -> Self {
40 Self {
41 kind: ActorKind::Service,
42 id,
43 }
44 }
45
46 pub fn system(id: ActorId) -> Self {
48 Self {
49 kind: ActorKind::System,
50 id,
51 }
52 }
53
54 pub fn loonfs_system() -> Self {
56 Self::system(ActorId::parse("loonfs").expect("`loonfs` should be a valid actor id"))
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
63#[serde(rename_all = "snake_case")]
64pub enum ActorKind {
65 User,
67 Service,
71 System,
75}
76
77impl ActorKind {
78 pub fn as_str(self) -> &'static str {
80 match self {
81 Self::User => "user",
82 Self::Service => "service",
83 Self::System => "system",
84 }
85 }
86}
87
88validation_error!(
89 ActorIdValidationError,
90 "invalid actor_id {value:?}: {reason}"
91);
92
93string_id! {
94 ActorId,
100 error = ActorIdValidationError,
101 validate = validate_actor_id,
102 schema(
103 description = "Opaque hosting-platform actor id: non-empty, at most 256 UTF-8 bytes, without leading or trailing whitespace or control characters.",
104 example = "usr_8f3c"
105 )
106}
107
108fn validate_actor_id(value: &str) -> Result<(), ActorIdValidationError> {
109 if value.is_empty() {
110 return Err(actor_id_error(value, "must not be empty"));
111 }
112 if value.len() > MAX_ACTOR_ID_BYTES {
113 return Err(actor_id_error(
114 value,
115 &format!("must be {MAX_ACTOR_ID_BYTES} bytes or fewer"),
116 ));
117 }
118 if value.trim() != value {
119 return Err(actor_id_error(
120 value,
121 "must not have leading or trailing whitespace",
122 ));
123 }
124 if value.chars().any(char::is_control) {
125 return Err(actor_id_error(value, "must not contain control characters"));
126 }
127 Ok(())
128}
129
130fn actor_id_error(value: &str, reason: &str) -> ActorIdValidationError {
131 ActorIdValidationError {
132 value: value.to_owned(),
133 reason: reason.to_owned(),
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::{ActorId, ActorKind, ActorRef};
140
141 #[test]
142 fn actor_kind_serializes_as_snake_case_strings() {
143 for (kind, json) in [
144 (ActorKind::User, r#""user""#),
145 (ActorKind::Service, r#""service""#),
146 (ActorKind::System, r#""system""#),
147 ] {
148 assert_eq!(serde_json::to_string(&kind).expect("serialize kind"), json);
149 assert_eq!(
150 serde_json::from_str::<ActorKind>(json).expect("deserialize kind"),
151 kind
152 );
153 }
154 }
155
156 #[test]
157 fn actor_ref_has_the_exact_wire_shape() {
158 let json = r#"{"kind":"user","id":"usr_8f3c"}"#;
159 let actor = ActorRef::user(ActorId::parse("usr_8f3c").expect("valid actor id"));
160
161 assert_eq!(
162 serde_json::to_string(&actor).expect("serialize actor"),
163 json
164 );
165 assert_eq!(
166 serde_json::from_str::<ActorRef>(json).expect("deserialize actor"),
167 actor
168 );
169 }
170
171 #[test]
172 fn actor_id_rejects_invalid_values_with_stable_reasons() {
173 let too_long = "x".repeat(257);
174 for (value, reason) in [
175 ("", "must not be empty"),
176 (&too_long, "must be 256 bytes or fewer"),
177 (" actor", "must not have leading or trailing whitespace"),
178 ("actor ", "must not have leading or trailing whitespace"),
179 ("actor\nid", "must not contain control characters"),
180 ("actor\0id", "must not contain control characters"),
181 ("actor\u{7f}id", "must not contain control characters"),
182 ] {
183 let error = ActorId::parse(value).expect_err("invalid actor id");
184 assert_eq!(error.value(), value);
185 assert_eq!(error.reason(), reason);
186 }
187 }
188
189 #[test]
190 fn actor_id_error_escapes_hostile_input() {
191 let error = ActorId::parse("actor\nid").expect_err("control character");
192
193 assert_eq!(
194 error.to_string(),
195 r#"invalid actor_id "actor\nid": must not contain control characters"#
196 );
197 }
198
199 #[test]
200 fn actor_id_accepts_external_syntax_and_round_trips() {
201 let exactly_256_bytes = "x".repeat(256);
202 for value in [
203 "auth0|64abc",
204 "AAD:uPn@Example",
205 "123e4567-e89b-12d3-a456-426614174000",
206 &exactly_256_bytes,
207 ] {
208 let parsed = ActorId::parse(value).expect("valid external actor id");
209 assert_eq!(parsed.as_str(), value);
210 assert_eq!(parsed.to_string(), value);
211 assert_eq!(ActorId::try_from(value).expect("try_from actor id"), parsed);
212 assert_eq!(value.parse::<ActorId>().expect("from_str actor id"), parsed);
213
214 let json = serde_json::to_string(&parsed).expect("serialize actor id");
215 assert_eq!(
216 serde_json::from_str::<ActorId>(&json).expect("deserialize actor id"),
217 parsed
218 );
219 }
220 }
221
222 #[test]
223 fn actor_id_utf8_limit_counts_bytes_not_characters() {
224 let exactly_256_bytes = "é".repeat(128);
225 let too_long = format!("{exactly_256_bytes}a");
226
227 ActorId::parse(&exactly_256_bytes).expect("256-byte unicode actor id");
228 assert_eq!(
229 ActorId::parse(&too_long)
230 .expect_err("257-byte unicode actor id")
231 .reason(),
232 "must be 256 bytes or fewer"
233 );
234 }
235
236 #[test]
237 fn actor_ref_rejects_unknown_kind_and_fields() {
238 assert!(serde_json::from_str::<ActorRef>(r#"{"kind":"robot","id":"x"}"#).is_err());
239 assert!(
240 serde_json::from_str::<ActorRef>(r#"{"kind":"user","id":"x","name":"Ada"}"#).is_err()
241 );
242 }
243}