Skip to main content

ucm_engine/
snapshot.rs

1//! Snapshot management for document versioning.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use ucm_core::{Document, DocumentVersion, Error, Result};
7
8/// Snapshot identifier
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct SnapshotId(pub String);
11
12impl SnapshotId {
13    pub fn new(name: impl Into<String>) -> Self {
14        Self(name.into())
15    }
16}
17
18impl std::fmt::Display for SnapshotId {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{}", self.0)
21    }
22}
23
24/// A snapshot of document state
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Snapshot {
27    /// Snapshot identifier (name)
28    pub id: SnapshotId,
29    /// Optional description
30    pub description: Option<String>,
31    /// When the snapshot was created
32    pub created_at: DateTime<Utc>,
33    /// Document version at snapshot time
34    pub document_version: DocumentVersion,
35    /// Serialized document data
36    pub data: SnapshotData,
37}
38
39/// Snapshot data storage
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum SnapshotData {
42    /// Full document copy
43    Full(SerializedDocument),
44    /// Delta from a base snapshot (future optimization)
45    Delta {
46        base: SnapshotId,
47        changes: Vec<SnapshotChange>,
48    },
49}
50
51/// Serialized document for storage
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SerializedDocument {
54    /// JSON representation of the document
55    pub json: String,
56}
57
58impl SerializedDocument {
59    pub fn from_document(doc: &Document) -> Result<Self> {
60        // We need to serialize the document structure
61        // For now, we'll create a serializable version
62        let serializable = SerializableDocument::from(doc);
63        let json = serde_json::to_string(&serializable)
64            .map_err(|e| Error::Internal(format!("Failed to serialize document: {}", e)))?;
65        Ok(Self { json })
66    }
67
68    pub fn to_document(&self) -> Result<Document> {
69        let serializable: SerializableDocument = serde_json::from_str(&self.json)
70            .map_err(|e| Error::Internal(format!("Failed to deserialize document: {}", e)))?;
71        Ok(serializable.into())
72    }
73}
74
75/// Serializable version of Document
76#[derive(Debug, Clone, Serialize, Deserialize)]
77struct SerializableDocument {
78    id: String,
79    root: String,
80    structure: HashMap<String, Vec<String>>,
81    blocks: HashMap<String, serde_json::Value>,
82    metadata: serde_json::Value,
83    version: DocumentVersion,
84}
85
86impl From<&Document> for SerializableDocument {
87    fn from(doc: &Document) -> Self {
88        let structure: HashMap<String, Vec<String>> = doc
89            .structure
90            .iter()
91            .map(|(k, v)| (k.to_string(), v.iter().map(|id| id.to_string()).collect()))
92            .collect();
93
94        let blocks: HashMap<String, serde_json::Value> = doc
95            .blocks
96            .iter()
97            .map(|(k, v)| (k.to_string(), serde_json::to_value(v).unwrap_or_default()))
98            .collect();
99
100        Self {
101            id: doc.id.0.clone(),
102            root: doc.root.to_string(),
103            structure,
104            blocks,
105            metadata: serde_json::to_value(&doc.metadata).unwrap_or_default(),
106            version: doc.version.clone(),
107        }
108    }
109}
110
111impl From<SerializableDocument> for Document {
112    fn from(s: SerializableDocument) -> Self {
113        use ucm_core::{Block, BlockId, DocumentId, DocumentMetadata};
114
115        let root: BlockId = s.root.parse().unwrap_or_else(|_| BlockId::root());
116
117        let structure: HashMap<BlockId, Vec<BlockId>> = s
118            .structure
119            .into_iter()
120            .filter_map(|(k, v)| {
121                let key: BlockId = k.parse().ok()?;
122                let values: Vec<BlockId> = v.into_iter().filter_map(|id| id.parse().ok()).collect();
123                Some((key, values))
124            })
125            .collect();
126
127        let blocks: HashMap<BlockId, Block> = s
128            .blocks
129            .into_iter()
130            .filter_map(|(k, v)| {
131                let key: BlockId = k.parse().ok()?;
132                let block: Block = serde_json::from_value(v).ok()?;
133                Some((key, block))
134            })
135            .collect();
136
137        let metadata: DocumentMetadata = serde_json::from_value(s.metadata).unwrap_or_default();
138
139        let mut doc = Document::new(DocumentId::new(s.id));
140        doc.root = root;
141        doc.structure = structure;
142        doc.blocks = blocks;
143        doc.metadata = metadata;
144        doc.version = s.version;
145        doc.rebuild_indices();
146        doc
147    }
148}
149
150/// Change record for delta snapshots
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum SnapshotChange {
153    AddBlock {
154        id: String,
155        block: serde_json::Value,
156    },
157    RemoveBlock {
158        id: String,
159    },
160    ModifyBlock {
161        id: String,
162        block: serde_json::Value,
163    },
164    UpdateStructure {
165        parent: String,
166        children: Vec<String>,
167    },
168}
169
170/// Manages document snapshots
171#[derive(Debug, Default)]
172pub struct SnapshotManager {
173    snapshots: HashMap<SnapshotId, Snapshot>,
174    max_snapshots: usize,
175}
176
177impl SnapshotManager {
178    pub fn new() -> Self {
179        Self {
180            snapshots: HashMap::new(),
181            max_snapshots: 100,
182        }
183    }
184
185    pub fn with_max_snapshots(max: usize) -> Self {
186        Self {
187            snapshots: HashMap::new(),
188            max_snapshots: max,
189        }
190    }
191
192    /// Create a snapshot of the document
193    pub fn create(
194        &mut self,
195        name: impl Into<String>,
196        doc: &Document,
197        description: Option<String>,
198    ) -> Result<SnapshotId> {
199        let id = SnapshotId::new(name);
200
201        // Check if we need to evict old snapshots
202        if self.snapshots.len() >= self.max_snapshots {
203            self.evict_oldest();
204        }
205
206        let data = SnapshotData::Full(SerializedDocument::from_document(doc)?);
207
208        let snapshot = Snapshot {
209            id: id.clone(),
210            description,
211            created_at: Utc::now(),
212            document_version: doc.version.clone(),
213            data,
214        };
215
216        self.snapshots.insert(id.clone(), snapshot);
217        Ok(id)
218    }
219
220    /// Restore a document from a snapshot
221    pub fn restore(&self, name: &str) -> Result<Document> {
222        let id = SnapshotId::new(name);
223        let snapshot = self
224            .snapshots
225            .get(&id)
226            .ok_or_else(|| Error::Internal(format!("Snapshot '{}' not found", name)))?;
227
228        match &snapshot.data {
229            SnapshotData::Full(serialized) => serialized.to_document(),
230            SnapshotData::Delta { .. } => {
231                // TODO: implement delta restoration
232                Err(Error::Internal("Delta snapshots not yet supported".into()))
233            }
234        }
235    }
236
237    /// Get a snapshot by name
238    pub fn get(&self, name: &str) -> Option<&Snapshot> {
239        self.snapshots.get(&SnapshotId::new(name))
240    }
241
242    /// List all snapshots
243    pub fn list(&self) -> Vec<&Snapshot> {
244        let mut snapshots: Vec<_> = self.snapshots.values().collect();
245        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
246        snapshots
247    }
248
249    /// Delete a snapshot
250    pub fn delete(&mut self, name: &str) -> bool {
251        self.snapshots.remove(&SnapshotId::new(name)).is_some()
252    }
253
254    /// Check if a snapshot exists
255    pub fn exists(&self, name: &str) -> bool {
256        self.snapshots.contains_key(&SnapshotId::new(name))
257    }
258
259    /// Get snapshot count
260    pub fn count(&self) -> usize {
261        self.snapshots.len()
262    }
263
264    /// Evict the oldest snapshot
265    fn evict_oldest(&mut self) {
266        if let Some(oldest) = self
267            .snapshots
268            .values()
269            .min_by_key(|s| s.created_at)
270            .map(|s| s.id.clone())
271        {
272            self.snapshots.remove(&oldest);
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use ucm_core::{Block, Content, DocumentId};
281
282    #[test]
283    fn test_snapshot_create_restore() {
284        let mut mgr = SnapshotManager::new();
285        let mut doc = Document::new(DocumentId::new("test"));
286
287        let root = doc.root;
288        doc.add_block(Block::new(Content::text("Hello"), Some("intro")), &root)
289            .unwrap();
290
291        mgr.create("v1", &doc, Some("First version".into()))
292            .unwrap();
293
294        let restored = mgr.restore("v1").unwrap();
295        assert_eq!(restored.block_count(), doc.block_count());
296    }
297
298    #[test]
299    fn test_snapshot_list() {
300        let mut mgr = SnapshotManager::new();
301        let doc = Document::create();
302
303        mgr.create("v1", &doc, None).unwrap();
304        mgr.create("v2", &doc, None).unwrap();
305        mgr.create("v3", &doc, None).unwrap();
306
307        assert_eq!(mgr.count(), 3);
308        assert_eq!(mgr.list().len(), 3);
309    }
310
311    #[test]
312    fn test_snapshot_delete() {
313        let mut mgr = SnapshotManager::new();
314        let doc = Document::create();
315
316        mgr.create("v1", &doc, None).unwrap();
317        assert!(mgr.exists("v1"));
318
319        mgr.delete("v1");
320        assert!(!mgr.exists("v1"));
321    }
322
323    #[test]
324    fn test_snapshot_eviction() {
325        let mut mgr = SnapshotManager::with_max_snapshots(2);
326        let doc = Document::create();
327
328        mgr.create("v1", &doc, None).unwrap();
329        mgr.create("v2", &doc, None).unwrap();
330        mgr.create("v3", &doc, None).unwrap();
331
332        assert_eq!(mgr.count(), 2);
333        assert!(!mgr.exists("v1")); // v1 should be evicted
334    }
335}