use std::{fmt::Debug, sync::Arc};
use dashmap::DashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchType {
Exact,
Prefix,
Regex,
Suffix,
}
pub trait KvBackend: Send + Sync + Debug {
fn get(&self, key: &str) -> Option<Arc<str>>;
fn set(&self, key: &str, value: Arc<str>) -> bool;
fn delete(&self, key: &str) -> bool;
fn entries(&self) -> Vec<(Arc<str>, Arc<str>)>;
#[expect(clippy::type_complexity, reason = "trait return type")]
fn lookup(&self, pattern: &str, match_type: MatchType) -> Result<Option<(Arc<str>, Arc<str>)>, String>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KvLookup {
Value(Arc<str>),
KeyNotFound,
StoreNotFound,
}
impl KvLookup {
pub fn is_value(&self) -> bool {
matches!(self, Self::Value(_))
}
pub fn is_key_not_found(&self) -> bool {
matches!(self, Self::KeyNotFound)
}
pub fn is_store_not_found(&self) -> bool {
matches!(self, Self::StoreNotFound)
}
pub fn into_value(self) -> Option<Arc<str>> {
match self {
Self::Value(v) => Some(v),
Self::KeyNotFound | Self::StoreNotFound => None,
}
}
}
#[derive(Debug, Clone)]
pub struct KvStoreRegistry {
#[expect(clippy::type_complexity, reason = "single-field struct wrapping DashMap")]
stores: Arc<DashMap<Arc<str>, Arc<dyn KvBackend>>>,
}
impl KvStoreRegistry {
pub fn new() -> Self {
Self {
stores: Arc::new(DashMap::new()),
}
}
pub fn get(&self, name: &str) -> Option<Arc<dyn KvBackend>> {
self.stores.get(name).map(|r| Arc::clone(r.value()))
}
pub fn get_or_create(&self, name: &str) -> Arc<dyn KvBackend> {
if let Some(existing) = self.stores.get(name) {
return Arc::clone(existing.value());
}
let backend: Arc<dyn KvBackend> = Arc::new(memory::InMemoryKvBackend::new());
let entry = self.stores.entry(Arc::from(name)).or_insert_with(|| {
tracing::info!(store = name, "kv store created");
Arc::clone(&backend)
});
Arc::clone(entry.value())
}
pub fn remove(&self, name: &str) -> bool {
let removed = self.stores.remove(name).is_some();
if removed {
tracing::info!(store = name, "kv store removed");
}
removed
}
pub fn lookup(&self, store: &str, key: &str) -> KvLookup {
let Some(backend) = self.get(store) else {
return KvLookup::StoreNotFound;
};
match backend.get(key) {
Some(v) => KvLookup::Value(v),
None => KvLookup::KeyNotFound,
}
}
pub fn store_names(&self) -> Vec<Arc<str>> {
self.stores.iter().map(|e| Arc::clone(e.key())).collect()
}
pub fn len(&self) -> usize {
self.stores.len()
}
pub fn is_empty(&self) -> bool {
self.stores.is_empty()
}
}
impl Default for KvStoreRegistry {
fn default() -> Self {
Self::new()
}
}
pub mod memory;
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, reason = "tests")]
mod tests {
use super::*;
#[test]
fn match_type_variants() {
assert_eq!(MatchType::Exact, MatchType::Exact);
assert_ne!(MatchType::Prefix, MatchType::Suffix);
}
#[test]
fn new_registry_is_empty() {
let registry = KvStoreRegistry::new();
assert!(registry.is_empty(), "new registry should have no stores");
assert_eq!(registry.len(), 0, "new registry length should be 0");
}
#[test]
fn default_registry_is_empty() {
let registry = KvStoreRegistry::default();
assert!(registry.is_empty(), "default registry should have no stores");
}
#[test]
fn get_returns_none_for_missing_store() {
let registry = KvStoreRegistry::new();
assert!(
registry.get("nonexistent").is_none(),
"get should return None for missing store"
);
}
#[test]
fn get_or_create_creates_empty_store() {
let registry = KvStoreRegistry::new();
let store = registry.get_or_create("test");
assert!(store.is_empty(), "newly created store should be empty");
assert_eq!(registry.len(), 1, "registry should have 1 store");
}
#[test]
fn get_or_create_returns_same_store() {
let registry = KvStoreRegistry::new();
let s1 = registry.get_or_create("test");
s1.set("key", Arc::from("value"));
let s2 = registry.get_or_create("test");
assert_eq!(
s2.get("key").as_deref(),
Some("value"),
"second get_or_create should return the same store"
);
assert_eq!(registry.len(), 1, "registry should still have 1 store");
}
#[test]
fn get_returns_existing_store() {
let registry = KvStoreRegistry::new();
registry.get_or_create("test").set("k", Arc::from("v"));
let store = registry.get("test").unwrap();
assert_eq!(store.get("k").as_deref(), Some("v"), "get should return existing store");
}
#[test]
fn remove_existing_store() {
let registry = KvStoreRegistry::new();
registry.get_or_create("temp");
assert!(registry.remove("temp"), "remove should return true for existing store");
assert!(registry.is_empty(), "registry should be empty after removal");
}
#[test]
fn remove_missing_store() {
let registry = KvStoreRegistry::new();
assert!(
!registry.remove("missing"),
"remove should return false for missing store"
);
}
#[test]
fn lookup_store_not_found() {
let registry = KvStoreRegistry::new();
assert_eq!(
registry.lookup("missing", "k"),
KvLookup::StoreNotFound,
"lookup on missing store should return StoreNotFound"
);
}
#[test]
fn lookup_key_not_found() {
let registry = KvStoreRegistry::new();
registry.get_or_create("test");
assert_eq!(
registry.lookup("test", "missing"),
KvLookup::KeyNotFound,
"lookup on missing key should return KeyNotFound"
);
}
#[test]
fn lookup_value_found() {
let registry = KvStoreRegistry::new();
registry.get_or_create("test").set("k", Arc::from("v"));
assert_eq!(
registry.lookup("test", "k"),
KvLookup::Value(Arc::from("v")),
"lookup should return the value"
);
}
#[test]
fn store_names_lists_all() {
let registry = KvStoreRegistry::new();
registry.get_or_create("alpha");
registry.get_or_create("beta");
let mut names = registry.store_names();
names.sort();
assert_eq!(names.len(), 2, "should have 2 store names");
assert_eq!(names[0].as_ref(), "alpha");
assert_eq!(names[1].as_ref(), "beta");
}
#[test]
fn clone_shares_state() {
let r1 = KvStoreRegistry::new();
r1.get_or_create("shared").set("k", Arc::from("v"));
let r2 = r1.clone();
assert_eq!(
r2.lookup("shared", "k"),
KvLookup::Value(Arc::from("v")),
"cloned registry should share state"
);
}
#[test]
fn kv_lookup_is_value() {
let v = KvLookup::Value(Arc::from("x"));
assert!(v.is_value());
assert!(!v.is_key_not_found());
assert!(!v.is_store_not_found());
}
#[test]
fn kv_lookup_is_key_not_found() {
let v = KvLookup::KeyNotFound;
assert!(!v.is_value());
assert!(v.is_key_not_found());
assert!(!v.is_store_not_found());
}
#[test]
fn kv_lookup_is_store_not_found() {
let v = KvLookup::StoreNotFound;
assert!(!v.is_value());
assert!(!v.is_key_not_found());
assert!(v.is_store_not_found());
}
#[test]
fn concurrent_get_or_create_same_name() {
let registry = Arc::new(KvStoreRegistry::new());
let handles: Vec<_> = (0..10)
.map(|_| {
let reg = Arc::clone(®istry);
std::thread::spawn(move || reg.get_or_create("race"))
})
.collect();
let stores: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(
registry.len(),
1,
"only one store should exist after concurrent creation"
);
stores[0].set("k", Arc::from("v"));
assert_eq!(
stores[9].get("k").as_deref(),
Some("v"),
"all handles should reference the same underlying store"
);
}
#[test]
fn kv_lookup_into_value() {
assert_eq!(
KvLookup::Value(Arc::from("x")).into_value(),
Some(Arc::from("x")),
"Value should unwrap"
);
assert_eq!(KvLookup::KeyNotFound.into_value(), None, "KeyNotFound should be None");
assert_eq!(
KvLookup::StoreNotFound.into_value(),
None,
"StoreNotFound should be None"
);
}
}