use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use crate::core::error::KitError;
use crate::core::meta::AsyncAutoBuilder;
use super::async_typemap::AsyncTypeMap;
use super::graph::{DependencyGraph, GraphError, ModuleEntry};
pub struct Unbuilt;
pub struct Ready;
#[allow(
clippy::type_complexity,
reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async dispatch type; mirrors AsyncAutoBuilder::build"
)]
pub(crate) type AsyncBuildFn = Box<
dyn for<'a> FnOnce(
&'a AsyncKit,
) -> Pin<
Box<
dyn Future<
Output = Result<
Box<dyn Any + Send + Sync>,
Box<dyn std::error::Error + Send + 'static>,
>,
> + Send
+ 'a,
>,
> + Send
+ Sync,
>;
pub struct AsyncKit<S = Unbuilt> {
builders: Arc<RwLock<HashMap<TypeId, AsyncBuildFn>>>,
graph: DependencyGraph,
configs: AsyncTypeMap,
capabilities: AsyncTypeMap,
_state: PhantomData<S>,
}
impl AsyncKit {
#[must_use]
pub fn new() -> Self {
AsyncKit {
builders: Arc::new(RwLock::new(HashMap::new())),
graph: DependencyGraph::new(),
configs: AsyncTypeMap::new(),
capabilities: AsyncTypeMap::new(),
_state: PhantomData,
}
}
pub fn register<M: AsyncAutoBuilder>(&mut self) -> Result<(), KitError> {
let entry = ModuleEntry {
type_id: TypeId::of::<M>(),
name: M::NAME,
dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
};
self.graph
.add(entry)
.map_err(|name| KitError::AlreadyRegistered { module: name })?;
let build_fn: AsyncBuildFn = Box::new(|kit| {
Box::pin(async move {
let cap = M::build(kit)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
Ok(Box::new(cap) as Box<dyn Any + Send + Sync>)
})
});
self.builders
.write()
.expect(
"AsyncKit builders lock poisoned: another thread panicked while holding the lock",
)
.insert(TypeId::of::<M>(), build_fn);
Ok(())
}
pub fn set_config<C: Clone + Send + Sync + 'static>(&self, config: C) {
self.configs.insert(config);
}
pub async fn build(self) -> Result<AsyncKit<Ready>, KitError> {
let sorted = match self.graph.validate() {
Ok(sorted) => sorted,
Err(GraphError::DependencyMissing { module, missing }) => {
return Err(KitError::DependencyMissing { module, missing });
}
Err(GraphError::CycleDetected { cycle }) => {
return Err(KitError::CycleDetected { cycle });
}
};
for type_id in &sorted {
let build_fn = self
.builders
.write()
.expect("AsyncKit builders lock poisoned: another thread panicked while holding the lock")
.remove(type_id)
.ok_or_else(|| KitError::MissingCapability {
key: self.module_name(*type_id),
})?;
let module_name = self.module_name(*type_id);
let fut = build_fn(&self);
match fut.await {
Ok(boxed) => self.capabilities.insert_boxed(*type_id, boxed),
Err(e) => {
return Err(KitError::BuildFailed {
context: module_name,
source: e,
});
}
}
}
Ok(AsyncKit {
builders: self.builders,
graph: self.graph,
configs: self.configs,
capabilities: self.capabilities,
_state: PhantomData::<Ready>,
})
}
fn module_name(&self, type_id: TypeId) -> &'static str {
self.graph.name_of(type_id).unwrap_or("<unknown>")
}
}
impl<S> AsyncKit<S> {
pub fn config<C: Clone + Send + Sync + 'static>(&self) -> Result<C, KitError> {
self.configs
.get_cloned::<C>()
.ok_or(KitError::MissingConfig {
key: std::any::type_name::<C>(),
})
}
pub fn require<M: AsyncAutoBuilder>(&self) -> Result<M::Capability, KitError> {
let type_id = TypeId::of::<M>();
self.capabilities
.get_cloned_by_type_id::<M::Capability>(type_id)
.ok_or(KitError::MissingCapability { key: M::NAME })
}
}
impl AsyncKit<Ready> {
#[must_use]
pub fn optional<M: AsyncAutoBuilder>(&self) -> Option<M::Capability> {
let type_id = TypeId::of::<M>();
self.capabilities
.get_cloned_by_type_id::<M::Capability>(type_id)
}
#[must_use]
pub fn contains<M: AsyncAutoBuilder>(&self) -> bool {
self.capabilities.contains_by_type_id(TypeId::of::<M>())
}
#[must_use]
pub fn contains_config<C: Clone + Send + Sync + 'static>(&self) -> bool {
self.configs.contains::<C>()
}
}
impl Default for AsyncKit {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for AsyncKit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AsyncKit<Unbuilt>")
.field("modules", &self.graph.entries().len())
.field("configs", &self.configs.len())
.finish()
}
}
impl std::fmt::Debug for AsyncKit<Ready> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AsyncKit<Ready>")
.field("modules", &self.graph.entries().len())
.field("configs", &self.configs.len())
.finish()
}
}
#[cfg(all(test, feature = "async"))]
mod tests {
use super::{AsyncKit, Ready};
use crate::core::error::KitError;
use crate::core::meta::{AsyncAutoBuilder, ModuleMeta};
use crate::test_helpers::{block_on, MockError};
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
struct MockCap {
value: i32,
}
struct MockModule;
impl ModuleMeta for MockModule {
const NAME: &'static str = "mock-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for MockModule {
type Capability = Arc<MockCap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(MockCap { value: 42 })) })
}
}
struct MockErrModule;
impl ModuleMeta for MockErrModule {
const NAME: &'static str = "mock-err-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for MockErrModule {
type Capability = Arc<MockCap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Err(MockError::Failed("intentional build failure".to_string())) })
}
}
struct MockCounterModule;
impl ModuleMeta for MockCounterModule {
const NAME: &'static str = "mock-counter-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for MockCounterModule {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
Box::pin(async move {
let counter = kit
.config::<Arc<AtomicUsize>>()
.map_err(|e| MockError::Failed(e.to_string()))?;
counter.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(()))
})
}
}
struct MissingDep;
struct MockMissingDepModule;
impl ModuleMeta for MockMissingDepModule {
const NAME: &'static str = "mock-missing-dep-module";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("missing-dep", TypeId::of::<MissingDep>())];
DEPS
}
}
impl AsyncAutoBuilder for MockMissingDepModule {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
struct MockCycleA;
impl ModuleMeta for MockCycleA {
const NAME: &'static str = "mock-cycle-a";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-cycle-b", TypeId::of::<MockCycleB>())];
DEPS
}
}
impl AsyncAutoBuilder for MockCycleA {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
struct MockCycleB;
impl ModuleMeta for MockCycleB {
const NAME: &'static str = "mock-cycle-b";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-cycle-a", TypeId::of::<MockCycleA>())];
DEPS
}
}
impl AsyncAutoBuilder for MockCycleB {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
#[test]
fn async_kit_new_returns_unbuilt_state() {
let kit = AsyncKit::new();
assert!(kit.builders.read().expect("lock poisoned").is_empty());
assert!(kit.graph.entries().is_empty());
assert_eq!(kit.configs.len(), 0);
assert_eq!(kit.capabilities.len(), 0);
}
#[test]
fn async_kit_register_stores_builder() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
assert_eq!(kit.builders.read().expect("lock poisoned").len(), 1);
assert_eq!(kit.graph.entries().len(), 1);
}
#[test]
fn async_kit_register_duplicate_returns_error() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("first register should succeed");
let err = kit
.register::<MockModule>()
.expect_err("duplicate register should error");
assert!(
matches!(
err,
KitError::AlreadyRegistered {
module: "mock-module"
}
),
"expected AlreadyRegistered, got {err:?}"
);
}
#[test]
fn async_kit_set_config_stores_value() {
let kit = AsyncKit::new();
kit.set_config(42i32);
assert_eq!(kit.config::<i32>().expect("config should exist"), 42);
}
#[test]
fn async_kit_set_config_overwrite() {
let kit = AsyncKit::new();
kit.set_config(1i32);
kit.set_config(2i32);
assert_eq!(kit.config::<i32>().expect("config should exist"), 2);
}
#[test]
fn async_kit_config_missing_returns_error() {
let kit = AsyncKit::new();
let err = kit
.config::<u64>()
.expect_err("missing config should error");
assert!(
matches!(err, KitError::MissingConfig { .. }),
"expected MissingConfig, got {err:?}"
);
}
#[test]
fn async_kit_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AsyncKit>();
}
#[test]
fn kit_error_is_send() {
fn assert_send<T: Send>() {}
assert_send::<KitError>();
}
#[test]
fn async_kit_build_result_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Result<AsyncKit<Ready>, KitError>>();
}
#[test]
fn async_kit_build_returns_ready_state() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
let built: AsyncKit<Ready> = block_on(kit.build()).expect("build should succeed");
let _ = built;
}
#[test]
fn async_kit_build_constructs_capability() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
let built = block_on(kit.build()).expect("build should succeed");
let cap = built
.capabilities
.get_cloned_by_type_id::<Arc<MockCap>>(TypeId::of::<MockModule>())
.expect("capability should be stored after build");
assert_eq!(cap.value, 42);
}
#[test]
fn async_kit_build_multiple_modules_in_topo_order() {
let mut kit = AsyncKit::new();
kit.set_config(Arc::new(AtomicUsize::new(0)));
kit.register::<MockModule>().expect("register module A");
kit.register::<MockCounterModule>()
.expect("register module B");
let built = block_on(kit.build()).expect("build should succeed");
assert_eq!(
built.capabilities.len(),
2,
"capabilities should contain both modules"
);
}
#[test]
fn async_kit_build_missing_dependency_returns_error() {
let mut kit = AsyncKit::new();
kit.register::<MockMissingDepModule>()
.expect("register should succeed (declares missing dep)");
let err =
block_on(kit.build()).expect_err("build should fail when a dependency is unregistered");
assert!(
matches!(
err,
KitError::DependencyMissing {
module: "mock-missing-dep-module",
missing: "missing-dep"
}
),
"expected DependencyMissing, got {err:?}"
);
}
#[test]
fn async_kit_build_cycle_returns_error() {
let mut kit = AsyncKit::new();
kit.register::<MockCycleA>().expect("register cycle A");
kit.register::<MockCycleB>().expect("register cycle B");
let err = block_on(kit.build()).expect_err("build should fail on cyclic dependency graph");
assert!(
matches!(err, KitError::CycleDetected { .. }),
"expected CycleDetected, got {err:?}"
);
}
#[test]
fn async_kit_build_calls_async_build_fn() {
let counter = Arc::new(AtomicUsize::new(0));
let mut kit = AsyncKit::new();
kit.set_config(Arc::clone(&counter));
kit.register::<MockCounterModule>()
.expect("register should succeed");
let _built = block_on(kit.build()).expect("build should succeed");
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"async build callback should have executed exactly once"
);
}
#[test]
fn async_kit_build_propagates_build_error() {
let mut kit = AsyncKit::new();
kit.register::<MockErrModule>()
.expect("register should succeed");
let err =
block_on(kit.build()).expect_err("build should fail when module build returns Err");
assert!(
matches!(
err,
KitError::BuildFailed {
context: "mock-err-module",
..
}
),
"expected BuildFailed for mock-err-module, got {err:?}"
);
}
#[test]
fn async_kit_ready_require_returns_capability() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
let built = block_on(kit.build()).expect("build should succeed");
let cap = built
.require::<MockModule>()
.expect("require on built module should succeed");
assert_eq!(cap.value, 42);
}
#[test]
fn async_kit_ready_require_missing_returns_error() {
let kit = AsyncKit::new();
let built = block_on(kit.build()).expect("empty build should succeed");
let err = built
.require::<MockModule>()
.expect_err("require on unbuilt module should error");
assert!(
matches!(err, KitError::MissingCapability { key: "mock-module" }),
"expected MissingCapability for mock-module, got {err:?}"
);
}
#[test]
fn async_kit_ready_optional_returns_some_for_built() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
let built = block_on(kit.build()).expect("build should succeed");
let cap = built
.optional::<MockModule>()
.expect("optional on built module should return Some");
assert_eq!(cap.value, 42);
}
#[test]
fn async_kit_ready_optional_returns_none_for_unbuilt() {
let kit = AsyncKit::new();
let built = block_on(kit.build()).expect("empty build should succeed");
assert!(
built.optional::<MockModule>().is_none(),
"optional on unbuilt module should return None"
);
}
#[test]
fn async_kit_ready_contains_returns_true_for_built() {
let mut kit = AsyncKit::new();
kit.register::<MockModule>()
.expect("register should succeed");
let built = block_on(kit.build()).expect("build should succeed");
assert!(
built.contains::<MockModule>(),
"contains should return true for built module"
);
}
#[test]
fn async_kit_ready_contains_returns_false_for_unbuilt() {
let kit = AsyncKit::new();
let built = block_on(kit.build()).expect("empty build should succeed");
assert!(
!built.contains::<MockModule>(),
"contains should return false for unbuilt module"
);
}
#[test]
fn async_kit_ready_contains_config_returns_true() {
let kit = AsyncKit::new();
kit.set_config(42i32);
let built = block_on(kit.build()).expect("build should succeed");
assert!(
built.contains_config::<i32>(),
"contains_config should return true for stored i32 config"
);
}
#[test]
fn async_kit_ready_contains_config_returns_false() {
let kit = AsyncKit::new();
kit.set_config(42i32);
let built = block_on(kit.build()).expect("build should succeed");
assert!(
!built.contains_config::<u64>(),
"contains_config should return false for absent u64 config"
);
}
impl From<KitError> for MockError {
fn from(e: KitError) -> Self {
MockError::Failed(e.to_string())
}
}
#[derive(Debug, Clone, PartialEq)]
struct Bcap {
n: i32,
}
#[derive(Debug, Clone, PartialEq)]
struct Acap {
b_val: i32,
}
struct MockBModule;
impl ModuleMeta for MockBModule {
const NAME: &'static str = "mock-b";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for MockBModule {
type Capability = Arc<Bcap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(Bcap { n: 42 })) })
}
}
struct MockAModule;
impl ModuleMeta for MockAModule {
const NAME: &'static str = "mock-a";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-b", TypeId::of::<MockBModule>())];
DEPS
}
}
impl AsyncAutoBuilder for MockAModule {
type Capability = Arc<Acap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
Box::pin(async move {
let b_cap: Arc<Bcap> = kit.require::<MockBModule>()?;
Ok(Arc::new(Acap { b_val: b_cap.n }))
})
}
}
#[derive(Debug, Clone, PartialEq)]
struct Ccap {
v: i32,
build_order: usize,
}
#[derive(Debug, Clone, PartialEq)]
struct ChainBcap {
c_val: i32,
build_order: usize,
}
#[derive(Debug, Clone, PartialEq)]
struct ChainAcap {
b_val: i32,
build_order: usize,
}
struct MockCModule;
impl ModuleMeta for MockCModule {
const NAME: &'static str = "mock-c";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for MockCModule {
type Capability = Arc<Ccap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
Box::pin(async move {
let counter = kit.config::<Arc<AtomicUsize>>()?;
let order = counter.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(Ccap {
v: 100,
build_order: order + 1,
}))
})
}
}
struct MockChainBModule;
impl ModuleMeta for MockChainBModule {
const NAME: &'static str = "mock-chain-b";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-c", TypeId::of::<MockCModule>())];
DEPS
}
}
impl AsyncAutoBuilder for MockChainBModule {
type Capability = Arc<ChainBcap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
Box::pin(async move {
let c_cap: Arc<Ccap> = kit.require::<MockCModule>()?;
let counter = kit.config::<Arc<AtomicUsize>>()?;
let order = counter.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(ChainBcap {
c_val: c_cap.v,
build_order: order + 1,
}))
})
}
}
struct MockChainAModule;
impl ModuleMeta for MockChainAModule {
const NAME: &'static str = "mock-chain-a";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-chain-b", TypeId::of::<MockChainBModule>())];
DEPS
}
}
impl AsyncAutoBuilder for MockChainAModule {
type Capability = Arc<ChainAcap>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
Box::pin(async move {
let b_cap: Arc<ChainBcap> = kit.require::<MockChainBModule>()?;
let counter = kit.config::<Arc<AtomicUsize>>()?;
let order = counter.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(ChainAcap {
b_val: b_cap.c_val,
build_order: order + 1,
}))
})
}
}
struct MockCycleA3;
impl ModuleMeta for MockCycleA3 {
const NAME: &'static str = "mock-cycle-a3";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-cycle-b3", TypeId::of::<MockCycleB3>())];
DEPS
}
}
impl AsyncAutoBuilder for MockCycleA3 {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
struct MockCycleB3;
impl ModuleMeta for MockCycleB3 {
const NAME: &'static str = "mock-cycle-b3";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-cycle-c3", TypeId::of::<MockCycleC3>())];
DEPS
}
}
impl AsyncAutoBuilder for MockCycleB3 {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
struct MockCycleC3;
impl ModuleMeta for MockCycleC3 {
const NAME: &'static str = "mock-cycle-c3";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: &[(&str, TypeId)] = &[("mock-cycle-a3", TypeId::of::<MockCycleA3>())];
DEPS
}
}
impl AsyncAutoBuilder for MockCycleC3 {
type Capability = Arc<()>;
type Error = MockError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
{
let _ = kit;
Box::pin(async move { Ok(Arc::new(())) })
}
}
#[test]
fn async_kit_di_dependency_built_before_dependent() {
let mut kit = AsyncKit::new();
kit.register::<MockBModule>().expect("register B");
kit.register::<MockAModule>().expect("register A");
let built = block_on(kit.build()).expect("build should succeed");
let a_cap = built
.require::<MockAModule>()
.expect("A's cap should be built");
assert_eq!(
a_cap.b_val, 42,
"A's cap must contain B's n=42 — proves B built before A"
);
}
#[test]
fn async_kit_di_require_returns_dependency_capability() {
let mut kit = AsyncKit::new();
kit.register::<MockBModule>().expect("register B");
kit.register::<MockAModule>().expect("register A");
let built = block_on(kit.build()).expect("build should succeed");
let b_cap = built.require::<MockBModule>().expect("B's cap");
let a_cap = built.require::<MockAModule>().expect("A's cap");
assert_eq!(b_cap.n, 42);
assert_eq!(
a_cap.b_val, 42,
"A's cap must contain B's n=42 — require worked inside build callback"
);
}
#[test]
fn async_kit_di_missing_dependency_returns_error() {
let mut kit = AsyncKit::new();
kit.register::<MockAModule>()
.expect("register A only (B missing)");
let err =
block_on(kit.build()).expect_err("build must fail when declared dep is unregistered");
assert!(
matches!(
err,
KitError::DependencyMissing {
module: "mock-a",
missing: "mock-b"
}
),
"expected DependencyMissing {{ module: \"mock-a\", missing: \"mock-b\" }}, got {err:?}"
);
}
#[test]
fn async_kit_di_three_node_cycle_returns_error() {
let mut kit = AsyncKit::new();
kit.register::<MockCycleA3>().expect("register cycle A3");
kit.register::<MockCycleB3>().expect("register cycle B3");
kit.register::<MockCycleC3>().expect("register cycle C3");
let err = block_on(kit.build()).expect_err("build must fail on 3-node cycle");
assert!(
matches!(err, KitError::CycleDetected { .. }),
"expected CycleDetected for 3-node cycle, got {err:?}"
);
}
#[test]
fn async_kit_di_transitive_dependency_chain() {
let mut kit = AsyncKit::new();
kit.set_config(Arc::new(AtomicUsize::new(0)));
kit.register::<MockCModule>().expect("register C");
kit.register::<MockChainBModule>()
.expect("register chain-B");
kit.register::<MockChainAModule>()
.expect("register chain-A");
let built = block_on(kit.build()).expect("build should succeed");
let c_cap = built.require::<MockCModule>().expect("C's cap");
let b_cap = built.require::<MockChainBModule>().expect("chain-B's cap");
let a_cap = built.require::<MockChainAModule>().expect("chain-A's cap");
assert_eq!(c_cap.build_order, 1, "C should be built first");
assert_eq!(b_cap.build_order, 2, "B should be built second");
assert_eq!(a_cap.build_order, 3, "A should be built third");
assert_eq!(c_cap.v, 100);
assert_eq!(
b_cap.c_val, 100,
"B's cap must contain C's v=100 — require::<MockCModule>() worked in B's build"
);
assert_eq!(
a_cap.b_val, 100,
"A's cap must transitively contain C's v=100 — transitive DI worked"
);
}
}