Crate di

source ·
Expand description

More Dependency Injection Crate

Crates.io MIT licensed

This library contains all of the fundamental abstractions for dependency injection (DI). A trait or struct can be used as the injected type.

Features

This crate provides the following features:

  • Default - Provides the abstractions for dependency injection, plus the builder and inject features
  • builder - Provides utility functions for configuring service descriptors
  • async - Provides features for using dependencies in an asynchronous context
  • inject - Provides constructor injection

Service Lifetimes

A service can have the following lifetimes:

  • Transient - a new instance is created every time it is requested
  • Singleton - a single, new instance is created the first time it is requested
  • Scoped - a new instance is created once per ServiceProvider the first time it is requested

Examples

Consider the following traits and structures:

use di::ServiceRef;

trait Foo {
    fn speak(&self) -> String;
}

trait Bar {
    fn speak(&self) -> String;
}

#[derive(Default)]
struct FooImpl { }

impl Foo for FooImpl {
    fn speak(&self) -> String {
        String::from("foo")
    }
}

struct BarImpl {
    foo: ServiceRef<dyn Foo>
}

impl BarImpl {
    fn new(foo: ServiceRef<dyn Foo>) -> Self {
        Self { foo }
    }
}

impl Bar for BarImpl {
    fn speak(&self) -> String {
        let mut text = self.foo.speak();
        text.push_str(" bar");
        text
    }
}

Service Registration and Resolution

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().unwrap();
    let bar = provider.get_required::<dyn Bar>();
    let text = bar.speak();

    assert_eq!(text, "foo bar")
}

Figure 1: Basic usage

Note: singleton and transient 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:

let mut services = ServiceCollection::new();

services.add(transient::<dyn Foo, Foo2>().from(|_| Rc::new(Foo2::default())));
services.try_add(transient::<dyn Foo, FooImpl>().from(|_| Rc::new(FooImpl::default())));

Scope Scenarios

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.

let provider = ServiceCollection::new()
    .add(
        scoped::<dyn Foo, FooImpl>()
        .from(|_| Rc::new(FooImpl::default())))
    .add(
        transient::<dyn Bar, BarImpl>()
        .from(|sp| Rc::new(BarImpl::new(sp.get_required::<dyn Foo>()))))
    .build_provider()
    .unwrap();

{
    // create a scope where Bar is shared
    let scope = provider.create_scope();
    let bar1 = provider.get_required::<dyn Bar>();
    let bar2 = provider.get_required::<dyn Bar>();
    
    assert!(Rc::ptr_eq(&bar1, &bar2));
}

{
    // create a new scope where Bar is shared and different from before
    let scope = provider.create_scope();
    let bar1 = provider.get_required::<dyn Bar>();
    let bar2 = provider.get_required::<dyn Bar>();
    
    assert!(Rc::ptr_eq(&bar1, &bar2));
}

Figure 2: Using scoped services

Note: scoped and transient are utility functions provided by the builder feature.

Validation

The consumers of a ServiceProvider expect that it is correctly configured and ready for use. There are edge cases, however, which could lead to runtime failures or incorrect behavior such as:

  • A required, dependent service that has not be registered
  • A circular dependency, which will trigger a stack overflow
  • A service with a singleton lifetime has a dependent service with a scoped lifetime

Intrinsic validation has been added to ensure this cannot happen. The build_provider() function will return Result<ServiceProvider, ValidationError>, which will either contain a valid ServiceProvider or a ValidationError that will detail all of the errors. From that point forward, the ServiceProvider will be considered semantically correct and safe to use. The same validation process can also be invoked imperatively on-demand by using the di::validate function.

The ServiceDescriptorBuilder cannot automatically determine the dependencies your service may require. This means that validation is an explicit, opt-in capability. If you do not configure any dependencies for a ServiceDescriptor, then no validation will occur.

fn main() {
    let mut services = ServiceCollection::new();

    services.add(
        singleton::<dyn Foo, FooImpl>()
        .from(|_| Rc::new(FooImpl::default())));
    services.add(
        transient::<dyn Bar, BarImpl>()
        .depends_on(exactly_one::<dyn Foo>())
        .from(|sp| Rc::new(BarImpl::new(sp.get_required::<dyn Foo>()))));

    match services.build_provider() {
        Ok(provider) => {
            let bar = provider.get_required::<dyn Bar>();
            assert_eq!(&bar.speak(), "foo bar");
        },
        Err(error) => {
            println!("The service configuration is invalid.\n{}", &error.to_string());
        }
    }
}

Figure 3: Validating service configuration

