use injectable_rs::Provider;
use injectable_rs::prelude::*;
use std::marker::PhantomData;
#[injectable]
#[derive(Default, Clone)]
struct Database;
#[injectable]
#[derive(Default, Clone)]
struct Cache;
#[injectable]
struct OptionalConsumer {
required: Inject<Database>,
optional_registered: Inject<Cache>,
}
struct UnregisteredHolder {
maybe: Option<Inject<String>>,
}
#[tokio::test]
async fn optional_inject_none_when_unregistered() {
let container = Container::builder().build().await.unwrap();
let ctx = container.context();
let maybe: Option<Inject<String>> = ctx.extract().await.unwrap();
assert!(
maybe.is_none(),
"Option<Inject<String>> must be None when String has no provider"
);
}
#[tokio::test]
async fn optional_inject_some_when_registered() {
let container = Container::builder()
.register("", DynProvider::from_value("hello".to_string()))
.build()
.await
.unwrap();
let ctx = container.context();
let maybe: Option<Inject<String>> = ctx.extract().await.unwrap();
assert!(
maybe.is_some(),
"Option<Inject<String>> must be Some when DynProvider is registered"
);
assert_eq!(maybe.unwrap().as_str(), "hello");
}
fn make_event_log(_ctx: &ResolveContext) -> Arc<Vec<String>> {
Arc::new(vec!["started".to_string()])
}
#[injectable]
struct EventLog {
#[injectable(inject(use_factory_sync = self::make_event_log))]
events: Arc<Vec<String>>,
}
#[tokio::test]
async fn generic_field_type_via_factory() {
let container = Container::builder().build().await.unwrap();
let log = container.resolve::<EventLog>().await.unwrap();
assert_eq!(log.events.as_ref(), &["started".to_string()]);
}
#[tokio::test]
async fn generic_field_singleton_shared() {
let container = Container::builder().build().await.unwrap();
let ctx = container.context();
let a: Inject<EventLog> = ctx.extract().await.unwrap();
let b: Inject<EventLog> = ctx.extract().await.unwrap();
assert!(
Inject::ptr_eq(&a, &b),
"singleton EventLog must share the same Arc"
);
assert!(
Arc::ptr_eq(&a.events, &b.events),
"singleton EventLog must share the same Arc<Vec<String>>"
);
}
use std::collections::HashMap;
fn make_config_map(_ctx: &ResolveContext) -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert("host".into(), "localhost".into());
m
}
#[injectable]
struct AppSettings {
#[injectable(inject(use_factory_sync = self::make_config_map))]
config: HashMap<String, String>,
}
#[tokio::test]
async fn hashmap_field_type_via_factory() {
let container = Container::builder().build().await.unwrap();
let settings = container.resolve::<AppSettings>().await.unwrap();
assert_eq!(
settings.config.get("host").map(String::as_str),
Some("localhost")
);
}
struct UserMarker;
struct OrderMarker;
#[derive(Clone, Debug)]
struct TypedCounter<Marker: 'static + Send + Sync> {
count: u32,
_marker: PhantomData<fn() -> Marker>, }
#[injectable(factory)]
fn make_user_counter(_db: Inject<Database>) -> TypedCounter<UserMarker> {
TypedCounter {
count: 0,
_marker: PhantomData,
}
}
#[injectable(factory)]
fn make_order_counter(_db: Inject<Database>) -> TypedCounter<OrderMarker> {
TypedCounter {
count: 100,
_marker: PhantomData,
}
}
#[injectable]
struct UserStats {
#[injectable(inject(use_factory_async = self::make_user_counter))]
counter: TypedCounter<UserMarker>,
}
#[injectable]
struct OrderStats {
#[injectable(inject(use_factory_async = self::make_order_counter))]
counter: TypedCounter<OrderMarker>,
}
#[tokio::test]
async fn phantom_type_user_counter_via_factory() {
let container = Container::builder().build().await.unwrap();
let stats = container.resolve::<UserStats>().await.unwrap();
assert_eq!(stats.counter.count, 0, "UserStats counter starts at 0");
}
#[tokio::test]
async fn phantom_type_order_counter_via_factory() {
let container = Container::builder().build().await.unwrap();
let stats = container.resolve::<OrderStats>().await.unwrap();
assert_eq!(stats.counter.count, 100, "OrderStats counter starts at 100");
}
#[tokio::test]
async fn two_phantom_specializations_coexist() {
let container = Container::builder().build().await.unwrap();
let user_stats = container.resolve::<UserStats>().await.unwrap();
let order_stats = container.resolve::<OrderStats>().await.unwrap();
assert_eq!(user_stats.counter.count, 0);
assert_eq!(order_stats.counter.count, 100);
}
#[injectable]
struct Wrapper<T: injectable_rs_runtime::Injectable + Send + Sync + 'static> {
inner: Inject<T>,
}
#[tokio::test]
async fn generic_struct_field_injection_database() {
let container = Container::builder().build().await.unwrap();
let svc = container.resolve::<Wrapper<Database>>().await.unwrap();
let _: &Database = &*svc.inner;
}
#[tokio::test]
async fn generic_struct_field_injection_cache() {
let container = Container::builder().build().await.unwrap();
let svc = container.resolve::<Wrapper<Cache>>().await.unwrap();
let _: &Cache = &*svc.inner;
}
#[tokio::test]
async fn generic_struct_two_specializations_coexist() {
let container = Container::builder().build().await.unwrap();
let _w_db = container.resolve::<Wrapper<Database>>().await.unwrap();
let _w_cache = container.resolve::<Wrapper<Cache>>().await.unwrap();
}
#[tokio::test]
async fn generic_struct_singleton_respected() {
let container = Container::builder().build().await.unwrap();
let ctx = container.context();
let a: Arc<Wrapper<Database>> = ctx.extract().await.unwrap();
let b: Arc<Wrapper<Database>> = ctx.extract().await.unwrap();
assert!(
Arc::ptr_eq(&a, &b),
"Wrapper<Database> singleton must be cached"
);
}
#[injectable]
struct App {
#[injectable(inject)]
wrapper_db: Arc<Wrapper<Database>>,
#[injectable(inject)]
wrapper_cache: Arc<Wrapper<Cache>>,
}
#[tokio::test]
async fn arc_of_generic_injectable_as_field() {
let container = Container::builder().build().await.unwrap();
let app = container.resolve::<App>().await.unwrap();
let _: &Database = &*app.wrapper_db.inner;
let _: &Cache = &*app.wrapper_cache.inner;
}
#[derive(Clone)]
struct Repo<Entity: 'static + Send + Sync + Clone> {
db: Arc<Database>,
_phantom: PhantomData<fn() -> Entity>,
}
#[derive(Clone, Debug)]
struct UserEntity;
#[derive(Clone, Debug)]
struct ProductEntity;
#[injectable]
impl<Entity: 'static + Send + Sync + Clone> Repo<Entity> {
#[injectable(ctor)]
fn new(#[injectable(inject)] db: Arc<Database>) -> Self {
Self {
db,
_phantom: PhantomData,
}
}
}
#[tokio::test]
async fn generic_ctor_injection_user_entity() {
let container = Container::builder().build().await.unwrap();
let repo = container.resolve::<Repo<UserEntity>>().await.unwrap();
let _: &Database = &*repo.db;
}
#[tokio::test]
async fn generic_ctor_injection_product_entity() {
let container = Container::builder().build().await.unwrap();
let repo = container.resolve::<Repo<ProductEntity>>().await.unwrap();
let _: &Database = &*repo.db;
}
#[tokio::test]
async fn generic_ctor_two_specializations_share_same_db_singleton() {
let container = Container::builder().build().await.unwrap();
let user_repo = container.resolve::<Repo<UserEntity>>().await.unwrap();
let product_repo = container.resolve::<Repo<ProductEntity>>().await.unwrap();
assert!(
Arc::ptr_eq(&user_repo.db, &product_repo.db),
"both Repo specializations must share the same singleton Database"
);
}