1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use ucm_core::{Document, DocumentVersion, Error, PortableDocument, Result};
7
8#[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#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Snapshot {
27 pub id: SnapshotId,
29 pub description: Option<String>,
31 pub created_at: DateTime<Utc>,
33 pub document_version: DocumentVersion,
35 pub data: SnapshotData,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum SnapshotData {
42 Full(SerializedDocument),
44 Delta {
46 base: SnapshotId,
47 changes: Vec<SnapshotChange>,
48 },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SerializedDocument {
54 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#[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#[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 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 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 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 Err(Error::Internal("Delta snapshots not yet supported".into()))
160 }
161 }
162 }
163
164 pub fn get(&self, name: &str) -> Option<&Snapshot> {
166 self.snapshots.get(&SnapshotId::new(name))
167 }
168
169 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 pub fn delete(&mut self, name: &str) -> bool {
178 self.snapshots.remove(&SnapshotId::new(name)).is_some()
179 }
180
181 pub fn exists(&self, name: &str) -> bool {
183 self.snapshots.contains_key(&SnapshotId::new(name))
184 }
185
186 pub fn count(&self) -> usize {
188 self.snapshots.len()
189 }
190
191 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")); }
262}