1use crate::errors::SisterResult;
13use crate::types::{Metadata, SisterType, UniqueId};
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct ContextId(pub UniqueId);
20
21impl ContextId {
22 pub fn new() -> Self {
24 Self(UniqueId::new())
25 }
26
27 pub fn default_context() -> Self {
29 Self(UniqueId::nil())
30 }
31
32 pub fn is_default(&self) -> bool {
34 self.0 == UniqueId::nil()
35 }
36}
37
38impl Default for ContextId {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl std::fmt::Display for ContextId {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "ctx_{}", self.0)
47 }
48}
49
50impl From<&str> for ContextId {
51 fn from(s: &str) -> Self {
52 let s = s.strip_prefix("ctx_").unwrap_or(s);
53 if let Ok(uuid) = uuid::Uuid::parse_str(s) {
54 Self(UniqueId::from_uuid(uuid))
55 } else {
56 Self::new()
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ContextSummary {
64 pub id: ContextId,
65 pub name: String,
66 pub created_at: DateTime<Utc>,
67 pub updated_at: DateTime<Utc>,
68 pub item_count: usize,
69 pub size_bytes: usize,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ContextInfo {
75 pub id: ContextId,
76 pub name: String,
77 pub created_at: DateTime<Utc>,
78 pub updated_at: DateTime<Utc>,
79 pub item_count: usize,
80 pub size_bytes: usize,
81 #[serde(default)]
82 pub metadata: Metadata,
83}
84
85impl From<ContextInfo> for ContextSummary {
86 fn from(info: ContextInfo) -> Self {
87 Self {
88 id: info.id,
89 name: info.name,
90 created_at: info.created_at,
91 updated_at: info.updated_at,
92 item_count: info.item_count,
93 size_bytes: info.size_bytes,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ContextSnapshot {
101 pub sister_type: SisterType,
103
104 pub version: crate::types::Version,
106
107 pub context_info: ContextInfo,
109
110 #[serde(with = "base64_serde")]
112 pub data: Vec<u8>,
113
114 #[serde(with = "hex_serde")]
116 pub checksum: [u8; 32],
117
118 pub snapshot_at: DateTime<Utc>,
120}
121
122impl ContextSnapshot {
123 pub fn verify(&self) -> bool {
125 let computed = blake3::hash(&self.data);
126 computed.as_bytes() == &self.checksum
127 }
128}
129
130pub trait SessionManagement {
145 fn start_session(&mut self, name: &str) -> SisterResult<ContextId>;
148
149 fn start_session_with_metadata(
151 &mut self,
152 name: &str,
153 metadata: Metadata,
154 ) -> SisterResult<ContextId> {
155 let _ = metadata;
156 self.start_session(name)
157 }
158
159 fn end_session(&mut self) -> SisterResult<()>;
162
163 fn current_session(&self) -> Option<ContextId>;
166
167 fn current_session_info(&self) -> SisterResult<ContextInfo>;
169
170 fn list_sessions(&self) -> SisterResult<Vec<ContextSummary>>;
172
173 fn get_session_info(&self, id: ContextId) -> SisterResult<ContextInfo> {
175 self.list_sessions()?
176 .into_iter()
177 .find(|s| s.id == id)
178 .map(|summary| ContextInfo {
179 id: summary.id,
180 name: summary.name,
181 created_at: summary.created_at,
182 updated_at: summary.updated_at,
183 item_count: summary.item_count,
184 size_bytes: summary.size_bytes,
185 metadata: Metadata::new(),
186 })
187 .ok_or_else(|| crate::errors::SisterError::context_not_found(id.to_string()))
188 }
189
190 fn export_session(&self, id: ContextId) -> SisterResult<ContextSnapshot>;
192
193 fn import_session(&mut self, snapshot: ContextSnapshot) -> SisterResult<ContextId>;
195}
196
197pub trait WorkspaceManagement {
210 fn create_workspace(&mut self, name: &str) -> SisterResult<ContextId>;
212
213 fn create_workspace_with_metadata(
215 &mut self,
216 name: &str,
217 metadata: Metadata,
218 ) -> SisterResult<ContextId> {
219 let _ = metadata;
220 self.create_workspace(name)
221 }
222
223 fn switch_workspace(&mut self, id: ContextId) -> SisterResult<()>;
225
226 fn current_workspace(&self) -> ContextId;
228
229 fn current_workspace_info(&self) -> SisterResult<ContextInfo>;
231
232 fn list_workspaces(&self) -> SisterResult<Vec<ContextSummary>>;
234
235 fn delete_workspace(&mut self, id: ContextId) -> SisterResult<()>;
238
239 fn rename_workspace(&mut self, id: ContextId, new_name: &str) -> SisterResult<()>;
241
242 fn export_workspace(&self, id: ContextId) -> SisterResult<ContextSnapshot>;
244
245 fn import_workspace(&mut self, snapshot: ContextSnapshot) -> SisterResult<ContextId>;
247
248 fn get_workspace_info(&self, id: ContextId) -> SisterResult<ContextInfo> {
250 self.list_workspaces()?
251 .into_iter()
252 .find(|w| w.id == id)
253 .map(|summary| ContextInfo {
254 id: summary.id,
255 name: summary.name,
256 created_at: summary.created_at,
257 updated_at: summary.updated_at,
258 item_count: summary.item_count,
259 size_bytes: summary.size_bytes,
260 metadata: Metadata::new(),
261 })
262 .ok_or_else(|| crate::errors::SisterError::context_not_found(id.to_string()))
263 }
264
265 fn workspace_exists(&self, id: ContextId) -> bool {
267 self.get_workspace_info(id).is_ok()
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SessionContext {
276 pub sister_type: SisterType,
278
279 pub context_id: ContextId,
281
282 pub context_name: String,
284
285 pub summary: String,
287
288 pub recent_items: Vec<String>,
290
291 #[serde(default)]
293 pub metadata: Metadata,
294}
295
296mod base64_serde {
298 use base64::{engine::general_purpose::STANDARD, Engine};
299 use serde::{Deserialize, Deserializer, Serializer};
300
301 pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
302 where
303 S: Serializer,
304 {
305 serializer.serialize_str(&STANDARD.encode(bytes))
306 }
307
308 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
309 where
310 D: Deserializer<'de>,
311 {
312 let s = String::deserialize(deserializer)?;
313 STANDARD.decode(&s).map_err(serde::de::Error::custom)
314 }
315}
316
317mod hex_serde {
319 use serde::{Deserialize, Deserializer, Serializer};
320
321 pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
322 where
323 S: Serializer,
324 {
325 serializer.serialize_str(&hex::encode(bytes))
326 }
327
328 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
329 where
330 D: Deserializer<'de>,
331 {
332 let s = String::deserialize(deserializer)?;
333 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
334 bytes
335 .try_into()
336 .map_err(|_| serde::de::Error::custom("invalid checksum length"))
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_context_id() {
346 let id = ContextId::new();
347 let s = id.to_string();
348 assert!(s.starts_with("ctx_"));
349
350 let default = ContextId::default_context();
351 assert!(default.is_default());
352 }
353
354 #[test]
355 fn test_context_id_from_str() {
356 let id = ContextId::new();
357 let s = id.to_string();
358 let parsed: ContextId = s.as_str().into();
359 assert!(!parsed.is_default() || id.is_default());
360 }
361}