use std::fmt::{Debug, Display};
use crossterm::style::Stylize;
use kv::{Bincode, Config, Store};
use miette::{Context, IntoDiagnostic};
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument};
pub type KVBucket<'a, KeyT, ValueT> = kv::Bucket<'a, KeyT, Bincode<ValueT>>;
mod default_settings {
use super::*;
#[derive(Debug, strum_macros::EnumString, Hash, PartialEq, Eq, Clone, Copy)]
pub enum Keys {
StoreFolderPath,
BucketName,
}
pub fn get(key: Keys) -> String {
match key {
Keys::StoreFolderPath => "kv_folder".to_string(),
Keys::BucketName => "my_bucket".to_string(),
}
}
}
#[instrument]
pub fn load_or_create_store(
maybe_db_folder_path: Option<&String>,
) -> miette::Result<Store> {
let db_folder_path = maybe_db_folder_path.cloned().unwrap_or_else(|| {
default_settings::get(default_settings::Keys::StoreFolderPath)
});
let cfg = Config::new(db_folder_path.clone());
let store =
Store::new(cfg)
.into_diagnostic()
.wrap_err(KvErrorCouldNot::CreateDbFolder {
db_folder_path: db_folder_path.clone(),
})?;
debug!(
"📑 {}",
format!(
"{}{}",
"load or create a store: ",
db_folder_path
)
);
Ok(store)
}
#[instrument(fields(store = ?store.path(), buckets = ?store.buckets()))]
pub fn load_or_create_bucket_from_store<
'a,
KeyT: for<'k> kv::Key<'k>,
ValueT: Serialize + for<'d> Deserialize<'d>,
>(
store: &Store,
maybe_bucket_name: Option<&String>,
) -> miette::Result<KVBucket<'a, KeyT, ValueT>> {
let bucket_name = maybe_bucket_name
.cloned()
.unwrap_or_else(|| default_settings::get(default_settings::Keys::BucketName));
let my_payload_bucket: KVBucket<KeyT, ValueT> = store
.bucket(Some(&bucket_name))
.into_diagnostic()
.wrap_err(KvErrorCouldNot::CreateBucketFromStore {
bucket_name: bucket_name.clone(),
})?;
debug!(
"📦 {}",
format!(
"{}{}",
"Load or create bucket from store, and instantiate: ",
bucket_name,
)
);
Ok(my_payload_bucket)
}
#[instrument(skip(bucket))]
pub fn insert_into_bucket<
'a,
KeyT: Debug + Display + for<'k> kv::Key<'k>,
ValueT: Debug + Serialize + for<'d> Deserialize<'d>,
>(
bucket: &'a KVBucket<'a, KeyT, ValueT>,
key: KeyT,
value: ValueT,
) -> miette::Result<()> {
let value_str = format!("{:?}", value).bold().cyan();
bucket
.set(&key, &Bincode(value))
.into_diagnostic()
.wrap_err(KvErrorCouldNot::SaveKeyValuePairToBucket)?;
debug!(
"🔽 {}",
format!(
"{}: {}: {}",
"Save key / value pair to bucket",
key.to_string(),
value_str
)
);
Ok(())
}
#[instrument(skip(bucket))]
pub fn get_from_bucket<
'a,
KeyT: Debug + Display + for<'k> kv::Key<'k>,
ValueT: Debug + Serialize + for<'d> Deserialize<'d>,
>(
bucket: &KVBucket<'a, KeyT, ValueT>,
key: KeyT,
) -> miette::Result<Option<ValueT>> {
let maybe_value: Option<Bincode<ValueT>> = bucket
.get(&key)
.into_diagnostic()
.wrap_err(KvErrorCouldNot::LoadKeyValuePairFromBucket)?;
let it = match maybe_value {
Some(Bincode(payload)) => Ok(Some(payload)),
_ => Ok(None),
};
debug!(
"🔼 {}",
format!(
"{}: {}: {}",
"Load key / value pair from bucket",
key.to_string(),
format!("{:?}", it)
)
);
it
}
#[instrument(skip(bucket))]
pub fn remove_from_bucket<
'a,
KeyT: Debug + Display + for<'k> kv::Key<'k>,
ValueT: Debug + Serialize + for<'d> Deserialize<'d>,
>(
bucket: &KVBucket<'a, KeyT, ValueT>,
key: KeyT,
) -> miette::Result<Option<ValueT>> {
let maybe_value: Option<Bincode<ValueT>> = bucket
.remove(&key)
.into_diagnostic()
.wrap_err(KvErrorCouldNot::RemoveKeyValuePairFromBucket)?;
let it = match maybe_value {
Some(Bincode(payload)) => Ok(Some(payload)),
_ => Ok(None),
};
debug!(
"❌ {}",
format!(
"{}: {}: {}",
"Delete key / value pair from bucket",
key.to_string(),
format!("{:?}", it)
)
);
it
}
#[instrument(skip(bucket))]
pub fn is_key_contained_in_bucket<
'a,
KeyT: Debug + Display + for<'k> kv::Key<'k>,
ValueT: Debug + Serialize + for<'d> Deserialize<'d>,
>(
bucket: &KVBucket<'a, KeyT, ValueT>,
key: KeyT,
) -> miette::Result<bool> {
let it = bucket
.contains(&key)
.into_diagnostic()
.wrap_err(KvErrorCouldNot::LoadKeyValuePairFromBucket)?;
debug!(
"🔼 {}",
format!(
"{}: {}: {}",
"Check if key is contained in bucket",
key.to_string(),
match it {
true => "true",
false => "false",
}
)
);
Ok(it)
}
pub fn iterate_bucket<
'a,
KeyT: Debug + Display + for<'k> kv::Key<'k>,
ValueT: Debug + Serialize + for<'d> Deserialize<'d>,
>(
bucket: &KVBucket<'a, KeyT, ValueT>,
mut fn_to_apply: impl FnMut(KeyT, ValueT),
) {
for item in bucket.iter().flatten() {
let Ok(key) = item.key::<KeyT>().into_diagnostic() else {
continue;
};
let Ok(encoded_value) = item.value::<Bincode<ValueT>>().into_diagnostic() else {
continue;
};
let Bincode(value) = encoded_value;
fn_to_apply(key, value);
}
}
pub mod kv_error {
#[allow(dead_code)]
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
pub enum KvErrorCouldNot {
#[error("📑 Could not create db folder: '{db_folder_path}' on disk")]
CreateDbFolder { db_folder_path: String },
#[error("📦 Could not create bucket from store: '{bucket_name}'")]
CreateBucketFromStore { bucket_name: String },
#[error("🔽 Could not save key/value pair to bucket")]
SaveKeyValuePairToBucket,
#[error("🔼 Could not load key/value pair from bucket")]
LoadKeyValuePairFromBucket,
#[error("❌ Could not remove key/value pair from bucket")]
RemoveKeyValuePairFromBucket,
#[error("🔍 Could not get item from iterator from bucket")]
GetItemFromIteratorFromBucket,
#[error("🔍 Could not get key from item from iterator from bucket")]
GetKeyFromItemFromIteratorFromBucket,
#[error("🔍 Could not get value from item from iterator from bucket")]
GetValueFromItemFromIteratorFromBucket,
#[error("⚡ Could not execute transaction")]
ExecuteTransaction,
}
}
use kv_error::*;
#[cfg(test)]
mod kv_tests {
use std::{collections::HashMap,
path::{Path, PathBuf}};
use serial_test::serial;
use tempfile::tempdir;
use tracing::{instrument, Level};
use super::*;
fn check_folder_exists(path: &Path) -> bool { path.exists() && path.is_dir() }
fn join_path_with_str(path: &Path, str: &str) -> PathBuf { path.join(str) }
fn setup_tracing() {
let _ = tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.compact()
.pretty()
.with_ansi(true)
.with_line_number(false)
.with_file(false)
.without_time()
.try_init();
}
fn get_path(dir: &tempfile::TempDir, folder_name: &str) -> PathBuf {
join_path_with_str(dir.path(), folder_name)
}
fn create_temp_folder() -> tempfile::TempDir {
tempdir().expect("Failed to create temp dir")
}
#[instrument]
fn perform_db_operations() -> miette::Result<()> {
let bucket_name = "bucket".to_string();
let dir = create_temp_folder();
let path_buf = get_path(&dir, "db_folder");
setup_tracing();
let path_str = path_buf.as_path().to_string_lossy().to_string();
let store = load_or_create_store(Some(&path_str))?;
assert!(check_folder_exists(path_buf.as_path()));
let bucket = load_or_create_bucket_from_store(&store, Some(&bucket_name))?;
assert!(!(is_key_contained_in_bucket(&bucket, "foo".to_string())?));
let mut count = 0;
for _ in bucket.iter() {
count += 1;
}
assert_eq!(count, 0);
insert_into_bucket(&bucket, "foo".to_string(), "bar".to_string())?;
assert!(is_key_contained_in_bucket(&bucket, "foo".to_string())?);
assert_eq!(
get_from_bucket(&bucket, "foo".to_string())?,
Some("bar".to_string())
);
let mut map = HashMap::new();
for result_item in bucket.iter() {
let item = result_item
.into_diagnostic()
.wrap_err(KvErrorCouldNot::GetItemFromIteratorFromBucket)?;
let key = item
.key::<String>()
.into_diagnostic()
.wrap_err(KvErrorCouldNot::GetKeyFromItemFromIteratorFromBucket)?;
let Bincode(payload) = item
.value::<Bincode<String>>()
.into_diagnostic()
.wrap_err(KvErrorCouldNot::GetValueFromItemFromIteratorFromBucket)?;
map.insert(key.to_string(), payload);
}
assert_eq!(map.get("foo"), Some(&"bar".to_string()));
assert_eq!(
remove_from_bucket(&bucket, "foo".to_string())?,
Some("bar".to_string())
);
assert!(!(is_key_contained_in_bucket(&bucket, "foo".to_string())?));
assert_eq!(remove_from_bucket(&bucket, "foo".to_string())?, None);
Ok(())
}
#[instrument]
fn perform_db_operations_error_conditions() -> miette::Result<()> {
let bucket_name = "bucket".to_string();
let dir = create_temp_folder();
let path_buf = get_path(&dir, "db_folder");
setup_tracing();
let path_str = path_buf.as_path().to_string_lossy().to_string();
let store = load_or_create_store(Some(&path_str))?;
assert!(check_folder_exists(path_buf.as_path()));
let bucket = load_or_create_bucket_from_store(&store, Some(&bucket_name))?;
insert_into_bucket(&bucket, "foo".to_string(), "bar".to_string())?;
store.drop_bucket(bucket_name).into_diagnostic()?;
let result = insert_into_bucket(&bucket, "foo".to_string(), "bar".to_string());
match result {
Err(e) => {
assert_eq!(e.to_string(), "🔽 Could not save key/value pair to bucket");
}
_ => {
panic!("Expected an error, but got None");
}
}
let result = get_from_bucket(&bucket, "foo".to_string());
match result {
Err(e) => {
let mut iter = e.chain();
assert_eq!(
iter.next().map(|it| it.to_string()).unwrap(),
"🔼 Could not load key/value pair from bucket"
);
let second = iter.next().map(|it| it.to_string()).unwrap();
assert!(second.contains("Error in Sled: Collection"));
assert!(second.contains("does not exist"));
let third = iter.next().map(|it| it.to_string()).unwrap();
assert!(third.contains("Collection"));
assert!(third.contains("does not exist"));
}
_ => {
panic!("Expected an error, but got None");
}
}
let result = remove_from_bucket(&bucket, "foo".to_string());
match result {
Err(e) => {
assert_eq!(
e.to_string(),
"❌ Could not remove key/value pair from bucket"
);
}
_ => {
panic!("Expected an error, but got None");
}
}
let result = is_key_contained_in_bucket(&bucket, "foo".to_string());
match result {
Err(e) => {
assert_eq!(
e.to_string(),
"🔼 Could not load key/value pair from bucket"
);
}
_ => {
panic!("Expected an error, but got None");
}
}
let result = bucket.iter().next();
match result {
Some(Err(e)) => {
assert!(e.to_string().contains("Error in Sled"));
assert!(e.to_string().contains("does not exist"));
}
_ => {
panic!("Expected an error, but got None");
}
}
Ok(())
}
#[serial]
#[test]
fn test_kv_operations() {
let result = perform_db_operations();
assert!(result.is_ok());
}
#[serial]
#[test]
fn test_kv_errors() {
let result = perform_db_operations_error_conditions();
assert!(result.is_ok());
}
}