Skip to main content

blueprint_store_local_database/
lib.rs

1mod error;
2pub use error::Error;
3
4use blueprint_std::collections::HashMap;
5use blueprint_std::fs;
6use blueprint_std::path::{Path, PathBuf};
7use blueprint_std::sync::Mutex;
8use serde::{Serialize, de::DeserializeOwned};
9use std::io::ErrorKind;
10
11/// A local database for storing key-value pairs.
12///
13/// The database is stored in a JSON file, which is updated every time
14/// a key-value pair is added, updated, or removed. Writes are atomic
15/// (write to temporary file, then rename) to prevent corruption.
16///
17/// # Example
18///
19/// ```no_run
20/// use blueprint_store_local_database::LocalDatabase;
21///
22/// # fn main() -> Result<(), blueprint_store_local_database::Error> {
23/// let db = LocalDatabase::<u64>::open("data.json")?;
24///
25/// db.set("key", 42)?;
26/// assert_eq!(db.get("key")?, Some(42));
27/// # Ok(()) }
28/// ```
29#[derive(Debug)]
30pub struct LocalDatabase<T> {
31    path: PathBuf,
32    data: Mutex<HashMap<String, T>>,
33}
34
35impl<T> LocalDatabase<T>
36where
37    T: Serialize + DeserializeOwned + Clone,
38{
39    /// Reads a `LocalDatabase` from the given path.
40    ///
41    /// If the file does not exist, an empty database is created.
42    ///
43    /// # Examples
44    ///
45    /// ```no_run
46    /// use blueprint_store_local_database::LocalDatabase;
47    ///
48    /// # fn main() -> Result<(), blueprint_store_local_database::Error> {
49    /// let db = LocalDatabase::<u64>::open("data.json")?;
50    /// assert!(db.is_empty()?);
51    /// # Ok(()) }
52    /// ```
53    ///
54    /// # Errors
55    ///
56    /// * The parent of `path` is not a directory
57    /// * Unable to write to `path`
58    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
59        let path = path.as_ref();
60        let parent_dir = path.parent().ok_or(Error::Io(std::io::Error::new(
61            ErrorKind::NotFound,
62            "parent directory not found",
63        )))?;
64
65        // Create the parent directory if it doesn't exist
66        fs::create_dir_all(parent_dir)?;
67
68        let data = if path.exists() {
69            let content = fs::read_to_string(path)?;
70            serde_json::from_str(&content).unwrap_or_default()
71        } else {
72            // Create an empty file with default empty JSON object
73            let empty_data = HashMap::new();
74            let json_string = serde_json::to_string(&empty_data)?;
75            fs::write(path, json_string)?;
76            empty_data
77        };
78
79        Ok(Self {
80            path: path.to_owned(),
81            data: Mutex::new(data),
82        })
83    }
84
85    /// Returns the number of key-value pairs in the database.
86    pub fn len(&self) -> Result<usize, Error> {
87        let data = self.lock()?;
88        Ok(data.len())
89    }
90
91    /// Checks if the database is empty.
92    pub fn is_empty(&self) -> Result<bool, Error> {
93        let data = self.lock()?;
94        Ok(data.is_empty())
95    }
96
97    /// Adds or updates a key-value pair in the database.
98    ///
99    /// # Errors
100    ///
101    /// * Unable to serialize the data
102    /// * Unable to write to disk
103    pub fn set(&self, key: &str, value: T) -> Result<(), Error> {
104        let mut data = self.lock()?;
105        data.insert(key.to_string(), value);
106        self.flush(&data)
107    }
108
109    /// Retrieves a value associated with the given key.
110    pub fn get(&self, key: &str) -> Result<Option<T>, Error> {
111        let data = self.lock()?;
112        Ok(data.get(key).cloned())
113    }
114
115    /// Removes a key-value pair from the database.
116    ///
117    /// Returns the removed value if the key existed.
118    pub fn remove(&self, key: &str) -> Result<Option<T>, Error> {
119        let mut data = self.lock()?;
120        let removed = data.remove(key);
121        if removed.is_some() {
122            self.flush(&data)?;
123        }
124        Ok(removed)
125    }
126
127    /// Returns a clone of all values in the database.
128    pub fn values(&self) -> Result<Vec<T>, Error> {
129        let data = self.lock()?;
130        Ok(data.values().cloned().collect())
131    }
132
133    /// Returns a clone of all key-value pairs in the database.
134    pub fn entries(&self) -> Result<Vec<(String, T)>, Error> {
135        let data = self.lock()?;
136        Ok(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
137    }
138
139    /// Finds the first value matching a predicate.
140    pub fn find<F>(&self, predicate: F) -> Result<Option<T>, Error>
141    where
142        F: Fn(&T) -> bool,
143    {
144        let data = self.lock()?;
145        Ok(data.values().find(|v| predicate(v)).cloned())
146    }
147
148    /// Atomically gets a value by key, applies a mutation, and flushes.
149    ///
150    /// Returns `true` if the key was found and updated.
151    pub fn update<F>(&self, key: &str, f: F) -> Result<bool, Error>
152    where
153        F: FnOnce(&mut T),
154    {
155        let mut data = self.lock()?;
156        if let Some(value) = data.get_mut(key) {
157            f(value);
158            self.flush(&data)?;
159            Ok(true)
160        } else {
161            Ok(false)
162        }
163    }
164
165    /// Replaces the entire database contents with a new map.
166    pub fn replace(&self, new_data: HashMap<String, T>) -> Result<(), Error> {
167        let mut data = self.lock()?;
168        *data = new_data;
169        self.flush(&data)
170    }
171
172    /// Checks if a key exists in the database.
173    pub fn contains_key(&self, key: &str) -> Result<bool, Error> {
174        let data = self.lock()?;
175        Ok(data.contains_key(key))
176    }
177
178    fn lock(&self) -> Result<std::sync::MutexGuard<'_, HashMap<String, T>>, Error> {
179        self.data.lock().map_err(|_| Error::Poisoned)
180    }
181
182    /// Atomically write to a temporary file and rename to the target path.
183    fn flush(&self, data: &HashMap<String, T>) -> Result<(), Error> {
184        let tmp = self.path.with_extension("tmp");
185        let json_string = serde_json::to_string(data)?;
186        fs::write(&tmp, &json_string)?;
187        fs::rename(&tmp, &self.path)?;
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use serde::{Deserialize, Serialize};
196    use tempfile::tempdir;
197
198    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
199    struct TestStruct {
200        field1: String,
201        field2: i32,
202    }
203
204    #[test]
205    fn test_create_new_database() {
206        let dir = tempdir().unwrap();
207        let db_path = dir.path().join("test.json");
208
209        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
210        assert!(db.is_empty().unwrap());
211        assert_eq!(db.len().unwrap(), 0);
212        assert!(db_path.exists());
213    }
214
215    #[test]
216    fn test_set_and_get() {
217        let dir = tempdir().unwrap();
218        let db_path = dir.path().join("test.json");
219
220        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
221        db.set("key1", 42).unwrap();
222        db.set("key2", 100).unwrap();
223
224        assert_eq!(db.get("key1").unwrap(), Some(42));
225        assert_eq!(db.get("key2").unwrap(), Some(100));
226        assert_eq!(db.get("nonexistent").unwrap(), None);
227        assert_eq!(db.len().unwrap(), 2);
228    }
229
230    #[test]
231    fn test_complex_type() {
232        let dir = tempdir().unwrap();
233        let db_path = dir.path().join("test.json");
234
235        let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
236
237        let test_struct = TestStruct {
238            field1: "test".to_string(),
239            field2: 42,
240        };
241
242        db.set("key1", test_struct.clone()).unwrap();
243        assert_eq!(db.get("key1").unwrap(), Some(test_struct));
244    }
245
246    #[test]
247    fn test_persistence() {
248        let dir = tempdir().unwrap();
249        let db_path = dir.path().join("test.json");
250
251        // Write data
252        {
253            let db = LocalDatabase::<u32>::open(&db_path).unwrap();
254            db.set("key1", 42).unwrap();
255            db.set("key2", 100).unwrap();
256        }
257
258        // Read data in new instance
259        {
260            let db = LocalDatabase::<u32>::open(&db_path).unwrap();
261            assert_eq!(db.get("key1").unwrap(), Some(42));
262            assert_eq!(db.get("key2").unwrap(), Some(100));
263            assert_eq!(db.len().unwrap(), 2);
264        }
265    }
266
267    #[test]
268    fn test_overwrite() {
269        let dir = tempdir().unwrap();
270        let db_path = dir.path().join("test.json");
271
272        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
273        db.set("key1", 42).unwrap();
274        assert_eq!(db.get("key1").unwrap(), Some(42));
275
276        db.set("key1", 100).unwrap();
277        assert_eq!(db.get("key1").unwrap(), Some(100));
278        assert_eq!(db.len().unwrap(), 1);
279    }
280
281    #[test]
282    fn test_invalid_json() {
283        let dir = tempdir().unwrap();
284        let db_path = dir.path().join("test.json");
285
286        // Write invalid JSON
287        fs::write(&db_path, "{invalid_json}").unwrap();
288
289        // Should create empty database when JSON is invalid
290        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
291        assert!(db.is_empty().unwrap());
292    }
293
294    #[test]
295    fn test_remove() {
296        let dir = tempdir().unwrap();
297        let db_path = dir.path().join("test.json");
298
299        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
300        db.set("key1", 42).unwrap();
301        db.set("key2", 100).unwrap();
302
303        let removed = db.remove("key1").unwrap();
304        assert_eq!(removed, Some(42));
305        assert_eq!(db.get("key1").unwrap(), None);
306        assert_eq!(db.len().unwrap(), 1);
307
308        // Verify persistence after remove
309        let db2 = LocalDatabase::<u32>::open(&db_path).unwrap();
310        assert_eq!(db2.get("key1").unwrap(), None);
311        assert_eq!(db2.get("key2").unwrap(), Some(100));
312    }
313
314    #[test]
315    fn test_remove_nonexistent() {
316        let dir = tempdir().unwrap();
317        let db_path = dir.path().join("test.json");
318
319        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
320        let removed = db.remove("nonexistent").unwrap();
321        assert_eq!(removed, None);
322    }
323
324    #[test]
325    fn test_values() {
326        let dir = tempdir().unwrap();
327        let db_path = dir.path().join("test.json");
328
329        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
330        db.set("a", 1).unwrap();
331        db.set("b", 2).unwrap();
332        db.set("c", 3).unwrap();
333
334        let mut values = db.values().unwrap();
335        values.sort_unstable();
336        assert_eq!(values, vec![1, 2, 3]);
337    }
338
339    #[test]
340    fn test_find() {
341        let dir = tempdir().unwrap();
342        let db_path = dir.path().join("test.json");
343
344        let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
345        db.set(
346            "a",
347            TestStruct {
348                field1: "hello".into(),
349                field2: 10,
350            },
351        )
352        .unwrap();
353        db.set(
354            "b",
355            TestStruct {
356                field1: "world".into(),
357                field2: 20,
358            },
359        )
360        .unwrap();
361
362        let found = db.find(|v| v.field2 == 20).unwrap();
363        assert_eq!(found.unwrap().field1, "world");
364
365        let not_found = db.find(|v| v.field2 == 99).unwrap();
366        assert!(not_found.is_none());
367    }
368
369    #[test]
370    fn test_update() {
371        let dir = tempdir().unwrap();
372        let db_path = dir.path().join("test.json");
373
374        let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
375        db.set(
376            "key1",
377            TestStruct {
378                field1: "original".into(),
379                field2: 0,
380            },
381        )
382        .unwrap();
383
384        let updated = db
385            .update("key1", |v| {
386                v.field1 = "modified".into();
387                v.field2 = 42;
388            })
389            .unwrap();
390        assert!(updated);
391
392        let value = db.get("key1").unwrap().unwrap();
393        assert_eq!(value.field1, "modified");
394        assert_eq!(value.field2, 42);
395
396        // Verify persistence
397        let db2 = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
398        let value = db2.get("key1").unwrap().unwrap();
399        assert_eq!(value.field1, "modified");
400    }
401
402    #[test]
403    fn test_update_nonexistent() {
404        let dir = tempdir().unwrap();
405        let db_path = dir.path().join("test.json");
406
407        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
408        let updated = db.update("nonexistent", |v| *v += 1).unwrap();
409        assert!(!updated);
410    }
411
412    #[test]
413    fn test_replace() {
414        let dir = tempdir().unwrap();
415        let db_path = dir.path().join("test.json");
416
417        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
418        db.set("old", 1).unwrap();
419
420        let mut new_data = HashMap::new();
421        new_data.insert("new1".to_string(), 10);
422        new_data.insert("new2".to_string(), 20);
423        db.replace(new_data).unwrap();
424
425        assert_eq!(db.get("old").unwrap(), None);
426        assert_eq!(db.get("new1").unwrap(), Some(10));
427        assert_eq!(db.get("new2").unwrap(), Some(20));
428
429        // Verify persistence
430        let db2 = LocalDatabase::<u32>::open(&db_path).unwrap();
431        assert_eq!(db2.get("new1").unwrap(), Some(10));
432    }
433
434    #[test]
435    fn test_contains_key() {
436        let dir = tempdir().unwrap();
437        let db_path = dir.path().join("test.json");
438
439        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
440        db.set("exists", 42).unwrap();
441
442        assert!(db.contains_key("exists").unwrap());
443        assert!(!db.contains_key("missing").unwrap());
444    }
445
446    #[test]
447    fn test_entries() {
448        let dir = tempdir().unwrap();
449        let db_path = dir.path().join("test.json");
450
451        let db = LocalDatabase::<u32>::open(&db_path).unwrap();
452        db.set("a", 1).unwrap();
453        db.set("b", 2).unwrap();
454
455        let mut entries = db.entries().unwrap();
456        entries.sort_by_key(|(k, _)| k.clone());
457        assert_eq!(entries, vec![("a".to_string(), 1), ("b".to_string(), 2)]);
458    }
459
460    #[test]
461    fn test_concurrent_access() {
462        use blueprint_std::sync::Arc;
463        use blueprint_std::thread;
464
465        let dir = tempdir().unwrap();
466        let db_path = dir.path().join("test.json");
467
468        let db = Arc::new(LocalDatabase::<u32>::open(&db_path).unwrap());
469        let mut handles = vec![];
470
471        // Spawn multiple threads to write to the database
472        for i in 0..10 {
473            let db_clone = Arc::clone(&db);
474            let handle = thread::spawn(move || {
475                db_clone.set(&format!("key{}", i), i).unwrap();
476            });
477            handles.push(handle);
478        }
479
480        // Wait for all threads to complete
481        for handle in handles {
482            handle.join().unwrap();
483        }
484
485        assert_eq!(db.len().unwrap(), 10);
486        for i in 0..10 {
487            assert_eq!(db.get(&format!("key{}", i)).unwrap(), Some(i));
488        }
489    }
490}