mod error;
pub use error::Error;
use blueprint_std::collections::HashMap;
use blueprint_std::fs;
use blueprint_std::path::{Path, PathBuf};
use blueprint_std::sync::Mutex;
use serde::{Serialize, de::DeserializeOwned};
use std::io::ErrorKind;
#[derive(Debug)]
pub struct LocalDatabase<T> {
path: PathBuf,
data: Mutex<HashMap<String, T>>,
}
impl<T> LocalDatabase<T>
where
T: Serialize + DeserializeOwned + Clone,
{
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let path = path.as_ref();
let parent_dir = path.parent().ok_or(Error::Io(std::io::Error::new(
ErrorKind::NotFound,
"parent directory not found",
)))?;
fs::create_dir_all(parent_dir)?;
let data = if path.exists() {
let content = fs::read_to_string(path)?;
serde_json::from_str(&content).unwrap_or_default()
} else {
let empty_data = HashMap::new();
let json_string = serde_json::to_string(&empty_data)?;
fs::write(path, json_string)?;
empty_data
};
Ok(Self {
path: path.to_owned(),
data: Mutex::new(data),
})
}
pub fn len(&self) -> Result<usize, Error> {
let data = self.lock()?;
Ok(data.len())
}
pub fn is_empty(&self) -> Result<bool, Error> {
let data = self.lock()?;
Ok(data.is_empty())
}
pub fn set(&self, key: &str, value: T) -> Result<(), Error> {
let mut data = self.lock()?;
data.insert(key.to_string(), value);
self.flush(&data)
}
pub fn get(&self, key: &str) -> Result<Option<T>, Error> {
let data = self.lock()?;
Ok(data.get(key).cloned())
}
pub fn remove(&self, key: &str) -> Result<Option<T>, Error> {
let mut data = self.lock()?;
let removed = data.remove(key);
if removed.is_some() {
self.flush(&data)?;
}
Ok(removed)
}
pub fn values(&self) -> Result<Vec<T>, Error> {
let data = self.lock()?;
Ok(data.values().cloned().collect())
}
pub fn entries(&self) -> Result<Vec<(String, T)>, Error> {
let data = self.lock()?;
Ok(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
}
pub fn find<F>(&self, predicate: F) -> Result<Option<T>, Error>
where
F: Fn(&T) -> bool,
{
let data = self.lock()?;
Ok(data.values().find(|v| predicate(v)).cloned())
}
pub fn update<F>(&self, key: &str, f: F) -> Result<bool, Error>
where
F: FnOnce(&mut T),
{
let mut data = self.lock()?;
if let Some(value) = data.get_mut(key) {
f(value);
self.flush(&data)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn replace(&self, new_data: HashMap<String, T>) -> Result<(), Error> {
let mut data = self.lock()?;
*data = new_data;
self.flush(&data)
}
pub fn contains_key(&self, key: &str) -> Result<bool, Error> {
let data = self.lock()?;
Ok(data.contains_key(key))
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, HashMap<String, T>>, Error> {
self.data.lock().map_err(|_| Error::Poisoned)
}
fn flush(&self, data: &HashMap<String, T>) -> Result<(), Error> {
let tmp = self.path.with_extension("tmp");
let json_string = serde_json::to_string(data)?;
fs::write(&tmp, &json_string)?;
fs::rename(&tmp, &self.path)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use tempfile::tempdir;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
struct TestStruct {
field1: String,
field2: i32,
}
#[test]
fn test_create_new_database() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
assert!(db.is_empty().unwrap());
assert_eq!(db.len().unwrap(), 0);
assert!(db_path.exists());
}
#[test]
fn test_set_and_get() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("key1", 42).unwrap();
db.set("key2", 100).unwrap();
assert_eq!(db.get("key1").unwrap(), Some(42));
assert_eq!(db.get("key2").unwrap(), Some(100));
assert_eq!(db.get("nonexistent").unwrap(), None);
assert_eq!(db.len().unwrap(), 2);
}
#[test]
fn test_complex_type() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
let test_struct = TestStruct {
field1: "test".to_string(),
field2: 42,
};
db.set("key1", test_struct.clone()).unwrap();
assert_eq!(db.get("key1").unwrap(), Some(test_struct));
}
#[test]
fn test_persistence() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
{
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("key1", 42).unwrap();
db.set("key2", 100).unwrap();
}
{
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
assert_eq!(db.get("key1").unwrap(), Some(42));
assert_eq!(db.get("key2").unwrap(), Some(100));
assert_eq!(db.len().unwrap(), 2);
}
}
#[test]
fn test_overwrite() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("key1", 42).unwrap();
assert_eq!(db.get("key1").unwrap(), Some(42));
db.set("key1", 100).unwrap();
assert_eq!(db.get("key1").unwrap(), Some(100));
assert_eq!(db.len().unwrap(), 1);
}
#[test]
fn test_invalid_json() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
fs::write(&db_path, "{invalid_json}").unwrap();
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
assert!(db.is_empty().unwrap());
}
#[test]
fn test_remove() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("key1", 42).unwrap();
db.set("key2", 100).unwrap();
let removed = db.remove("key1").unwrap();
assert_eq!(removed, Some(42));
assert_eq!(db.get("key1").unwrap(), None);
assert_eq!(db.len().unwrap(), 1);
let db2 = LocalDatabase::<u32>::open(&db_path).unwrap();
assert_eq!(db2.get("key1").unwrap(), None);
assert_eq!(db2.get("key2").unwrap(), Some(100));
}
#[test]
fn test_remove_nonexistent() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
let removed = db.remove("nonexistent").unwrap();
assert_eq!(removed, None);
}
#[test]
fn test_values() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("a", 1).unwrap();
db.set("b", 2).unwrap();
db.set("c", 3).unwrap();
let mut values = db.values().unwrap();
values.sort_unstable();
assert_eq!(values, vec![1, 2, 3]);
}
#[test]
fn test_find() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
db.set(
"a",
TestStruct {
field1: "hello".into(),
field2: 10,
},
)
.unwrap();
db.set(
"b",
TestStruct {
field1: "world".into(),
field2: 20,
},
)
.unwrap();
let found = db.find(|v| v.field2 == 20).unwrap();
assert_eq!(found.unwrap().field1, "world");
let not_found = db.find(|v| v.field2 == 99).unwrap();
assert!(not_found.is_none());
}
#[test]
fn test_update() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
db.set(
"key1",
TestStruct {
field1: "original".into(),
field2: 0,
},
)
.unwrap();
let updated = db
.update("key1", |v| {
v.field1 = "modified".into();
v.field2 = 42;
})
.unwrap();
assert!(updated);
let value = db.get("key1").unwrap().unwrap();
assert_eq!(value.field1, "modified");
assert_eq!(value.field2, 42);
let db2 = LocalDatabase::<TestStruct>::open(&db_path).unwrap();
let value = db2.get("key1").unwrap().unwrap();
assert_eq!(value.field1, "modified");
}
#[test]
fn test_update_nonexistent() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
let updated = db.update("nonexistent", |v| *v += 1).unwrap();
assert!(!updated);
}
#[test]
fn test_replace() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("old", 1).unwrap();
let mut new_data = HashMap::new();
new_data.insert("new1".to_string(), 10);
new_data.insert("new2".to_string(), 20);
db.replace(new_data).unwrap();
assert_eq!(db.get("old").unwrap(), None);
assert_eq!(db.get("new1").unwrap(), Some(10));
assert_eq!(db.get("new2").unwrap(), Some(20));
let db2 = LocalDatabase::<u32>::open(&db_path).unwrap();
assert_eq!(db2.get("new1").unwrap(), Some(10));
}
#[test]
fn test_contains_key() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("exists", 42).unwrap();
assert!(db.contains_key("exists").unwrap());
assert!(!db.contains_key("missing").unwrap());
}
#[test]
fn test_entries() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = LocalDatabase::<u32>::open(&db_path).unwrap();
db.set("a", 1).unwrap();
db.set("b", 2).unwrap();
let mut entries = db.entries().unwrap();
entries.sort_by_key(|(k, _)| k.clone());
assert_eq!(entries, vec![("a".to_string(), 1), ("b".to_string(), 2)]);
}
#[test]
fn test_concurrent_access() {
use blueprint_std::sync::Arc;
use blueprint_std::thread;
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.json");
let db = Arc::new(LocalDatabase::<u32>::open(&db_path).unwrap());
let mut handles = vec![];
for i in 0..10 {
let db_clone = Arc::clone(&db);
let handle = thread::spawn(move || {
db_clone.set(&format!("key{}", i), i).unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(db.len().unwrap(), 10);
for i in 0..10 {
assert_eq!(db.get(&format!("key{}", i)).unwrap(), Some(i));
}
}
}