rust-dix-macros 0.6.0

Procedural macros for rust-dix — a Rust dependency injection framework
Documentation
  • Coverage
  • 20%
    1 out of 5 items documented0 out of 4 items with examples
  • Size
  • Source code size: 48.58 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 360.56 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • lusida2026

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.6"       # 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().

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 write factory closures manually:

let provider = ServiceCollection::new()
    .singleton(|p| {
        Arc::new(MyService::new(
            p.get::<Logger>(),
            p.get_keyed::<Cache>("main"),
        ))
    })
    .build().unwrap();

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 — panics if T not registered
#[inject] Option<Arc<T>> Optional shared — None if T not registered
#[inject(key = "k")] Arc<T> Keyed shared, panics if key missing
#[inject(key = "k")] Option<Arc<T>> Optional keyed shared
#[inject(owned)] bare T Required owned — panics 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 registered implementations (default + keyed); key not supported
#[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.


#[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.


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.