dirtydata_core/
storage.rs1use std::fs;
22use std::path::{Path, PathBuf};
23
24use crate::ir::Graph;
25use crate::patch::Patch;
26use crate::types::PatchId;
27
28const DIRTYDATA_DIR: &str = ".dirtydata";
30
31#[derive(Debug, thiserror::Error)]
33pub enum StorageError {
34 #[error("I/O error: {0}")]
35 Io(#[from] std::io::Error),
36
37 #[error("serialization error: {0}")]
38 Serialize(#[from] serde_json::Error),
39
40 #[error("project not initialized — run `dirtydata init`")]
41 NotInitialized,
42
43 #[error("patch {0} not found")]
44 PatchNotFound(PatchId),
45}
46
47pub struct Storage {
49 root: PathBuf,
50}
51
52impl Storage {
53 pub fn open(project_root: &Path) -> Result<Self, StorageError> {
55 let root = project_root.join(DIRTYDATA_DIR);
56 if !root.exists() {
57 return Err(StorageError::NotInitialized);
58 }
59 Ok(Self { root })
60 }
61
62 pub fn init(project_root: &Path) -> Result<Self, StorageError> {
64 let root = project_root.join(DIRTYDATA_DIR);
65 fs::create_dir_all(root.join("ir"))?;
66 fs::create_dir_all(root.join("patches"))?;
67 fs::create_dir_all(root.join("intents"))?;
68 fs::create_dir_all(root.join("refs").join("heads"))?;
69
70 fs::write(root.join("HEAD"), "ref: refs/heads/main")?;
72
73 let config = serde_json::json!({
75 "version": "0.1.0",
76 "hash_algorithm": "blake3",
77 "id_scheme": "ulid"
78 });
79 fs::write(
80 root.join("config.json"),
81 serde_json::to_string_pretty(&config)?,
82 )?;
83
84 let index = PatchIndex {
86 patches: Vec::new(),
87 };
88 fs::write(
89 root.join("patches").join("index.json"),
90 serde_json::to_string_pretty(&index)?,
91 )?;
92
93 let graph = Graph::new();
95 fs::write(
96 root.join("ir").join("current.json"),
97 serde_json::to_string_pretty(&graph)?,
98 )?;
99
100 Ok(Self { root })
101 }
102
103 pub fn load_graph(&self) -> Result<Graph, StorageError> {
107 let path = self.root.join("ir").join("current.json");
108 let data = fs::read_to_string(&path)?;
109 let graph = serde_json::from_str(&data)?;
110 Ok(graph)
111 }
112
113 pub fn save_graph(&self, graph: &Graph) -> Result<(), StorageError> {
115 let path = self.root.join("ir").join("current.json");
116 let data = serde_json::to_string_pretty(graph)?;
117 fs::write(path, data)?;
118 Ok(())
119 }
120
121 pub fn read_head(&self) -> Result<String, StorageError> {
125 let head_path = self.root.join("HEAD");
126 if !head_path.exists() {
127 return Ok("main".to_string());
128 }
129 let content = fs::read_to_string(head_path)?;
130 let content = content.trim();
131 if content.starts_with("ref: refs/heads/") {
132 Ok(content.replace("ref: refs/heads/", ""))
133 } else {
134 Ok(content.to_string()) }
136 }
137
138 pub fn write_head(&self, branch: &str) -> Result<(), StorageError> {
140 let head_path = self.root.join("HEAD");
141 fs::write(head_path, format!("ref: refs/heads/{}", branch))?;
142 Ok(())
143 }
144
145 pub fn read_branch(&self, branch: &str) -> Result<Option<PatchId>, StorageError> {
147 let path = self.root.join("refs").join("heads").join(branch);
148 if !path.exists() {
149 return Ok(None);
150 }
151 let content = fs::read_to_string(path)?;
152 let content = content.trim();
153 if content.is_empty() {
154 return Ok(None);
155 }
156 Ok(content.parse::<PatchId>().ok())
157 }
158
159 pub fn write_branch(&self, branch: &str, patch_id: PatchId) -> Result<(), StorageError> {
161 let path = self.root.join("refs").join("heads").join(branch);
162 fs::write(path, patch_id.to_string())?;
163 Ok(())
164 }
165
166 pub fn list_branches(&self) -> Result<Vec<String>, StorageError> {
168 let mut branches = Vec::new();
169 let heads_dir = self.root.join("refs").join("heads");
170 if heads_dir.exists() {
171 for entry in fs::read_dir(heads_dir)? {
172 let entry = entry?;
173 if entry.file_type()?.is_file() {
174 if let Some(name) = entry.file_name().to_str() {
175 branches.push(name.to_string());
176 }
177 }
178 }
179 }
180 branches.sort();
181 Ok(branches)
182 }
183
184 pub fn save_patch(&self, patch: &Patch) -> Result<(), StorageError> {
188 let filename = format!("{}.json", patch.identity);
189 let path = self.root.join("patches").join(&filename);
190 let data = serde_json::to_string_pretty(patch)?;
191 fs::write(path, data)?;
192
193 let mut index = self.load_patch_index()?;
195 let entry = PatchIndexEntry {
196 id: patch.identity,
197 parents: patch.parents.clone(),
198 timestamp: patch.timestamp,
199 hash: patch.deterministic_hash,
200 };
201 if !index.patches.iter().any(|e| e.id == patch.identity) {
203 index.patches.push(entry);
204 self.save_patch_index(&index)?;
205 }
206
207 let current_branch = self.read_head()?;
209 self.write_branch(¤t_branch, patch.identity)?;
210
211 Ok(())
212 }
213
214 pub fn load_patch(&self, id: &PatchId) -> Result<Patch, StorageError> {
216 let filename = format!("{}.json", id);
217 let path = self.root.join("patches").join(&filename);
218 if !path.exists() {
219 return Err(StorageError::PatchNotFound(*id));
220 }
221 let data = fs::read_to_string(path)?;
222 let patch = serde_json::from_str(&data)?;
223 Ok(patch)
224 }
225
226 pub fn load_all_patches(&self) -> Result<Vec<Patch>, StorageError> {
228 let index = self.load_patch_index()?;
229 let mut patches = Vec::new();
230 for entry in &index.patches {
231 patches.push(self.load_patch(&entry.id)?);
232 }
233 Ok(patches)
234 }
235
236 fn load_patch_index(&self) -> Result<PatchIndex, StorageError> {
238 let path = self.root.join("patches").join("index.json");
239 let data = fs::read_to_string(path)?;
240 let index = serde_json::from_str(&data)?;
241 Ok(index)
242 }
243
244 pub fn load_patch_ancestry(&self, tip: PatchId) -> Result<Vec<Patch>, StorageError> {
246 let mut ancestry = Vec::new();
247 let mut current = Some(tip);
248
249 while let Some(id) = current {
250 let patch = self.load_patch(&id)?;
251 current = patch.parents.first().copied();
253 ancestry.push(patch);
254 }
255
256 ancestry.reverse();
258 Ok(ancestry)
259 }
260
261 fn save_patch_index(&self, index: &PatchIndex) -> Result<(), StorageError> {
263 let path = self.root.join("patches").join("index.json");
264 let data = serde_json::to_string_pretty(index)?;
265 fs::write(path, data)?;
266 Ok(())
267 }
268
269 pub fn root(&self) -> &Path {
271 &self.root
272 }
273}
274
275#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
277pub struct PatchIndex {
278 pub patches: Vec<PatchIndexEntry>,
279}
280
281#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
283pub struct PatchIndexEntry {
284 pub id: PatchId,
285 pub parents: Vec<PatchId>,
286 pub timestamp: crate::types::Timestamp,
287 pub hash: crate::types::Hash,
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::ir::Node;
294 use crate::patch::{Operation, Patch};
295
296 #[test]
297 fn test_init_and_load() {
298 let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
299 fs::create_dir_all(&tmp).unwrap();
300
301 let storage = Storage::init(&tmp).unwrap();
302 let graph = storage.load_graph().unwrap();
303 assert!(graph.nodes.is_empty());
304 assert_eq!(graph.revision.0, 0);
305
306 fs::remove_dir_all(&tmp).ok();
308 }
309
310 #[test]
311 fn test_save_and_load_patch() {
312 let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
313 fs::create_dir_all(&tmp).unwrap();
314
315 let storage = Storage::init(&tmp).unwrap();
316
317 let node = Node::new_source("Sine");
318 let patch = Patch::from_operations(vec![Operation::AddNode(node)]);
319 storage.save_patch(&patch).unwrap();
320
321 let loaded = storage.load_patch(&patch.identity).unwrap();
322 assert_eq!(loaded.identity, patch.identity);
323 assert_eq!(loaded.operations.len(), 1);
324
325 fs::remove_dir_all(&tmp).ok();
327 }
328
329 #[test]
330 fn test_full_roundtrip() {
331 let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
332 fs::create_dir_all(&tmp).unwrap();
333
334 let storage = Storage::init(&tmp).unwrap();
335
336 let mut graph = storage.load_graph().unwrap();
338 let node = Node::new_processor("Gain");
339 let patch = Patch::from_operations(vec![Operation::AddNode(node.clone())]);
340 graph.apply(&patch).unwrap();
341
342 storage.save_graph(&graph).unwrap();
344 storage.save_patch(&patch).unwrap();
345
346 let reloaded = storage.load_graph().unwrap();
348 assert_eq!(reloaded.nodes.len(), 1);
349 assert!(reloaded.nodes.contains_key(&node.id));
350
351 let patches = storage.load_all_patches().unwrap();
353 let replayed = Graph::replay(&patches).unwrap();
354 assert_eq!(replayed.nodes.len(), reloaded.nodes.len());
355
356 fs::remove_dir_all(&tmp).ok();
358 }
359}