Expand description
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
(trait) - a trait that indicates a struct is capable of providing a specific implementation of some injectable trait. This is generated for you if you usecoi::Inject
(derive) orcoi::Provide
(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 tocoi::Container::resolve
.coi::ContainerBuilder
- a builder for the above container to simplify construction and guarantee immutability after construction.
§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 ananalyze
fn, which will return an error if any misconfiguration is detected. See the docs foranalyze
for more details.Container
will get adot_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 todeps.dot
then randot -Tsvg deps.dot -o deps.svg
):
§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§
- container
- A macro to simplify building of
Container
s. - provide_
closure - Helper macro to ease use of “debug” feature when providing closures
Structs§
- Container
- A struct that manages all injected types.
- Container
Builder - A builder used to construct a
Container
. - Registration
- A struct used to provide a registration to a container. It wraps a registration kind and a provider.
Enums§
- Analysis
Error debug
- Possible errors generated when running
Container::analyze
. - Error
- Errors produced by this crate
- Registration
Kind - Control when
Container
will callProvide::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 Aliases§
- Result
- Type alias to
Result<T, coi::Error>
.