Extensibility
Using dependency injection in your own application is certainly useful; however, the strength of the more-di
crate really starts to shine when you enable DI composition across different crates. It effectively enables a DI ecosystem that crate library authors can elect to make required or opt into as a conditional feature.
Configuration
There are few requirements to make it possible to interleave DI into a library. This will typically be configured in Cargo.toml
.
[package]
name = "logger"
version = "1.0.0"
description = "An example logger"
[lib]
name = "logger"
[features]
di = ["more-di"]
async = ["more-di/async"] # our 'async' feature actives the 'more-di/async' feature
[dependencies.more-di]
version = "3.0"
default-features = false
features = ["inject"]
optional = true # only bring di when requested
The next part is to create a trait that can apply the extensions. It is not a hard requirement, but this typically takes the form of:
pub trait <Feature>ServiceExtensions
- Defined in the
crate::ext
module
The library module would then configure the extensions as optional.
lib.rs
#[cfg(feature = "di")]
mod di_ext;
#[cfg(feature = "di")]
pub mod ext {
use super::*;
pub use di_ext::*;
}
The extensions will then look something like the following and apply to ServiceCollection
.
di_ext.rs
use di::*;
// define extensions that can be applied to ServiceCollection
// note: remember to flow a mutable reference to make it easy
// to compose with other extensions
pub trait CustomServiceExtensions {
fn add_custom_services(&mut self) -> &mut Self;
}
impl CustomServiceExtensions for ServiceCollection {
fn add_custom_services(&mut self) -> &mut Self {
// add custom services
self.try_add(transient::<dyn Service, DefaultImpl>())
}
}
Example
Let's consider several composable library crates for logging.
Logger Crate
This crate would include the core abstractions common to all extenders.
// map to DI type when enabled
#[cfg(feature = "di")]
pub type Ref<T> = di::Ref<T>;
// default to Rc<T>
#[cfg(not(feature = "di"))]
pub type Ref<T> = std::rc::Rc<T>;
pub trait Logger {
fn log(&self, text: &str);
}
pub trait LoggerSource {
fn log(&self, text: &str);
}
// #[injectable(Logger)] only when the "di" feature is enabled
#[cfg_attr(feature = "di", di::injectable(Logger))]
pub struct DefaultLogger {
loggers: Vec<Ref<dyn LoggerSource>>,
}
impl DefaultLogger {
pub fn new(loggers: impl Iterator<Item = Ref<dyn LoggerSource>>) -> Self {
Self {
loggers: loggers.collect(),
}
}
}
impl Logger for DefaultLogger {
fn log(&self, text: &str) {
for logger in self.loggers {
logger.log(text)
}
}
}
#[cfg(feature = "di")]
pub mod ext {
use di::*;
pub trait LoggerServiceExtensions {
fn add_logging(&mut self) -> &mut Self;
}
impl LoggerServiceExtensions for ServiceCollection {
fn add_logging(&mut self) -> &mut Self {
self.try_add(DefaultLogger::singleton())
}
}
}
Console Logger Crate
This crate would provide a logger which writes to the console.
#[cfg_attr(feature = "di", di::injectable(LoggerSource))]
pub struct ConsoleLogger;
impl LoggerSource for ConsoleLogger {
fn log(&self, text: &str) {
println!("{}", text)
}
}
#[cfg(feature = "di")]
pub mod ext {
use di::*;
pub trait ConsoleLoggerServiceExtensions {
fn add_console_logging(&mut self) -> &mut Self;
}
impl ConsoleLoggerServiceExtensions for ServiceCollection {
fn add_console_logging(&mut self) -> &mut Self {
self.try_add(ConsoleLogger::transient())
}
}
}
File Logger Crate
This crate would provide a logger which writes to a file.
use std::fs::File;
pub struct FileLogger {
file: File,
}
impl FileLogger {
pub fn new<S: AsRef<str>>(filename: S) -> Self {
Self {
file: File::create(Path::new(s.as_ref())).unwrap(),
}
}
}
impl LoggerSource for FileLogger {
fn log(&self, text: &str) {
self.file.write_all(text.as_bytes()).ok()
}
}
#[cfg(feature = "di")]
pub mod ext {
use di::*;
pub trait FileLoggerServiceExtensions {
fn add_file_logging<S: AsRef<str>>(&mut self, filename: S) -> &mut Self;
}
impl FileLoggerServiceExtensions for ServiceCollection {
fn add_file_logging<S: AsRef<str>>(&mut self, filename: S) -> &mut Self {
let path = filename.as_ref().clone();
self.try_add(transient::<dyn LoggerSource, FileLogger>()
.from(move |_| Ref::new(FileLogger::new(path))))
}
}
}
Putting It All Together
We can now put it all together in an application. Our configuration will look something like:
[package]
name = "myapp"
version = "1.0.0"
description = "An example application"
[[bin]]
name = "myapp"
path = "main.rs"
[dependencies]
more-di = "3.0"
logger = { "1.0.0", features = ["di"] }
console-logger = { "1.0.0", features = ["di"] }
file-logger = { "1.0.0", features = ["di"] }
main.rs
use di::*;
use logger::{*, ext::*};
use console_logger::{*, ext::*};
use file_logger::{*, ext::*};
fn main() {
let provider = ServiceCollection::new()
.add_logging()
.add_console_logging()
.add_file_logging("example.log")
.build_provider()
.unwrap();
let logger = provider.get_required::<dyn Logger>();
logger.log("Hello world!");
}
Dependency Injected Enabled Crates
The following are crates which provide DI extensions: