[][src]Crate coi

Coi provides an easy to use dependency injection framework. Currently, this crate provides the following:

  • coi::Inject (trait) - a marker trait that indicates a trait or struct is injectable.
  • coi::Provide - a trait that indicates a struct is capable of providing a specific implementation of some injectable trait. This is generated for you if you use coi::Inject (derive), but can also be written manually.
  • coi::Container - a container to manage the lifetime of all dependencies. This is still in its early stages, and currently only supports objects that are recreated with each request to coi::Container::resolve.
  • coi::ContainerBuilder - a builder for the above container to simplify construction and guarantee immutability after construction.

How this crate works

For any trait you wish to abstract over, have it inherit the Inject trait. For structs, impl Inject for that struct, e.g.

trait Trait1: Inject {}

struct Struct1;

impl Inject for Struct1 {}

Then, in order to register the injectable item with the coi::ContainerBuilder, you also need a struct that impls Provide<Output = T> where T is your trait or struct. Provide exposes a provide fn that takes &self and &Container. When manually implementing Provide you must resolve all dependencies with container. Here's an example below:

trait Dependency: Inject {}

struct Impl1 {
    dependency: Arc<dyn Dependency>,
}

impl Impl1 {
    fn new(dependency: Arc<dyn Dependency>) -> Self {
        Self { dependency }
    }
}

impl Inject for Impl1 {}

impl Trait1 for Impl1 {}

struct Trait1Provider;

impl Provide for Trait1Provider {
    type Output = dyn Trait1;

    fn provide(&self, container: &Container) -> coi::Result<Arc<Self::Output>> {
        let dependency = container.resolve::<dyn Dependency>("dependency")?;
        Ok(Arc::new(Impl1::new(dependency)) as Arc<dyn Trait1>)
    }
}

The "dependency" above of course needs to be registered in order for the call to resolve to not error out:

struct DepImpl;

impl Dependency for DepImpl {}

impl Inject for DepImpl {}

struct DependencyProvider;

impl Provide for DependencyProvider {
    type Output = dyn Dependency;

    fn provide(&self, _: &Container) -> coi::Result<Arc<Self::Output>> {
        Ok(Arc::new(DepImpl) as Arc<dyn Dependency>)
    }
}

let mut container = container! {
    trait1 => Trait1Provider,
    dependency => DependencyProvider,
};
let trait1 = container.resolve::<dyn Trait1>("trait1");

In general, you usually won't want to write all of that. You would instead want to use the procedural macro (see example below). The detailed docs for that are at coi::Inject (derive)

Example

use coi::{container, Inject};
use std::sync::Arc;

// Mark injectable traits by inheriting the `Inject` trait.
trait Trait1: Inject {
    fn describe(&self) -> &'static str;
}

// For structs that will provide the implementation of an injectable trait, derive `Inject`
// and specify which expr will be used to inject which trait. The method can be any path.
// The arguments for the method are derived from fields marked with the attribute `#[inject]`
// (See Impl2 below).
#[derive(Inject)]
// Currently, only one trait can be provided, but this will likely be expanded on in future
// versions of this crate.
#[provides(dyn Trait1 with Impl1)]
struct Impl1;

// Don't forget to actually implement the trait ;).
impl Trait1 for Impl1 {
    fn describe(&self) -> &'static str {
        "I'm impl1!"
    }
}

// Mark injectable traits by inheriting the `Inject` trait.
trait Trait2: Inject {
    fn deep_describe(&self) -> String;
}

// For structs that will provide the implementation of an injectable trait, derive `Inject`
// and specify which method will be used to inject which trait. The arguments for the method
// are derived from fields marked with the attribute `#[inject]`, so the parameter name must
// match a field name.
#[derive(Inject)]
#[provides(dyn Trait2 with Impl2::new(trait1))]
struct Impl2 {
    // The name of the field is important! It must match the name that's registered in the
    // container when the container is being built! This is similar to the behavior of
    // dependency injection libraries in other languages.
    #[inject]
    trait1: Arc<dyn Trait1>,
}

// Implement the provider method
impl Impl2 {
    // Note: The param name here doesn't actually matter.
    fn new(trait1: Arc<dyn Trait1>) -> Self {
        Self { trait1 }
    }
}

