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;
#[cfg(test)]
use crate::database::ConnectionPool;
use crate::database::{DbPool, 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<DbPool>;
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))
})
}
}
pub struct DbNexusCacheModule;
impl ModuleMeta for DbNexusCacheModule {
const NAME: &'static str = "dbnexus-cache";
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 DbNexusCacheModule {
type Capability = Arc<dyn crate::domain::DbCacheProvider + 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);
Ok(Arc::new(adapter) as Arc<dyn crate::domain::DbCacheProvider + Send + Sync>)
})
}
}
#[cfg(all(feature = "audit", feature = "sql-parser"))]
pub struct DbNexusAuditModule;
#[cfg(all(feature = "audit", feature = "sql-parser"))]
impl ModuleMeta for DbNexusAuditModule {
const NAME: &'static str = "dbnexus-audit";
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()
}
}
#[cfg(all(feature = "audit", feature = "sql-parser"))]
impl AsyncAutoBuilder for DbNexusAuditModule {
type Capability = Arc<dyn crate::domain::AuditStorage>;
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 pool = kit
.require::<DbNexusModule>()
.map_err(|e| DbError::Config(format!("require DbNexusModule: {e}")))?;
let storage = crate::domain::DbAuditStorage::new(pool);
storage
.init()
.await
.map_err(|e| DbError::Config(format!("audit storage init: {e}")))?;
Ok(Arc::new(storage) as Arc<dyn crate::domain::AuditStorage>)
})
}
}
#[cfg(feature = "health-check")]
#[derive(Clone)]
pub struct DbHealthCapability {
pool: Arc<DbPool>,
}
#[cfg(feature = "health-check")]
impl DbHealthCapability {
pub async fn snapshot(&self) -> serde_json::Value {
self.pool.health_snapshot().await
}
pub fn pool(&self) -> &Arc<DbPool> {
&self.pool
}
}
#[cfg(feature = "health-check")]
pub struct DbNexusHealthModule;
#[cfg(feature = "health-check")]
impl ModuleMeta for DbNexusHealthModule {
const NAME: &'static str = "dbnexus-health";
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()
}
}
#[cfg(feature = "health-check")]
impl AsyncAutoBuilder for DbNexusHealthModule {
type Capability = DbHealthCapability;
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 pool = kit
.require::<DbNexusModule>()
.map_err(|e| DbError::Config(format!("require DbNexusModule: {e}")))?;
Ok(DbHealthCapability { pool })
})
}
}
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<DbPool>>();
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 = kit
.require::<DbNexusModule>()
.expect("require DbNexusModule");
let pool: Arc<dyn ConnectionPool + Send + Sync> = pool;
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>();
}
#[cfg(feature = "pool-warmup")]
#[tokio::test]
async fn health_check_healthy_after_pool_warmup() {
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 = 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".to_string(),
},
);
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);
}
#[cfg(feature = "pool-warmup")]
#[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_async().await;
}
#[test]
fn t413_satellite_modules_satisfy_bounds() {
fn assert_cap<T: Clone + Send + Sync + 'static>() {}
assert_cap::<Arc<dyn crate::domain::DbCacheProvider + Send + Sync>>();
#[cfg(all(feature = "audit", feature = "sql-parser"))]
assert_cap::<Arc<dyn crate::domain::AuditStorage>>();
#[cfg(feature = "health-check")]
assert_cap::<DbHealthCapability>();
}
#[tokio::test]
async fn t413_cache_capability_requireable() {
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url: "sqlite::memory:".to_string(),
..Default::default()
});
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
kit.register::<DbNexusModule>()
.expect("register DbNexusModule");
kit.register::<DbNexusCacheModule>()
.expect("register DbNexusCacheModule");
let kit = kit.build().await.expect("AsyncKit::build");
let cache: Arc<dyn crate::domain::DbCacheProvider + Send + Sync> =
kit.require::<DbNexusCacheModule>().expect("require cache");
cache
.set("t413-key", b"t413-value".to_vec(), None)
.await
.expect("cache set");
let got = cache.get("t413-key").await.expect("cache get");
assert_eq!(got.as_deref(), Some(&b"t413-value"[..]), "缓存能力应可读写");
}
#[cfg(all(feature = "audit", feature = "sql-parser", feature = "health-check"))]
#[tokio::test]
async fn t413_all_capabilities_requireable() {
let db_path = std::env::temp_dir().join(format!("dbnexus_t413_{}.db", std::process::id()));
let url = format!("sqlite:{}?mode=rwc", db_path.display());
let mut kit = AsyncKit::new();
kit.set_config(OxcacheConfig::default());
kit.set_config(DbConfig {
url,
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");
kit.register::<DbNexusCacheModule>()
.expect("register DbNexusCacheModule");
kit.register::<DbNexusAuditModule>()
.expect("register DbNexusAuditModule");
kit.register::<DbNexusHealthModule>()
.expect("register DbNexusHealthModule");
let kit = kit
.build()
.await
.expect("AsyncKit::build should build all four modules");
let pool = kit.require::<DbNexusModule>().expect("require pool");
assert!(pool.config().url.contains("t413"), "池能力可用");
let cache = kit.require::<DbNexusCacheModule>().expect("require cache");
cache
.set("k", b"v".to_vec(), None)
.await
.expect("cache set");
assert_eq!(
cache.get("k").await.expect("cache get").as_deref(),
Some(&b"v"[..])
);
let audit = kit.require::<DbNexusAuditModule>().expect("require audit");
let event = crate::domain::AuditEvent::create("t413_entities", "42", "admin");
audit.store(&event).await.expect("audit store");
let events = audit
.query(&crate::domain::AuditQueryFilters::default())
.await
.expect("audit query");
assert!(
events
.iter()
.any(|e| e.entity_type == "t413_entities" && e.entity_id == "42"),
"审计能力应可写入并查回事件"
);
let health = kit
.require::<DbNexusHealthModule>()
.expect("require health");
let snapshot = health.snapshot().await;
assert!(
snapshot["pool"]["saturation"].is_number() && snapshot["status"].is_string(),
"健康能力应输出结构化快照: {snapshot}"
);
let _ = std::fs::remove_file(&db_path);
}
}