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, PortableDocument, 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        let json = serde_json::to_string(&doc.to_portable())
61            .map_err(|e| Error::Internal(format!("Failed to serialize document: {}", e)))?;
62        Ok(Self { json })
63    }
64
65    pub fn to_document(&self) -> Result<Document> {
66        let serializable: PortableDocument = serde_json::from_str(&self.json)
67            .map_err(|e| Error::Internal(format!("Failed to deserialize document: {}", e)))?;
68        serializable.to_document()
69    }
70
71    pub fn to_portable(&self) -> Result<PortableDocument> {
72        serde_json::from_str(&self.json)
73            .map_err(|e| Error::Internal(format!("Failed to deserialize document: {}", e)))
74    }
75}
76
77/// Change record for delta snapshots
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub enum SnapshotChange {
80    AddBlock {
81        id: String,
82        block: serde_json::Value,
83    },
84    RemoveBlock {
85        id: String,
86    },
87    ModifyBlock {
88        id: String,
89        block: serde_json::Value,
90    },
91    UpdateStructure {
92        parent: String,
93        children: Vec<String>,
94    },
95}
96
97/// Manages document snapshots
98#[derive(Debug, Default)]
99pub struct SnapshotManager {
100    snapshots: HashMap<SnapshotId, Snapshot>,
101    max_snapshots: usize,
102}
103
104impl SnapshotManager {
105    pub fn new() -> Self {
106        Self {
107            snapshots: HashMap::new(),
108            max_snapshots: 100,
109        }
110    }
111
112    pub fn with_max_snapshots(max: usize) -> Self {
113        Self {
114            snapshots: HashMap::new(),
115            max_snapshots: max,
116        }
117    }
118
119    /// Create a snapshot of the document
120    pub fn create(
121        &mut self,
122        name: impl Into<String>,
123        doc: &Document,
124        description: Option<String>,
125    ) -> Result<SnapshotId> {
126        let id = SnapshotId::new(name);
127
128        // Check if we need to evict old snapshots
129        if self.snapshots.len() >= self.max_snapshots {
130            self.evict_oldest();
131        }
132
133        let data = SnapshotData::Full(SerializedDocument::from_document(doc)?);
134
135        let snapshot = Snapshot {
136            id: id.clone(),
137            description,
138            created_at: Utc::now(),
139            document_version: doc.version.clone(),
140            data,
141        };
142
143        self.snapshots.insert(id.clone(), snapshot);
144        Ok(id)
145    }
146
147    /// Restore a document from a snapshot
148    pub fn restore(&self, name: &str) -> Result<Document> {
149        let id = SnapshotId::new(name);
150        let snapshot = self
151            .snapshots
152            .get(&id)
153            .ok_or_else(|| Error::Internal(format!("Snapshot '{}' not found", name)))?;
154
155        match &snapshot.data {
156            SnapshotData::Full(serialized) => serialized.to_document(),
157            SnapshotData::Delta { .. } => {
158                // TODO: implement delta restoration
159                Err(Error::Internal("Delta snapshots not yet supported".into()))
160            }
161        }
162    }
163
164    /// Get a snapshot by name
165    pub fn get(&self, name: &str) -> Option<&Snapshot> {
166        self.snapshots.get(&SnapshotId::new(name))
167    }
168
169    /// List all snapshots
170    pub fn list(&self) -> Vec<&Snapshot> {
171        let mut snapshots: Vec<_> = self.snapshots.values().collect();
172        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
173        snapshots
174    }
175
176    /// Delete a snapshot
177    pub fn delete(&mut self, name: &str) -> bool {
178        self.snapshots.remove(&SnapshotId::new(name)).is_some()
179    }
180
181    /// Check if a snapshot exists
182    pub fn exists(&self, name: &str) -> bool {
183        self.snapshots.contains_key(&SnapshotId::new(name))
184    }
185
186    /// Get snapshot count
187    pub fn count(&self) -> usize {
188        self.snapshots.len()
189    }
190
191    /// Evict the oldest snapshot
192    fn evict_oldest(&mut self) {
193        if let Some(oldest) = self
194            .snapshots
195            .values()
196            .min_by_key(|s| s.created_at)
197            .map(|s| s.id.clone())
198        {
199            self.snapshots.remove(&oldest);
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use ucm_core::{Block, Content, DocumentId};
208
209    #[test]
210    fn test_snapshot_create_restore() {
211        let mut mgr = SnapshotManager::new();
212        let mut doc = Document::new(DocumentId::new("test"));
213
214        let root = doc.root;
215        doc.add_block(Block::new(Content::text("Hello"), Some("intro")), &root)
216            .unwrap();
217
218        mgr.create("v1", &doc, Some("First version".into()))
219            .unwrap();
220
221        let restored = mgr.restore("v1").unwrap();
222        assert_eq!(restored.block_count(), doc.block_count());
223    }
224
225    #[test]
226    fn test_snapshot_list() {
227        let mut mgr = SnapshotManager::new();
228        let doc = Document::create();
229
230        mgr.create("v1", &doc, None).unwrap();
231        mgr.create("v2", &doc, None).unwrap();
232        mgr.create("v3", &doc, None).unwrap();
233
234        assert_eq!(mgr.count(), 3);
235        assert_eq!(mgr.list().len(), 3);
236    }
237
238    #[test]
239    fn test_snapshot_delete() {
240        let mut mgr = SnapshotManager::new();
241        let doc = Document::create();
242
243        mgr.create("v1", &doc, None).unwrap();
244        assert!(mgr.exists("v1"));
245
246        mgr.delete("v1");
247        assert!(!mgr.exists("v1"));
248    }
249
250    #[test]
251    fn test_snapshot_eviction() {
252        let mut mgr = SnapshotManager::with_max_snapshots(2);
253        let doc = Document::create();
254
255        mgr.create("v1", &doc, None).unwrap();
256        mgr.create("v2", &doc, None).unwrap();
257        mgr.create("v3", &doc, None).unwrap();
258
259        assert_eq!(mgr.count(), 2);
260        assert!(!mgr.exists("v1")); // v1 should be evicted
261    }
262}