Note: singleton, transient, and exactly_one are utility functions provided by the builder feature.

Inject Feature

The Injectable trait can be implemented so that structures can be injected as a single, supported trait or as themselves.

use di::*;
use std::rc::Rc;

impl Injectable for FooImpl {
    fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
        ServiceDescriptorBuilder::<dyn Foo, Self>::new(lifetime, Type::of::<Self>())
            .from(|_| Rc::new(FooImpl::default()))
    }
}

impl Injectable for BarImpl {
    fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
        ServiceDescriptorBuilder::<dyn Bar, Self>::new(lifetime, Type::of::<Self>())
            .from(|sp| Rc::new(BarImpl::new(sp.get_required::<dyn Foo>())))
    }
}

Figure 4: 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:

use di::injectable;
use std::rc::Rc;

#[injectable(Bar)]
impl BarImpl {
    fn new(foo: Rc<dyn Foo>) -> Self {
        Self { foo: foo }
    }
}

Figure 5: Automatically implementing Injectable

Injection Rules

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 dependency
  • ServiceProvider.get - return an optional dependency
  • ServiceProvider.get_required- return a required dependency (or panic)
  • ServiceProvider.get_all - return all dependencies of a known type, which could be zero

This 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. For every injected type T, the appropriate ServiceDependency configuration is also added so that injected types can be validated.

The following is an advanced example with all of these concepts applied:

trait Logger {
    fn log(&self, message: &str);
}

trait Translator {
    fn translate(&self, text: &str, lang: &str) -> String;
}

#[injectable(Bar)]
impl BarImpl {
    #[inject]
    fn create(
        foo: ServiceRef<dyn Foo>,
        translator: Option<ServiceRef<dyn Translator>>,
        loggers: Vec<ServiceRef<dyn Logger>>) -> Self {
        Self {
            foo: foo,
            translator,
            loggers: loggers,
        }
    }
}

Figure 6: Advanced Injectable configuration

Which will expand to:

impl Injectable for BarImpl {
    fn inject(lifetime: ServiceLifetime) -> ServiceDescriptor {
        ServiceDescriptorBuilder::<dyn Bar, Self>::new(lifetime, Type::of::<Self>())
            .depends_on(ServiceDependency::new(Type::of::<dyn Foo>(), ServiceCardinality::ExactlyOne))
            .depends_on(ServiceDependency::new(Type::of::<dyn Translator>(), ServiceCardinality::ZeroOrOne))
            .depends_on(ServiceDependency::new(Type::of::<dyn Logger>(), ServiceCardinality::ZeroOrMore))
            .from(|sp| Rc::new(
                BarImpl::create(
                    sp.get_required::<dyn Foo>(),
                    sp.get::<dyn Translator>(),
                    sp.get_all::<dyn Logger>().collect())))
    }
}

Figure 7: Advanced Injectable implementation

Simplified Registration

Blanket implementations are provided for:

  • Injectable.singleton
  • Injectable.scoped
  • Injectable.transient

This simplifies registration to:

fn main() {
    let provider = ServiceCollection::new()
        .add(FooImpl::singleton())
        .add(BarImpl::transient())
        .build_provider()
        .unwrap();

    let bar = provider.get_required::<dyn Bar>();
    let text = bar.speak();

    assert_eq!(text, "foo bar")
}

Figure 8: inject feature usage

License

This project is licensed under the MIT license.

Structs

Represents a service collection.
Represents a service dependency.
Represents the description of a service with its service type, implementation, and lifetime.
Represents a service provider.
Represents a type.
Represents an validation error.

Enums

Represents the possible cardinalities of a service dependency.
Represents the possible service lifetimes.

Traits

Defines the behavior of an injectable type.

Functions

Creates a new service dependency with a cardinality of exactly one (1:1).
Creates a new singleton service descriptor for an existing service instance.
Creates a new singleton service descriptor for an existing service instance.
Initializes a new scoped service descriptor builder.
Initializes a new scoped service descriptor.
Initializes a new singleton service descriptor builder.
Initializes a new singleton service descriptor builder.
Initializes a new singleton service descriptor.
Initializes a new transient service descriptor builder.
Initializes a new transient service descriptor builder.
Initializes a new transient service descriptor.
Validates the specified service collection.
Creates a new service dependency with a cardinality of zero or more (0:*).
Creates a new service dependency with a cardinality of zero or one (0:1).

Type Definitions

Represents the callback function used to create a service.
Represents the type alias for a service reference.

Attribute Macros

Represents the metadata used to identify an injected function.
Represents the metadata used to implement the Injectable trait.