blueprint_store_local_database/
lib.rs1mod 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#[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 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 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 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 pub fn len(&self) -> Result<usize, Error> {
87 let data = self.lock()?;
88 Ok(data.len())
89 }
90
91 pub fn is_empty(&self) -> Result<bool, Error> {
93 let data = self.lock()?;
94 Ok(data.is_empty())
95 }
96
97 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 pub fn get(&self, key: &str) -> Result<Option<T>, Error> {
111 let data = self.lock()?;
112 Ok(data.get(key).cloned())
113 }
114
115 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 pub fn values(&self) -> Result<Vec<T>, Error> {
129 let data = self.lock()?;
130 Ok(data.values().cloned().collect())
131 }
132
133 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 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 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 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 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 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 {
253 let db = LocalDatabase::<u32>::open(&db_path).unwrap();
254 db.set("key1", 42).unwrap();
255 db.set("key2", 100).unwrap();
256 }
257
258 {
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 fs::write(&db_path, "{invalid_json}").unwrap();
288
289 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 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 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 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 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 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}