This library contains all of the fundamental abstractions for dependency injection (DI).
A trait
or struct
can be used as the injected type.
This crate provides the following features:
A service can have the following lifetimes:
ServiceProvider
the first time it is requestedConsider the following traits and structures:
```rust use di::ServiceRef;
trait Foo { fn speak(&self) -> String; }
trait Bar { fn speak(&self) -> String; }
struct FooImpl { }
impl Foo for FooImpl { fn speak(&self) -> String { String::from("foo") } }
struct BarImpl {
foo: ServiceRef
impl BarImpl {
fn new(foo: ServiceRef
impl Bar for BarImpl { fn speak(&self) -> String { let mut text = self.foo.speak(); text.push_str(" bar"); text } } ```
```rust fn main() { let mut services = ServiceCollection::new();
services.add(
singleton::<dyn Foo, FooImpl>()
.from(|_| Rc::new(FooImpl::default())));
services.add(
transient::<dyn Bar, BarImpl>()
.from(|sp| Rc::new(BarImpl::new(sp.get_required::<dyn Foo>()))));
let provider = services.build_provider();
let bar = provider.get_required::<dyn Bar>();
let text = bar.speak();
assert_eq!(text, "foo bar")
} ```
Figure 1: Basic usage
Note:
singleton
andtransient
are utility functions provided by the builder feature.
In the preceding example, ServiceCollection::add
is used to add ServiceDescriptor
instances.
The framework also provides ServiceCollection::try_add
, which only registers the service if
there isn't already an implementation registered.
In the following example, the call to try_add
has no effect because the service has already
been registered:
```rust let mut services = ServiceCollection::new();
services.add(transient::
There scenarios where a service needs to be scoped; for example, for the lifetime of a HTTP request. A service definitely shouldn't live for the life of the application (e.g. singleton), but it also shouldn't be created each time it's requested within the request (e.g. transient). A scoped service lives for the lifetime of the container it was created from.
```rust
let provider = ServiceCollection::new()
.add(
scoped::
{
// create a scope where Bar is shared
let scope = provider.createscope();
let bar1 = provider.getrequired::
assert!(Rc::ptr_eq(&bar1, &bar2));
}
{
// create a new scope where Bar is shared and different from before
let scope = provider.createscope();
let bar1 = provider.getrequired::
assert!(Rc::ptr_eq(&bar1, &bar2));
} ```
Figure 2: Using scoped services
The Injectable
trait can be implemented so that structures can be injected as a
single, supported trait or as themselves.
```rust use di::*; use std::rc::Rc;
impl Injectable for FooImpl {
fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
ServiceDescriptorBuilder::
impl Injectable for BarImpl {
fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
ServiceDescriptorBuilder::
Figure 3: Implementing Injectable
While implementing Injectable
might be necessary or desired in a handful of scenarios, it is mostly tedious ceremony.
If the injection call site were known, then it would be possible to provide the implementation automatically. This is exactly
what the #[injectable]
attribute provides.
Instead of implementing Injectable
by hand, the implementation simply applies a decorator:
```rust use di::injectable; use std::rc::Rc;
impl BarImpl {
fn new(foo: Rc
Figure 4: Automatically implementing Injectable
Notice that the attribute is decorated on the impl
of the struct as opposed to a trait implementation. This is because this is
the location where the associated function that will be used to construct the struct is expected to be found. This allows the
attribute to inspect the injection call site to build the proper implementation. The attribute contains the trait to be satisfied.
If this process where reversed, it would require a lookahead or lookbehind to search for the implementation.
By default, the attribute will search for an associated function named new
. The function does not need to be pub
. This is
a simple convention that works for most cases; however, if you want to use a different name, the intended function must be
decorated with the #[inject]
attribute. This attribute simply indicates which function to use. If new
and a decorated function
are defined, the decorated function will take precedence. If multiple functions have #[inject]
applied, an error will occur.
Call site arguments must conform to the return values from:
ServiceProvider
- return the provider itself as a dependencyServiceProvider.get
- return an optional dependencyServiceProvider.get_required
- return a required dependency (or panic)ServiceProvider.get_all
- return all dependencies of a known type, which could be zeroThis means that the only allowed arguments are:
ServiceRef<T>
Option<ServiceRef<T>>
Vec<ServiceRef<T>>
ServiceProvider
ServiceRef<T>
is a provided type alias for Rc<T>
by default, but becomes Arc<T>
when the async feature is enabled. Rc<T>
and Arc<T>
are also allowed anywhere ServiceRef<T>
is allowed.
The following is an advanced example with all of these concepts applied:
```rust trait Logger { fn log(&self, message: &str); }
trait Translator { fn translate(&self, text: &str, lang: &str) -> String; }
impl BarImpl {
#[inject]
fn create(
foo: ServiceRef
Figure 5: Advanced Injectable
configuration
Which will expand to:
rust
impl Injectable for BarImpl {
fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
ServiceDescriptorBuilder::<dyn Bar, Self>::new(lifetime, Type::of::<Self>())
.from(|sp| Rc::new(
BarImpl::create(
sp.get_required::<dyn Foo>(),
sp.get::<dyn Translator>(),
sp.get_all::<dyn Logger>().collect())))
}
}
Figure 6: Advanced Injectable
implementation
Blanket implementations are provided for:
Injectable.singleton
Injectable.scoped
Injectable.transient
This simplifies registration to:
```rust fn main() { let provider = ServiceCollection::new() .add(FooImpl::singleton()) .add(BarImpl::transient()) .build_provider();
let bar = provider.get_required::<dyn Bar>();
let text = bar.speak();
assert_eq!(text, "foo bar")
} ``` Figure 7: inject feature usage
This project is licensed under the [MIT license].