auto-di 0.6.0

Ergonomic async-aware automatic dependency injection for Rust
Documentation
# 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.6.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]` automatically removes DI-shaped parameters from the caller
signature and resolves them. `Arc<T>`, `Option<Arc<T>>`, `Vec<Arc<T>>`,
`Provider<T>`, and `Lazy<T>` are detected without an attribute:

```rust
#[injected]
fn calculate(
    config: Arc<AppConfig>,
    #[inject(2)] offset: i32,
    multiplier: i32,
) -> i32 {
    (config.base + offset) * multiplier
}

let result = calculate(3).await;
```

Every `#[injected]` function also gets a `_with` variant containing the full
original signature. Use the short name for automatic injection, or `_with` when
you want to pass every dependency manually:

```rust
calculate(3).await;                          // config auto-resolved
calculate_with(config, 2, 3);                // everything supplied manually
```

Rust does not support function overloading, so one function name cannot accept
both arities. The generated `_with` variant keeps both paths explicit and
type-safe.

Use `#[inject(expression)]` for literal/default or transformation overrides.
Use `#[argument]` when an `Arc<T>`-shaped value must remain caller-supplied.

The same syntax works on methods with a receiver. A generated wrapper preserves
the function's original return type, so it can directly satisfy Actix, Axum,
Rocket, or another framework's handler contract. Resolution failures go to the
process-wide handler and panic with a clear message by default. Override it
before serving requests when your application has its own fatal-error policy:

```rust
auto_di::set_resolution_error_handler(|error| panic!("DI failure: {error}"))?;
```

Use `#[injected(fallible)]` when the caller should receive
`Result<OriginalReturn, DiError>` and handle resolution failure itself.
Detection is based on the parameter's Rust type shape rather than the runtime
registry, keeping expansion deterministic across crates.

## 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.6.0" }
```

```rust
#[di::singleton]
fn dependency() -> Dependency { Dependency }
```

## Example

Run the complete example from this repository:

```bash
cargo run -p auto-di --example showcase
```