use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::errors::AppError;
use crate::repositories::{SystemSetting, SystemSettingsRepository};
use crate::services::EncryptionService;
const DEFAULT_CACHE_TTL_SECS: u64 = 60;
#[derive(Clone)]
struct CachedSetting {
value: String,
is_secret: bool,
}
pub struct SettingsService {
repo: Arc<dyn SystemSettingsRepository>,
encryption: Option<EncryptionService>,
cache: RwLock<HashMap<String, CachedSetting>>,
last_refresh: RwLock<Option<Instant>>,
cache_ttl: Duration,
}
impl SettingsService {
pub fn new(repo: Arc<dyn SystemSettingsRepository>) -> Self {
Self {
repo,
encryption: None,
cache: RwLock::new(HashMap::new()),
last_refresh: RwLock::new(None),
cache_ttl: Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
}
}
pub fn with_encryption(
repo: Arc<dyn SystemSettingsRepository>,
encryption: EncryptionService,
) -> Self {
Self {
repo,
encryption: Some(encryption),
cache: RwLock::new(HashMap::new()),
last_refresh: RwLock::new(None),
cache_ttl: Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
}
}
pub fn with_ttl(repo: Arc<dyn SystemSettingsRepository>, ttl_secs: u64) -> Self {
Self {
repo,
encryption: None,
cache: RwLock::new(HashMap::new()),
last_refresh: RwLock::new(None),
cache_ttl: Duration::from_secs(ttl_secs),
}
}
pub fn with_encryption_and_ttl(
repo: Arc<dyn SystemSettingsRepository>,
encryption: EncryptionService,
ttl_secs: u64,
) -> Self {
Self {
repo,
encryption: Some(encryption),
cache: RwLock::new(HashMap::new()),
last_refresh: RwLock::new(None),
cache_ttl: Duration::from_secs(ttl_secs),
}
}
async fn needs_refresh(&self) -> bool {
let last = self.last_refresh.read().await;
match *last {
None => true,
Some(instant) => instant.elapsed() > self.cache_ttl,
}
}
pub async fn refresh(&self) -> Result<(), AppError> {
let settings = self.repo.get_all().await?;
let mut cache = self.cache.write().await;
cache.clear();
for setting in settings {
cache.insert(
setting.key,
CachedSetting {
value: setting.value,
is_secret: setting.is_secret,
},
);
}
let mut last_refresh = self.last_refresh.write().await;
*last_refresh = Some(Instant::now());
Ok(())
}
async fn ensure_fresh(&self) -> Result<(), AppError> {
if self.needs_refresh().await {
self.refresh().await?;
}
Ok(())
}
pub async fn get(&self, key: &str) -> Result<Option<String>, AppError> {
self.ensure_fresh().await?;
let cache = self.cache.read().await;
Ok(cache.get(key).map(|s| s.value.clone()))
}
pub async fn get_secret(&self, key: &str) -> Result<Option<String>, AppError> {
self.ensure_fresh().await?;
let cache = self.cache.read().await;
match cache.get(key) {
None => Ok(None),
Some(cached) => {
if !cached.is_secret {
return Ok(Some(cached.value.clone()));
}
if cached.value.is_empty() {
return Ok(Some(String::new()));
}
let encryption = self.encryption.as_ref().ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"EncryptionService required to read secret '{}'",
key
))
})?;
let decrypted = encryption.decrypt(&cached.value)?;
Ok(Some(decrypted))
}
}
}
pub async fn set_secret(
&self,
key: &str,
plaintext: &str,
category: &str,
updated_by: Option<uuid::Uuid>,
) -> Result<(), AppError> {
let encryption = self.encryption.as_ref().ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"EncryptionService required to write secret '{}'",
key
))
})?;
let encrypted = encryption.encrypt(plaintext)?;
let version = format!("v{}", encryption.key_version());
let mut setting =
SystemSetting::new_secret(key.to_string(), encrypted, category.to_string(), &version);
setting.updated_by = updated_by;
self.repo.upsert(setting).await?;
let mut last_refresh = self.last_refresh.write().await;
*last_refresh = None;
Ok(())
}
pub async fn set(
&self,
key: &str,
value: &str,
category: &str,
updated_by: Option<uuid::Uuid>,
) -> Result<(), AppError> {
let mut setting =
SystemSetting::new(key.to_string(), value.to_string(), category.to_string());
setting.updated_by = updated_by;
self.repo.upsert(setting).await?;
let mut last_refresh = self.last_refresh.write().await;
*last_refresh = None;
Ok(())
}
pub async fn is_secret(&self, key: &str) -> Result<bool, AppError> {
self.ensure_fresh().await?;
let cache = self.cache.read().await;
Ok(cache.get(key).map(|s| s.is_secret).unwrap_or(false))
}
pub async fn get_u64(&self, key: &str) -> Result<Option<u64>, AppError> {
let value = self.get(key).await?;
Ok(value.and_then(|v| v.parse().ok()))
}
pub async fn get_u32(&self, key: &str) -> Result<Option<u32>, AppError> {
let value = self.get(key).await?;
Ok(value.and_then(|v| v.parse().ok()))
}
pub async fn get_u8(&self, key: &str) -> Result<Option<u8>, AppError> {
let value = self.get(key).await?;
Ok(value.and_then(|v| v.parse().ok()))
}
pub async fn get_bool(&self, key: &str) -> Result<Option<bool>, AppError> {
let value = self.get(key).await?;
Ok(value.and_then(|v| match v.to_lowercase().as_str() {
"true" | "1" | "yes" => Some(true),
"false" | "0" | "no" => Some(false),
_ => None,
}))
}
pub async fn require_u64(&self, key: &str) -> Result<u64, AppError> {
self.get_u64(key).await?.ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"Required setting '{}' not found or invalid",
key
))
})
}
pub async fn require_u32(&self, key: &str) -> Result<u32, AppError> {
self.get_u32(key).await?.ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"Required setting '{}' not found or invalid",
key
))
})
}
pub async fn require_u8(&self, key: &str) -> Result<u8, AppError> {
self.get_u8(key).await?.ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"Required setting '{}' not found or invalid",
key
))
})
}
pub async fn get_all_cached(&self) -> Result<HashMap<String, String>, AppError> {
self.ensure_fresh().await?;
let cache = self.cache.read().await;
Ok(cache
.iter()
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect())
}
pub async fn get_by_category_prefix(
&self,
prefix: &str,
) -> Result<HashMap<String, String>, AppError> {
self.ensure_fresh().await?;
let cache = self.cache.read().await;
let prefix_with_sep = if prefix.ends_with('.') || prefix.ends_with('_') {
prefix.to_string()
} else {
format!("{}.", prefix)
};
Ok(cache
.iter()
.filter(|(k, _)| k.starts_with(&prefix_with_sep) || k.starts_with(prefix))
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect())
}
pub fn get_cached_sync(&self, key: &str) -> Option<String> {
self.cache
.try_read()
.ok()
.and_then(|cache| cache.get(key).map(|s| s.value.clone()))
}
pub fn get_cached_u32_sync(&self, key: &str) -> Option<u32> {
self.get_cached_sync(key).and_then(|v| v.parse().ok())
}
pub fn get_cached_u64_sync(&self, key: &str) -> Option<u64> {
self.get_cached_sync(key).and_then(|v| v.parse().ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repositories::InMemorySystemSettingsRepository;
#[tokio::test]
async fn test_get_setting() {
let repo = Arc::new(InMemorySystemSettingsRepository::with_defaults());
let service = SettingsService::new(repo);
let value = service.get("privacy_period_secs").await.unwrap();
assert_eq!(value, Some("604800".to_string()));
}
#[tokio::test]
async fn test_get_u64() {
let repo = Arc::new(InMemorySystemSettingsRepository::with_defaults());
let service = SettingsService::new(repo);
let value = service.get_u64("privacy_period_secs").await.unwrap();
assert_eq!(value, Some(604800));
}
#[tokio::test]
async fn test_require_u64_missing() {
let repo = Arc::new(InMemorySystemSettingsRepository::new());
let service = SettingsService::new(repo);
let result = service.require_u64("nonexistent").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cache_refresh() {
let repo = Arc::new(InMemorySystemSettingsRepository::with_defaults());
let service = SettingsService::with_ttl(repo.clone(), 0);
let _ = service.get("privacy_period_secs").await.unwrap();
use crate::repositories::{SystemSetting, SystemSettingsRepository};
repo.upsert(SystemSetting::new(
"privacy_period_secs".to_string(),
"1000".to_string(),
"privacy".to_string(),
))
.await
.unwrap();
let value = service.get_u64("privacy_period_secs").await.unwrap();
assert_eq!(value, Some(1000));
}
#[tokio::test]
async fn test_get_all_cached() {
let repo = Arc::new(InMemorySystemSettingsRepository::with_defaults());
let service = SettingsService::new(repo);
let all = service.get_all_cached().await.unwrap();
assert_eq!(all.len(), 19); assert_eq!(all.get("privacy_period_secs"), Some(&"604800".to_string()));
}
}