use async_trait::async_trait;
use dashmap::DashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use std::{env, path::PathBuf};
#[async_trait::async_trait]
#[typetag::serde]
pub trait ConfigManagerType: Send + Sync {
async fn as_vec(&self) -> Vec<(String, String)> {
let mut config = vec![];
for key in self.keys().await {
if let Some(value) = self.get(&key).await {
config.push((key, value));
}
}
config
}
async fn keys(&self) -> Vec<String>;
async fn get(&self, key: &str) -> Option<String>;
async fn del(&self, key: &str);
async fn set(&self, key: &str, value: &str) -> Result<(), String>;
fn clone_box(&self) -> Box<dyn ConfigManagerType>;
fn debug_box(&self) -> String;
}
#[derive(Serialize, Deserialize )]
pub struct ConfigManager(pub Box<dyn ConfigManagerType>);
impl ConfigManager {
pub fn into_inner(self) -> Box<dyn ConfigManagerType> {
self.0
}
}
impl Clone for ConfigManager {
fn clone(&self) -> Self {
ConfigManager(self.0.clone_box())
}
}
impl std::fmt::Debug for ConfigManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.debug_box())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EnvConfigManager;
impl EnvConfigManager {
pub fn new(env_file: PathBuf) -> Box<Self> {
if env_file.exists() {
dotenvy::from_path(env_file.clone()).ok();
info!("Loaded .env from {}", env_file.display());
} else {
error!("could not load .env from {}",env_file.display())
}
Box::new(Self)
}
}
#[typetag::serde]
#[async_trait]
impl ConfigManagerType for EnvConfigManager {
async fn keys(&self) -> Vec<String> {
env::vars().map(|(k, _)| k).collect()
}
async fn get(&self, key: &str) -> Option<String> {
env::var(key).ok()
}
async fn set(&self, key: &str, value: &str) -> Result<(), String> {
unsafe { env::set_var(key, value); };
Ok(())
}
async fn del(&self, key: &str,) {
unsafe { env::remove_var(key ); };
}
fn clone_box(&self) -> Box<dyn ConfigManagerType> {
Box::new(self.clone())
}
fn debug_box(&self) -> String {
"EnvConfigManager".to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MapConfigManager {
#[schemars(with = "std::collections::HashMap<String, String>")]
map: DashMap<String, String>,
}
impl MapConfigManager {
pub fn new() -> Box<Self> {
Box::new(Self{map:DashMap::new()})
}
}
#[typetag::serde]
#[async_trait]
impl ConfigManagerType for MapConfigManager {
async fn keys(&self) -> Vec<String> {
self.map.iter().map(|entry| entry.key().clone()).collect()
}
async fn get(&self, key: &str) -> Option<String> {
self.map.get(key).map(|v| v.clone())
}
async fn set(&self, key: &str, value: &str) -> Result<(), String> {
self.map.insert(key.to_string(), value.to_string());
Ok(())
}
async fn del(&self, key: &str,) {
self.map.remove(key);
}
fn clone_box(&self) -> Box<dyn ConfigManagerType> {
Box::new(self.clone())
}
fn debug_box(&self) -> String {
format!("MapConfigManager({} entries)", self.map.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs::write;
#[tokio::test]
async fn test_map_config_manager_basic() {
let mgr = MapConfigManager::new();
mgr.set("foo", "bar").await.unwrap();
assert_eq!(mgr.get("foo").await, Some("bar".to_string()));
mgr.set("foo", "baz").await.unwrap();
assert_eq!(mgr.get("foo").await, Some("baz".to_string()));
let keys = mgr.keys().await;
assert_eq!(keys, vec!["foo".to_string()]);
mgr.del("foo").await;
assert_eq!(mgr.get("foo").await, None);
}
#[tokio::test]
async fn test_map_config_manager_as_vec() {
let mgr = MapConfigManager::new();
mgr.set("a", "1").await.unwrap();
mgr.set("b", "2").await.unwrap();
let mut config = mgr.as_vec().await;
config.sort();
assert_eq!(
config,
vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string())
]
);
}
#[tokio::test]
async fn test_env_config_manager_read_only() {
let key = "TEMP_TEST_ENV_VAR";
let value = "test_value";
let old_value = std::env::var(key).ok();
unsafe{std::env::set_var(key, value)};
let mgr = EnvConfigManager::new(PathBuf::from("/nonexistent.env"));
assert_eq!(mgr.get(key).await, Some(value.to_string()));
assert!(mgr.keys().await.contains(&key.to_string()));
if let Some(v) = old_value {
unsafe{std::env::set_var(key, v)};
} else {
unsafe{std::env::remove_var(key)};
}
}
#[tokio::test]
async fn test_env_config_manager_set_and_delete_safely() {
let key = "TEMP_ENV_VAR_FOR_TEST";
let value = "secret";
let backup = std::env::var(key).ok();
let mgr = EnvConfigManager::new(PathBuf::from("/nonexistent.env"));
mgr.set(key, value).await.unwrap();
assert_eq!(std::env::var(key).ok(), Some(value.to_string()));
assert_eq!(mgr.get(key).await, Some(value.to_string()));
mgr.del(key).await;
assert_eq!(std::env::var(key).ok(), None);
if let Some(v) = backup {
unsafe {std::env::set_var(key, v)};
}
}
#[tokio::test]
async fn test_env_config_manager_with_temp_env_file() {
let dir = tempdir().unwrap();
let env_path = dir.path().join(".env");
let content = "API_KEY=abc123\nLOG_LEVEL=debug\n";
write(&env_path, content).unwrap();
let mgr = EnvConfigManager::new(env_path.clone());
assert_eq!(mgr.get("API_KEY").await, Some("abc123".to_string()));
assert_eq!(mgr.get("LOG_LEVEL").await, Some("debug".to_string()));
}
}