feat(kvs): implement basic persistent key-value store
This commit is contained in:
		
							parent
							
								
									49714b5b53
								
							
						
					
					
						commit
						cfe44fd861
					
				
							
								
								
									
										16
									
								
								espanso-kvs/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								espanso-kvs/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "espanso-kvs"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
 | 
			
		||||
edition = "2018"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
log = "0.4.14"
 | 
			
		||||
anyhow = "1.0.38"
 | 
			
		||||
thiserror = "1.0.23"
 | 
			
		||||
serde = { version = "1.0.123", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.62"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
tempdir = "0.3.7"
 | 
			
		||||
							
								
								
									
										104
									
								
								espanso-kvs/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								espanso-kvs/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,104 @@
 | 
			
		|||
/*
 | 
			
		||||
 * This file is part of espanso.
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2019-2021 Federico Terzi
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use serde::{de::DeserializeOwned, Serialize};
 | 
			
		||||
 | 
			
		||||
mod persistent;
 | 
			
		||||
 | 
			
		||||
pub trait KVS: Send + Sync {
 | 
			
		||||
  fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>>;
 | 
			
		||||
  fn set<T: Serialize>(&self, key: &str, value: T) -> Result<()>;
 | 
			
		||||
  fn delete(&self, key: &str) -> Result<()>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_persistent(base_dir: &Path) -> Result<impl KVS> {
 | 
			
		||||
  persistent::PersistentJsonKVS::new(base_dir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
  use super::*;
 | 
			
		||||
 | 
			
		||||
  use tempdir::TempDir;
 | 
			
		||||
 | 
			
		||||
  pub fn use_test_directory(callback: impl FnOnce(&Path)) {
 | 
			
		||||
    let dir = TempDir::new("kvstempconfig").unwrap();
 | 
			
		||||
 | 
			
		||||
    callback(
 | 
			
		||||
      &dir.path(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_base_types() {
 | 
			
		||||
    use_test_directory(|base_dir| {
 | 
			
		||||
      let kvs = get_persistent(base_dir).unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<String>("my_key").unwrap().is_none(), true);
 | 
			
		||||
      assert_eq!(kvs.get::<bool>("another_key").unwrap().is_none(), true);
 | 
			
		||||
 | 
			
		||||
      kvs.set("my_key", "test".to_string()).unwrap();
 | 
			
		||||
      kvs.set("another_key", false).unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<String>("my_key").unwrap().unwrap(), "test");
 | 
			
		||||
      assert_eq!(kvs.get::<bool>("another_key").unwrap().unwrap(), false);
 | 
			
		||||
 | 
			
		||||
      kvs.delete("my_key").unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<String>("my_key").unwrap().is_none(), true);
 | 
			
		||||
      assert_eq!(kvs.get::<bool>("another_key").unwrap().unwrap(), false);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_type_mismatch() {
 | 
			
		||||
    use_test_directory(|base_dir| {
 | 
			
		||||
      let kvs = get_persistent(base_dir).unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<String>("my_key").unwrap().is_none(), true);
 | 
			
		||||
 | 
			
		||||
      kvs.set("my_key", "test".to_string()).unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<bool>("my_key").is_err(), true);
 | 
			
		||||
      assert_eq!(kvs.get::<String>("my_key").is_ok(), true);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_delete_non_existing_key() {
 | 
			
		||||
    use_test_directory(|base_dir| {
 | 
			
		||||
      let kvs = get_persistent(base_dir).unwrap();
 | 
			
		||||
 | 
			
		||||
      kvs.delete("my_key").unwrap();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_invalid_key_name() {
 | 
			
		||||
    use_test_directory(|base_dir| {
 | 
			
		||||
      let kvs = get_persistent(base_dir).unwrap();
 | 
			
		||||
 | 
			
		||||
      assert_eq!(kvs.get::<String>("invalid key name").is_err(), true);
 | 
			
		||||
      assert_eq!(kvs.get::<String>("").is_err(), true);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								espanso-kvs/src/persistent.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								espanso-kvs/src/persistent.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,153 @@
 | 
			
		|||
/*
 | 
			
		||||
 * This file is part of espanso.
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2019-2021 Federico Terzi
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use std::{
 | 
			
		||||
  collections::HashMap,
 | 
			
		||||
  path::{Path, PathBuf},
 | 
			
		||||
  sync::{Arc, Mutex},
 | 
			
		||||
};
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
use super::KVS;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_KVS_DIR_NAME: &str = "kvs";
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct PersistentJsonKVS {
 | 
			
		||||
  kvs_dir: PathBuf,
 | 
			
		||||
  store: Arc<Mutex<HashMap<String, Value>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PersistentJsonKVS {
 | 
			
		||||
  pub fn new(base_dir: &Path) -> Result<Self> {
 | 
			
		||||
    let kvs_dir = base_dir.join(DEFAULT_KVS_DIR_NAME);
 | 
			
		||||
    if !kvs_dir.is_dir() {
 | 
			
		||||
      std::fs::create_dir_all(&kvs_dir)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(Self {
 | 
			
		||||
      kvs_dir,
 | 
			
		||||
      store: Arc::new(Mutex::new(HashMap::new())),
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl KVS for PersistentJsonKVS {
 | 
			
		||||
  fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
 | 
			
		||||
    if !is_valid_key_name(key) {
 | 
			
		||||
      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut lock = self.store.lock().expect("unable to obtain KVS read lock");
 | 
			
		||||
 | 
			
		||||
    if let Some(cached_value) = lock.get(key) {
 | 
			
		||||
      let converted_value = serde_json::from_value(cached_value.clone())?;
 | 
			
		||||
      return Ok(Some(converted_value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Not found in the cache, read from the file
 | 
			
		||||
    let target_file = self.kvs_dir.join(key);
 | 
			
		||||
    if target_file.is_file() {
 | 
			
		||||
      let content = std::fs::read_to_string(&target_file)?;
 | 
			
		||||
      let deserialized_value: Value = serde_json::from_str(&content)?;
 | 
			
		||||
      let converted_value = serde_json::from_value(deserialized_value.clone())?;
 | 
			
		||||
 | 
			
		||||
      lock.insert(key.to_string(), deserialized_value);
 | 
			
		||||
 | 
			
		||||
      return Ok(Some(converted_value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(None)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fn set<T: serde::Serialize>(&self, key: &str, value: T) -> Result<()> {
 | 
			
		||||
    if !is_valid_key_name(key) {
 | 
			
		||||
      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut lock = self.store.lock().expect("unable to obtain KVS write lock");
 | 
			
		||||
 | 
			
		||||
    let serialized_value = serde_json::to_value(value)?;
 | 
			
		||||
    let serialized_string = serde_json::to_string(&serialized_value)?;
 | 
			
		||||
 | 
			
		||||
    lock.insert(key.to_string(), serialized_value);
 | 
			
		||||
 | 
			
		||||
    let target_file = self.kvs_dir.join(key);
 | 
			
		||||
    std::fs::write(target_file, serialized_string)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fn delete(&self, key: &str) -> Result<()> {
 | 
			
		||||
    if !is_valid_key_name(key) {
 | 
			
		||||
      return Err(PersistentJsonKVSError::InvalidKey(key.to_string()).into());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut lock = self.store.lock().expect("unable to obtain KVS delete lock");
 | 
			
		||||
 | 
			
		||||
    lock.remove(key);
 | 
			
		||||
 | 
			
		||||
    let target_file = self.kvs_dir.join(key);
 | 
			
		||||
    if target_file.is_file() {
 | 
			
		||||
      std::fs::remove_file(target_file)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn is_valid_key_name(key: &str) -> bool {
 | 
			
		||||
  if key.len() == 0 || key.len() > 200 {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug)]
 | 
			
		||||
pub enum PersistentJsonKVSError {
 | 
			
		||||
  #[error("The provided key `{0}` is is invalid. Keys must only be composed of ascii letters, numbers and underscores.")]
 | 
			
		||||
  InvalidKey(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
  use super::*;
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_valid_key_names() {
 | 
			
		||||
    assert!(is_valid_key_name("key"));
 | 
			
		||||
    assert!(is_valid_key_name("key_name"));
 | 
			
		||||
    assert!(is_valid_key_name("Another_long_key_name_2"));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #[test]
 | 
			
		||||
  fn test_invalid_key_names() {
 | 
			
		||||
    assert!(!is_valid_key_name(""));
 | 
			
		||||
    assert!(!is_valid_key_name("with space"));
 | 
			
		||||
    assert!(!is_valid_key_name("with/special"));
 | 
			
		||||
    assert!(!is_valid_key_name("with\\special"));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user