pub trait Config: Send + Sync {
fn get_string(&self, key: &str) -> Option<String>;
fn get_int(&self, key: &str) -> Option<i64>;
fn get_bool(&self, key: &str) -> Option<bool>;
fn get_float(&self, key: &str) -> Option<f64>;
}
use crate::InklogConfig;
use crate::InklogError;
pub struct InklogConfigAdapter {
config: InklogConfig,
}
impl InklogConfigAdapter {
pub fn new() -> Result<Self, InklogError> {
let config = InklogConfig::load_sync()
.map_err(|e| InklogError::ConfigError(format!("Failed to load config: {}", e)))?;
Ok(Self { config })
}
pub fn from_config(config: InklogConfig) -> Self {
Self { config }
}
pub fn inner(&self) -> &InklogConfig {
&self.config
}
pub fn inner_mut(&mut self) -> &mut InklogConfig {
&mut self.config
}
}
impl std::convert::AsRef<InklogConfig> for InklogConfigAdapter {
fn as_ref(&self) -> &InklogConfig {
&self.config
}
}
impl std::convert::AsMut<InklogConfig> for InklogConfigAdapter {
fn as_mut(&mut self) -> &mut InklogConfig {
&mut self.config
}
}
impl Default for InklogConfigAdapter {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self::from_config(InklogConfig::default()))
}
}
impl Config for InklogConfigAdapter {
fn get_string(&self, key: &str) -> Option<String> {
match key {
"global.level" => Some(self.config.global.level.clone()),
"global.format" => Some(self.config.global.format.clone()),
"global.masking_enabled" => Some(self.config.global.masking_enabled.to_string()),
"global.auto_fallback" => Some(self.config.global.auto_fallback.to_string()),
"global.fallback_initial_delay_ms" => {
Some(self.config.global.fallback_initial_delay_ms.to_string())
}
"global.fallback_max_delay_ms" => {
Some(self.config.global.fallback_max_delay_ms.to_string())
}
"global.fallback_max_retries" => {
Some(self.config.global.fallback_max_retries.to_string())
}
"console_sink.enabled" => self
.config
.console_sink
.as_ref()
.map(|c| c.enabled.to_string()),
"console_sink.colored" => self
.config
.console_sink
.as_ref()
.map(|c| c.colored.to_string()),
"console_sink.stderr_levels" => self.config.console_sink.as_ref().map(|c| {
c.stderr_levels
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(",")
}),
"console_sink.masking_enabled" => self
.config
.console_sink
.as_ref()
.map(|c| c.masking_enabled.to_string()),
"file_sink.enabled" => self
.config
.file_sink
.as_ref()
.map(|f| f.enabled.to_string()),
"file_sink.path" => self
.config
.file_sink
.as_ref()
.map(|f| f.path.to_string_lossy().to_string()),
"file_sink.max_size" => self.config.file_sink.as_ref().map(|f| f.max_size.clone()),
"file_sink.rotation_time" => self
.config
.file_sink
.as_ref()
.map(|f| f.rotation_time.clone()),
"file_sink.keep_files" => self
.config
.file_sink
.as_ref()
.map(|f| f.keep_files.to_string()),
"file_sink.compress" => self
.config
.file_sink
.as_ref()
.map(|f| f.compress.to_string()),
"file_sink.compression_level" => self
.config
.file_sink
.as_ref()
.map(|f| f.compression_level.to_string()),
"file_sink.encrypt" => self
.config
.file_sink
.as_ref()
.map(|f| f.encrypt.to_string()),
"file_sink.retention_days" => self
.config
.file_sink
.as_ref()
.map(|f| f.retention_days.to_string()),
"file_sink.max_total_size" => self
.config
.file_sink
.as_ref()
.map(|f| f.max_total_size.clone()),
"file_sink.cleanup_interval_minutes" => self
.config
.file_sink
.as_ref()
.map(|f| f.cleanup_interval_minutes.to_string()),
"file_sink.batch_size" => self
.config
.file_sink
.as_ref()
.map(|f| f.batch_size.to_string()),
"file_sink.flush_interval_ms" => self
.config
.file_sink
.as_ref()
.map(|f| f.flush_interval_ms.to_string()),
"file_sink.masking_enabled" => self
.config
.file_sink
.as_ref()
.map(|f| f.masking_enabled.to_string()),
"database_sink.enabled" => self
.config
.database_sink
.as_ref()
.map(|d| d.enabled.to_string()),
"database_sink.url" => self.config.database_sink.as_ref().map(|d| d.url.clone()),
"database_sink.pool_size" => self
.config
.database_sink
.as_ref()
.map(|d| d.pool_size.to_string()),
"database_sink.batch_size" => self
.config
.database_sink
.as_ref()
.map(|d| d.batch_size.to_string()),
"database_sink.flush_interval_ms" => self
.config
.database_sink
.as_ref()
.map(|d| d.flush_interval_ms.to_string()),
"database_sink.table_name" => self
.config
.database_sink
.as_ref()
.map(|d| d.table_name.clone()),
"database_sink.archive_format" => self
.config
.database_sink
.as_ref()
.map(|d| d.archive_format.clone()),
"performance.worker_threads" => {
Some(self.config.performance.worker_threads.to_string())
}
"performance.channel_capacity" => {
Some(self.config.performance.channel_capacity.to_string())
}
"http_server.enabled" => self
.config
.http_server
.as_ref()
.map(|h| h.enabled.to_string()),
"http_server.host" => self.config.http_server.as_ref().map(|h| h.host.clone()),
"http_server.port" => self.config.http_server.as_ref().map(|h| h.port.to_string()),
"http_server.metrics_path" => self
.config
.http_server
.as_ref()
.map(|h| h.metrics_path.clone()),
"http_server.health_path" => self
.config
.http_server
.as_ref()
.map(|h| h.health_path.clone()),
"file_sink.encryption_key_env" => self
.config
.file_sink
.as_ref()
.and_then(|f| f.encryption_key_env.clone()),
_ => None,
}
}
fn get_int(&self, key: &str) -> Option<i64> {
self.get_string(key).and_then(|s| s.parse().ok())
}
fn get_bool(&self, key: &str) -> Option<bool> {
self.get_string(key).and_then(|s| s.parse().ok())
}
fn get_float(&self, key: &str) -> Option<f64> {
self.get_string(key).and_then(|s| s.parse().ok())
}
}
use std::collections::HashMap;
use std::sync::RwLock;
pub struct MockConfig {
values: RwLock<HashMap<String, String>>,
}
impl MockConfig {
pub fn new() -> Self {
Self {
values: RwLock::new(HashMap::new()),
}
}
pub fn with_value(self, key: &str, value: &str) -> Self {
{
let mut values = self.values.write().unwrap();
values.insert(key.to_string(), value.to_string());
}
self
}
pub fn set(&self, key: &str, value: &str) {
let mut values = self.values.write().unwrap();
values.insert(key.to_string(), value.to_string());
}
}
impl Default for MockConfig {
fn default() -> Self {
Self::new()
}
}
impl Config for MockConfig {
fn get_string(&self, key: &str) -> Option<String> {
let values = self.values.read().unwrap();
values.get(key).cloned()
}
fn get_int(&self, key: &str) -> Option<i64> {
self.get_string(key).and_then(|s| s.parse().ok())
}
fn get_bool(&self, key: &str) -> Option<bool> {
self.get_string(key).and_then(|s| s.parse().ok())
}
fn get_float(&self, key: &str) -> Option<f64> {
self.get_string(key).and_then(|s| s.parse().ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InklogConfig;
#[test]
fn test_inklog_config_adapter_from_default_config() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_string("global.level"), Some("info".to_string()));
assert_eq!(adapter.get_int("performance.worker_threads"), Some(3));
}
#[test]
fn test_inklog_config_adapter_get_string() {
let mut config = InklogConfig::default();
config.global.level = "debug".to_string();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(
adapter.get_string("global.level"),
Some("debug".to_string())
);
assert_eq!(adapter.get_string("nonexistent.key"), None);
}
#[test]
fn test_inklog_config_adapter_get_int() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_int("performance.worker_threads"), Some(3));
assert_eq!(adapter.get_int("nonexistent.key"), None);
}
#[test]
fn test_inklog_config_adapter_get_bool() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_bool("global.auto_fallback"), Some(true));
assert_eq!(adapter.get_bool("global.masking_enabled"), Some(true));
}
#[test]
fn test_inklog_config_adapter_file_sink() {
let config = InklogConfig {
file_sink: Some(crate::FileSinkConfig {
enabled: true,
path: std::path::PathBuf::from("/var/log/app.log"),
max_size: "200MB".to_string(),
compress: true,
..Default::default()
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_bool("file_sink.enabled"), Some(true));
assert_eq!(
adapter.get_string("file_sink.path"),
Some("/var/log/app.log".to_string())
);
assert_eq!(
adapter.get_string("file_sink.max_size"),
Some("200MB".to_string())
);
assert_eq!(adapter.get_bool("file_sink.compress"), Some(true));
}
#[test]
fn test_inklog_config_adapter_http_server() {
let config = InklogConfig {
http_server: Some(crate::HttpServerConfig {
enabled: true,
host: "0.0.0.0".to_string(),
port: 9090,
..Default::default()
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_bool("http_server.enabled"), Some(true));
assert_eq!(
adapter.get_string("http_server.host"),
Some("0.0.0.0".to_string())
);
assert_eq!(adapter.get_int("http_server.port"), Some(9090));
}
#[test]
fn test_mock_config_new() {
let config = MockConfig::new();
assert_eq!(config.get_string("any_key"), None);
}
#[test]
fn test_mock_config_default() {
let config = MockConfig::default();
assert_eq!(config.get_string("any_key"), None);
}
#[test]
fn test_mock_config_with_value() {
let config = MockConfig::new()
.with_value("level", "debug")
.with_value("port", "8080")
.with_value("enabled", "true");
assert_eq!(config.get_string("level"), Some("debug".to_string()));
assert_eq!(config.get_string("port"), Some("8080".to_string()));
assert_eq!(config.get_string("enabled"), Some("true".to_string()));
}
#[test]
fn test_mock_config_get_string() {
let config = MockConfig::new().with_value("name", "test_value");
assert_eq!(config.get_string("name"), Some("test_value".to_string()));
assert_eq!(config.get_string("nonexistent"), None);
}
#[test]
fn test_mock_config_get_int() {
let config = MockConfig::new()
.with_value("port", "8080")
.with_value("invalid_int", "not_a_number");
assert_eq!(config.get_int("port"), Some(8080));
assert_eq!(config.get_int("invalid_int"), None);
assert_eq!(config.get_int("nonexistent"), None);
}
#[test]
fn test_mock_config_get_bool() {
let config = MockConfig::new()
.with_value("enabled", "true")
.with_value("disabled", "false")
.with_value("invalid_bool", "yes");
assert_eq!(config.get_bool("enabled"), Some(true));
assert_eq!(config.get_bool("disabled"), Some(false));
assert_eq!(config.get_bool("invalid_bool"), None);
assert_eq!(config.get_bool("nonexistent"), None);
}
#[test]
#[allow(clippy::approx_constant)]
fn test_mock_config_get_float() {
let config = MockConfig::new()
.with_value("ratio", "3.14159")
.with_value("invalid_float", "not_a_float");
assert_eq!(config.get_float("ratio"), Some(3.14159));
assert_eq!(config.get_float("invalid_float"), None);
assert_eq!(config.get_float("nonexistent"), None);
}
#[test]
fn test_mock_config_set_runtime() {
let config = MockConfig::new().with_value("level", "debug");
assert_eq!(config.get_string("level"), Some("debug".to_string()));
config.set("level", "info");
assert_eq!(config.get_string("level"), Some("info".to_string()));
config.set("new_key", "new_value");
assert_eq!(config.get_string("new_key"), Some("new_value".to_string()));
}
#[test]
fn test_mock_config_thread_safety() {
use std::sync::Arc;
use std::thread;
let config = Arc::new(MockConfig::new().with_value("counter", "0"));
let mut handles = vec![];
for i in 0..10 {
let cfg = Arc::clone(&config);
handles.push(thread::spawn(move || {
cfg.set("counter", &i.to_string());
}));
}
for handle in handles {
handle.join().unwrap();
}
let final_value = config.get_int("counter");
assert!(final_value.is_some());
assert!((0..10).contains(&final_value.unwrap()));
}
#[test]
fn test_adapter_inner_and_inner_mut() {
let config = InklogConfig::default();
let mut adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.inner().global.level, "info");
adapter.inner_mut().global.level = "debug".to_string();
assert_eq!(adapter.inner().global.level, "debug");
}
#[test]
fn test_adapter_as_ref_as_mut() {
let config = InklogConfig::default();
let mut adapter = InklogConfigAdapter::from_config(config);
let ref_config: &InklogConfig = AsRef::as_ref(&adapter);
assert_eq!(ref_config.global.level, "info");
let mut_config: &mut InklogConfig = AsMut::as_mut(&mut adapter);
mut_config.global.level = "warn".to_string();
assert_eq!(AsRef::<InklogConfig>::as_ref(&adapter).global.level, "warn");
}
#[test]
fn test_adapter_default() {
let adapter = InklogConfigAdapter::default();
assert_eq!(adapter.get_string("global.level"), Some("info".to_string()));
}
#[test]
fn test_adapter_global_fallback_fields() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(
adapter.get_string("global.fallback_initial_delay_ms"),
Some("1000".to_string())
);
assert_eq!(
adapter.get_string("global.fallback_max_delay_ms"),
Some("60000".to_string())
);
assert_eq!(
adapter.get_string("global.fallback_max_retries"),
Some("10".to_string())
);
assert_eq!(
adapter.get_int("global.fallback_initial_delay_ms"),
Some(1000)
);
assert_eq!(adapter.get_int("global.fallback_max_retries"), Some(10));
}
#[test]
fn test_adapter_console_sink_all_fields() {
let config = InklogConfig {
console_sink: Some(crate::ConsoleSinkConfig {
enabled: true,
colored: false,
stderr_levels: vec!["error".to_string(), "fatal".to_string()],
masking_enabled: true,
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_bool("console_sink.enabled"), Some(true));
assert_eq!(adapter.get_bool("console_sink.colored"), Some(false));
assert_eq!(
adapter.get_string("console_sink.stderr_levels"),
Some("error,fatal".to_string())
);
assert_eq!(adapter.get_bool("console_sink.masking_enabled"), Some(true));
}
#[test]
fn test_adapter_console_sink_none_returns_none() {
let config = InklogConfig {
console_sink: None,
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_string("console_sink.enabled"), None);
assert_eq!(adapter.get_string("console_sink.colored"), None);
assert_eq!(adapter.get_string("console_sink.stderr_levels"), None);
assert_eq!(adapter.get_string("console_sink.masking_enabled"), None);
}
#[test]
fn test_adapter_file_sink_all_fields() {
let config = InklogConfig {
file_sink: Some(crate::FileSinkConfig {
enabled: true,
path: std::path::PathBuf::from("/var/log/app.log"),
max_size: "200MB".to_string(),
rotation_time: "hourly".to_string(),
keep_files: 7,
compress: false,
compression_level: 5,
encrypt: true,
encryption_key_env: Some("MY_KEY".to_string()),
retention_days: 14,
max_total_size: "2GB".to_string(),
cleanup_interval_minutes: 30,
batch_size: 200,
flush_interval_ms: 50,
masking_enabled: false,
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(
adapter.get_string("file_sink.rotation_time"),
Some("hourly".to_string())
);
assert_eq!(adapter.get_int("file_sink.keep_files"), Some(7));
assert_eq!(adapter.get_int("file_sink.compression_level"), Some(5));
assert_eq!(adapter.get_bool("file_sink.encrypt"), Some(true));
assert_eq!(adapter.get_int("file_sink.retention_days"), Some(14));
assert_eq!(
adapter.get_string("file_sink.max_total_size"),
Some("2GB".to_string())
);
assert_eq!(
adapter.get_int("file_sink.cleanup_interval_minutes"),
Some(30)
);
assert_eq!(adapter.get_int("file_sink.batch_size"), Some(200));
assert_eq!(adapter.get_int("file_sink.flush_interval_ms"), Some(50));
assert_eq!(adapter.get_bool("file_sink.masking_enabled"), Some(false));
assert_eq!(
adapter.get_string("file_sink.encryption_key_env"),
Some("MY_KEY".to_string())
);
}
#[test]
fn test_adapter_file_sink_none_returns_none() {
let config = InklogConfig {
file_sink: None,
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_string("file_sink.enabled"), None);
assert_eq!(adapter.get_string("file_sink.encryption_key_env"), None);
assert_eq!(adapter.get_int("file_sink.batch_size"), None);
}
#[test]
fn test_adapter_database_sink_all_fields() {
let config = InklogConfig {
database_sink: Some(crate::DatabaseSinkConfig {
enabled: true,
url: "postgres://localhost/logs".to_string(),
pool_size: 20,
batch_size: 500,
flush_interval_ms: 250,
table_name: "app_logs".to_string(),
archive_format: "parquet".to_string(),
..Default::default()
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_bool("database_sink.enabled"), Some(true));
assert_eq!(adapter.get_int("database_sink.pool_size"), Some(20));
assert_eq!(adapter.get_int("database_sink.batch_size"), Some(500));
assert_eq!(
adapter.get_int("database_sink.flush_interval_ms"),
Some(250)
);
assert_eq!(
adapter.get_string("database_sink.table_name"),
Some("app_logs".to_string())
);
assert_eq!(
adapter.get_string("database_sink.archive_format"),
Some("parquet".to_string())
);
}
#[test]
fn test_adapter_database_sink_none_returns_none() {
let config = InklogConfig {
database_sink: None,
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_string("database_sink.enabled"), None);
assert_eq!(adapter.get_string("database_sink.url"), None);
}
#[test]
fn test_adapter_http_server_all_fields() {
let config = InklogConfig {
http_server: Some(crate::HttpServerConfig {
enabled: true,
host: "0.0.0.0".to_string(),
port: 8080,
metrics_path: "/custom_metrics".to_string(),
health_path: "/custom_health".to_string(),
..Default::default()
}),
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(
adapter.get_string("http_server.metrics_path"),
Some("/custom_metrics".to_string())
);
assert_eq!(
adapter.get_string("http_server.health_path"),
Some("/custom_health".to_string())
);
assert_eq!(adapter.get_int("http_server.port"), Some(8080));
}
#[test]
fn test_adapter_http_server_none_returns_none() {
let config = InklogConfig {
http_server: None,
..Default::default()
};
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_string("http_server.enabled"), None);
assert_eq!(adapter.get_string("http_server.metrics_path"), None);
assert_eq!(adapter.get_string("http_server.health_path"), None);
}
#[test]
fn test_adapter_get_float() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(
adapter.get_float("global.fallback_initial_delay_ms"),
Some(1000.0)
);
assert_eq!(adapter.get_float("nonexistent.key"), None);
}
#[test]
fn test_adapter_get_int_invalid_returns_none() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
assert_eq!(adapter.get_int("global.level"), None);
}
}