# auto-di
Async-aware automatic dependency injection for Rust. Providers register through
`inventory`; constructor parameters form the dependency graph, and instances
are initialized safely with Tokio `OnceCell`.
The public model is intentionally small:
- `#[singleton]` registers a constructor-managed dependency.
- `#[provider]` registers a factory function or factory method.
## Installation
```toml
[dependencies]
auto-di = "0.4.0"
```
## Quick start
```rust
use std::sync::Arc;
use auto_di::{application, resolve, singleton};
struct Database;
#[singleton]
async fn database() -> Database {
Database
}
struct UserService {
database: Arc<Database>,
}
#[singleton]
impl UserService {
fn new(database: Arc<Database>) -> Self {
Self { database }
}
}
#[application]
async fn main() -> Result<(), auto_di::DiError> {
let service = resolve::<UserService>().await?;
Ok(())
}
```
No registration list or manual container wiring is required.
## Field injection
`#[injectable]` generates singleton construction directly from struct fields.
Fields are automatically resolved by type; use `#[inject(...)]` to override a
field with a literal or transformation closure:
```rust
#[injectable]
struct Facade {
database: Arc<Database>,
#[inject(3)]
retries: usize,
#[inject(|client: Arc<HttpClient>| ClientHandle(client))]
client: ClientHandle,
}
```
`Arc<T>`, optional, collection, trait, `Provider<T>`, and `Lazy<T>` fields are
injected without cloning. An owned `T` field or owned closure input requires
`T: Clone`; prefer `Arc<T>` for singleton identity and zero-copy injection.
## Injected functions and methods
`#[injected]` removes explicitly marked `#[inject]` parameters from the caller
signature and resolves them automatically:
```rust
#[injected]
fn calculate(
#[inject] config: Arc<AppConfig>,
#[inject(2)] offset: i32,
multiplier: i32,
) -> i32 {
(config.base + offset) * multiplier
}
let result = calculate(3).await?;
```
The same syntax works on methods with a receiver. Generated wrappers are async
and return `Result<OriginalReturn, DiError>`, because dependency resolution can
fail. Injection is explicit rather than guessed from the registry, so ordinary
caller parameters remain unambiguous.
## Providers
A provider can be a standalone sync, async, or fallible function:
```rust
#[provider]
fn config() -> AppConfig {
AppConfig::default()
}
#[provider]
async fn database(config: Arc<AppConfig>) -> Result<Database, DatabaseError> {
Database::connect(&config.database_url).await
}
```
A singleton can expose additional providers directly:
```rust
#[singleton]
impl Application {
fn new(database: Arc<Database>) -> Self {
Self { database }
}
#[provider]
fn cache(&self) -> Cache {
Cache::new()
}
}
```
If a provider creates a dependency required by its owning singleton, make it an
associated function without `&self`. Otherwise the graph would be circular:
```rust
#[singleton]
impl Application {
fn new(config: Arc<AppConfig>) -> Self {
Self { config }
}
#[provider]
fn config() -> AppConfig {
AppConfig::default()
}
}
```
Direct calls such as `self.cache()` bypass the container and are rejected by
the macro. Inject the provided type as a parameter instead.
## Injection forms
Constructors and providers support:
```rust
Arc<T> // required dependency
Option<Arc<T>> // optional dependency
Vec<Arc<T>> // all implementations
Arc<dyn Trait> // selected trait implementation
Vec<Arc<dyn Trait>> // all trait implementations
Provider<T> // resolve later
Lazy<T> // resolve once on first access
```
`Provider<T>` and `Lazy<T>` preserve the originating container, active profile,
and request context.
## Multiple implementations
```rust
trait Payments: Send + Sync {}
#[singleton(name = "stripe", primary)]
fn stripe() -> Arc<dyn Payments> {
Arc::new(Stripe)
}
#[singleton(name = "razorpay")]
fn razorpay() -> Arc<dyn Payments> {
Arc::new(Razorpay)
}
#[singleton]
impl Checkout {
fn new(
default: Arc<dyn Payments>,
#[qualifier("razorpay")] indian: Arc<dyn Payments>,
) -> Self {
Self { default, indian }
}
}
```
Duplicate names, multiple primary implementations, and unresolved ambiguity are
reported as DI errors.
## Scopes
The default scope is singleton:
```rust
#[singleton]
fn shared_client() -> Client { Client::new() }
#[singleton(scope = "prototype")]
fn job() -> Job { Job::new() }
#[singleton(scope = "request")]
fn request_metadata() -> RequestMetadata { RequestMetadata::new() }
```
Resolve request-scoped dependencies through a request context:
```rust
let request = auto_di::global_container()?.request_context();
let metadata = request.resolve::<RequestMetadata>().await?;
```
A singleton is not allowed to capture a request-scoped dependency.
## Constructor options
`#[singleton]` and `#[provider]` accept:
- `name = "..."`
- `primary`
- `scope = "singleton" | "prototype" | "request"`
- `eager`
- `blocking` for expensive synchronous work
- `profile = "development"`
- `condition = "ENV_KEY"` or `condition = "ENV_KEY=value"`
- `post_construct = "async_method"`
- `pre_destroy = "async_method"`
Lifecycle methods may return `()` or `Result<(), E>`.
## Profiles and conditions
Active profiles come from comma-separated `APP_PROFILES`:
```rust
#[singleton(profile = "development")]
fn development_database() -> Database { /* ... */ }
#[singleton(condition = "CACHE_ENABLED=true")]
fn cache() -> Cache { /* ... */ }
```
A local container can select profiles explicitly:
```rust
let container = auto_di::Container::with_profiles(["test"])?;
```
## Configuration properties
```rust
#[configuration_properties("database")]
struct DatabaseProperties {
url: String, // DATABASE_URL
pool_size: usize, // DATABASE_POOL_SIZE
}
```
## Validation and lifecycle
`Container::validate()` initializes and validates every active singleton. It
surfaces missing dependencies, ambiguous providers, circular graphs, and scope
violations during startup. `#[application]` runs validation automatically and
runs shutdown hooks in dependency-safe topological order.
The resolver also detects cycles formed concurrently by different Tokio tasks,
so they return an error instead of waiting on each other's `OnceCell` forever.
## Renaming the dependency
Cargo aliases are supported:
```toml
[dependencies]
di = { package = "auto-di", version = "0.4.0" }
```
```rust
#[di::singleton]
fn dependency() -> Dependency { Dependency }
```
## Example
Run the complete example from this repository:
```bash
cargo run -p auto-di --example showcase
```