use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use trait_kit::prelude::*;
use oxcache::integrations::kit::OxcacheModule;
use crate::database::{ConnectionPool, DbPoolBuilder};
use crate::foundation::DbConfig;
use crate::foundation::DbError;
use crate::integrations::OxcacheDbCacheAdapter;
pub struct DbNexusModule;
impl ModuleMeta for DbNexusModule {
const NAME: &'static str = "dbnexus";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: OnceLock<Vec<(&'static str, TypeId)>> = OnceLock::new();
DEPS.get_or_init(|| vec![("oxcache", TypeId::of::<OxcacheModule>())])
.as_slice()
}
}
impl AsyncAutoBuilder for DbNexusModule {
type Capability = Arc<dyn ConnectionPool + Send + Sync>;
type Error = DbError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let cache_cap = kit
.require::<OxcacheModule>()
.map_err(|e| DbError::Config(format!("require OxcacheModule: {e}")))?;
let adapter = OxcacheDbCacheAdapter::new(cache_cap);
let config: DbConfig = kit
.config()
.map_err(|e| DbError::Config(format!("read DbConfig: {e}")))?;
let pool = DbPoolBuilder::new()
.config(config)
.cache_provider(Arc::new(adapter))
.build()
.await?;
Ok(Arc::new(pool) as Arc<dyn ConnectionPool + Send + Sync>)
})
}
}
impl AsyncLifecycle for DbNexusModule {
}
impl AsyncHealthCheck for DbNexusModule {
fn check(cap: &Self::Capability) -> HealthStatus {
let status = cap.status();
if status.total == 0 {
HealthStatus::unhealthy("no connections established")
} else if status.idle > 0 && status.wait_count == 0 {
HealthStatus::Healthy
} else if status.wait_count > 0 {
HealthStatus::degraded(format!(
"{} waiting, {}/{} active/total",
status.wait_count, status.active, status.total
))
} else {
HealthStatus::degraded(format!(
"no idle connections, {}/{} active/total",
status.active, status.total
))
}
}
}
pub struct DbNexusBuildObserver {
built_count: std::sync::atomic::AtomicU64,
error_count: std::sync::atomic::AtomicU64,
}
impl DbNexusBuildObserver {
#[must_use]
pub fn new() -> Self {
Self {
built_count: std::sync::atomic::AtomicU64::new(0),
error_count: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn built_count(&self) -> u64 {
self.built_count.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn error_count(&self) -> u64 {
self.error_count.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for DbNexusBuildObserver {
fn default() -> Self {
Self::new()
}
}
impl BuildObserver for DbNexusBuildObserver {
fn on_module_start(&self, _module_name: &'static str) {
}
fn on_module_built(&self, _module_name: &'static str, _elapsed: Duration) {
self.built_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn on_build_error(&self, _module_name: &'static str, _error: &TraitKitError) {
self.error_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxcache::integrations::kit::OxcacheConfig;
#[test]
fn dbnexus_module_meta_name() {
assert_eq!(DbNexusModule::NAME, "dbnexus");
}
#[test]
fn dbnexus_module_meta_dependencies() {
let deps = DbNexusModule::dependencies();
assert_eq!(deps.len(), 1, "DbNexusModule should depend on 1 module");
assert_eq!(deps[0].0, "oxcache", "dep name should be 'oxcache'");
assert_eq!(
deps[0].1,
TypeId::of::<OxcacheModule>(),
"dep TypeId should match OxcacheModule"
);
}
#[test]
fn dbnexus_module_satisfies_async_auto_builder_bounds() {
fn assert_cap<T: Clone + Send + Sync + 'static>() {}
assert_cap::<Arc<dyn ConnectionPool + Send + Sync>>();
fn assert_err<T: std::error::Error + Send + 'static>() {}
assert_err::<DbError>();
}
#[tokio::test]
async fn dbnexus_module_build_returns_connection_pool() {
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: crate::foundation::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>().expect("register OxcacheModule");
kit.register::<DbNexusModule>().expect("register DbNexusModule");
let kit = kit.build().await.expect("AsyncKit::build");
let pool: Arc<dyn ConnectionPool + Send + Sync> =
kit.require::<DbNexusModule>().expect("require DbNexusModule");
let _status = pool.status();
let config = pool.config();
assert_eq!(config.url, "sqlite::memory:");
}
#[tokio::test]
async fn dbnexus_module_build_fails_without_oxcache() {
let mut kit = AsyncKit::new();
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
});
kit.register::<DbNexusModule>().expect("register DbNexusModule");
let err = kit.build().await.expect_err("build should fail");
let msg = err.to_string();
assert!(
msg.contains("oxcache"),
"error should mention oxcache dependency, got: {msg}"
);
}
#[test]
fn dbnexus_module_satisfies_async_lifecycle() {
fn assert_lifecycle<T: AsyncLifecycle>() {}
assert_lifecycle::<DbNexusModule>();
}
#[test]
fn dbnexus_module_satisfies_async_health_check() {
fn assert_hc<T: AsyncHealthCheck>() {}
assert_hc::<DbNexusModule>();
}
#[test]
fn build_observer_satisfies_build_observer_trait() {
fn assert_obs<T: BuildObserver>() {}
assert_obs::<DbNexusBuildObserver>();
}
#[tokio::test]
async fn health_check_unhealthy_before_first_use() {
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: crate::foundation::PoolConfig {
max_connections: 5,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>().expect("register OxcacheModule");
kit.register::<DbNexusModule>().expect("register DbNexusModule");
let kit = kit.build().await.expect("AsyncKit::build");
let pool: Arc<dyn ConnectionPool + Send + Sync> =
kit.require::<DbNexusModule>().expect("require DbNexusModule");
let status = DbNexusModule::check(&pool);
assert!(
status.is_healthy(),
"expected Healthy after pool pre-warm, got: {status:?}"
);
}
#[test]
fn health_check_unhealthy_when_no_connections() {
let pool_status = crate::database::PoolStatus {
total: 0,
active: 0,
idle: 0,
wait_count: 0,
max_waiters: 0,
borrow_count: 0,
max_active: 0,
};
if pool_status.total == 0 {
let status = HealthStatus::unhealthy("no connections established");
assert!(!status.is_healthy());
}
}
#[test]
fn build_observer_counts() {
let obs = DbNexusBuildObserver::new();
assert_eq!(obs.built_count(), 0);
assert_eq!(obs.error_count(), 0);
obs.on_module_built("oxcache", Duration::from_millis(5));
obs.on_module_built("dbnexus", Duration::from_millis(10));
assert_eq!(obs.built_count(), 2);
assert_eq!(obs.error_count(), 0);
obs.on_build_error("failing-module", &TraitKitError::MissingCapability { key: "x" });
assert_eq!(obs.built_count(), 2);
assert_eq!(obs.error_count(), 1);
}
#[test]
fn build_observer_default_is_zeroed() {
let obs = DbNexusBuildObserver::default();
assert_eq!(obs.built_count(), 0);
assert_eq!(obs.error_count(), 0);
}
#[tokio::test]
async fn full_kit_with_lifecycle_health_observer() {
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
pool_config: crate::foundation::PoolConfig {
max_connections: 3,
min_connections: 1,
..Default::default()
},
..Default::default()
});
kit.register::<OxcacheModule>().expect("register OxcacheModule");
kit.register::<DbNexusModule>().expect("register DbNexusModule");
kit.register_lifecycle::<DbNexusModule>();
kit.register_health_check::<DbNexusModule>();
let observer = Arc::new(DbNexusBuildObserver::new());
kit.with_observer(observer.clone());
let kit = kit.build().await.expect("AsyncKit::build");
assert!(
observer.built_count() >= 2,
"expected >= 2 built modules, got {}",
observer.built_count()
);
let health = kit.health_check::<DbNexusModule>().expect("health_check");
assert!(
health.is_healthy(),
"expected Healthy after pool pre-warm, got: {health:?}"
);
kit.shutdown();
}
}