use injectable_rs::Provider;
use injectable_rs::prelude::*;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicU32, Ordering};
#[injectable]
#[derive(Default, Clone, Debug)]
struct Db;
#[derive(Clone, Debug)]
struct Repository<Entity: 'static + Send + Sync + Clone> {
db: Arc<Db>,
_phantom: PhantomData<fn() -> Entity>,
}
#[derive(Clone, Debug)]
struct UserEntity {
pub id: u32,
}
#[derive(Clone, Debug)]
struct ProductEntity {
pub id: u32,
}
struct UserRepositoryProvider;
#[async_trait::async_trait]
impl Provider<Repository<UserEntity>> for UserRepositoryProvider {
async fn provide(
ctx: &injectable_rs_runtime::ResolveContext,
) -> injectable_rs_runtime::InjectableResult<Repository<UserEntity>> {
let db: Arc<Db> = ctx.extract().await?;
Ok(Repository {
db,
_phantom: PhantomData,
})
}
}
impl injectable_rs_runtime::Injectable for Repository<UserEntity> {
type Provider = UserRepositoryProvider;
const IS_SINGLETON: bool = true;
}
struct ProductRepositoryProvider;
#[async_trait::async_trait]
impl Provider<Repository<ProductEntity>> for ProductRepositoryProvider {
async fn provide(
ctx: &injectable_rs_runtime::ResolveContext,
) -> injectable_rs_runtime::InjectableResult<Repository<ProductEntity>> {
let db: Arc<Db> = ctx.extract().await?;
Ok(Repository {
db,
_phantom: PhantomData,
})
}
}
impl injectable_rs_runtime::Injectable for Repository<ProductEntity> {
type Provider = ProductRepositoryProvider;
const IS_SINGLETON: bool = true;
}
#[tokio::test]
async fn manual_injectable_for_concrete_generic_type() {
let container = Container::builder().build().await.unwrap();
let repo = container.resolve::<Repository<UserEntity>>().await.unwrap();
let _: &Db = &*repo.db;
}
#[tokio::test]
async fn two_generic_specializations_coexist() {
let container = Container::builder().build().await.unwrap();
let user_repo = container.resolve::<Repository<UserEntity>>().await.unwrap();
let product_repo = container
.resolve::<Repository<ProductEntity>>()
.await
.unwrap();
assert!(
Arc::ptr_eq(&user_repo.db, &product_repo.db),
"both repos share the same singleton Db"
);
}
#[tokio::test]
async fn generic_singleton_cached_across_resolutions() {
let container = Container::builder().build().await.unwrap();
let a = container.resolve::<Repository<UserEntity>>().await.unwrap();
let b = container.resolve::<Repository<UserEntity>>().await.unwrap();
assert!(
Arc::ptr_eq(&a.db, &b.db),
"Repository<UserEntity> singletons share the same Db"
);
}
#[derive(Clone, Debug, PartialEq)]
struct TypedId<Marker: 'static + Send + Sync>(u64, PhantomData<fn() -> Marker>);
struct UserId;
struct OrderId;
static NEXT_USER_ID: AtomicU32 = AtomicU32::new(1);
static NEXT_ORDER_ID: AtomicU32 = AtomicU32::new(100);
#[injectable(factory)]
fn make_user_id(_db: Inject<Db>) -> TypedId<UserId> {
let id = NEXT_USER_ID.fetch_add(1, Ordering::SeqCst) as u64;
TypedId(id, PhantomData)
}
#[injectable(factory)]
fn make_order_id(_db: Inject<Db>) -> TypedId<OrderId> {
let id = NEXT_ORDER_ID.fetch_add(1, Ordering::SeqCst) as u64;
TypedId(id, PhantomData)
}
#[injectable]
struct UserContext {
#[injectable(inject(use_factory_async = self::make_user_id))]
id: TypedId<UserId>,
db: Inject<Db>,
}
#[injectable]
struct OrderContext {
#[injectable(inject(use_factory_async = self::make_order_id))]
id: TypedId<OrderId>,
db: Inject<Db>,
}
#[tokio::test]
async fn typed_id_phantom_via_factory() {
let container = Container::builder().build().await.unwrap();
let user_ctx = container.resolve::<UserContext>().await.unwrap();
let order_ctx = container.resolve::<OrderContext>().await.unwrap();
assert!(user_ctx.id.0 >= 1, "user id allocated");
assert!(order_ctx.id.0 >= 100, "order id allocated");
}
pub struct RecordService {
users: Arc<Repository<UserEntity>>,
products: Arc<Repository<ProductEntity>>,
}
#[injectable]
impl RecordService {
#[injectable(ctor)]
fn new(
#[injectable(inject)] users: Arc<Repository<UserEntity>>,
#[injectable(inject)] products: Arc<Repository<ProductEntity>>,
) -> Self {
Self { users, products }
}
pub fn find_user(&self, id: u32) -> UserEntity {
let _ = &*self.users.db;
UserEntity { id }
}
pub fn find_product(&self, id: u32) -> ProductEntity {
let _ = &*self.products.db;
ProductEntity { id }
}
}
#[tokio::test]
async fn service_depending_on_generic_repos() {
let container = Container::builder().build().await.unwrap();
let svc = container.resolve::<RecordService>().await.unwrap();
let user = svc.find_user(42);
let product = svc.find_product(7);
assert_eq!(user.id, 42);
assert_eq!(product.id, 7);
}
#[tokio::test]
async fn vec_field_via_dyn_provider() {
let container = Container::builder()
.register(
"",
DynProvider::from_value(vec!["alpha".to_string(), "beta".to_string()]),
)
.build()
.await
.unwrap();
let tags: Vec<String> = container.resolve_external().await.unwrap();
assert_eq!(tags, vec!["alpha", "beta"]);
}
#[derive(Clone, Debug)]
struct TupleResource {
value: u32,
}
#[injectable]
impl TupleResource {
#[injectable(ctor)]
fn new() -> Self {
Self { value: 42 }
}
}
#[injectable]
struct TupleServiceA(#[injectable(inject)] Arc<TupleResource>);
#[injectable]
struct TupleServiceB(#[injectable(inject)] Arc<TupleResource>);
#[tokio::test]
async fn tuple_struct_inject_arc_singleton() {
let container = Container::builder().build().await.unwrap();
let a: TupleServiceA = container.resolve().await.unwrap();
let b: TupleServiceB = container.resolve().await.unwrap();
assert!(
Arc::ptr_eq(&a.0, &b.0),
"TupleServiceA and TupleServiceB must share the same Arc<TupleResource>"
);
}
#[tokio::test]
async fn tuple_struct_inject_deref_works() {
let container = Container::builder().build().await.unwrap();
let svc: TupleServiceA = container.resolve().await.unwrap();
assert_eq!(svc.0.value, 42);
}