Crate coi

source ·
Expand description

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

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
// `#[coi(inject)]` (See Impl2 below).
#[derive(Inject)]
#[coi(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 `#[coi(inject)]`, so the parameter name
// must match a field name.
#[derive(Inject)]
#[coi(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.
    #[coi(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
// have 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());

How this crate works in more detail

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 above). The detailed docs for that are at coi::Inject (derive)

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.toml
[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)]
#[coi(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

  • A macro to simplify building of Containers.
  • Helper macro to ease use of “debug” feature when providing closures

Structs

  • A struct that manages all injected types.
  • A builder used to construct a Container.
  • A struct used to provide a registration to a container. It wraps a registration kind and a provider.

Enums

Traits

  • A marker trait for injectable traits and structs.
  • A trait to manage the construction of an injectable trait or struct.

Type Aliases

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

Derive Macros

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