mod common;
use rust_dix::*;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
struct DbPool {
conn_str: String,
pool_id: u64,
}
struct RedisCache {
url: String,
cache_id: u64,
}
trait IPaymentGateway: Send + Sync {
fn name(&self) -> &str;
fn charge(&self, amount: u64) -> bool;
}
struct WechatPay {
#[allow(dead_code)]
merchant_id: String,
}
impl IPaymentGateway for WechatPay {
fn name(&self) -> &str {
"wechat"
}
fn charge(&self, _amount: u64) -> bool {
true
}
}
struct Alipay {
#[allow(dead_code)]
app_id: String,
}
impl IPaymentGateway for Alipay {
fn name(&self) -> &str {
"alipay"
}
fn charge(&self, _amount: u64) -> bool {
true
}
}
#[derive(Clone)]
struct AppConfig {
max_retries: u32,
payment_provider: String,
}
struct UserRepository {
db: Arc<DbPool>,
cache: Arc<RedisCache>,
config: Arc<AppConfig>,
}
struct OrderService {
repo: Arc<UserRepository>,
}
struct NotificationService {
config: Arc<AppConfig>,
}
#[tokio::test]
async fn ecommerce_order_processing_full_lifecycle() {
static NEXT_POOL_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_CACHE_ID: AtomicU64 = AtomicU64::new(1);
static REPO_BUILDS: AtomicU64 = AtomicU64::new(0);
static ORDER_BUILDS: AtomicU64 = AtomicU64::new(0);
let provider = ServiceCollection::new()
.async_singleton(|_| {
let id = NEXT_POOL_ID.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
Arc::new(DbPool {
conn_str: format!("postgres://db{}/mydb", id),
pool_id: id,
})
})
})
.async_singleton(|_| {
let id = NEXT_CACHE_ID.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
Arc::new(RedisCache {
url: format!("redis://cache{}/0", id),
cache_id: id,
})
})
})
.async_keyed_singleton::<dyn IPaymentGateway>("wechat", |_| {
Box::pin(async {
Arc::new(WechatPay {
merchant_id: "mch_123".into(),
}) as Arc<dyn IPaymentGateway>
})
})
.async_keyed_singleton::<dyn IPaymentGateway>("alipay", |_| {
Box::pin(async {
Arc::new(Alipay {
app_id: "app_456".into(),
}) as Arc<dyn IPaymentGateway>
})
})
.singleton(|_| {
Arc::new(AppConfig {
max_retries: 3,
payment_provider: "wechat".into(),
})
})
.scoped(|r| {
REPO_BUILDS.fetch_add(1, Ordering::SeqCst);
let db: Arc<DbPool> = r.get_any(std::any::type_name::<DbPool>())
.and_then(|a| a.downcast::<Arc<DbPool>>().ok().map(|d| Arc::clone(&*d)))
.expect("DbPool not resolved");
let cache: Arc<RedisCache> = r.get_any(std::any::type_name::<RedisCache>())
.and_then(|a| a.downcast::<Arc<RedisCache>>().ok().map(|d| Arc::clone(&*d)))
.expect("RedisCache not resolved");
let config: Arc<AppConfig> = r.get_any(std::any::type_name::<AppConfig>())
.and_then(|a| a.downcast::<Arc<AppConfig>>().ok().map(|d| Arc::clone(&*d)))
.expect("AppConfig not resolved");
Arc::new(UserRepository { db, cache, config })
})
.transient(|r| {
ORDER_BUILDS.fetch_add(1, Ordering::SeqCst);
let repo: Arc<UserRepository> = r.get_any(std::any::type_name::<UserRepository>())
.and_then(|a| a.downcast::<Arc<UserRepository>>().ok().map(|d| Arc::clone(&*d)))
.expect("UserRepository not resolved");
Arc::new(OrderService { repo })
})
.transient(|r| {
let config: Arc<AppConfig> = r.get_any(std::any::type_name::<AppConfig>())
.and_then(|a| a.downcast::<Arc<AppConfig>>().ok().map(|d| Arc::clone(&*d)))
.expect("AppConfig not resolved");
Arc::new(NotificationService { config })
})
.build_async()
.await
.unwrap();
let db: Arc<DbPool> = provider.get_async().await.unwrap();
assert_eq!(db.pool_id, 1);
assert_eq!(db.conn_str, "postgres://db1/mydb");
let cache: Arc<RedisCache> = provider.get_async().await.unwrap();
assert_eq!(cache.cache_id, 1);
assert_eq!(cache.url, "redis://cache1/0");
let config: Arc<AppConfig> = provider.get().unwrap();
assert_eq!(config.max_retries, 3);
assert_eq!(config.payment_provider, "wechat");
let wechat: Arc<dyn IPaymentGateway> = provider.get_keyed_async("wechat").await.unwrap();
assert_eq!(wechat.name(), "wechat");
assert!(wechat.charge(100));
let alipay: Arc<dyn IPaymentGateway> = provider.get_keyed_async("alipay").await.unwrap();
assert_eq!(alipay.name(), "alipay");
assert!(alipay.charge(200));
let scope1 = provider.scope();
let repo1: Arc<UserRepository> = scope1.get().unwrap();
assert_eq!(repo1.db.pool_id, 1);
assert_eq!(repo1.cache.cache_id, 1);
assert_eq!(repo1.config.max_retries, 3);
let repo1_again: Arc<UserRepository> = scope1.get().unwrap();
assert!(Arc::ptr_eq(&repo1, &repo1_again));
assert_eq!(REPO_BUILDS.load(Ordering::SeqCst), 1);
let order1a: Arc<OrderService> = scope1.get().unwrap();
let order1b: Arc<OrderService> = scope1.get().unwrap();
assert!(!Arc::ptr_eq(&order1a, &order1b), "OrderService must be fresh each resolution");
assert!(Arc::ptr_eq(&order1a.repo, &order1b.repo), "OrderService must share scoped UserRepository");
assert_eq!(ORDER_BUILDS.load(Ordering::SeqCst), 2);
let notif1: Arc<NotificationService> = scope1.get().unwrap();
assert_eq!(notif1.config.max_retries, 3);
let scope2 = provider.scope();
let repo2: Arc<UserRepository> = scope2.get().unwrap();
assert!(!Arc::ptr_eq(&repo1, &repo2), "UserRepository must be independent across scopes");
assert_eq!(REPO_BUILDS.load(Ordering::SeqCst), 2);
assert_eq!(repo2.db.pool_id, 1);
assert_eq!(repo2.cache.cache_id, 1);
let root_repo: Arc<UserRepository> = provider.get().unwrap();
assert!(!Arc::ptr_eq(&repo1, &root_repo));
assert!(!Arc::ptr_eq(&repo2, &root_repo));
assert_eq!(REPO_BUILDS.load(Ordering::SeqCst), 3);
assert!(Arc::ptr_eq(&repo1.db, &repo2.db));
assert!(Arc::ptr_eq(&repo1.db, &root_repo.db));
assert!(Arc::ptr_eq(&repo1.cache, &repo2.cache));
assert!(Arc::ptr_eq(&repo1.cache, &root_repo.cache));
assert_eq!(NEXT_POOL_ID.load(Ordering::SeqCst), 2, "DbPool factory should execute once");
assert_eq!(NEXT_CACHE_ID.load(Ordering::SeqCst), 2, "RedisCache factory should execute once");
assert_eq!(REPO_BUILDS.load(Ordering::SeqCst), 3);
assert_eq!(ORDER_BUILDS.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn async_build_validates_sync_dependencies() {
struct A;
struct B;
let result = ServiceCollection::new()
.singleton(|r| {
let _ = r.get_any(std::any::type_name::<B>());
Arc::new(A)
})
.singleton(|r| {
let _ = r.get_any(std::any::type_name::<A>());
Arc::new(B)
})
.async_singleton(|_| {
Box::pin(async { Arc::new(DbPool { conn_str: "ok".into(), pool_id: 0 }) })
})
.build_async()
.await;
assert!(result.is_err());
if let Err(RdiError::CircularDependency(cycle)) = &result {
assert!(cycle.contains("A") || cycle.contains("B"));
} else {
panic!("expected CircularDependency, got {:?}", result.err());
}
}
#[tokio::test]
async fn async_captive_dependency_rejected() {
struct ScopedDep;
struct BadSingleton {
_scoped: Arc<ScopedDep>,
}
let result = ServiceCollection::new()
.scoped(|_| Arc::new(ScopedDep))
.singleton(|r| {
let dep: Arc<ScopedDep> = r
.get_any(std::any::type_name::<ScopedDep>())
.and_then(|a| a.downcast::<Arc<ScopedDep>>().ok().map(|d| Arc::clone(&*d)))
.unwrap();
Arc::new(BadSingleton { _scoped: dep })
})
.async_singleton(|_| {
Box::pin(async { Arc::new(DbPool { conn_str: "ok".into(), pool_id: 0 }) })
})
.build_async()
.await;
assert!(result.is_err());
if let Err(RdiError::SingletonDependsOnScoped { .. }) = &result {
} else {
panic!("expected SingletonDependsOnScoped, got {:?}", result.err());
}
}
#[tokio::test]
async fn async_singleton_can_depend_on_sync_singleton() {
let provider = ServiceCollection::new()
.singleton(|_| {
Arc::new(AppConfig {
max_retries: 5,
payment_provider: "alipay".into(),
})
})
.async_singleton(|p: Arc<ServiceProvider>| {
Box::pin(async move {
let config: Arc<AppConfig> = p.get().unwrap();
Arc::new(DbPool {
conn_str: format!("pg://host?retries={}", config.max_retries),
pool_id: 42,
})
})
})
.build_async()
.await
.unwrap();
let pool: Arc<DbPool> = provider.get_async().await.unwrap();
assert_eq!(pool.conn_str, "pg://host?retries=5");
}
#[tokio::test]
async fn async_multiple_scopes_independent() {
static NEXT: AtomicU64 = AtomicU64::new(0);
struct ScopedCounter(u64);
let provider = ServiceCollection::new()
.async_scoped(|_| {
let id = NEXT.fetch_add(1, Ordering::SeqCst);
Box::pin(async move { Arc::new(ScopedCounter(id)) })
})
.build_async()
.await
.unwrap();
let scope1 = provider.scope();
let c1: Arc<ScopedCounter> = scope1.get_async().await.unwrap();
let c1b: Arc<ScopedCounter> = scope1.get_async().await.unwrap();
let scope2 = provider.scope();
let c2: Arc<ScopedCounter> = scope2.get_async().await.unwrap();
assert!(!Arc::ptr_eq(&c1, &c2));
assert_ne!(c1.0, c2.0);
assert!(Arc::ptr_eq(&c1, &c1b));
assert_eq!(c1.0, c1b.0);
assert_eq!(NEXT.load(Ordering::SeqCst), 2);
}