use async_trait::async_trait;
use parking_lot::RwLock;
use serde::{Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "caching")]
use moka::future::Cache;
#[cfg(feature = "caching")]
use std::time::{Duration, Instant};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum DynamicError {
#[error("Backend error: {0}")]
Backend(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Key not found: {0}")]
KeyNotFound(String),
#[error("Invalid value type")]
InvalidType,
#[error("Cache error: {0}")]
Cache(String),
#[cfg(feature = "hot-reload")]
#[error("Hot reload error: {0}")]
HotReload(String),
}
pub type DynamicResult<T> = Result<T, DynamicError>;
#[async_trait]
pub trait DynamicBackend: Send + Sync {
async fn get(&self, key: &str) -> DynamicResult<Option<serde_json::Value>>;
async fn set(
&self,
key: &str,
value: &serde_json::Value,
ttl: Option<u64>,
) -> DynamicResult<()>;
async fn delete(&self, key: &str) -> DynamicResult<()>;
async fn exists(&self, key: &str) -> DynamicResult<bool>;
async fn keys(&self) -> DynamicResult<Vec<String>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubscriptionId(uuid::Uuid);
impl SubscriptionId {
fn new() -> Self {
Self(uuid::Uuid::now_v7())
}
}
type ObserverCallback = Box<dyn Fn(&str, Option<&serde_json::Value>) + Send + Sync>;
#[cfg(feature = "caching")]
#[derive(Clone)]
struct CachedValue {
value: serde_json::Value,
expires_at: Option<Instant>,
}
#[cfg(feature = "caching")]
impl CachedValue {
fn new(value: serde_json::Value, ttl: Option<Duration>) -> Self {
Self {
value,
expires_at: ttl.map(|d| Instant::now() + d),
}
}
fn is_expired(&self) -> bool {
self.expires_at
.map(|exp| Instant::now() > exp)
.unwrap_or(false)
}
}
pub struct DynamicSettings {
backend: Arc<dyn DynamicBackend>,
#[cfg(feature = "caching")]
cache: Option<Cache<String, CachedValue>>,
observers: Arc<RwLock<HashMap<SubscriptionId, ObserverCallback>>>,
#[cfg(feature = "hot-reload")]
hot_reload: Option<Arc<tokio::sync::Mutex<super::hot_reload::HotReloadManager>>>,
}
impl DynamicSettings {
pub fn new(backend: Arc<dyn DynamicBackend>) -> Self {
Self {
backend,
#[cfg(feature = "caching")]
cache: None,
observers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "hot-reload")]
hot_reload: None,
}
}
#[cfg(feature = "caching")]
pub fn enable_cache(&mut self, capacity: u64, _default_ttl: Option<Duration>) {
self.cache = Some(Cache::builder().max_capacity(capacity).build());
}
pub async fn get<T: DeserializeOwned>(&self, key: &str) -> DynamicResult<Option<T>> {
#[cfg(feature = "caching")]
if let Some(cache) = &self.cache
&& let Some(cached) = cache.get(key).await
{
if !cached.is_expired() {
return serde_json::from_value(cached.value.clone())
.map(Some)
.map_err(DynamicError::from);
} else {
cache.invalidate(key).await;
}
}
let value = self.backend.get(key).await?;
#[cfg(feature = "caching")]
if let (Some(cache), Some(val)) = (&self.cache, &value) {
cache
.insert(key.to_string(), CachedValue::new(val.clone(), None))
.await;
}
match value {
Some(v) => serde_json::from_value(v)
.map(Some)
.map_err(DynamicError::from),
None => Ok(None),
}
}
pub async fn set<T: Serialize>(
&self,
key: &str,
value: &T,
ttl: Option<u64>,
) -> DynamicResult<()> {
let json_value = serde_json::to_value(value)?;
self.backend.set(key, &json_value, ttl).await?;
#[cfg(feature = "caching")]
if let Some(cache) = &self.cache {
let cached_value = CachedValue::new(json_value.clone(), ttl.map(Duration::from_secs));
cache.insert(key.to_string(), cached_value).await;
}
self.notify_observers(key, Some(&json_value)).await;
Ok(())
}
pub async fn delete(&self, key: &str) -> DynamicResult<()> {
self.backend.delete(key).await?;
#[cfg(feature = "caching")]
if let Some(cache) = &self.cache {
cache.invalidate(key).await;
}
self.notify_observers(key, None).await;
Ok(())
}
pub async fn exists(&self, key: &str) -> DynamicResult<bool> {
self.backend.exists(key).await
}
pub async fn keys(&self) -> DynamicResult<Vec<String>> {
self.backend.keys().await
}
pub fn subscribe<F>(&self, callback: F) -> SubscriptionId
where
F: Fn(&str, Option<&serde_json::Value>) + Send + Sync + 'static,
{
let id = SubscriptionId::new();
self.observers.write().insert(id, Box::new(callback));
id
}
pub fn unsubscribe(&self, id: SubscriptionId) {
self.observers.write().remove(&id);
}
#[cfg(feature = "caching")]
pub async fn invalidate_cache(&self, key: &str) {
if let Some(cache) = &self.cache {
cache.invalidate(key).await;
}
}
#[cfg(feature = "caching")]
pub async fn clear_cache(&self) {
if let Some(cache) = &self.cache {
cache.invalidate_all();
}
}
#[cfg(feature = "hot-reload")]
pub async fn watch_file(&self, path: &std::path::Path) -> DynamicResult<()> {
if let Some(hot_reload) = &self.hot_reload {
hot_reload
.lock()
.await
.watch(path)
.await
.map_err(DynamicError::Backend)?;
}
Ok(())
}
#[cfg(feature = "hot-reload")]
pub async fn unwatch_file(&self, path: &std::path::Path) -> DynamicResult<()> {
if let Some(hot_reload) = &self.hot_reload {
hot_reload
.lock()
.await
.unwatch(path)
.await
.map_err(DynamicError::Backend)?;
}
Ok(())
}
#[cfg(feature = "hot-reload")]
pub async fn stop_watching(&self) -> DynamicResult<()> {
if let Some(hot_reload) = &self.hot_reload {
hot_reload
.lock()
.await
.stop()
.await
.map_err(DynamicError::Backend)?;
}
Ok(())
}
#[cfg(feature = "hot-reload")]
pub fn with_hot_reload(mut self) -> Self {
let manager = super::hot_reload::HotReloadManager::new();
self.hot_reload = Some(Arc::new(tokio::sync::Mutex::new(manager)));
self
}
async fn notify_observers(&self, key: &str, value: Option<&serde_json::Value>) {
let observers = self.observers.read();
for callback in observers.values() {
callback(key, value);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestBackend {
data: Arc<RwLock<HashMap<String, serde_json::Value>>>,
}
impl TestBackend {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl DynamicBackend for TestBackend {
async fn get(&self, key: &str) -> DynamicResult<Option<serde_json::Value>> {
Ok(self.data.read().get(key).cloned())
}
async fn set(
&self,
key: &str,
value: &serde_json::Value,
_ttl: Option<u64>,
) -> DynamicResult<()> {
self.data.write().insert(key.to_string(), value.clone());
Ok(())
}
async fn delete(&self, key: &str) -> DynamicResult<()> {
self.data.write().remove(key);
Ok(())
}
async fn exists(&self, key: &str) -> DynamicResult<bool> {
Ok(self.data.read().contains_key(key))
}
async fn keys(&self) -> DynamicResult<Vec<String>> {
Ok(self.data.read().keys().cloned().collect())
}
}
#[tokio::test]
async fn test_basic_crud() {
let backend = Arc::new(TestBackend::new());
let settings = DynamicSettings::new(backend);
settings.set("key", &"value", None).await.unwrap();
let value: String = settings.get("key").await.unwrap().unwrap();
assert_eq!(value, "value");
assert!(settings.exists("key").await.unwrap());
settings.delete("key").await.unwrap();
assert!(!settings.exists("key").await.unwrap());
}
#[tokio::test]
async fn test_type_safety() {
let backend = Arc::new(TestBackend::new());
let settings = DynamicSettings::new(backend);
settings.set("number", &42, None).await.unwrap();
settings.set("boolean", &true, None).await.unwrap();
settings.set("string", &"text", None).await.unwrap();
let number: i32 = settings.get("number").await.unwrap().unwrap();
let boolean: bool = settings.get("boolean").await.unwrap().unwrap();
let string: String = settings.get("string").await.unwrap().unwrap();
assert_eq!(number, 42);
assert!(boolean);
assert_eq!(string, "text");
}
#[tokio::test]
async fn test_observer_pattern() {
let backend = Arc::new(TestBackend::new());
let settings = DynamicSettings::new(backend);
let called = Arc::new(RwLock::new(false));
let called_clone = called.clone();
let _sub_id = settings.subscribe(move |key, _value| {
if key == "test" {
*called_clone.write() = true;
}
});
settings.set("test", &"value", None).await.unwrap();
assert!(*called.read());
}
#[tokio::test]
async fn test_unsubscribe() {
let backend = Arc::new(TestBackend::new());
let settings = DynamicSettings::new(backend);
let called = Arc::new(RwLock::new(0));
let called_clone = called.clone();
let sub_id = settings.subscribe(move |_, _| {
*called_clone.write() += 1;
});
settings.set("key1", &"value1", None).await.unwrap();
assert_eq!(*called.read(), 1);
settings.unsubscribe(sub_id);
settings.set("key2", &"value2", None).await.unwrap();
assert_eq!(*called.read(), 1); }
#[tokio::test]
async fn test_keys() {
let backend = Arc::new(TestBackend::new());
let settings = DynamicSettings::new(backend);
settings.set("key1", &"value1", None).await.unwrap();
settings.set("key2", &"value2", None).await.unwrap();
settings.set("key3", &"value3", None).await.unwrap();
let keys = settings.keys().await.unwrap();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
assert!(keys.contains(&"key3".to_string()));
}
#[cfg(feature = "caching")]
#[tokio::test]
async fn test_caching() {
let backend = Arc::new(TestBackend::new());
let mut settings = DynamicSettings::new(backend.clone());
settings.enable_cache(10, Some(Duration::from_secs(60)));
settings
.set("cached_key", &"cached_value", None)
.await
.unwrap();
let value1: String = settings.get("cached_key").await.unwrap().unwrap();
backend
.set("cached_key", &serde_json::json!("modified"), None)
.await
.unwrap();
let value2: String = settings.get("cached_key").await.unwrap().unwrap();
assert_eq!(value1, value2);
assert_eq!(value2, "cached_value");
settings.invalidate_cache("cached_key").await;
let value3: String = settings.get("cached_key").await.unwrap().unwrap();
assert_eq!(value3, "modified");
}
}