use injectable_rs::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[injectable]
#[derive(Default, Clone)]
pub struct Config;
#[injectable]
#[derive(Default, Clone)]
pub struct Database;
#[injectable]
#[derive(Default, Clone)]
pub struct Cache;
#[injectable]
pub struct UserService {
db: Inject<Database>,
cache: Inject<Cache>,
}
#[injectable]
pub struct Repository {
db: Inject<Database>,
}
#[injectable]
pub struct OwnedService {
#[injectable(inject)]
db: Arc<Database>,
#[injectable(inject)]
cache: Arc<Cache>,
}
#[injectable]
pub struct MixedService {
db: Inject<Database>, #[injectable(inject)]
config: Arc<Config>,
}
#[derive(Default)]
pub struct ConfigWithPort {
pub port: u16,
}
#[injectable]
impl ConfigWithPort {
#[injectable(ctor)]
fn new() -> Self {
Self::default()
}
}
#[tokio::test]
async fn test_field_injection_with_inject_fields() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<UserService>().await;
assert!(
service.is_ok(),
"should resolve UserService with Inject fields"
);
}
#[tokio::test]
async fn test_field_injection_single_dependency() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let repo = container.resolve::<Repository>().await;
assert!(
repo.is_ok(),
"should resolve Repository with Inject<Database>"
);
}
#[tokio::test]
async fn test_field_injection_owned_fields() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<OwnedService>().await;
assert!(
service.is_ok(),
"should resolve OwnedService with bare Injectable fields"
);
}
#[tokio::test]
async fn test_field_injection_mixed_fields() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<MixedService>().await;
assert!(
service.is_ok(),
"should resolve MixedService with mixed field types"
);
}
#[tokio::test]
async fn test_field_injection_shared_references() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service1 = container
.resolve::<Repository>()
.await
.expect("resolve repo");
let service2 = container
.resolve::<Repository>()
.await
.expect("resolve repo");
let _db1 = &*service1.db;
let _db2 = &*service2.db;
}
#[tokio::test]
async fn test_unit_struct_injection() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let config = container.resolve::<Config>().await;
assert!(config.is_ok(), "should resolve unit struct Config");
}
#[tokio::test]
async fn test_default_constructor_injection() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let config = container.resolve::<ConfigWithPort>().await;
assert!(config.is_ok(), "should resolve ConfigWithPort via default");
assert_eq!(config.unwrap().port, 0, "default port should be 0");
}
#[tokio::test]
async fn test_container_build() {
let container = Container::builder()
.build()
.await
.expect("container should build successfully");
let _ctx = container.context();
}
#[tokio::test]
async fn test_resolve_leaf_type() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let config = container.resolve::<Config>().await;
assert!(config.is_ok(), "should resolve Config");
}
#[tokio::test]
async fn test_resolve_multiple_types() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let config = container
.resolve::<Config>()
.await
.expect("should resolve Config");
let db = container
.resolve::<Database>()
.await
.expect("should resolve Database");
let _ = config;
let _ = db;
}
#[tokio::test]
async fn test_register_external_sync() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok(reqwest::Client::new())))
.build()
.await
.expect("container should build");
let client = container.resolve_external::<reqwest::Client>().await;
assert!(client.is_ok(), "should resolve external reqwest::Client");
let _client = client.unwrap();
}
#[tokio::test]
#[ignore] async fn test_register_external_async() {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let container = Container::builder()
.register("", DynProvider::new(move || {
let url = database_url.clone();
async move {
sqlx::SqlitePool::connect(&url).await.map_err(|e| {
InjectableError::ConstructionFailed {
type_name: "sqlx::SqlitePool",
reason: e.to_string(),
}
})
}
}))
.build()
.await
.expect("container should build");
let pool = container.resolve_external::<sqlx::SqlitePool>().await;
assert!(pool.is_ok(), "should resolve external sqlx::SqlitePool");
}
#[tokio::test]
#[ignore] async fn test_register_external_with_ctx_dependencies() {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let container = Container::builder()
.register("", DynProvider::with_ctx(move |ctx| {
let url = database_url.clone();
async move {
let _config: Inject<Config> = ctx.extract().await?;
sqlx::SqlitePool::connect(&url).await.map_err(|e| {
InjectableError::ConstructionFailed {
type_name: "sqlx::SqlitePool",
reason: e.to_string(),
}
})
}
}))
.build()
.await
.expect("container should build");
let pool = container.resolve_external::<sqlx::SqlitePool>().await;
assert!(
pool.is_ok(),
"should resolve sqlx::SqlitePool that depends on Config"
);
}
#[tokio::test]
async fn test_register_multiple_external_types() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok(reqwest::Client::new())))
.register("", DynProvider::sync(|| {
Ok("redis://localhost:6379".to_string())
}))
.build()
.await
.expect("container should build");
let _client = container
.resolve_external::<reqwest::Client>()
.await
.expect("reqwest::Client");
let cache = container
.resolve_external::<String>()
.await
.expect("String");
assert_eq!(cache, "redis://localhost:6379");
}
#[tokio::test]
async fn test_register_external_chain_dependencies() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok(reqwest::Client::new())))
.register("", DynProvider::with_ctx(|ctx| async move {
let _http = ctx.resolve_external::<reqwest::Client>().await?;
Ok("cache://connected".to_string())
}))
.build()
.await
.expect("container should build");
let cache = container.resolve_external::<String>().await;
assert!(
cache.is_ok(),
"should resolve String that depends on reqwest::Client"
);
let cache = cache.unwrap();
assert_eq!(cache, "cache://connected");
}
#[tokio::test]
async fn test_resolve_external_missing() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let result = container.resolve_external::<reqwest::Client>().await;
assert!(
result.is_err(),
"should fail for unregistered external type"
);
let err = result.unwrap_err();
assert!(
matches!(err, InjectableError::MissingDependency { .. }),
"should be MissingDependency error, got: {err}"
);
}
#[tokio::test]
async fn test_register_overwrites_previous() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok("first".to_string())))
.register("", DynProvider::sync(|| Ok("second".to_string())))
.build()
.await
.expect("container should build");
let value = container
.resolve_external::<String>()
.await
.expect("should resolve");
assert_eq!(value, "second", "second registration should win");
}
#[tokio::test]
async fn test_multiple_tokens_same_type() {
let container = Container::builder()
.register("primary", DynProvider::sync(|| Ok(1_u32)))
.register("replica", DynProvider::sync(|| Ok(2_u32)))
.register("analytics", DynProvider::sync(|| Ok(3_u32)))
.build()
.await
.expect("container should build");
let primary: u32 = container.resolve_external_with_token("primary").await.unwrap();
let replica: u32 = container.resolve_external_with_token("replica").await.unwrap();
let analytics: u32 = container.resolve_external_with_token("analytics").await.unwrap();
assert_eq!(primary, 1);
assert_eq!(replica, 2);
assert_eq!(analytics, 3);
}
#[tokio::test]
async fn test_default_token_does_not_resolve_named() {
let container = Container::builder()
.register("primary", DynProvider::sync(|| Ok(42_u32)))
.build()
.await
.expect("container should build");
let result = container.resolve_external::<u32>().await;
assert!(result.is_err(), "default token should not resolve named provider");
}
#[tokio::test]
async fn test_named_and_default_tokens_independent() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok(0_u32))) .register("primary", DynProvider::sync(|| Ok(1_u32))) .build()
.await
.expect("container should build");
let default_val: u32 = container.resolve_external().await.unwrap();
let named_val: u32 = container.resolve_external_with_token("primary").await.unwrap();
assert_eq!(default_val, 0);
assert_eq!(named_val, 1);
}
#[tokio::test]
async fn test_try_resolve_external_with_token_returns_none_for_missing() {
let container = Container::builder()
.register("primary", DynProvider::sync(|| Ok(1_u32)))
.build()
.await
.unwrap();
let result = container.try_resolve_external_with_token::<u32>("missing").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_different_tokens_not_duplicate() {
Container::builder()
.register("a", DynProvider::sync(|| Ok(1_u8)))
.register("b", DynProvider::sync(|| Ok(2_u8)))
.register("c", DynProvider::sync(|| Ok(3_u8)))
.build()
.await
.expect("different tokens for same type should not be a duplicate");
}
#[tokio::test]
async fn test_mixed_owned_and_external_resolution() {
let container = Container::builder()
.register("", DynProvider::sync(|| Ok(reqwest::Client::new())))
.build()
.await
.expect("container should build");
let config = container
.resolve::<Config>()
.await
.expect("should resolve Config");
let _client = container
.resolve_external::<reqwest::Client>()
.await
.expect("should resolve reqwest::Client");
let _ = config;
}
#[test]
fn test_dyn_provider_sync() {
let _provider: DynProvider<reqwest::Client> = DynProvider::sync(|| Ok(reqwest::Client::new()));
}
#[test]
fn test_dyn_provider_async() {
let _provider: DynProvider<sqlx::SqlitePool> = DynProvider::new(|| async {
let url = std::env::var("DATABASE_URL").unwrap_or_default();
sqlx::SqlitePool::connect(&url)
.await
.map_err(|e| InjectableError::ConstructionFailed {
type_name: "sqlx::SqlitePool",
reason: e.to_string(),
})
});
}
#[test]
fn test_dyn_provider_with_ctx() {
let _provider: DynProvider<sqlx::SqlitePool> = DynProvider::with_ctx(|_ctx| async {
let url = std::env::var("DATABASE_URL").unwrap_or_default();
sqlx::SqlitePool::connect(&url)
.await
.map_err(|e| InjectableError::ConstructionFailed {
type_name: "sqlx::SqlitePool",
reason: e.to_string(),
})
});
}
#[test]
fn test_registry_empty() {
let registry = ProviderRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(!registry.has::<reqwest::Client>());
}
#[test]
fn test_registry_has() {
let mut registry = ProviderRegistry::new();
registry.register("", DynProvider::sync(|| Ok(reqwest::Client::new())));
assert!(registry.has::<reqwest::Client>());
assert!(!registry.has::<sqlx::SqlitePool>());
assert_eq!(registry.len(), 1);
}
#[test]
fn test_registry_debug() {
let registry = ProviderRegistry::new();
let debug = format!("{registry:?}");
assert!(debug.contains("ProviderRegistry"));
}
#[test]
fn test_inject_wrapping() {
let value = Arc::new(Config);
let inject = Inject::new(value.clone());
let inner = inject.into_inner();
assert!(Arc::ptr_eq(&value, &inner));
}
#[test]
fn test_inject_from_arc() {
let arc: Arc<Config> = Arc::new(Config);
let inject: Inject<Config> = Inject::from(arc);
let _ = inject;
}
#[test]
fn test_inject_into_arc() {
let inject = Inject::new(Arc::new(Config));
let arc: Arc<Config> = inject.into();
let _ = arc;
}
#[test]
fn test_inject_clone() {
let inject = Inject::new(Arc::new(Config));
let cloned = inject.clone();
let _ = cloned;
}
#[test]
fn test_inject_destructure_pub_field() {
let inject = Inject::new(Arc::new(Config));
let Inject(arc) = inject;
let _config = &*arc;
}
#[tokio::test]
async fn test_inject_destructure_after_extract() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let Inject(db_arc) = Inject::<Database>::extract(container.context())
.await
.expect("should extract Database");
let _ = &*db_arc;
}
#[tokio::test]
async fn test_inject_destructure_multiple() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let Inject(db) = Inject::<Database>::extract(container.context())
.await
.unwrap();
let Inject(cache) = Inject::<Cache>::extract(container.context()).await.unwrap();
let _ = (&*db, &*cache);
}
#[test]
fn test_graph_empty() {
let graph = DependencyGraph::empty();
assert!(graph.is_empty());
assert!(graph.validate().is_ok());
}
#[test]
fn test_graph_single_leaf() {
let graph = DependencyGraph::new(vec![GraphNode::leaf("Config")]);
assert!(!graph.is_empty());
assert!(graph.validate().is_ok());
}
#[test]
fn test_graph_simple_dependency() {
let graph = DependencyGraph::new(vec![
GraphNode::new("UserService", &["Database"]),
GraphNode::leaf("Database"),
]);
assert!(graph.validate().is_ok());
}
#[test]
fn test_graph_diamond_dependency() {
let graph = DependencyGraph::new(vec![
GraphNode::new("UserService", &["Database", "Cache"]),
GraphNode::new("Database", &["Config"]),
GraphNode::new("Cache", &["Config"]),
GraphNode::leaf("Config"),
]);
assert!(graph.validate().is_ok());
}
#[test]
fn test_graph_circular_dependency_detection() {
let graph = DependencyGraph::new(vec![
GraphNode::new("UserService", &["AuthService"]),
GraphNode::new("AuthService", &["SessionManager"]),
GraphNode::new("SessionManager", &["UserService"]),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let has_cycle = errors.iter().any(|e| {
matches!(
e,
injectable_rs_graph::ValidationError::CircularDependency { .. }
)
});
assert!(has_cycle, "should detect circular dependency");
}
#[test]
fn test_graph_missing_dependency() {
let graph = DependencyGraph::new(vec![GraphNode::new("UserService", &["Database"])]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let has_missing = errors.iter().any(|e| {
matches!(
e,
injectable_rs_graph::ValidationError::MissingDependency { .. }
)
});
assert!(has_missing, "should detect missing dependency");
}
#[test]
fn test_graph_duplicate_node() {
let graph = DependencyGraph::new(vec![GraphNode::leaf("Config"), GraphNode::leaf("Config")]);
let result = graph.validate();
assert!(result.is_err());
}
#[test]
fn test_graph_topological_order() {
let graph = DependencyGraph::new(vec![
GraphNode::new("UserService", &["Database", "Cache"]),
GraphNode::new("Database", &["Config"]),
GraphNode::new("Cache", &["Config"]),
GraphNode::leaf("Config"),
]);
let order = graph
.topological_order()
.expect("valid graph should have topological order");
let config_pos = order.iter().position(|&n| n == "Config").unwrap();
let db_pos = order.iter().position(|&n| n == "Database").unwrap();
let cache_pos = order.iter().position(|&n| n == "Cache").unwrap();
let user_pos = order.iter().position(|&n| n == "UserService").unwrap();
assert!(config_pos < db_pos);
assert!(config_pos < cache_pos);
assert!(db_pos < user_pos);
assert!(cache_pos < user_pos);
}
#[test]
fn test_graph_destruction_order() {
let graph = DependencyGraph::new(vec![
GraphNode::new("UserService", &["Database"]),
GraphNode::leaf("Database"),
]);
let order = graph
.destruction_order()
.expect("valid graph should have destruction order");
let user_pos = order.iter().position(|&n| n == "UserService").unwrap();
let db_pos = order.iter().position(|&n| n == "Database").unwrap();
assert!(
user_pos < db_pos,
"UserService should be destroyed before Database"
);
}
#[test]
fn test_injectable_error_display() {
let err = InjectableError::CircularDependency {
type_name: "UserService",
chain: vec![
"UserService".into(),
"AuthService".into(),
"UserService".into(),
],
};
let msg = err.to_string();
assert!(msg.contains("circular dependency"));
}
#[test]
fn test_missing_dependency_error_display() {
let err = InjectableError::MissingDependency {
type_name: "Database",
};
let msg = err.to_string();
assert!(msg.contains("missing dependency"));
}
#[test]
fn test_construction_failed_error() {
let err = InjectableError::ConstructionFailed {
type_name: "Service",
reason: "timeout".into(),
};
let msg = err.to_string();
assert!(msg.contains("construction"));
}
#[test]
fn test_validation_error_display() {
let err = injectable_rs_graph::ValidationError::CircularDependency {
chain: vec!["A".into(), "B".into(), "A".into()],
};
let msg = err.to_string();
assert!(msg.contains("circular dependency"));
}
#[test]
fn test_validation_error_missing() {
let err = injectable_rs_graph::ValidationError::MissingDependency {
source: "Service".into(),
missing: "Database".into(),
};
let msg = err.to_string();
assert!(msg.contains("Service"));
}
#[test]
fn test_graph_node_leaf() {
let node = GraphNode::leaf("Config");
assert_eq!(node.name, "Config");
assert!(node.is_leaf());
assert_eq!(node.dependency_count(), 0);
}
#[test]
fn test_graph_node_with_deps() {
let node = GraphNode::new("Service", &["Database", "Cache"]);
assert_eq!(node.name, "Service");
assert!(!node.is_leaf());
assert_eq!(node.dependency_count(), 2);
}
#[test]
fn test_empty_singleton_store() {
let store = EmptySingletonStore;
assert_eq!(store.len(), 0);
assert!(store.is_empty());
assert!(store.validate().is_ok());
}
#[test]
fn test_resolve_context_debug() {
let ctx = ResolveContext::from_store(Arc::new(EmptySingletonStore));
let debug = format!("{ctx:?}");
assert!(debug.contains("ResolveContext"));
}
#[test]
fn test_container_builder_default() {
let builder = ContainerBuilder::default();
let debug = format!("{builder:?}");
assert!(debug.contains("ContainerBuilder"));
}
#[tokio::test]
async fn test_concurrent_resolutions() {
let container = Arc::new(
Container::builder()
.build()
.await
.expect("container should build"),
);
let mut handles = Vec::new();
for _ in 0..10 {
let c = container.clone();
handles.push(tokio::spawn(async move { c.resolve::<Config>().await }));
}
for handle in handles {
let result = handle.await.expect("task should complete");
assert!(result.is_ok(), "concurrent resolution should succeed");
}
}
#[tokio::test]
async fn test_concurrent_external_resolutions() {
static CONSTRUCT_COUNT: AtomicUsize = AtomicUsize::new(0);
let container = Arc::new(
Container::builder()
.register("", DynProvider::sync(|| {
CONSTRUCT_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(reqwest::Client::new())
}))
.build()
.await
.expect("container should build"),
);
let mut handles = Vec::new();
for _ in 0..10 {
let c = container.clone();
handles.push(tokio::spawn(async move {
c.resolve_external::<reqwest::Client>().await
}));
}
for handle in handles {
let result = handle.await.expect("task should complete");
assert!(
result.is_ok(),
"concurrent external resolution should succeed"
);
let _ = result.unwrap();
}
assert_eq!(CONSTRUCT_COUNT.load(Ordering::SeqCst), 10);
}
#[test]
fn test_scope_singleton_depends_on_singleton_is_ok() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("UserService", &["Database"], "singleton"),
GraphNode::leaf_with_scope("Database", "singleton"),
]);
assert!(
graph.validate().is_ok(),
"singleton → singleton should be valid"
);
}
#[test]
fn test_scope_transient_depends_on_singleton_is_ok() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("RequestHandler", &["Database"], "transient"),
GraphNode::leaf_with_scope("Database", "singleton"),
]);
assert!(
graph.validate().is_ok(),
"transient → singleton should be valid"
);
}
#[test]
fn test_scope_transient_depends_on_transient_is_ok() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("HandlerA", &["HandlerB"], "transient"),
GraphNode::leaf_with_scope("HandlerB", "transient"),
]);
assert!(
graph.validate().is_ok(),
"transient → transient should be valid"
);
}
#[test]
fn test_scope_singleton_depends_on_transient_is_error() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("SingletonService", &["TransientHandler"], "singleton"),
GraphNode::leaf_with_scope("TransientHandler", "transient"),
]);
let result = graph.validate();
assert!(result.is_err(), "singleton → transient should be invalid");
let errors = result.unwrap_err();
let has_scope_mismatch = errors
.iter()
.any(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }));
assert!(
has_scope_mismatch,
"should detect ScopeMismatch error, got: {:?}",
errors
);
}
#[test]
fn test_scope_mismatch_identifies_both_scopes() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("MySingleton", &["MyTransient"], "singleton"),
GraphNode::leaf_with_scope("MyTransient", "transient"),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let scope_mismatch = errors.iter().find_map(|e| match e {
injectable_rs_graph::ValidationError::ScopeMismatch {
source,
source_scope,
dependency,
dependency_scope,
} => Some((
source.clone(),
source_scope.clone(),
dependency.clone(),
dependency_scope.clone(),
)),
_ => None,
});
let (source, source_scope, dep, dep_scope) = scope_mismatch.expect("should find ScopeMismatch");
assert_eq!(source, "MySingleton");
assert_eq!(source_scope, "singleton");
assert_eq!(dep, "MyTransient");
assert_eq!(dep_scope, "transient");
}
#[test]
fn test_scope_mismatch_display_message() {
let err = injectable_rs_graph::ValidationError::ScopeMismatch {
source: "SingletonService".to_string(),
source_scope: "singleton".to_string(),
dependency: "TransientHandler".to_string(),
dependency_scope: "transient".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("SingletonService"),
"should mention source type"
);
assert!(msg.contains("singleton"), "should mention source scope");
assert!(
msg.contains("TransientHandler"),
"should mention dependency type"
);
assert!(msg.contains("transient"), "should mention dependency scope");
assert!(msg.contains("wider-scope"), "should explain the rule");
}
#[test]
fn test_scope_multiple_mismatches_detected() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("SingletonA", &["TransientX"], "singleton"),
GraphNode::with_scope("SingletonB", &["TransientY"], "singleton"),
GraphNode::leaf_with_scope("TransientX", "transient"),
GraphNode::leaf_with_scope("TransientY", "transient"),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let mismatch_count = errors
.iter()
.filter(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }))
.count();
assert_eq!(
mismatch_count, 2,
"should detect both scope mismatches, got {} errors: {:?}",
mismatch_count, errors
);
}
#[test]
fn test_scope_mixed_valid_and_invalid() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("Service", &["SingletonDep", "TransientDep"], "singleton"),
GraphNode::leaf_with_scope("SingletonDep", "singleton"),
GraphNode::leaf_with_scope("TransientDep", "transient"),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let mismatch_count = errors
.iter()
.filter(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }))
.count();
assert_eq!(mismatch_count, 1, "should detect exactly one ScopeMismatch");
}
#[test]
fn test_scope_graph_node_default_scope_is_singleton() {
let node = GraphNode::new("MyType", &["DepA"]);
assert_eq!(node.scope, "singleton");
assert!(node.is_singleton());
let leaf = GraphNode::leaf("MyLeaf");
assert_eq!(leaf.scope, "singleton");
assert!(leaf.is_singleton());
}
#[test]
fn test_scope_graph_node_with_scope_constructors() {
let node = GraphNode::with_scope("Handler", &["Config"], "transient");
assert_eq!(node.scope, "transient");
assert!(node.is_transient());
assert!(!node.is_singleton());
let leaf = GraphNode::leaf_with_scope("Handler", "transient");
assert_eq!(leaf.scope, "transient");
assert!(leaf.is_transient());
}
#[test]
fn test_scope_diamond_with_mixed_scopes() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("SingletonA", &["SingletonB", "TransientC"], "singleton"),
GraphNode::with_scope("SingletonB", &["TransientD"], "singleton"),
GraphNode::leaf_with_scope("TransientC", "transient"),
GraphNode::leaf_with_scope("TransientD", "transient"),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let mismatch_count = errors
.iter()
.filter(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }))
.count();
assert_eq!(
mismatch_count, 2,
"should detect both scope mismatches in diamond"
);
}
#[test]
fn test_scope_no_mismatch_when_dependency_not_in_graph() {
let graph = DependencyGraph::new(vec![GraphNode::with_scope(
"MySingleton",
&["UnknownTransient"],
"singleton",
)]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let has_scope_mismatch = errors
.iter()
.any(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }));
let has_missing = errors.iter().any(|e| {
matches!(
e,
injectable_rs_graph::ValidationError::MissingDependency { .. }
)
});
assert!(
!has_scope_mismatch,
"should NOT report ScopeMismatch for unknown deps"
);
assert!(
has_missing,
"should report MissingDependency for unknown deps"
);
}
#[injectable(has_post_construct)]
#[derive(Default)]
pub struct ServiceWithPostConstruct;
#[async_trait::async_trait]
impl PostConstruct for ServiceWithPostConstruct {
async fn post_construct(&self) -> HookResult {
POST_CONSTRUCT_CALL_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
static POST_CONSTRUCT_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[tokio::test]
async fn test_post_construct_hook_is_called_on_resolve() {
let before = POST_CONSTRUCT_CALL_COUNT.load(Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<ServiceWithPostConstruct>().await;
assert!(service.is_ok(), "should resolve ServiceWithPostConstruct");
let delta = POST_CONSTRUCT_CALL_COUNT.load(Ordering::SeqCst) - before;
assert!(
delta >= 1,
"post_construct should have been called at least once, delta was {}",
delta
);
}
#[tokio::test]
async fn test_post_construct_called_every_resolution() {
let before = POST_CONSTRUCT_CALL_COUNT.load(Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let _s1 = container
.resolve::<ServiceWithPostConstruct>()
.await
.unwrap();
let _s2 = container
.resolve::<ServiceWithPostConstruct>()
.await
.unwrap();
let _s3 = container
.resolve::<ServiceWithPostConstruct>()
.await
.unwrap();
let delta = POST_CONSTRUCT_CALL_COUNT.load(Ordering::SeqCst) - before;
assert!(
delta >= 3,
"post_construct should have been called at least 3 times (one per resolve), delta was {}",
delta
);
}
#[derive(Default)]
pub struct ServiceWithStatefulPostConstruct {
pub initialized: std::sync::atomic::AtomicBool,
}
#[injectable]
impl ServiceWithStatefulPostConstruct {
#[injectable(ctor)]
fn new() -> Self {
Self::default()
}
#[injectable(post_construct)]
async fn on_ready(&self) -> HookResult {
self.initialized.store(true, Ordering::SeqCst);
STATEFUL_POST_CONSTRUCT_RAN.store(true, Ordering::SeqCst);
Ok(())
}
}
static STATEFUL_POST_CONSTRUCT_RAN: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[tokio::test]
async fn test_post_construct_runs_after_construction() {
STATEFUL_POST_CONSTRUCT_RAN.store(false, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container
.resolve::<ServiceWithStatefulPostConstruct>()
.await;
assert!(service.is_ok());
let service = service.unwrap();
assert!(
service.initialized.load(Ordering::SeqCst),
"post_construct should have set initialized=true after construction"
);
assert!(
STATEFUL_POST_CONSTRUCT_RAN.load(Ordering::SeqCst),
"post_construct should have run"
);
}
#[injectable]
#[derive(Default)]
pub struct ServiceWithoutPostConstruct;
#[tokio::test]
async fn test_no_post_construct_without_attribute() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<ServiceWithoutPostConstruct>().await;
assert!(
service.is_ok(),
"should resolve ServiceWithoutPostConstruct without hooks"
);
}
#[injectable(has_post_construct)]
pub struct ServiceWithDepsAndHook {
_db: Inject<Database>,
}
#[async_trait::async_trait]
impl PostConstruct for ServiceWithDepsAndHook {
async fn post_construct(&self) -> HookResult {
DEPS_HOOK_CALL_COUNT.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
static DEPS_HOOK_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[tokio::test]
async fn test_post_construct_with_field_injection() {
DEPS_HOOK_CALL_COUNT.store(0, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<ServiceWithDepsAndHook>().await;
assert!(service.is_ok(), "should resolve ServiceWithDepsAndHook");
let call_count = DEPS_HOOK_CALL_COUNT.load(Ordering::SeqCst);
assert_eq!(
call_count, 1,
"post_construct should have been called after dependency resolution"
);
}
#[derive(Clone)]
pub struct ServiceWithPreDestruct {
pub name: String,
}
impl ServiceWithPreDestruct {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
}
}
}
static PRE_DESTRUCT_CALL_ORDER: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
#[async_trait::async_trait]
impl PreDestruct for ServiceWithPreDestruct {
async fn pre_destruct(&self) -> HookResult {
let mut order = PRE_DESTRUCT_CALL_ORDER.lock().unwrap();
order.push(self.name.clone());
Ok(())
}
}
#[tokio::test]
async fn test_pre_destruct_can_be_called_directly() {
PRE_DESTRUCT_CALL_ORDER.lock().unwrap().clear();
let service = ServiceWithPreDestruct::new("test-service");
service
.pre_destruct()
.await
.expect("pre_destruct should succeed");
let order = PRE_DESTRUCT_CALL_ORDER.lock().unwrap();
assert_eq!(order.len(), 1, "pre_destruct should have been called once");
assert_eq!(order[0], "test-service");
}
#[tokio::test]
async fn test_container_shutdown_calls_registered_destructors() {
PRE_DESTRUCT_CALL_ORDER.lock().unwrap().clear();
let container = Container::builder()
.build()
.await
.expect("container should build");
let service_a = Arc::new(ServiceWithPreDestruct::new("service-a"));
let service_b = Arc::new(ServiceWithPreDestruct::new("service-b"));
let service_c = Arc::new(ServiceWithPreDestruct::new("service-c"));
container
.context()
.register_destructor(service_a.clone() as Arc<dyn PreDestruct>);
container
.context()
.register_destructor(service_b.clone() as Arc<dyn PreDestruct>);
container
.context()
.register_destructor(service_c.clone() as Arc<dyn PreDestruct>);
container.shutdown().await.expect("shutdown should succeed");
let order = PRE_DESTRUCT_CALL_ORDER.lock().unwrap();
let our_order: Vec<_> = order
.iter()
.filter(|s| matches!(s.as_str(), "service-a" | "service-b" | "service-c"))
.collect();
assert_eq!(
our_order.len(),
3,
"all three pre_destruct hooks should have been called"
);
assert_eq!(
our_order[0], "service-c",
"service-c should be destroyed first (reverse order)"
);
assert_eq!(
our_order[1], "service-b",
"service-b should be destroyed second"
);
assert_eq!(
our_order[2], "service-a",
"service-a should be destroyed last"
);
}
#[tokio::test]
async fn test_container_destructor_count() {
let container = Container::builder()
.build()
.await
.expect("container should build");
assert_eq!(
container.destructor_count().await,
0,
"should start with zero destructors"
);
let service = Arc::new(ServiceWithPreDestruct::new("test"));
container
.context()
.register_destructor(service.clone() as Arc<dyn PreDestruct>);
assert_eq!(
container.destructor_count().await,
1,
"should have one destructor after registration"
);
}
#[tokio::test]
async fn test_container_shutdown_is_idempotent() {
PRE_DESTRUCT_CALL_ORDER.lock().unwrap().clear();
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = Arc::new(ServiceWithPreDestruct::new("once-only"));
container
.context()
.register_destructor(service.clone() as Arc<dyn PreDestruct>);
container.shutdown().await.expect("shutdown should succeed");
container
.shutdown()
.await
.expect("second shutdown should succeed");
let order = PRE_DESTRUCT_CALL_ORDER.lock().unwrap();
let our_order: Vec<_> = order.iter().filter(|s| s.as_str() == "once-only").collect();
assert_eq!(
our_order.len(),
1,
"pre_destruct should only be called once despite two shutdown calls"
);
}
#[test]
fn test_scope_validation_with_derive_macro_scope_attribute() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("MySingletonService", &["MyTransientHandler"], "singleton"),
GraphNode::leaf_with_scope("MyTransientHandler", "transient"),
]);
let result = graph.validate();
assert!(
result.is_err(),
"singleton depending on transient should fail validation"
);
let errors = result.unwrap_err();
let has_scope_mismatch = errors
.iter()
.any(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }));
assert!(has_scope_mismatch);
}
#[test]
fn test_scope_transitive_dependency_mismatch() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("TopService", &["MiddleService"], "singleton"),
GraphNode::with_scope("MiddleService", &["TransientWorker"], "singleton"),
GraphNode::leaf_with_scope("TransientWorker", "transient"),
]);
let result = graph.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let mismatch_count = errors
.iter()
.filter(|e| matches!(e, injectable_rs_graph::ValidationError::ScopeMismatch { .. }))
.count();
assert_eq!(
mismatch_count, 1,
"should detect exactly one ScopeMismatch (MiddleService → TransientWorker)"
);
}
#[test]
fn test_scope_all_valid_chains() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("RequestHandler", &["AppService"], "transient"),
GraphNode::with_scope("AppService", &["Database"], "singleton"),
GraphNode::leaf_with_scope("Database", "singleton"),
]);
assert!(
graph.validate().is_ok(),
"all valid scope combinations should pass validation"
);
}
#[test]
fn test_scope_topological_order_with_scopes() {
let graph = DependencyGraph::new(vec![
GraphNode::with_scope("RequestHandler", &["AppService"], "transient"),
GraphNode::with_scope("AppService", &["Database"], "singleton"),
GraphNode::leaf_with_scope("Database", "singleton"),
]);
let order = graph
.topological_order()
.expect("should have valid topological order");
let db_pos = order.iter().position(|&n| n == "Database").unwrap();
let app_pos = order.iter().position(|&n| n == "AppService").unwrap();
let handler_pos = order.iter().position(|&n| n == "RequestHandler").unwrap();
assert!(db_pos < app_pos, "Database should come before AppService");
assert!(
app_pos < handler_pos,
"AppService should come before RequestHandler"
);
}
pub struct CtorServiceWithInject {
db: Inject<Database>,
}
#[injectable]
impl CtorServiceWithInject {
#[injectable(ctor)]
fn new(db: Inject<Database>) -> Self {
Self { db }
}
}
#[tokio::test]
async fn test_injectable_impl_with_inject_param() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceWithInject>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceWithInject via #[injectable]"
);
let service = service.unwrap();
let _db = &*service.db;
}
pub struct CtorServiceWithArc {
db: Arc<Database>,
}
#[injectable]
impl CtorServiceWithArc {
#[injectable(ctor)]
fn new(#[injectable(inject)] db: Arc<Database>) -> Self {
Self { db }
}
}
#[tokio::test]
async fn test_injectable_impl_with_arc_param() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceWithArc>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceWithArc via #[injectable]"
);
let service = service.unwrap();
let _db = &*service.db;
}
#[derive(Default, Clone)]
pub struct CloneableConfig {
pub value: u32,
}
#[injectable]
impl CloneableConfig {
#[injectable(ctor)]
fn new() -> Self {
Self::default()
}
}
pub struct CtorServiceWithOwned {
config: Arc<CloneableConfig>,
db: Arc<Database>,
}
#[injectable]
impl CtorServiceWithOwned {
#[injectable(ctor)]
fn new(#[injectable(inject)] config: Arc<CloneableConfig>, #[injectable(inject)] db: Arc<Database>) -> Self {
Self { config, db }
}
}
#[tokio::test]
async fn test_injectable_impl_with_owned_param() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceWithOwned>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceWithOwned with plain T param (requires Clone)"
);
let service = service.unwrap();
assert_eq!(
service.config.value, 0,
"default CloneableConfig should have value 0"
);
}
pub struct CtorServiceMultiDeps {
db: Inject<Database>,
cache: Inject<Cache>,
config: Inject<Config>,
}
#[injectable]
impl CtorServiceMultiDeps {
#[injectable(ctor)]
fn new(db: Inject<Database>, cache: Inject<Cache>, config: Inject<Config>) -> Self {
Self { db, cache, config }
}
}
#[tokio::test]
async fn test_injectable_impl_with_multiple_deps() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceMultiDeps>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceMultiDeps with 3 Inject dependencies"
);
}
pub struct CtorServiceAsync {
db: Inject<Database>,
}
#[injectable]
impl CtorServiceAsync {
#[injectable(ctor)]
async fn new(db: Inject<Database>) -> Self {
Self { db }
}
}
#[tokio::test]
async fn test_injectable_impl_with_async_constructor() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceAsync>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceAsync with async constructor"
);
}
pub struct CtorServiceNoDeps {
initialized: bool,
}
#[injectable]
impl CtorServiceNoDeps {
#[injectable(ctor)]
fn new() -> Self {
Self { initialized: true }
}
}
#[tokio::test]
async fn test_injectable_impl_with_no_deps() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceNoDeps>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceNoDeps with zero-parameter constructor"
);
assert!(
service.unwrap().initialized,
"constructor should have set initialized=true"
);
}
static IMPL_POST_CONSTRUCT_COUNT: AtomicUsize = AtomicUsize::new(0);
pub struct CtorServiceWithPostConstruct {
initialized: std::sync::atomic::AtomicBool,
}
#[injectable]
impl CtorServiceWithPostConstruct {
#[injectable(ctor)]
fn new() -> Self {
Self {
initialized: std::sync::atomic::AtomicBool::new(false),
}
}
#[injectable(post_construct)]
fn init(&self) {
self.initialized.store(true, Ordering::SeqCst);
IMPL_POST_CONSTRUCT_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn test_injectable_impl_post_construct_hook_runs() {
IMPL_POST_CONSTRUCT_COUNT.store(0, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<CtorServiceWithPostConstruct>().await;
assert!(
service.is_ok(),
"should resolve CtorServiceWithPostConstruct"
);
let service = service.unwrap();
assert!(
service.initialized.load(Ordering::SeqCst),
"post_construct hook should have set initialized=true"
);
assert_eq!(
IMPL_POST_CONSTRUCT_COUNT.load(Ordering::SeqCst),
1,
"post_construct should have been called exactly once"
);
}
static IMPL_PRE_DESTRUCT_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct CtorServiceWithPreDestruct {
name: &'static str,
}
#[injectable]
impl CtorServiceWithPreDestruct {
#[injectable(ctor)]
fn new() -> Self {
Self { name: "test" }
}
#[injectable(pre_destruct)]
async fn cleanup(&self) {
IMPL_PRE_DESTRUCT_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn test_injectable_impl_pre_destruct_hook_registers() {
IMPL_PRE_DESTRUCT_COUNT.store(0, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let _service = container.resolve::<CtorServiceWithPreDestruct>().await;
assert!(
_service.is_ok(),
"should resolve CtorServiceWithPreDestruct"
);
let count = container.destructor_count().await;
assert_eq!(
count, 1,
"should have 1 registered destructor after resolving CtorServiceWithPreDestruct"
);
}
#[tokio::test]
async fn test_injectable_impl_pre_destruct_hook_runs_on_shutdown() {
IMPL_PRE_DESTRUCT_COUNT.store(0, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let _service = container
.resolve::<CtorServiceWithPreDestruct>()
.await
.unwrap();
container.shutdown().await.expect("shutdown should succeed");
assert_eq!(
IMPL_PRE_DESTRUCT_COUNT.load(Ordering::SeqCst),
1,
"pre_destruct hook should have been called on shutdown"
);
}
static FULL_LIFECYCLE_POST_COUNT: AtomicUsize = AtomicUsize::new(0);
static FULL_LIFECYCLE_PRE_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct CtorServiceFullLifecycle {
pub initialized: bool,
}
#[injectable]
impl CtorServiceFullLifecycle {
#[injectable(ctor)]
fn new() -> Self {
Self { initialized: false }
}
#[injectable(post_construct)]
fn on_ready(&self) {
FULL_LIFECYCLE_POST_COUNT.fetch_add(1, Ordering::SeqCst);
}
#[injectable(pre_destruct)]
fn on_shutdown(&self) {
FULL_LIFECYCLE_PRE_COUNT.fetch_add(1, Ordering::SeqCst);
}
}
#[tokio::test]
async fn test_injectable_impl_full_lifecycle() {
FULL_LIFECYCLE_POST_COUNT.store(0, Ordering::SeqCst);
FULL_LIFECYCLE_PRE_COUNT.store(0, Ordering::SeqCst);
let container = Container::builder()
.build()
.await
.expect("container should build");
let _service = container.resolve::<CtorServiceFullLifecycle>().await;
assert!(_service.is_ok(), "should resolve CtorServiceFullLifecycle");
assert_eq!(
FULL_LIFECYCLE_POST_COUNT.load(Ordering::SeqCst),
1,
"post_construct should have run"
);
assert_eq!(
FULL_LIFECYCLE_PRE_COUNT.load(Ordering::SeqCst),
0,
"pre_destruct should NOT have run yet"
);
container.shutdown().await.expect("shutdown should succeed");
assert_eq!(
FULL_LIFECYCLE_PRE_COUNT.load(Ordering::SeqCst),
1,
"pre_destruct should have run on shutdown"
);
}
#[derive(Debug, Clone)]
pub struct ServiceWithSucceedingPostConstruct;
#[injectable]
impl ServiceWithSucceedingPostConstruct {
#[injectable(ctor)]
fn new() -> Self {
Self
}
#[injectable(post_construct)]
fn init(&self) -> Result<(), std::io::Error> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ServiceWithFailingPostConstruct;
#[injectable]
impl ServiceWithFailingPostConstruct {
#[injectable(ctor)]
fn new() -> Self {
Self
}
#[injectable(post_construct)]
fn init(&self) -> Result<(), std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"post_construct intentionally failed",
))
}
}
#[tokio::test]
async fn test_post_construct_result_ok_propagates() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container
.resolve::<ServiceWithSucceedingPostConstruct>()
.await;
assert!(
service.is_ok(),
"should resolve when post_construct returns Ok"
);
}
#[tokio::test]
async fn test_post_construct_result_err_propagates() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let result = container.resolve::<ServiceWithFailingPostConstruct>().await;
match result {
Err(InjectableError::LifecycleHookFailed {
type_name,
hook,
reason,
}) => {
assert_eq!(hook, "post_construct");
assert!(
reason.contains("intentionally failed"),
"reason should contain the original error message, got: {reason}"
);
let _ = type_name;
}
other => panic!("expected LifecycleHookFailed, got: {other:?}"),
}
}
#[derive(Debug, Clone)]
pub struct ServiceWithFalliblePreDestruct;
#[injectable]
impl ServiceWithFalliblePreDestruct {
#[injectable(ctor)]
fn new() -> Self {
Self
}
#[injectable(pre_destruct)]
async fn cleanup(&self) -> Result<(), std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"cleanup failed on purpose",
))
}
}
#[tokio::test]
async fn test_pre_destruct_err_accumulated_on_shutdown() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let _service = container.resolve::<ServiceWithFalliblePreDestruct>().await;
let result = container.shutdown().await;
match result {
Err(InjectableError::ShutdownFailed { errors }) => {
assert!(!errors.is_empty(), "should have at least one error");
let found = errors.iter().any(|e| {
if let InjectableError::LifecycleHookFailed { hook, reason, .. } = e {
*hook == "pre_destruct" && reason.contains("cleanup failed on purpose")
} else {
false
}
});
assert!(
found,
"should find our cleanup error in the accumulated errors"
);
}
other => panic!("expected ShutdownFailed, got: {other:?}"),
}
}
pub struct ServiceWithInfallibleHook;
#[injectable]
impl ServiceWithInfallibleHook {
#[injectable(ctor)]
fn new() -> Self {
Self
}
#[injectable(post_construct)]
fn on_ready(&self) {
}
}
#[tokio::test]
async fn test_infallible_post_construct_works() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<ServiceWithInfallibleHook>().await;
assert!(
service.is_ok(),
"should resolve when post_construct returns ()"
);
}
pub struct TransientCtorService {
pub id: u32,
}
#[injectable(scope = Transient)]
impl TransientCtorService {
#[injectable(ctor)]
fn new() -> Self {
Self { id: 42 }
}
}
#[tokio::test]
async fn test_injectable_impl_with_transient_scope() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<TransientCtorService>().await;
assert!(
service.is_ok(),
"should resolve TransientCtorService with transient scope"
);
assert_eq!(service.unwrap().id, 42);
}
#[test]
fn test_constructor_callable_outside_di() {
let db = Arc::new(Database);
let service = CtorServiceWithArc::new(db);
let _db_ref = &*service.db;
let config = Arc::new(CloneableConfig { value: 99 });
let db = Arc::new(Database);
let service = CtorServiceWithOwned::new(config, db);
assert_eq!(service.config.value, 99);
}
pub struct MixedDefaultAndInject {
pub db: Inject<Database>,
pub port: u16,
pub host: String,
}
#[injectable]
impl MixedDefaultAndInject {
#[injectable(ctor)]
fn new(db: Inject<Database>) -> Self {
Self { db, port: 0, host: String::new() }
}
}
#[tokio::test]
async fn test_inject_attribute_in_default_struct() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<MixedDefaultAndInject>().await;
assert!(service.is_ok(), "should resolve MixedDefaultAndInject");
let service = service.unwrap();
assert_eq!(service.port, 0, "port should be defaulted");
assert_eq!(service.host, "", "host should be defaulted");
let _db: &Database = &service.db;
}
pub struct PartialInjectService {
db: Inject<Database>,
name: String,
cache: Inject<Cache>,
}
#[injectable]
impl PartialInjectService {
#[injectable(ctor)]
fn new(db: Inject<Database>, cache: Inject<Cache>) -> Self {
Self { db, name: String::new(), cache }
}
}
#[tokio::test]
async fn test_non_injectable_field_via_constructor() {
let container = Container::builder()
.build()
.await
.expect("container should build");
let service = container.resolve::<PartialInjectService>().await;
assert!(service.is_ok(), "should resolve PartialInjectService");
let service = service.unwrap();
assert_eq!(service.name, "", "name is set by constructor");
let _db: &Database = &service.db;
let _cache: &Cache = &service.cache;
}
static SHARED_SVC2_CTOR: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct SharedSvc2;
#[injectable]
impl SharedSvc2 {
#[injectable(ctor)]
fn new() -> Self {
SHARED_SVC2_CTOR.fetch_add(1, Ordering::SeqCst);
Self
}
}
#[injectable]
pub struct ConsumerArc {
#[injectable(inject)]
svc: Arc<SharedSvc2>,
}
async fn make_owned_svc2_from_ctx(ctx: &ResolveContext) -> Result<SharedSvc2, InjectableError> {
let arc: Arc<SharedSvc2> = ctx.extract().await?;
Ok((*arc).clone())
}
#[injectable]
#[derive(Clone)]
pub struct ConsumerOwned {
#[injectable(inject(use_factory_async=self::make_owned_svc2_from_ctx))]
svc: SharedSvc2,
}
#[tokio::test]
async fn test_arc_field_shares_singleton() {
let before = SHARED_SVC2_CTOR.load(Ordering::SeqCst);
let ctx = Container::builder().build().await.unwrap();
let ctx = ctx.context();
let a = Inject::<ConsumerArc>::extract(ctx).await.unwrap();
let b = Inject::<ConsumerArc>::extract(ctx).await.unwrap();
assert!(Arc::ptr_eq(&a.svc, &b.svc),
"Arc<T> field must point to the same singleton allocation across resolutions");
let delta = SHARED_SVC2_CTOR.load(Ordering::SeqCst) - before;
assert_eq!(delta, 1,
"singleton SharedSvc2 must be constructed once, not once per consumer");
}
#[tokio::test]
async fn test_owned_singleton_field_respects_scope_via_clone() {
let before = SHARED_SVC2_CTOR.load(Ordering::SeqCst);
let container = Container::builder().build().await.unwrap();
let ctx = container.context();
let _consumer = Inject::<ConsumerOwned>::extract(ctx).await.unwrap();
let _cached = Arc::<SharedSvc2>::extract(ctx).await.unwrap();
let delta = SHARED_SVC2_CTOR.load(Ordering::SeqCst) - before;
assert_eq!(delta, 1,
"singleton SharedSvc2 must be constructed once, even when held as \
an owned Clone field (delta={delta})");
}
static ARC_CLONE_SVC_CTOR: AtomicUsize = AtomicUsize::new(0);
static OWNED_CLONE_SVC_CTOR: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
pub struct ArcCloneSvc;
#[injectable]
impl ArcCloneSvc {
#[injectable(ctor)]
fn new() -> Self {
ARC_CLONE_SVC_CTOR.fetch_add(1, Ordering::SeqCst);
Self
}
}
#[derive(Clone)]
pub struct OwnedCloneSvc;
#[injectable]
impl OwnedCloneSvc {
#[injectable(ctor)]
fn new() -> Self {
OWNED_CLONE_SVC_CTOR.fetch_add(1, Ordering::SeqCst);
Self
}
}
#[injectable]
pub struct ArcCloneConsumer {
#[injectable(inject)]
svc: Arc<ArcCloneSvc>,
}
async fn clone_owned_clone_svc(
ctx: &ResolveContext,
) -> Result<OwnedCloneSvc, InjectableError> {
let arc: Arc<OwnedCloneSvc> = ctx.extract().await?;
Ok((*arc).clone())
}
#[injectable]
#[derive(Clone)]
pub struct OwnedCloneConsumer {
#[injectable(inject(use_factory_async = self::clone_owned_clone_svc))]
svc: OwnedCloneSvc,
}
#[tokio::test]
async fn test_arc_inject_field_shares_singleton() {
let before = ARC_CLONE_SVC_CTOR.load(Ordering::SeqCst);
let ctx = Container::builder().build().await.unwrap();
let ctx = ctx.context();
let a = Inject::<ArcCloneConsumer>::extract(ctx).await.unwrap();
let b = Inject::<ArcCloneConsumer>::extract(ctx).await.unwrap();
assert!(Arc::ptr_eq(&a.svc, &b.svc),
"Arc<T> field must share the same singleton Arc");
assert_eq!(ARC_CLONE_SVC_CTOR.load(Ordering::SeqCst) - before, 1,
"singleton must be constructed once");
}
#[tokio::test]
async fn test_owned_field_via_factory_clones_singleton() {
let before = OWNED_CLONE_SVC_CTOR.load(Ordering::SeqCst);
let ctx = Container::builder().build().await.unwrap();
let ctx = ctx.context();
let _c = Inject::<OwnedCloneConsumer>::extract(ctx).await.unwrap();
assert_eq!(OWNED_CLONE_SVC_CTOR.load(Ordering::SeqCst) - before, 1,
"singleton constructed once even when field is owned");
}
#[tokio::test]
async fn test_extract_unit_tuple() {
let ctx = Container::builder().build().await.unwrap();
let (): () = <() as injectable_rs::Extract>::extract(ctx.context()).await.unwrap();
}
#[tokio::test]
async fn test_extract_1_tuple() {
let ctx = Container::builder().build().await.unwrap();
let (arc,): (Arc<Database>,) =
<(Arc<Database>,) as injectable_rs::Extract>::extract(ctx.context()).await.unwrap();
let _: &Database = &*arc;
}
#[tokio::test]
async fn test_extract_2_tuple() {
let ctx = Container::builder().build().await.unwrap();
let (db, cfg): (Arc<Database>, Arc<Config>) =
<(Arc<Database>, Arc<Config>) as injectable_rs::Extract>::extract(ctx.context()).await.unwrap();
let _ = (db, cfg);
}
#[injectable]
pub struct InjectableAttrService {
db: Inject<Database>,
#[injectable(inject)]
config: Arc<Config>,
}
#[injectable(scope = Transient)]
pub struct TransientAttrService {
db: Inject<Database>,
}
pub struct ImplAttrService {
name: &'static str,
}
#[injectable]
impl ImplAttrService {
#[injectable(ctor)]
fn new() -> Self {
Self { name: "impl-attr" }
}
}
#[tokio::test]
async fn test_injectable_attr_on_struct() {
let ctx = Container::builder().build().await.unwrap();
let svc = Inject::<InjectableAttrService>::extract(ctx.context()).await.unwrap();
let _: &Database = &*svc.db;
}
#[tokio::test]
async fn test_injectable_attr_on_impl() {
let container = Container::builder().build().await.unwrap();
let svc = container.resolve::<ImplAttrService>().await;
let _ = svc;
}
#[injectable(factory)]
fn make_label(_config: Inject<Config>) -> String {
"label-from-config".to_string()
}
#[injectable(factory)]
fn make_arc_label(#[injectable(inject)] _db: Arc<Database>) -> String {
"db-label-from-arc".to_string()
}
#[injectable(factory)]
fn make_checked_label(_config: Inject<Config>) -> Result<String, std::convert::Infallible> {
Ok("checked-label".to_string())
}
#[injectable(factory)]
async fn make_async_label(_config: Inject<Config>) -> String {
"async-label-value".to_string()
}
#[injectable]
pub struct FactoryConsumer {
#[injectable(inject(use_factory_async = self::make_label))]
label: String,
}
#[injectable]
pub struct ArcFactoryConsumer {
#[injectable(inject(use_factory_async = self::make_arc_label))]
label: String,
}
#[injectable]
pub struct ResultFactoryConsumer {
#[injectable(inject(use_factory_async = self::make_checked_label))]
label: String,
}
#[injectable]
pub struct AsyncFactoryConsumer {
#[injectable(inject(use_factory_async = self::make_async_label))]
label: String,
}
#[tokio::test]
async fn test_inject_fn_inject_t_param() {
let ctx = Container::builder().build().await.unwrap();
let svc = ctx.resolve::<FactoryConsumer>().await.unwrap();
assert!(svc.label.starts_with("label-from-"));
}
#[tokio::test]
async fn test_inject_fn_arc_param() {
let ctx = Container::builder().build().await.unwrap();
let svc = ctx.resolve::<ArcFactoryConsumer>().await.unwrap();
assert!(svc.label.starts_with("db-label-"));
}
#[tokio::test]
async fn test_inject_fn_result_return() {
let ctx = Container::builder().build().await.unwrap();
let svc = ctx.resolve::<ResultFactoryConsumer>().await.unwrap();
assert!(svc.label.starts_with("checked-"));
}
#[tokio::test]
async fn test_inject_fn_async_body() {
let ctx = Container::builder().build().await.unwrap();
let svc = ctx.resolve::<AsyncFactoryConsumer>().await.unwrap();
assert!(svc.label.starts_with("async-label-"));
}
#[tokio::test]
async fn test_prelude_items_accessible() {
use injectable_rs::prelude::*;
#[injectable]
pub struct PreludeService {
db: Inject<Database>,
}
let container = Container::builder().build().await.unwrap();
let _svc = container.resolve::<PreludeService>().await.unwrap();
}