Crates.io | kamikaze_di |
lib.rs | kamikaze_di |
version | 0.1.0 |
source | src |
created_at | 2019-06-04 18:36:16.086584 |
updated_at | 2019-06-04 18:36:16.086584 |
description | Exploration of Dependency Injection in Rust |
homepage | https://github.com/fabianbadoi/kamikaze_di |
repository | https://github.com/fabianbadoi/kamikaze_di |
max_upload_size | |
id | 138978 |
size | 36,697 |
This is what a dependency injection container for Rust. It's inspired by container libraries in other languages.
I mostly want to know what people think, and if anyone would want to use something like this.
let config = container.resolve::<Config>(); // simple resolve via the Resolver trait
let config: Config = cotantiner.inject(); // using Injector and Inject/InjectAsRc
// deriving the Inject trait teaches the container how to create it
#[derive(Inject, Clone)]
struct DabatabaseConnection {
config: Config,
...
}
See examples and docs for more.
Get both the base crate and the derive crate.
[dependencies]
kamikaze_di = "0.1.0"
kamikaze_di_derive = "0.1.0"
# or use this for slightly better debug! logs in the derive crate
# log = "0.4.6"
# kamikaze_di_derive = { version = "0.1.0", features="logging" }
This requires rust nightly.
There are two important concepts in Rust: ownershipt and mutability. Both influence the design of our DI container.
Data can only have one owner, so who ownes what when do you do:
let db = container.resolve::<Database>();
The db
object comes from inside the container. It must have owned it at some point, and now the current scope does.
So what happens when we resolve Database
again?
One way of going about it is to have the container act as a factory. While that's desired sometimes (and supported
via .register_factory::<T>()
), it's certainly not a sane default, how would we share things?
If we copy or clone objects before returning them, then we can share things. But there are things that should probably never be shared.
Other languages don't have this problem since everything lives in the heap and is reference counted. Sounds like Rc<>, doesn't it.
The type signature of all the register functions on the container builder is something like:
fn register<T>(&mut self, item: T) -> Result<()> where T: Clone
We always require Clone, some types will be OK with this. For the others, you can use Rc
let database = ...;
builder.register(Rc::new(database));
Rc can also be used with trait objects:
let database: MysqlConnection = ...;
builder.register::<Rc<Database>>(Rc::new(database));
I made the decision to use Clone/Rc early on, I'm very unsure it was the right one.
If you're getting cloned objects, mutability is your responsibility. If you're using Rc, there's a different story: Rc::get_mut() will always return None because the container will always keep a refence to it. You will need to use interior mutability.
That's a very good question.
Basically, I don't want to add it. I just want to start a discussion, I don't intend to maintain a tool I won't use myself, and I don't write enough Rust code to do that.
If the AutoResolvable
trait is in scope, the container will try to figure out how to create dependencies itself.
This would usually be done with reflection at runtime, but rust doesn't support that.
Any type implements Inject
or InjectAsRc
can be resolved this way. Of course, writing all that code youself is
tedious. So why not just derive that?
// Just derive this trait
#[derive(Inject, Clone)]
struct YourStruct {
// ...
}
All of that types dependencies will need to either derive Inject
, InjectAsRc
or be registered with the container.
You will get pretty decent error messages when types can't be resolved. Here's what you get if you unwrap() an error.
could not resolve Jester::voice_box : Rc < VoiceBox > ...
It's not perfect, the error doesn't use the full path of the type, but it's probably good enough to figure out what went wrong.
This project should only panic on circular dependencies, any other panic is a bug.
There are examples in repo and the documentation.