use async_trait::async_trait;
use std::sync::Arc;
use crate::InklogConfig;
use crate::InklogError;
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
use crate::integrations::Database;
#[cfg(test)]
use crate::integrations::MockCache;
use crate::integrations::{Cache, Config, InklogConfigAdapter, OxCacheAdapter};
use crate::{LoggerDependencies, LoggerManager};
pub struct InklogContainer {
cache: Arc<dyn Cache>,
config: Arc<dyn Config>,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: Option<Arc<dyn Database>>,
}
impl InklogContainer {
pub fn new() -> Result<Self, InklogError> {
let cache = Arc::new(OxCacheAdapter::new()?);
let config = Arc::new(InklogConfigAdapter::new()?);
Ok(Self {
cache,
config,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: None,
})
}
pub fn from_config(config: InklogConfig) -> Result<Self, InklogError> {
let cache = Arc::new(OxCacheAdapter::new()?);
let config = Arc::new(InklogConfigAdapter::from_config(config));
Ok(Self {
cache,
config,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: None,
})
}
pub fn builder() -> InklogContainerBuilder {
InklogContainerBuilder::default()
}
#[deprecated(since = "0.3.0", note = "use LoggerManager::builder() instead")]
pub async fn create_logger(&self) -> Result<LoggerManager, InklogError> {
let deps = LoggerDependencies {
cache: Some(Arc::clone(&self.cache)),
config: Some(Arc::clone(&self.config)),
custom_sinks: Vec::new(),
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: self.database.clone(),
};
LoggerManager::build_with_deps(deps).await
}
pub fn cache(&self) -> Arc<dyn Cache> {
Arc::clone(&self.cache)
}
pub fn config(&self) -> Arc<dyn Config> {
Arc::clone(&self.config)
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub fn database(&self) -> Option<Arc<dyn Database>> {
self.database.clone()
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub fn set_database(&mut self, database: Arc<dyn Database>) {
self.database = Some(database);
}
}
struct NoopCache;
#[async_trait]
impl Cache for NoopCache {
async fn get(&self, _key: &str) -> Result<Option<String>, InklogError> {
Ok(None)
}
async fn set(&self, _key: &str, _value: String) -> Result<(), InklogError> {
Ok(())
}
async fn delete(&self, _key: &str) -> Result<bool, InklogError> {
Ok(false)
}
async fn exists(&self, _key: &str) -> Result<bool, InklogError> {
Ok(false)
}
}
impl Default for InklogContainer {
fn default() -> Self {
let cache = match OxCacheAdapter::new() {
Ok(cache) => Arc::new(cache) as Arc<dyn Cache>,
Err(e) => {
tracing::warn!(
error = %e,
"InklogContainer default cache init failed; using no-op cache"
);
Arc::new(NoopCache) as Arc<dyn Cache>
}
};
let config = match InklogConfigAdapter::new() {
Ok(config) => Arc::new(config) as Arc<dyn Config>,
Err(e) => {
tracing::warn!(
error = %e,
"InklogContainer default config init failed; using default config"
);
Arc::new(InklogConfigAdapter::from_config(InklogConfig::default()))
as Arc<dyn Config>
}
};
Self {
cache,
config,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: None,
}
}
}
impl std::fmt::Debug for InklogContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut builder = f.debug_struct("InklogContainer");
builder
.field("cache", &"Arc<dyn Cache>")
.field("config", &"Arc<dyn Config>");
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
builder.field(
"database",
&self.database.as_ref().map(|_| "Arc<dyn Database>"),
);
builder.finish()
}
}
#[derive(Default)]
pub struct InklogContainerBuilder {
cache: Option<Arc<dyn Cache>>,
config: Option<Arc<dyn Config>>,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: Option<Arc<dyn Database>>,
}
impl InklogContainerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn cache(mut self, cache: Arc<dyn Cache>) -> Self {
self.cache = Some(cache);
self
}
pub fn config(mut self, config: Arc<dyn Config>) -> Self {
self.config = Some(config);
self
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub fn database(mut self, database: Arc<dyn Database>) -> Self {
self.database = Some(database);
self
}
pub fn build(self) -> Result<InklogContainer, InklogError> {
let cache = match self.cache {
Some(cache) => cache,
None => Arc::new(OxCacheAdapter::new()?),
};
let config = match self.config {
Some(config) => config,
None => Arc::new(InklogConfigAdapter::new()?),
};
Ok(InklogContainer {
cache,
config,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
database: self.database,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn test_container_new() {
let container = InklogContainer::new();
assert!(container.is_ok());
}
#[test]
#[serial]
fn test_container_default() {
let container = InklogContainer::default();
let _cache = container.cache();
let _config = container.config();
}
#[test]
#[serial]
fn test_container_from_config() {
let config = InklogConfig::default();
let container = InklogContainer::from_config(config);
assert!(container.is_ok());
}
#[test]
#[serial]
fn test_container_builder_default() {
let container = InklogContainer::builder().build();
assert!(container.is_ok());
}
#[test]
fn test_container_builder_with_adapters() {
let container = InklogContainer::builder()
.cache(Arc::new(
OxCacheAdapter::new().expect("Failed to create cache"),
))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build();
assert!(container.is_ok());
}
#[tokio::test]
async fn test_container_cache_shared() {
let container = InklogContainer::builder()
.cache(Arc::new(
OxCacheAdapter::new().expect("Failed to create cache"),
))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
let cache1 = container.cache();
let cache2 = container.cache();
cache1.set("key", "value".to_string()).await.unwrap();
let value = cache2.get("key").await.unwrap();
assert_eq!(value, Some("value".to_string()));
}
#[test]
fn test_container_config_shared() {
let config = InklogConfig::default();
let adapter = InklogConfigAdapter::from_config(config);
let container = InklogContainer::builder()
.cache(Arc::new(
OxCacheAdapter::new().expect("Failed to create cache"),
))
.config(Arc::new(adapter))
.build()
.unwrap();
let config1 = container.config();
let config2 = container.config();
assert_eq!(
config1.get_string("global.level"),
config2.get_string("global.level")
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[allow(deprecated)] async fn test_container_create_logger() {
let container = InklogContainer::builder()
.cache(Arc::new(
OxCacheAdapter::new().expect("Failed to create cache"),
))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
let result = container.create_logger().await;
assert!(result.is_ok());
}
#[test]
fn test_container_debug() {
let container = InklogContainer::builder()
.cache(Arc::new(
OxCacheAdapter::new().expect("Failed to create cache"),
))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
let debug_str = format!("{:?}", container);
assert!(debug_str.contains("InklogContainer"));
assert!(debug_str.contains("Arc<dyn Cache>"));
assert!(debug_str.contains("Arc<dyn Config>"));
}
#[test]
fn test_container_with_mock_cache() {
let container = InklogContainer::builder()
.cache(Arc::new(MockCache::new()))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build();
assert!(container.is_ok());
}
#[tokio::test]
async fn test_container_mock_cache_operations() {
let container = InklogContainer::builder()
.cache(Arc::new(MockCache::new()))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
let cache = container.cache();
cache
.set("test_key", "test_value".to_string())
.await
.unwrap();
let value = cache.get("test_key").await.unwrap();
assert_eq!(value, Some("test_value".to_string()));
assert!(cache.exists("test_key").await.unwrap());
assert!(cache.delete("test_key").await.unwrap());
assert!(!cache.exists("test_key").await.unwrap());
}
#[test]
#[serial]
fn test_container_builder_new() {
let builder = InklogContainerBuilder::new();
let container = builder.build();
assert!(container.is_ok());
}
#[test]
#[serial]
fn test_container_builder_new_equals_default() {
let from_new = InklogContainerBuilder::new();
let from_default = InklogContainerBuilder::default();
assert!(from_new.build().is_ok());
assert!(from_default.build().is_ok());
}
#[test]
#[serial]
fn test_container_builder_build_returns_err_when_default_config_init_fails() {
let dir = tempfile::tempdir().expect("Failed to create tempdir");
let bad = dir.path().join("invalid.toml");
std::fs::write(&bad, "this is = = not valid toml [[[").expect("Failed to write config");
unsafe {
std::env::set_var("INKLOG_CONFIG_PATH", &bad);
}
let result = InklogContainer::builder().build();
unsafe {
std::env::remove_var("INKLOG_CONFIG_PATH");
}
assert!(
result.is_err(),
"build() should return Err instead of panicking when default config init fails"
);
}
#[test]
#[serial]
fn test_container_default_does_not_panic_when_config_init_fails() {
let dir = tempfile::tempdir().expect("Failed to create tempdir");
let bad = dir.path().join("invalid.toml");
std::fs::write(&bad, "this is = = not valid toml [[[").expect("Failed to write config");
unsafe {
std::env::set_var("INKLOG_CONFIG_PATH", &bad);
}
let container = InklogContainer::default();
unsafe {
std::env::remove_var("INKLOG_CONFIG_PATH");
}
let _cache = container.cache();
let _config = container.config();
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
#[tokio::test]
async fn test_container_database_returns_none_by_default() {
let container = InklogContainer::builder()
.cache(Arc::new(MockCache::new()))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
assert!(container.database().is_none());
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
#[tokio::test]
async fn test_container_set_database_and_get() {
use crate::LogRecord;
use crate::integrations::MockDatabaseAdapter;
let mut container = InklogContainer::builder()
.cache(Arc::new(MockCache::new()))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.build()
.unwrap();
assert!(container.database().is_none());
let mock_db = Arc::new(MockDatabaseAdapter::new());
let db: Arc<dyn Database> = Arc::clone(&mock_db) as Arc<dyn Database>;
container.set_database(Arc::clone(&db));
let retrieved = container.database();
assert!(retrieved.is_some());
let retrieved_db = retrieved.unwrap();
assert!(retrieved_db.is_healthy().await);
let records = vec![LogRecord::new(
tracing::Level::INFO,
"test::module".to_string(),
"test message".to_string(),
)];
let count = retrieved_db.insert_batch(&records).await.unwrap();
assert_eq!(count, 1);
assert_eq!(mock_db.record_count(), 1);
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
#[tokio::test]
async fn test_container_builder_database() {
use crate::integrations::MockDatabaseAdapter;
let mock_db = Arc::new(MockDatabaseAdapter::new());
let db: Arc<dyn Database> = Arc::clone(&mock_db) as Arc<dyn Database>;
let container = InklogContainer::builder()
.cache(Arc::new(MockCache::new()))
.config(Arc::new(InklogConfigAdapter::from_config(
InklogConfig::default(),
)))
.database(Arc::clone(&db))
.build()
.unwrap();
let retrieved = container.database();
assert!(retrieved.is_some());
let retrieved_db = retrieved.unwrap();
assert!(retrieved_db.is_healthy().await);
assert_eq!(mock_db.record_count(), 0);
}
}