use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::IntoIter;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufReader, BufWriter};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Serialize)]
pub struct Store<V> {
pub path: PathBuf,
inner: HashMap<String, V>,
}
impl<V> Store<V>
where
V: DeserializeOwned + Serialize,
{
pub fn open(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
let mut store = Self {
path: path.as_ref().into(),
inner: HashMap::<String, V>::new(),
};
if fs::exists(&path)? {
store.inner = serde_json::from_reader(BufReader::new(File::open(&path)?))?;
}
Ok(store)
}
pub fn sync(&self) -> Result<(), std::io::Error> {
let file = File::create(&self.path)?;
let writer = BufWriter::new(file);
serde_json::to_writer(writer, &self.inner)?;
Ok(())
}
}
impl<V> Deref for Store<V> {
type Target = HashMap<String, V>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<V> DerefMut for Store<V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<V> IntoIterator for Store<V> {
type Item = (String, V);
type IntoIter = IntoIter<String, V>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn new_store_contains_no_data() {
let s = TmpStore::new();
assert!(s.store.is_empty(), "unexpected data found in new store");
}
#[test]
fn sync_persists_changes_to_store() {
let mut tmp = TmpStore::new();
assert!(
tmp.store.insert("k1".into(), "v1".into()).is_none(),
"key should not already be present in new empty store"
);
tmp.store.sync().unwrap();
let s2 =
Store::<String>::open(&tmp.store.path).expect("opening existing store should succeed");
assert_eq!("v1", s2.get("k1").unwrap(), "expected data not returned");
}
#[test]
fn open_or_create_fn_accepts_nonexistent_path() {
let s = Store::<String>::open("bogus");
assert!(s.is_ok(), "unexpected error: {:?}", s.err());
}
#[test]
#[cfg(not(windows))] fn open_or_create_fn_errors_on_invalid_path() {
use std::fs;
let tmp_dir = TempDir::new().unwrap();
let mut path = tmp_dir.path().join("not_a_directory");
fs::write(&path, "").unwrap();
path.push("store_file");
let s = Store::<String>::open(&path);
assert!(s.is_err(), "want error for invalid path, got {s:?}");
}
struct TmpStore {
_tmp_dir: TempDir,
store: Store<String>,
}
impl TmpStore {
fn new() -> Self {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path().join("store.kv");
File::create(&path).unwrap();
TmpStore {
_tmp_dir: tmp_dir,
store: Store {
path,
inner: HashMap::new(),
},
}
}
}
}