rust-dix-macros 0.8.1

Procedural macros for rust-dix — a Rust dependency injection framework
Documentation

rust-dix-macros provides procedural macros for the rust-dix DI framework. These macros shift work from runtime to compile time, eliminating boilerplate and letting you declare DI configuration as close to your types as possible.

[dependencies]
rust-dix = "0.8"       # re-exports all macros — you usually don't need rust-dix-macros directly

Note: You rarely need to add rust-dix-macros as a direct dependency. The rust-dix crate re-exports all macros (Inject, module, inject, register). Just use rust_dix::* and you're set.


Macros

#[rust_dix::inject] — Attribute-based auto-registration (recommended)

What it does: Combines constructor generation and service registration in one attribute. The attribute can be placed on either a struct or a trait impl block, with a single optional lifetime argument that defaults to singleton.

ServiceCollection::from_injected() collects all registrations at startup.

Placement 1 — on a struct (registers as concrete type):

use rust_dix::*;

#[rust_dix::inject]          // singleton by default
struct Config;

#[rust_dix::inject(transient)]
struct Worker { dep: Arc<Config> }

Placement 2 — on a trait impl (registers as dyn Trait, auto-detected):

trait UserRepo { /* ... */ }

#[rust_dix::inject(transient)]   // on the struct first
struct PgUserRepo { db: Arc<DbPool> }

#[rust_dix::inject]              // then on the impl — registers as `dyn UserRepo`
impl UserRepo for PgUserRepo { /* ... */ }