// Again, don't forget to actually implement the trait ;).
impl Trait2 for Impl2 {
    fn deep_describe(&self) -> String {
        format!("I'm impl2! and I have {}", self.trait1.describe())
    }
}

// "Provider" structs are automatically generated through the `Inject` attribute. They
// append `Provider` to the name of the struct that is being derive (make sure you don't
// any structs with the same name or your code will fail to compile.
// Reminder: Make sure you use the same key here as the field names of the structs that
// require these impls.
let mut container = container! {
    trait1 => Impl1Provider,
    trait2 => Impl2Provider,
};

// Once the container is built, you can now resolve any particular instance by its key and
// the trait it provides. This crate currently only supports `Arc<dyn Trait>`, but this may
// be expanded in a future version of the crate.
let trait2 = container
    // Note: Getting the key wrong will produce an error telling you which key in the
    // chain of dependencies caused the failure (future versions might provider a vec of
    // chain that lead to the failure). Getting the type wrong will only tell you which key
    // had the wrong type. This is because at runtime, we do not have any type information,
    // only unique ids (that change during each compilation).
    .resolve::<dyn Trait2>("trait2")
    .expect("Should exist");
println!("Deep description: {}", trait2.deep_describe());

Debugging

To turn on debugging features, enable the debug feature (see below), then you'll have access to the following changes:

  • Formatting a container with {:?} will also list the dependencies (in A: Vec<B> style)
  • Container will get an analyze fn, which will return an error if any misconfiguration is detected. See the docs for analyze for more details.
  • Container will get a dot_graph fn, which will return a string that can be passed to graphviz's dot command to generate a graph. The image below was generated with the sample project that's in this crate's repository (output saved to deps.dot then ran dot -Tsvg deps.dot -o deps.svg ):
%3 0 Singleton - pool 1 Scoped - repository 1->0 2 Scoped - service 2->1

Features

Compilation taking too long? Turn off features you're not using.

To not use the default:

# Cargo.toml
[dependencies]
coi = { version = "...", default-features = false }

Why the #$*%T won't my container work!?

To turn on debugging features:

# Cargo.tomlk
[dependencies]
coi = { version = "...", default-features = false, features = ["debug"] }
  • default: derive - Procedural macros are re-exported.
  • debug: Debug impl
  • None - Procedural macros are not re-exported.

Help

External traits

Want to inject a trait that's not marked Inject? There's a very simple solution! It works even if the intended trait is not part of your crate.

// other.rs
pub trait Trait {
    ...
}

// your_lib.rs
use coi::Inject;
use other::Trait;

// Just inherit the intended trait and `Inject` on a trait in your crate,
// and make sure to also impl both traits for the intended provider.
pub trait InjectableTrait : Trait + Inject {}

#[derive(Inject)]
#[provides(pub dyn InjectableTrait with Impl{})]
struct Impl {
    ...
}

impl Trait for Impl {
    ...
}

impl InjectableTrait for Impl {}

Where are the factory registrations!?

If you're familiar with dependency injection in other languages, you might be used to factory registration where you can provide a method/closure/lambda/etc. during registration. Since the crate works off of the Provide trait, you would have to manually implement Provide for your factory method. This would also require you to manually retrieve your dependencies from the passed in Container as shown in the docs above.

Why can't I derive Inject when my struct contains a reference?

In order to store all of the resolved types, we have to use std::any::Any, which, unfortunately, has the restriction Any: 'static. This is because it's not yet known if there's a safe way to downcast to a type with a reference (See the comments in this tracking issue).

Macros

container

A macro to simplify building of Containers.

Structs

Container

A struct that manages all injected types.

ContainerBuilder

A builder used to construct a Container.

Registration

Enums

Error

Errors produced by this crate

RegistrationKind

Control when Container will call Provide::provide.

Traits

Inject

A marker trait for injectable traits and structs.

Provide

A trait to manage the construction of an injectable trait or struct.

Type Definitions

Result

Type alias to Result<T, coi::Error>.

Derive Macros

Inject

Generates an impl for Inject and also generates a "Provider" struct with its own Provide impl.

Provide

There might be some cases where you need to have data passed in with your provider.