use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use trait_kit::AsyncReady;
use trait_kit::prelude::*;
use dbnexus::DbNexusModule;
use crate::InklogError;
use crate::integrations::infra::Database;
use crate::integrations::infra::database::DbNexusAdapter;
pub struct InklogModule;
impl ModuleMeta for InklogModule {
const NAME: &'static str = "inklog";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: OnceLock<Vec<(&'static str, TypeId)>> = OnceLock::new();
DEPS.get_or_init(|| vec![("dbnexus", TypeId::of::<DbNexusModule>())])
.as_slice()
}
}
impl AsyncAutoBuilder for InklogModule {
type Capability = Arc<dyn Database + Send + Sync>;
type Error = InklogError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let pool = kit.require::<DbNexusModule>().map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::database_error(crate::i18n::tr_args("config-require_dbnexus", args))
})?;
let adapter = DbNexusAdapter::from_connection_pool(
pool,
crate::support::io::sink::entity::TABLE_NAME,
)?;
Ok(Arc::new(adapter) as Arc<dyn Database + Send + Sync>)
})
}
}
impl AsyncLifecycle for InklogModule {
fn on_ready<'a>(
kit: &'a AsyncKit<AsyncReady>,
) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>> {
Box::pin(async move {
kit.require::<DbNexusModule>().map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::database_error(crate::i18n::tr_args("config-db_not_available", args))
})?;
tracing::debug!("InklogModule: on_ready — database dependency verified");
Ok(())
})
}
fn on_shutdown<'a>(
_cap: &'a Self::Capability,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async {
tracing::debug!("InklogModule: on_shutdown — releasing database adapter");
})
}
}
impl AsyncHealthCheck for InklogModule {
fn check(_cap: &Self::Capability) -> HealthStatus {
HealthStatus::Healthy
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inklog_module_meta_name() {
assert_eq!(InklogModule::NAME, "inklog");
}
#[test]
fn inklog_module_meta_dependencies() {
let deps = InklogModule::dependencies();
assert_eq!(deps.len(), 1, "InklogModule should depend on 1 module");
assert_eq!(deps[0].0, "dbnexus", "dep name should be 'dbnexus'");
assert_eq!(
deps[0].1,
TypeId::of::<DbNexusModule>(),
"dep TypeId should match DbNexusModule"
);
}
#[test]
fn inklog_module_satisfies_async_auto_builder_bounds() {
fn assert_cap<T: Clone + Send + Sync + 'static>() {}
assert_cap::<Arc<dyn Database + Send + Sync>>();
fn assert_err<T: std::error::Error + Send + 'static>() {}
assert_err::<InklogError>();
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_build_returns_database() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
let kit = kit.build().await.expect("AsyncKit::build");
let db: Arc<dyn Database + Send + Sync> =
kit.require::<InklogModule>().expect("require InklogModule");
assert!(db.is_healthy().await);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_build_fails_without_dbnexus() {
let mut kit = AsyncKit::new();
kit.register::<InklogModule>()
.expect("register InklogModule");
let err = kit.build().await.expect_err("build should fail");
let msg = err.to_string();
assert!(
msg.contains("dbnexus"),
"error should mention dbnexus dependency, got: {msg}"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_health_check_returns_healthy() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
let built = kit.build().await.expect("AsyncKit::build");
let db: Arc<dyn Database + Send + Sync> = built
.require::<InklogModule>()
.expect("require InklogModule");
let status = InklogModule::check(&db);
assert_eq!(status, HealthStatus::Healthy);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_lifecycle_on_ready_succeeds() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
kit.register_lifecycle::<InklogModule>();
let built = kit
.build()
.await
.expect("build with lifecycle should succeed");
drop(built);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_lifecycle_shutdown_completes() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
kit.register_lifecycle::<InklogModule>();
let built = kit.build().await.expect("build");
built.shutdown();
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_lifecycle_and_health_full_integration() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
kit.register_lifecycle::<InklogModule>();
kit.register_health_check::<InklogModule>();
let built = kit.build().await.expect("build");
let status = built
.health_check::<InklogModule>()
.expect("health_check should succeed");
assert_eq!(status, HealthStatus::Healthy);
built.shutdown();
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_module_build_with_observer() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
use super::super::InklogBuildObserver;
let mut kit = AsyncKit::new();
kit.with_observer(Arc::new(InklogBuildObserver));
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
let built = kit.build().await.expect("build with observer");
let db: Arc<dyn Database + Send + Sync> =
built.require::<InklogModule>().expect("require db");
assert!(db.is_healthy().await);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn inklog_scope_insert_require_roundtrip() {
use dbnexus::foundation::config::DbConfig;
use oxcache::integrations::kit::{OxcacheConfig, OxcacheModule};
use super::super::{create_inklog_scope, populate_inklog_scope};
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: dbnexus::foundation::config::PoolConfig {
max_connections: 2,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<InklogModule>()
.expect("register InklogModule");
let built = kit.build().await.expect("build");
let db = built.require::<InklogModule>().expect("require from kit");
let scope = create_inklog_scope();
populate_inklog_scope(&scope, db);
let retrieved = scope.require::<InklogModule>().expect("require from scope");
assert!(retrieved.is_healthy().await);
}
}