The trait type is read from impl Trait for Type, so there is no need to spell out as = dyn Trait — the macro infers it. The impl-block form reuses the constructor generated by the struct-level annotation (or by an explicit #[derive(Inject)]), so you can register the same type under as many trait interfaces as you like:

#[rust_dix::inject]
struct BootLoader;

#[rust_dix::inject] impl EventHandler for BootLoader { /* ... */ }
#[rust_dix::inject] impl StartupTask   for BootLoader { /* ... */ }

Supported syntax:

Attribute Placement Registers as
#[rust_dix::inject] struct Concrete type, Singleton (default)
#[rust_dix::inject(singleton)] struct Concrete type, Singleton
#[rust_dix::inject(scoped)] struct Concrete type, Scoped
#[rust_dix::inject(transient)] struct Concrete type, Transient
#[rust_dix::inject] impl Trait for T dyn Trait, Singleton (default)
#[rust_dix::inject(scoped)] impl Trait for T dyn Trait, Scoped
#[rust_dix::inject(transient)] impl Trait for T dyn Trait, Transient

Supports both named-field structs and unit structs (zero fields):

#[rust_dix::inject]
struct UnitService;  // unit struct — works!

#[rust_dix::inject(transient)]
struct NamedService {
    dep: Arc<UnitService>,
}

This macro subsumes #[derive(Inject)] — when placed on a struct, it internally generates the same constructor function (__rdi_construct_{Type}) using the same field attribute syntax (#[inject], #[inject(owned)], #[inject(key = "...")], etc.). You do NOT need both. The impl-block placement does not generate a constructor — it reuses the one already produced by the struct placement.

Migration note: The old verbose syntax #[rust_dix::inject_attr(singleton, as = dyn Trait<...>)] has been removed. Use the impl-block placement instead.


#[derive(Inject)] — Auto-generated constructor

What it does: Reads the struct fields and their #[inject(...)] attributes, then generates a factory function __rdi_construct_{TypeName}() that resolves each marked field from the container. Unmarked fields use Default::default().

The generated function is #[doc(hidden)] internal surface: the macros use it to wire #[rust_dix::inject] / from_injected() auto-registration. Call it directly only when registering a custom factory closure — prefer #[rust_dix::inject] + from_injected() in application code.

Note: This derive macro is subsumed by #[rust_dix::inject] on a struct. You only need #[derive(Inject)] if you want to generate the constructor without automatic registration (e.g., to register manually with a custom factory closure).

Without it, you hand-write the factory closure and resolve every dependency yourself. Note that the closure receives &dyn IServiceResolver, where the generic get::<T>() is not callable (Self: Sized); manual resolution needs get_any + downcast, or capturing already-resolved Arcs in the closure.

With it, the macro generates the exact same code:

#[derive(Inject)]
struct MyService {
    #[inject]
    logger: Arc<Logger>,
    #[inject(key = "main")]
    cache: Arc<Cache>,
}
// Generates: __rdi_construct_MyService(resolver) -> Arc<MyService>

The generated function can be used anywhere you'd write a factory closure:

let provider = ServiceCollection::new()
    .singleton(__rdi_construct_MyService)
    .build().unwrap();

Field attributes (#[inject(...)]):

Explicit injection: only marked fields consult the resolver. Unmarked fields use Default::default() and are treated as internal state. Optionality is driven by the field type (Option<...>), not by a marker.

Attribute Field type Behavior
(none) any (Default) Internal field — uses Default::default()
#[inject] Arc<T> Required shared — Err(ServiceNotFound) (+ field context via try-construct)
#[inject] Option<Arc<T>> Optional shared — None if T not registered
#[inject(key = "k")] Arc<T> Keyed shared — Err(KeyedServiceNotFound) if key missing
#[inject(key = "k")] Option<Arc<T>> Optional keyed shared
#[inject(owned)] bare T Required owned — Err if not registered or Singleton
#[inject(owned)] Option<T> Optional owned — None if not registered or Singleton
#[inject(owned, key = "k")] bare T / Option<T> Keyed owned
#[inject] Vec<Arc<T>> Polymorphic — collects all non-keyed registrations in registration order (keyed implementations need get_all_keyed on the provider); no key attribute
#[inject(provider)] Arc<ServiceProvider> Injects the provider for on-demand resolution

Type checking is strict: #[inject] accepts Arc<T>, Option<Arc<T>>, or Vec<Arc<T>>, #[inject(owned)] only accepts bare T / Option<T>. Mismatched markers produce a compile-time error.

Built-in container services need no registration and also resolve in eagerly built singletons (0.7.1+): #[inject] scope_factory: Arc<dyn ScopeFactory>, #[inject] provider: Arc<ServiceProvider>, and the dedicated #[inject(provider)]. The built-in ScopeFactory is weak-backed and cached as a singleton, so holding it in a singleton does not create an Arc cycle; the container must outlive the services created from it. Prefer ScopeFactory over provider in singletons (holding Arc<ServiceProvider> creates a strong cycle), and resolve lazily in methods — resolving through the injected provider inside a build-time constructor is rejected.


#[rust_dix::module] — Compile-time module scanning

What it does: Scans a mod block for rust_dix::register!() declarations at compile time, then generates a __rdi_build_provider_{module_name}() function that constructs a fully configured ServiceProvider.

Without it, you register services one by one in a central setup function:

fn build_provider() -> ServiceProvider {
    ServiceCollection::new()
        .singleton(|_| Arc::new(MyService::default()))
        .singleton(|_| Arc::new(MyPlugin::default()))
        .keyed_singleton("verbose", |_| Arc::new(Logger::default()))
        .build().unwrap()
}

With it, you declare registrations right inside the module:

#[rust_dix::module]
mod services {
    rust_dix::register!(singleton: MyService);
    rust_dix::register!(singleton: dyn IPlugin => MyPlugin);
    rust_dix::register!(keyed "verbose": singleton: Logger);
}

// Generates: services::__rdi_build_provider_services() -> Result<Arc<ServiceProvider>>
let provider = services::__rdi_build_provider_services().unwrap();

Supported declarations:

Syntax Meaning
register!(singleton: T) Register T as singleton using Default
register!(scoped: T) Register T as scoped using Default
register!(transient: T) Register T as transient using Default
register!(singleton: dyn Trait => Impl) Register Impl as singleton implementing Trait
register!(keyed "k": singleton: T) Register T as keyed singleton
register!(keyed "k": scoped: T) Register T as keyed scoped
register!(factory singleton: T => expr) Register with a custom factory expression

When to use it:

  • You want service registrations physically close to the module that owns them
  • You are building a library or plugin and want to ship a pre-configured provider builder
  • You prefer a declarative DSL over method chaining

rust_dix::register! — The declaration macro

This is the macro used inside #[rust_dix::module] blocks to declare service registrations. It expands to nothing at the item level — its only effect is being collected by the enclosing #[rust_dix::module] attribute.

You can also use register! standalone as a macro_export:

rust_dix::register!(singleton: MyService);

Migration note: The function-like macro was renamed from inject!() to register!() to avoid the proc-macro namespace conflict (Rust does not allow an attribute macro and a function-like macro to share the same name in the same crate). This freed up the name inject for the attribute macro described above.


Behavior notes (0.8.x)

The macros themselves are unchanged in 0.8.x, but the container semantics they feed into are:

  • Single-service resolution is "last registration wins" (MEDI parity). If the same type is registered twice — e.g. an #[rust_dix::inject] auto-registration plus a manual singleton override — the last registration is what get() returns. Use try_add* when you want "only add if absent".
  • Vec<Arc<T>> injection only collects non-keyed registrations (registration order). Keyed implementations are resolved explicitly with get_all_keyed::<T>().
  • Strict scope validation is the default: resolving a Scoped service from the root provider returns Err(RdiError::ScopedFromRootProvider). Annotate request-scoped services with #[inject(scoped)] and resolve them inside provider.scope().
  • Dispose is explicit and ordered: Scope::dispose() / Drop releases scoped instances in reverse creation order; ServiceProvider::dispose() releases singletons and root-scoped instances. Constructor-time resolution through an injected provider is rejected with a typed FactoryPanic.

Relationship with rust-dix

rust-dix-macros is a companion to rust-dix. You can use rust-dix without any macros — just write factory closures manually — and everything works. The macros exist to eliminate repetition when you have many services or want to declare DI configuration declaratively.

Approach Pros Cons
#[rust_dix::inject] (recommended) Zero boilerplate, auto-registration, impl-block trait inference Requires inventory; unmarked fields need Default
Manual factories Full control, explicit Boilerplate for many services
#[derive(Inject)] Zero boilerplate for constructors Manual registration still needed
#[rust_dix::module] Declarative, centralized Less flexible for dynamic registration

All approaches can be mixed freely in the same project.


License

MIT.