1 unstable release
0.1.0 | Jun 4, 2019 |
---|
#462 in Build Utils
Used in kamikaze_di_derive
37KB
525 lines
Kamikaze DI
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.
What it looks like in action
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.
Installation
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.
Discussion
There are two important concepts in Rust: ownershipt and mutability. Both influence the design of our DI container.
Ownership
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?
Factories
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?
Copies
If we copy or clone objects before returning them, then we can share things. But there are things that should probably never be shared.
Cloning the unclonable?
Other languages don't have this problem since everything lives in the heap and is reference counted. Sounds like Rc<>, doesn't it.
Using Rc
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));
Why not &T?
I made the decision to use Clone/Rc early on, I'm very unsure it was the right one.
What about mutablility?
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.
What about Sync
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.
Auto-derive
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.
Errors
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.
Panics
This project should only panic on circular dependencies, any other panic is a bug.
Examples
There are examples in repo and the documentation.
Dependencies
~86KB