use std::collections::BTreeSet;
use arkhe_kernel::abi::{EntityId, Tick, TypeCode};
use serde::{Deserialize, Serialize};
use crate::action::ActionCompute;
use crate::actor::ActorId;
use crate::brand::ShellId;
use crate::component::BoundedString;
use crate::context::{ActionContext, ActionError};
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 SpaceId(EntityId);
impl SpaceId {
#[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 SpaceKind {
Flat = 0,
Tree = 1,
Graph = 2,
Hashtag = 3,
ActorFeed = 4,
Extension {
type_code: TypeCode,
} = 255,
}
#[non_exhaustive]
#[repr(u8)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Visibility {
Public = 0,
RestrictedByRole = 1,
SubscribersOnly = 2,
PrivateInvite = 3,
Encrypted = 4,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0201, schema_version = 1)]
pub struct SpaceConfig {
pub schema_version: u16,
pub shell_id: ShellId,
pub slug: BoundedString<32>,
pub kind: SpaceKind,
pub visibility: Visibility,
pub creator: ActorId,
pub parent_space: Option<SpaceId>,
pub created_tick: Tick,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0202, schema_version = 1)]
pub struct ParentChainDepth {
pub schema_version: u16,
pub depth: u8,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0203, schema_version = 1)]
pub struct SpaceMembership {
pub schema_version: u16,
pub members: BTreeSet<ActorId>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct SpaceConfigDraft {
pub schema_version: u16,
pub shell_id: ShellId,
pub slug: BoundedString<32>,
pub kind: SpaceKind,
pub visibility: Visibility,
pub parent_space: Option<SpaceId>,
pub created_tick: Tick,
}
impl SpaceConfigDraft {
#[must_use]
fn into_config(self, creator: ActorId) -> SpaceConfig {
SpaceConfig {
schema_version: self.schema_version,
shell_id: self.shell_id,
slug: self.slug,
kind: self.kind,
visibility: self.visibility,
creator,
parent_space: self.parent_space,
created_tick: self.created_tick,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_0201, schema_version = 1, band = 1)]
pub struct CreateSpace {
pub schema_version: u16,
pub config: SpaceConfigDraft,
}
impl ActionCompute for CreateSpace {
#[arkhe_pure]
fn compute<'i>(&self, ctx: &mut ActionContext<'i>) -> Result<(), ActionError> {
let creator = ctx.acting_actor().ok_or(ActionError::AuthorizationFailed(
"space requires an authenticated actor",
))?;
ctx.ensure_actor_eligible(creator, ctx.tick())?;
let child_depth: u8 = match self.config.parent_space {
Some(parent_id) => {
let parent_depth = ctx
.read::<ParentChainDepth>(parent_id.get())?
.ok_or(ActionError::InvalidInput("parent space not found"))?;
let next = parent_depth.depth.saturating_add(1);
if next > MAX_SPACE_DEPTH {
return Err(ActionError::InvalidInput("space depth exceeded"));
}
next
}
None => 0,
};
let config = self.config.clone().into_config(creator);
let space_entity = ctx.spawn_entity_for::<SpaceConfig>()?;
ctx.set_component(space_entity, &config)?;
ctx.set_component(
space_entity,
&ParentChainDepth {
schema_version: 1,
depth: child_depth,
},
)?;
Ok(())
}
}
pub const MAX_SPACE_DEPTH: u8 = 64;
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::action::ArkheAction;
use crate::component::ArkheComponent;
fn ent(v: u64) -> EntityId {
EntityId::new(v).unwrap()
}
#[test]
fn space_config_serde_roundtrip_postcard() {
let cfg = SpaceConfig {
schema_version: 1,
shell_id: ShellId([0x01; 16]),
slug: BoundedString::<32>::new("general").unwrap(),
kind: SpaceKind::Tree,
visibility: Visibility::Public,
creator: ActorId::new(ent(42)),
parent_space: None,
created_tick: Tick(0),
};
let bytes = postcard::to_stdvec(&cfg).unwrap();
let back: SpaceConfig = postcard::from_bytes(&bytes).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn space_membership_preserves_canonical_order() {
let mut set = BTreeSet::new();
set.insert(ActorId::new(ent(3)));
set.insert(ActorId::new(ent(1)));
set.insert(ActorId::new(ent(2)));
let m = SpaceMembership {
schema_version: 1,
members: set,
};
let serialized_once = postcard::to_stdvec(&m).unwrap();
let mut set2 = BTreeSet::new();
set2.insert(ActorId::new(ent(2)));
set2.insert(ActorId::new(ent(1)));
set2.insert(ActorId::new(ent(3)));
let m2 = SpaceMembership {
schema_version: 1,
members: set2,
};
assert_eq!(serialized_once, postcard::to_stdvec(&m2).unwrap());
}
#[test]
fn space_config_action_type_codes() {
assert_eq!(SpaceConfig::TYPE_CODE, 0x0003_0201);
assert_eq!(ParentChainDepth::TYPE_CODE, 0x0003_0202);
assert_eq!(SpaceMembership::TYPE_CODE, 0x0003_0203);
assert_eq!(CreateSpace::TYPE_CODE, 0x0001_0201);
assert_eq!(CreateSpace::BAND, 1);
}
#[test]
fn max_space_depth_is_sixty_four() {
assert_eq!(MAX_SPACE_DEPTH, 64);
}
fn draft() -> SpaceConfigDraft {
SpaceConfigDraft {
schema_version: 1,
shell_id: ShellId([0x01; 16]),
slug: BoundedString::<32>::new("general").unwrap(),
kind: SpaceKind::Flat,
visibility: Visibility::Public,
parent_space: None,
created_tick: Tick(0),
}
}
#[test]
fn create_space_records_injected_creator_not_a_wire_field() {
use crate::action::ActionCompute;
use arkhe_kernel::abi::{CapabilityMask, InstanceId, Principal};
let injected = ActorId::new(ent(0xC1));
let act = CreateSpace {
schema_version: 1,
config: draft(),
};
let mut c = ActionContext::new(
[0u8; 32],
InstanceId::new(1).unwrap(),
Tick(7),
Principal::System,
CapabilityMask::SYSTEM,
)
.with_actor(Some(injected));
act.compute(&mut c).expect("injected creator → compute ok");
let recorded = c.ops().iter().find_map(|op| match op {
arkhe_kernel::state::Op::SetComponent {
type_code, bytes, ..
} if *type_code == TypeCode(SpaceConfig::TYPE_CODE) => {
postcard::from_bytes::<SpaceConfig>(bytes).ok()
}
_ => None,
});
assert_eq!(
recorded.expect("config present").creator,
injected,
"recorded creator must equal the injected acting actor",
);
}
#[test]
fn create_space_without_injected_actor_rejects() {
use crate::action::ActionCompute;
use arkhe_kernel::abi::{CapabilityMask, InstanceId, Principal};
let act = CreateSpace {
schema_version: 1,
config: draft(),
};
let mut c = ActionContext::new(
[0u8; 32],
InstanceId::new(1).unwrap(),
Tick(7),
Principal::System,
CapabilityMask::SYSTEM,
);
let err = act
.compute(&mut c)
.expect_err("no injected actor must reject");
assert!(matches!(err, ActionError::AuthorizationFailed(_)));
assert!(c.ops().is_empty(), "no Ops on rejection");
}
}