Crate runtime_injector[][src]

Runtime dependency injection.

By default, services provided by the Injector are not thread-safe. This is because Rc<T> is used to hold instances of the services, which is not a thread-safe pointer type. This can be changed by disabling default features and enabling the “arc” feature:

runtime_injector = {
    version = "*",
    default_features = false,
    features = ["arc"]
}

Runtime dependency injection (rather than compile-time)

Runtime dependency injection allows for custom configuration of services during runtime rather than needing to determine what services are used at compile time. This means you can read a config when your application starts, determine what implementations you want to use for your interfaces, and assign those at runtime. This is also slower than compile-time dependency injection, so if pointer indirection, dynamic dispatch, or heap allocations are a concern, then a compile-time dependency injection library might be preferred instead.

Interfaces

Proper inversion of control requires that each service requests its dependencies without actually caring how those dependencies are implemented. For instance, suppose you are working with a database. A service which depends on interacting with that database may request a dependency that can interact with that database without needing to know the concrete type being used. This is done using dynamic dispatch to allow the concrete type to be determined at runtime (rather than using generics to determine the implementations at compile time).

Service lifetimes

Lifetimes of services created by the Injector are controlled by the provider used to construct those lifetimes. Currently, there are three built-in service provider types:

  • Singleton: A service is created only the first time it is requested and that single instance is reused for each future request.
  • Transient: A service is created each time it is requested.
  • Constant: Used for services that are not created using a factory function and instead can have their instance provided to the container directly. This behaves similar to singleton in that the same instance is provided each time the service is requested.

Custom service providers can also be created by implementing the TypedProvider trait.

Example

use runtime_injector::{interface, Injector, Svc, IntoSingleton};
use std::error::Error;

// Some type that represents a user
struct User;

// This is our interface. In practice, multiple structs can implement this
// trait, and we don't care what the concrete type is most of the time in
// our other services as long as it implements this trait. Because of this,
// we're going to use dynamic dispatch later so that we can determine the
// concrete type at runtime (vs. generics, which are determined instead at
// compile time).
//
// The `Send` and `Sync` supertrait requirements are only necessary when
// compiling with the "arc" feature to allow for service pointer
// downcasting.
trait DataService: Send + Sync {
    fn get_user(&self, user_id: &str) -> Option<User>;
}

// We can use a data service which connects to a SQL database.
#[derive(Default)]
struct SqlDataService;
impl DataService for SqlDataService {
    fn get_user(&self, _user_id: &str) -> Option<User> { todo!() }
}

// ... Or we can mock out the data service entirely!
#[derive(Default)]
struct MockDataService;
impl DataService for MockDataService {
    fn get_user(&self, _user_id: &str) -> Option<User> { Some(User) }
}

// Specify which types implement the DataService interface. This does not
// determine the actual implementation used. It only registers the types as
// possible implementations of the DataService interface.
interface!(DataService = [ SqlDataService, MockDataService ]);

// Here's another service our application uses. This service depends on our
// data service, however it doesn't care how that service is actually
// implemented as long as it works. Because of that, we're using dynamic
// dispatch to allow the implementation to be determined at runtime.
struct UserService {
    data_service: Svc<dyn DataService>,
}

impl UserService {
    // This is just a normal constructor. The only requirement is that each
    // parameter is a valid injectable dependency.
    pub fn new(data_service: Svc<dyn DataService>) -> Self {
        UserService { data_service }
    }

    pub fn get_user(&self, user_id: &str) -> Option<User> {
        // UserService doesn't care how the user is actually retrieved
        self.data_service.get_user(user_id)
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    // This is where we register our services. Each call to `.provide` adds
    // a new service provider to our container, however nothing is actually
    // created until it is requested. This means we can add providers for
    // types we aren't actually going to use without worrying about
    // constructing instances of those types that we aren't actually using.
    let mut builder = Injector::builder();
    builder.provide(UserService::new.singleton());
    builder.provide(SqlDataService::default.singleton());
    builder.provide(MockDataService::default.singleton());

    // Note that we can register closures as providers as well
    builder.provide((|_: Svc<dyn DataService>| "Hello, world!").singleton());
    builder.provide((|_: Option<Svc<i32>>| 120.9).singleton());
     
    // Let's choose to use the MockDataService as our data service
    builder.implement::<dyn DataService, MockDataService>();
     
    // Now that we've registered all our providers and implementations, we
    // can start relying on our container to create our services for us!
    let mut injector = builder.build();
    let user_service: Svc<UserService> = injector.get()?;
    let _user = user_service.get_user("john");
     
    Ok(())
}

Macros

interface

Marks a trait as being an interface for many other types. This means that a request for the given trait can resolve to any of the types indicated by this macro invocation.

Structs

ConstantProvider

A provider which returns a constant, predetermined value. Note that this is technically a singleton service in that it does not recreate the value each time it is requested.

Injector

A runtime dependency injection container. This holds all the bindings between service types and their providers, as well as all the mappings from interfaces to their implementations (if they differ).

InjectorBuilder

A builder for an Injector.

ServiceInfo

Type information about a service.

SingletonProvider

A service provider that only creates a single instance of the service. The service is created only during its first request. Any subsequent requests return service pointers to the same service.

TransientProvider

A service provider that creates an instance of the service each time it is requested. This will never return two service pointers to the same instance of a service.

Enums

InjectError

An error that has occurred during creation of a service.

Traits

Interface

Indicates that a type can resolve services. The most basic implementation of this trait is that each sized service type can resolve itself. This is done by requesting the exact implementation of itself from the injector. However, the injector cannot provide exact implementations for dynamic types (dyn Trait). For this reason, any interfaces using traits must be declared explicitly before use. This trait should usually be implemented by the interface! macro.

InterfaceFor

Marker trait that indicates that a type is an interface for another type. Each sized type is an interface for itself, and each dyn Trait is an interface for the types that it can resolve. This trait should usually be implemented by the interface! macro, and is strictly used to enforce stronger type checking when assigning implementations for interfaces.

IntoSingleton

Defines a conversion into a singleton provider. This trait is automatically implemented for all service factories.

IntoTransient

Defines a conversion into a transient provider. This trait is automatically implemented for all service factories.

Provider

Weakly typed service provider. Given an injector, this will provide an implementation of a service. This is automatically implemented for all types that implement TypedProvider, and TypedProvider should be preferred if possible to allow for stronger type checking.

Request

A request to an injector.

Service

Implemented automatically on types that are capable of being a service.

ServiceFactory

A factory for creating instances of a service. All functions of arity 12 or less are automatically service factories if the arguments to that function are valid service requests and the return value is a valid service type.

TypedProvider

A strongly-typed service provider. Types which implement this provide instances of a service type when requested. Examples of typed providers include providers created from service factories or constant providers. This should be preferred over Provider for custom service providers if possible due to the strong type guarantees this provides. Provider is automatically implemented for all types which implement TypedProvider.

Functions

constant

Create a service from a constant value. While the service itself will never be exposed through a mutable reference, if it supports interior mutability, its fields still can be mutated. Since the provider created with this function doesn’t recreate the value each time it’s requested, state can be stored in this manner.

Type Definitions

DynSvc

A reference-counted service pointer holding an instance of dyn Any.

InjectResult

A result from attempting to inject dependencies into a service and construct an instance of it.

Svc

A reference-counted pointer holding a service. The pointer type is determined by the feature flags passed to this crate.