pub(crate) mod file_handling;
use crate::store::mem::MemStore;
use crate::store::persistence::file_handling::*;
use std::io;
use std::path::{Path, PathBuf};
pub struct PersistentStore {
pub path: PathBuf,
pub filename: PathBuf,
pub store: MemStore,
pub write_on_update: bool,
}
impl PersistentStore {
pub async fn new<P: AsRef<Path>>(storage_loc: P) -> io::Result<Self> {
let folder = storage_loc
.as_ref()
.parent()
.expect("unable to get parent directory");
let filename = storage_loc
.as_ref()
.file_name()
.expect("unable to get filename");
let path = create_directory(folder).await?;
Ok(Self {
path,
filename: filename.into(),
store: MemStore::new(),
write_on_update: false,
})
}
pub async fn from_existing<P: AsRef<Path>>(storage_loc: P) -> io::Result<Self> {
let mut store = Self::new(storage_loc).await?;
store.load().await.expect("unable to load store");
Ok(store)
}
pub async fn from_store<P: AsRef<Path>>(
storage_loc: P,
memstore: MemStore,
) -> io::Result<Self> {
let mut persistent_store = Self::new(storage_loc).await?;
persistent_store.store = memstore;
Ok(persistent_store)
}
pub async fn insert_string(&mut self, key: &str, value: &str) -> io::Result<()> {
let result = self.store.insert_string(key, value);
if self.write_on_update {
self.write().await?;
}
result
}
pub fn get_string(&self, key: &str) -> io::Result<String> {
self.store.get_string(key)
}
pub async fn remove_string(&mut self, key: &str) -> io::Result<String> {
let result = self.store.remove_string(key)?;
if self.write_on_update {
self.write().await?;
}
Ok(result)
}
pub async fn clear_strings(&mut self) -> io::Result<()> {
self.store.clear_strings()?;
if self.write_on_update {
self.write().await?;
}
Ok(())
}
pub async fn incr(&mut self, key: impl AsRef<str>) -> io::Result<isize> {
let result = self.store.incr(key);
if self.write_on_update {
self.write().await?;
}
result
}
pub async fn decr(&mut self, key: impl AsRef<str>) -> io::Result<isize> {
let result = self.store.decr(key);
if self.write_on_update {
self.write().await?;
}
result
}
pub fn get_string_store_ref(&self) -> &std::collections::HashMap<String, String> {
self.store.get_string_store_ref()
}
pub fn set_write_on_update(&mut self, set: bool) {
self.write_on_update = set;
}
async fn load(&mut self) -> io::Result<()> {
let path = self.path.join(&self.filename);
let contents = load_store(&path).await?;
if contents.is_empty() {
return Ok(());
}
let vault: MemStore = serde_json::from_str(&contents)?;
self.store.strings = vault.strings;
Ok(())
}
pub async fn write(&self) -> io::Result<()> {
let path = self.path.join(&self.filename);
write_store(&path, &self.store).await?;
Ok(())
}
}
#[cfg(test)]
mod persistent_store {
use super::*;
use std::path::PathBuf;
use tempdir::TempDir;
fn create_test_directory() -> io::Result<PathBuf> {
let td = TempDir::new("teststore")?;
Ok(td.path().to_path_buf())
}
#[tokio::test]
async fn empty_store() -> io::Result<()> {
let td = create_test_directory()?;
let path = td.join("rubinstore.json");
let ps = PersistentStore::new(&path).await?;
assert_eq!(ps.store.strings.len(), 0);
assert_eq!(ps.path, td);
assert_eq!(ps.filename, PathBuf::from("rubinstore.json"));
assert!(ps.path.exists());
Ok(())
}
#[tokio::test]
async fn write_out_store() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let ps = PersistentStore::new(&rubinstore).await?;
ps.write().await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn setting_write_on_update() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&rubinstore).await?;
assert!(!ps.write_on_update);
ps.insert_string("key1", "value1").await?;
assert!(!rubinstore.exists());
ps.set_write_on_update(true);
ps.insert_string("key2", "value2").await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn add_and_write() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&rubinstore).await?;
ps.insert_string("key1", "value1").await?;
assert_eq!(ps.store.strings.len(), 1);
ps.write().await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn add_a_load_of_strings() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&rubinstore).await?;
for i in 0..100_000 {
let key = format!("key-{}", i);
let value = format!("value-{}", i);
ps.insert_string(&key, &value).await?;
}
assert!(ps.store.strings.len() == 100_000);
ps.write().await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn add_string_and_increment_counter() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&rubinstore).await?;
for i in 0..100_000 {
let key = format!("key-{}", i);
let value = format!("value-{}", i);
ps.insert_string(&key, &value).await?;
}
for _ in 0..10_000 {
ps.incr("view-counter").await?;
}
assert_eq!(ps.store.counters.retrieve("view-counter").unwrap(), 10_000);
assert!(ps.store.strings.len() == 100_000);
ps.write().await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn add_string_and_decrement_counter() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&rubinstore).await?;
for i in 0..100_000 {
let key = format!("key-{}", i);
let value = format!("value-{}", i);
ps.insert_string(&key, &value).await?;
}
for _ in 0..10_000 {
ps.decr("view-counter").await?;
}
assert_eq!(ps.store.counters.retrieve("view-counter").unwrap(), -10_000);
assert!(ps.store.strings.len() == 100_000);
ps.write().await?;
assert!(rubinstore.exists());
Ok(())
}
#[tokio::test]
async fn load_existing_store() -> io::Result<()> {
let td = create_test_directory()?;
let path = td.join("rubinstore.json");
let mut ps = PersistentStore::new(&path).await?;
ps.set_write_on_update(true);
ps.insert_string("key1", "value1").await?;
drop(ps);
let ps = PersistentStore::from_existing(path).await?;
assert_eq!(ps.store.strings.len(), 1);
let result = ps.get_string("key1")?;
assert_eq!(result, "value1");
Ok(())
}
#[tokio::test]
async fn load_from_memstore() -> io::Result<()> {
let td = create_test_directory()?;
let rubinstore = td.join("rubinstore.json");
let mut ms = MemStore::new();
for i in 0..10 {
let key = format!("key-{}", i);
let value = format!("value-{}", i);
let _ = ms.insert_string(&key, &value);
}
let mut ps = PersistentStore::from_store(&rubinstore, ms).await?;
ps.set_write_on_update(true);
assert_eq!(ps.store.strings.len(), 10);
let _ = ps.insert_string("key-11", "value-11").await?;
assert_eq!(ps.store.strings.len(), 11);
assert!(rubinstore.exists());
Ok(())
}
}