use core::marker::PhantomData;
use arkhe_kernel::abi::{EntityId, Tick};
use serde::{Deserialize, Serialize};
use crate::action::{ActionCompute, ArkheAction as _};
use crate::brand::{ShellBrand, ShellId};
use crate::component::{ArkheComponent as _, BoundedString};
use crate::context::{ensure_schema_version, ActionContext, ActionError};
use crate::user::UserId;
use crate::ArkheAction;
use crate::ArkheComponent;
use crate::arkhe_pure;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ActorId(EntityId);
impl ActorId {
#[inline]
#[must_use]
pub fn new(id: EntityId) -> Self {
Self(id)
}
#[inline]
#[must_use]
pub fn get(self) -> EntityId {
self.0
}
}
#[non_exhaustive]
#[repr(u8)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum ActorKind {
Human = 0,
Bot = 1,
System = 2,
Anonymous = 3,
}
mod state_seal {
pub trait Sealed {}
}
pub trait ActorState: state_seal::Sealed + 'static {
const NAME: &'static str;
}
#[derive(Debug)]
pub enum Anonymous {}
#[derive(Debug)]
pub enum Authenticated {}
#[derive(Debug)]
pub enum Suspended {}
impl state_seal::Sealed for Anonymous {}
impl state_seal::Sealed for Authenticated {}
impl state_seal::Sealed for Suspended {}
impl ActorState for Anonymous {
const NAME: &'static str = "anonymous";
}
impl ActorState for Authenticated {
const NAME: &'static str = "authenticated";
}
impl ActorState for Suspended {
const NAME: &'static str = "suspended";
}
pub struct Actor<'s, S: ActorState> {
brand: ShellBrand<'s>,
id: ActorId,
_state: PhantomData<fn() -> S>,
}
impl<'s, S: ActorState> Clone for Actor<'s, S> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<'s, S: ActorState> Copy for Actor<'s, S> {}
impl<'s, S: ActorState> core::fmt::Debug for Actor<'s, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Actor")
.field("id", &self.id)
.field("state", &S::NAME)
.finish()
}
}
impl<'s, S: ActorState> Actor<'s, S> {
#[inline]
#[must_use]
pub fn id(self) -> ActorId {
self.id
}
#[inline]
#[must_use]
pub fn brand(self) -> ShellBrand<'s> {
self.brand
}
}
impl<'s> Actor<'s, Anonymous> {
#[inline]
#[must_use]
pub fn new_anonymous(brand: ShellBrand<'s>, id: ActorId) -> Self {
Self {
brand,
id,
_state: PhantomData,
}
}
#[inline]
#[must_use]
pub fn authenticate(self, _user_id: UserId) -> Actor<'s, Authenticated> {
Actor {
brand: self.brand,
id: self.id,
_state: PhantomData,
}
}
}
impl<'s> Actor<'s, Authenticated> {
#[inline]
#[must_use]
pub fn suspend(self) -> Actor<'s, Suspended> {
Actor {
brand: self.brand,
id: self.id,
_state: PhantomData,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0101, schema_version = 1)]
pub struct ActorProfile {
pub schema_version: u16,
pub shell_id: ShellId,
pub handle: BoundedString<32>,
pub kind: ActorKind,
pub created_tick: Tick,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0102, schema_version = 1)]
pub struct UserBinding {
pub schema_version: u16,
pub user_id: UserId,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_0101, schema_version = 1, band = 1)]
pub struct RegisterActor {
pub schema_version: u16,
pub profile: ActorProfile,
pub user: UserId,
}
impl ActionCompute for RegisterActor {
#[arkhe_pure]
fn compute<'i>(&self, ctx: &mut ActionContext<'i>) -> Result<(), ActionError> {
ensure_schema_version(Self::SCHEMA_VERSION, self.schema_version)?;
ensure_schema_version(ActorProfile::SCHEMA_VERSION, self.profile.schema_version)?;
if ctx
.actor_by_handle(self.profile.shell_id, &self.profile.handle)
.is_some()
{
return Err(ActionError::ActorHandleCollision {
shell_id: self.profile.shell_id,
handle: self.profile.handle.clone(),
});
}
let actor_entity = ctx.spawn_entity_for::<ActorProfile>()?;
ctx.set_component(actor_entity, &self.profile)?;
ctx.set_component(
actor_entity,
&UserBinding {
schema_version: UserBinding::SCHEMA_VERSION,
user_id: self.user,
},
)?;
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::component::ArkheComponent;
fn ent(v: u64) -> EntityId {
EntityId::new(v).unwrap()
}
#[test]
fn actor_typestate_transitions_anonymous_authenticated_suspended() {
ShellBrand::run(|brand| {
let id = ActorId::new(ent(1));
let anon: Actor<'_, Anonymous> = Actor::new_anonymous(brand, id);
let user_id = UserId::new(ent(2));
let auth: Actor<'_, Authenticated> = anon.authenticate(user_id);
let susp: Actor<'_, Suspended> = auth.suspend();
assert_eq!(susp.id(), id);
});
}
#[test]
fn actor_state_names_are_distinct() {
assert_eq!(Anonymous::NAME, "anonymous");
assert_eq!(Authenticated::NAME, "authenticated");
assert_eq!(Suspended::NAME, "suspended");
}
#[test]
fn actor_profile_serde_roundtrip_postcard() {
let p = ActorProfile {
schema_version: 1,
shell_id: ShellId([0xAB; 16]),
handle: BoundedString::<32>::new("alice").unwrap(),
kind: ActorKind::Human,
created_tick: Tick(100),
};
let bytes = postcard::to_stdvec(&p).unwrap();
let back: ActorProfile = postcard::from_bytes(&bytes).unwrap();
assert_eq!(p, back);
}
#[test]
fn actor_profile_exposes_type_code_and_schema_version() {
assert_eq!(ActorProfile::TYPE_CODE, 0x0003_0101);
assert_eq!(ActorProfile::SCHEMA_VERSION, 1);
}
fn test_ctx() -> ActionContext<'static> {
use arkhe_kernel::abi::{CapabilityMask, InstanceId, Principal};
ActionContext::new(
[0u8; 32],
InstanceId::new(1).unwrap(),
Tick(7),
Principal::System,
CapabilityMask::SYSTEM,
)
}
fn register_actor(v: u64) -> RegisterActor {
RegisterActor {
schema_version: 1,
profile: ActorProfile {
schema_version: 1,
shell_id: ShellId([0xAB; 16]),
handle: BoundedString::<32>::new("alice").unwrap(),
kind: ActorKind::Human,
created_tick: Tick(7),
},
user: UserId::new(ent(v)),
}
}
#[test]
fn register_actor_spawns_actor_then_sets_profile_and_binding() {
use arkhe_kernel::abi::TypeCode;
use arkhe_kernel::state::Op;
let mut c = test_ctx();
register_actor(7).compute(&mut c).expect("compute ok");
let ops = c.drain_ops();
assert_eq!(ops.len(), 3, "spawn + ActorProfile + UserBinding");
let Op::SpawnEntity { id: actor_id, .. } = &ops[0] else {
panic!("op 0 must spawn the actor entity, got {:?}", ops[0]);
};
match &ops[1] {
Op::SetComponent {
entity, type_code, ..
} => {
assert_eq!(entity, actor_id, "profile lands on the spawned actor");
assert_eq!(*type_code, TypeCode(ActorProfile::TYPE_CODE));
}
other => panic!("expected SetComponent(ActorProfile), got {:?}", other),
}
match &ops[2] {
Op::SetComponent {
entity,
type_code,
bytes,
..
} => {
assert_eq!(entity, actor_id, "binding lands on the spawned actor");
assert_eq!(*type_code, TypeCode(UserBinding::TYPE_CODE));
let binding: UserBinding = postcard::from_bytes(bytes).unwrap();
assert_eq!(binding.user_id, UserId::new(ent(7)));
}
other => panic!("expected SetComponent(UserBinding), got {:?}", other),
}
}
#[test]
fn register_actor_rejects_handle_collision_via_index() {
use crate::context::ActorHandleIndex;
struct OneOccupant {
shell: ShellId,
handle: BoundedString<32>,
holder: ActorId,
}
impl ActorHandleIndex for OneOccupant {
fn lookup(&self, shell: ShellId, handle: &BoundedString<32>) -> Option<ActorId> {
(shell == self.shell && *handle == self.handle).then_some(self.holder)
}
}
let act = register_actor(7);
let index = OneOccupant {
shell: act.profile.shell_id,
handle: act.profile.handle.clone(),
holder: ActorId::new(ent(99)),
};
let mut c = test_ctx().with_actor_handle_index(&index);
let err = act
.compute(&mut c)
.expect_err("occupied handle must reject");
match err {
ActionError::ActorHandleCollision { shell_id, handle } => {
assert_eq!(shell_id, act.profile.shell_id);
assert_eq!(handle, act.profile.handle);
}
other => panic!("expected ActorHandleCollision, got {:?}", other),
}
assert!(c.ops().is_empty(), "no Ops on rejection");
}
#[test]
fn register_actor_rejects_wire_schema_mismatch() {
let mut c = test_ctx();
let mut act = register_actor(7);
act.schema_version = 0xBEEF;
let err = act.compute(&mut c).expect_err("action field");
assert!(
matches!(
err,
ActionError::SchemaMismatch {
expected: 1,
got: 0xBEEF,
}
),
"got {err:?}",
);
assert!(c.ops().is_empty(), "no Ops on rejection");
let mut act = register_actor(7);
act.profile.schema_version = 0xBEEF;
let err = act.compute(&mut c).expect_err("profile field");
assert!(
matches!(
err,
ActionError::SchemaMismatch {
expected: 1,
got: 0xBEEF,
}
),
"got {err:?}",
);
assert!(c.ops().is_empty(), "no Ops on rejection");
}
#[test]
fn register_actor_exposes_trait_consts() {
use crate::action::ArkheAction;
assert_eq!(RegisterActor::TYPE_CODE, 0x0001_0101);
assert_eq!(RegisterActor::SCHEMA_VERSION, 1);
assert_eq!(RegisterActor::BAND, 1);
const { assert!(!RegisterActor::IDEMPOTENT) };
}
}