1use core::marker::PhantomData;
9
10use arkhe_kernel::abi::{EntityId, Tick};
11use serde::{Deserialize, Serialize};
12
13use crate::brand::{ShellBrand, ShellId};
14use crate::component::BoundedString;
15use crate::user::UserId;
16use crate::ArkheComponent;
17
18#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct ActorId(EntityId);
22
23impl ActorId {
24 #[inline]
28 #[must_use]
29 pub fn new(id: EntityId) -> Self {
30 Self(id)
31 }
32
33 #[inline]
35 #[must_use]
36 pub fn get(self) -> EntityId {
37 self.0
38 }
39}
40
41#[non_exhaustive]
43#[repr(u8)]
44#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
45pub enum ActorKind {
46 Human = 0,
48 Bot = 1,
50 System = 2,
52 Anonymous = 3,
54}
55
56mod state_seal {
57 pub trait Sealed {}
59}
60
61pub trait ActorState: state_seal::Sealed + 'static {
67 const NAME: &'static str;
69}
70
71#[derive(Debug)]
73pub enum Anonymous {}
74#[derive(Debug)]
76pub enum Authenticated {}
77#[derive(Debug)]
79pub enum Suspended {}
80
81impl state_seal::Sealed for Anonymous {}
82impl state_seal::Sealed for Authenticated {}
83impl state_seal::Sealed for Suspended {}
84
85impl ActorState for Anonymous {
86 const NAME: &'static str = "anonymous";
87}
88impl ActorState for Authenticated {
89 const NAME: &'static str = "authenticated";
90}
91impl ActorState for Suspended {
92 const NAME: &'static str = "suspended";
93}
94
95pub struct Actor<'s, S: ActorState> {
101 brand: ShellBrand<'s>,
102 id: ActorId,
103 _state: PhantomData<fn() -> S>,
104}
105
106impl<'s, S: ActorState> Clone for Actor<'s, S> {
107 #[inline]
108 fn clone(&self) -> Self {
109 *self
110 }
111}
112impl<'s, S: ActorState> Copy for Actor<'s, S> {}
113
114impl<'s, S: ActorState> core::fmt::Debug for Actor<'s, S> {
115 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116 f.debug_struct("Actor")
117 .field("id", &self.id)
118 .field("state", &S::NAME)
119 .finish()
120 }
121}
122
123impl<'s, S: ActorState> Actor<'s, S> {
124 #[inline]
126 #[must_use]
127 pub fn id(self) -> ActorId {
128 self.id
129 }
130
131 #[inline]
133 #[must_use]
134 pub fn brand(self) -> ShellBrand<'s> {
135 self.brand
136 }
137}
138
139impl<'s> Actor<'s, Anonymous> {
140 #[inline]
142 #[must_use]
143 pub fn new_anonymous(brand: ShellBrand<'s>, id: ActorId) -> Self {
144 Self {
145 brand,
146 id,
147 _state: PhantomData,
148 }
149 }
150
151 #[inline]
155 #[must_use]
156 pub fn authenticate(self, _user_id: UserId) -> Actor<'s, Authenticated> {
157 Actor {
158 brand: self.brand,
159 id: self.id,
160 _state: PhantomData,
161 }
162 }
163}
164
165impl<'s> Actor<'s, Authenticated> {
166 #[inline]
170 #[must_use]
171 pub fn suspend(self) -> Actor<'s, Suspended> {
172 Actor {
173 brand: self.brand,
174 id: self.id,
175 _state: PhantomData,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
182#[arkhe(type_code = 0x0003_0101, schema_version = 1)]
183pub struct ActorProfile {
184 pub schema_version: u16,
186 pub shell_id: ShellId,
188 pub handle: BoundedString<32>,
190 pub kind: ActorKind,
192 pub created_tick: Tick,
194}
195
196#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
199#[arkhe(type_code = 0x0003_0102, schema_version = 1)]
200pub struct UserBinding {
201 pub schema_version: u16,
203 pub user_id: UserId,
205}
206
207#[cfg(test)]
208#[allow(clippy::unwrap_used, clippy::expect_used)]
209mod tests {
210 use super::*;
211 use crate::component::ArkheComponent;
212
213 fn ent(v: u64) -> EntityId {
214 EntityId::new(v).unwrap()
215 }
216
217 #[test]
218 fn actor_typestate_transitions_anonymous_authenticated_suspended() {
219 ShellBrand::run(|brand| {
220 let id = ActorId::new(ent(1));
221 let anon: Actor<'_, Anonymous> = Actor::new_anonymous(brand, id);
222 let user_id = UserId::new(ent(2));
223 let auth: Actor<'_, Authenticated> = anon.authenticate(user_id);
224 let susp: Actor<'_, Suspended> = auth.suspend();
225 assert_eq!(susp.id(), id);
226 });
227 }
228
229 #[test]
230 fn actor_state_names_are_distinct() {
231 assert_eq!(Anonymous::NAME, "anonymous");
232 assert_eq!(Authenticated::NAME, "authenticated");
233 assert_eq!(Suspended::NAME, "suspended");
234 }
235
236 #[test]
237 fn actor_profile_serde_roundtrip_postcard() {
238 let p = ActorProfile {
239 schema_version: 1,
240 shell_id: ShellId([0xAB; 16]),
241 handle: BoundedString::<32>::new("alice").unwrap(),
242 kind: ActorKind::Human,
243 created_tick: Tick(100),
244 };
245 let bytes = postcard::to_stdvec(&p).unwrap();
246 let back: ActorProfile = postcard::from_bytes(&bytes).unwrap();
247 assert_eq!(p, back);
248 }
249
250 #[test]
251 fn actor_profile_exposes_type_code_and_schema_version() {
252 assert_eq!(ActorProfile::TYPE_CODE, 0x0003_0101);
253 assert_eq!(ActorProfile::SCHEMA_VERSION, 1);
254 }
255}