1use std::collections::BTreeSet;
4
5use arkhe_kernel::abi::{EntityId, Tick, TypeCode};
6use serde::{Deserialize, Serialize};
7
8use crate::action::ActionCompute;
9use crate::actor::ActorId;
10use crate::brand::ShellId;
11use crate::component::BoundedString;
12use crate::context::{ActionContext, ActionError};
13use crate::ArkheAction;
14use crate::ArkheComponent;
15use crate::arkhe_pure;
17
18#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct SpaceId(EntityId);
22
23impl SpaceId {
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]
44#[repr(u8)]
45#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
46pub enum SpaceKind {
47 Flat = 0,
49 Tree = 1,
51 Graph = 2,
53 Hashtag = 3,
55 ActorFeed = 4,
57 Extension {
59 type_code: TypeCode,
61 } = 255,
62}
63
64#[non_exhaustive]
66#[repr(u8)]
67#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
68pub enum Visibility {
69 Public = 0,
71 RestrictedByRole = 1,
73 SubscribersOnly = 2,
75 PrivateInvite = 3,
77 Encrypted = 4,
79}
80
81#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
83#[arkhe(type_code = 0x0003_0201, schema_version = 1)]
84pub struct SpaceConfig {
85 pub schema_version: u16,
87 pub shell_id: ShellId,
89 pub slug: BoundedString<32>,
91 pub kind: SpaceKind,
93 pub visibility: Visibility,
95 pub creator: ActorId,
97 pub parent_space: Option<SpaceId>,
99 pub created_tick: Tick,
101}
102
103#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
106#[arkhe(type_code = 0x0003_0202, schema_version = 1)]
107pub struct ParentChainDepth {
108 pub schema_version: u16,
110 pub depth: u8,
112}
113
114#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
116#[arkhe(type_code = 0x0003_0203, schema_version = 1)]
117pub struct SpaceMembership {
118 pub schema_version: u16,
120 pub members: BTreeSet<ActorId>,
123}
124
125#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheAction)]
127#[arkhe(type_code = 0x0001_0201, schema_version = 1, band = 1)]
128pub struct CreateSpace {
129 pub schema_version: u16,
131 pub config: SpaceConfig,
133}
134
135impl ActionCompute for CreateSpace {
136 #[arkhe_pure]
137 fn compute<'i>(&self, ctx: &mut ActionContext<'i>) -> Result<(), ActionError> {
138 ctx.ensure_actor_eligible(self.config.creator, ctx.tick())?;
142
143 let child_depth: u8 = match self.config.parent_space {
148 Some(parent_id) => {
149 let parent_depth = ctx
150 .read::<ParentChainDepth>(parent_id.get())?
151 .ok_or(ActionError::InvalidInput("parent space not found"))?;
152 let next = parent_depth.depth.saturating_add(1);
153 if next > MAX_SPACE_DEPTH {
154 return Err(ActionError::InvalidInput("space depth exceeded"));
155 }
156 next
157 }
158 None => 0,
159 };
160
161 let space_entity = ctx.spawn_entity_for::<SpaceConfig>()?;
162 ctx.set_component(space_entity, &self.config)?;
163 ctx.set_component(
164 space_entity,
165 &ParentChainDepth {
166 schema_version: 1,
167 depth: child_depth,
168 },
169 )?;
170 Ok(())
171 }
172}
173
174pub const MAX_SPACE_DEPTH: u8 = 64;
177
178#[cfg(test)]
179#[allow(clippy::unwrap_used, clippy::expect_used)]
180mod tests {
181 use super::*;
182 use crate::action::ArkheAction;
183 use crate::component::ArkheComponent;
184
185 fn ent(v: u64) -> EntityId {
186 EntityId::new(v).unwrap()
187 }
188
189 #[test]
190 fn space_config_serde_roundtrip_postcard() {
191 let cfg = SpaceConfig {
192 schema_version: 1,
193 shell_id: ShellId([0x01; 16]),
194 slug: BoundedString::<32>::new("general").unwrap(),
195 kind: SpaceKind::Tree,
196 visibility: Visibility::Public,
197 creator: ActorId::new(ent(42)),
198 parent_space: None,
199 created_tick: Tick(0),
200 };
201 let bytes = postcard::to_stdvec(&cfg).unwrap();
202 let back: SpaceConfig = postcard::from_bytes(&bytes).unwrap();
203 assert_eq!(cfg, back);
204 }
205
206 #[test]
207 fn space_membership_preserves_canonical_order() {
208 let mut set = BTreeSet::new();
209 set.insert(ActorId::new(ent(3)));
210 set.insert(ActorId::new(ent(1)));
211 set.insert(ActorId::new(ent(2)));
212 let m = SpaceMembership {
213 schema_version: 1,
214 members: set,
215 };
216 let serialized_once = postcard::to_stdvec(&m).unwrap();
217
218 let mut set2 = BTreeSet::new();
219 set2.insert(ActorId::new(ent(2)));
220 set2.insert(ActorId::new(ent(1)));
221 set2.insert(ActorId::new(ent(3)));
222 let m2 = SpaceMembership {
223 schema_version: 1,
224 members: set2,
225 };
226 assert_eq!(serialized_once, postcard::to_stdvec(&m2).unwrap());
227 }
228
229 #[test]
230 fn space_config_action_type_codes() {
231 assert_eq!(SpaceConfig::TYPE_CODE, 0x0003_0201);
232 assert_eq!(ParentChainDepth::TYPE_CODE, 0x0003_0202);
233 assert_eq!(SpaceMembership::TYPE_CODE, 0x0003_0203);
234 assert_eq!(CreateSpace::TYPE_CODE, 0x0001_0201);
235 assert_eq!(CreateSpace::BAND, 1);
236 }
237
238 #[test]
239 fn max_space_depth_is_sixty_four() {
240 assert_eq!(MAX_SPACE_DEPTH, 64);
241 }
242}