Dependency injection for Rust
Run the following Cargo command in your project directory:
cargo add blackbox_di
Or add the following line to your Cargo.toml:
blackbox_di = "0.1"
Annotate interface with #[interface]
:
#[interface]
trait IService {
fn call(&self);
}
Annotate service structure with #[injectable]
:
#[injectable]
struct Service {}
Annotate impl block with #[implements]
:
#[implements]
impl IService for Service {
fn call() {
println!("Service calling");
}
}
Annotate module structure with #[module]
and specify Service
structure as a provider
:
#[module]
struct RootModule {
#[provider]
service: Service
}
#[launch]
async fn launch() {
let app = build::<RootModule>(BuildParams::default()).await;
let service = app
.get_by_token::<Service>(&get_token::<Service>)
.unwrap();
// short (equivalent to above)
let service = app.get::<Service>().unwrap();
service.call();
}
Specify #[inject]
for injectable dependencies:
#[injectable]
struct Repo {}
#[injectable]
struct Service {
#[inject]
repo: Ref<Repo>
}
Don't forget to specify the Repo
in the RootModule
module:
#[module]
struct RootModule {
#[provider]
repo: Repo
#[provider]
service: Service
}
You can specify a token instead of a type:
#[injectable]
struct Repo {}
#[injectable]
struct Service {
#[inject("REPO_TOKEN")]
repo: Ref<Repo>
}
And then:
#[module]
struct RootModule {
#[provider("REPO_TOKEN")]
repo: Repo
#[provider]
service: Service
}
Or use a constant as a token:
const REPO_TOKEN: &str = "REPO_TOKEN";
#[injectable]
struct Repo {}
#[injectable]
struct Service {
#[inject(REPO_TOKEN)]
repo: Ref<Repo>
}
#[module]
struct RootModule {
#[provider(REPO_TOKEN)]
repo: Repo
#[provider]
service: Service
}
You also can use interfaces for injectable dependencies:
const REPO_TOKEN: &str = "REPO_TOKEN";
#[interface]
trait IRepo {}
#[injectable]
struct Repo {}
#[implements]
impl IRepo for Repo {}
#[injectable]
struct Service {
#[inject(REPO_TOKEN)]
repo: Ref<dyn IRepo>
}
#[module]
struct RootModule {
#[provider(REPO_TOKEN)]
repo: Repo
#[provider]
service: Service
}
Or just use an existing implementation of the interface:
#[interface]
trait IRepo {}
#[injectable]
struct Repo {}
#[implements]
impl IRepo for Repo {}
#[injectable]
struct Service {
#[inject(use Repo)]
repo: Ref<dyn IRepo>
}
#[module]
struct RootModule {
#[provider]
repo: Repo
#[provider]
service: Service
}
If a service has non-injection dependencies:
#[injectable]
struct Service {
#[inject]
repo: Ref<Repo>
greeting: String
}
You should specify a factory function:
#[implements]
impl Service {
#[factory]
fn new(repo: Ref<Repo>) -> Service {
Service {
repo,
greeting: String::from("Hello")
}
}
}
Or for interfaces:
#[injectable]
struct Service {
#[inject(use Repo)]
repo: Ref<dyn IRepo>
greeting: String
}
#[implements]
impl Service {
#[factory]
fn new(repo: Ref<dyn IRepo>) -> Service {
Service {
repo,
greeting: String::from("Hello")
}
}
}
Injectable services with non-injectable
dependencies must have the factory
functions.
To have mutable non-injectable deps, you need specify these dependencies with RefMut<...>
:
#[injectable]
struct Service {
#[inject(use Repo)]
repo: Ref<dyn IRepo>
greeting: RefMut<String>
}
#[implements]
impl Service {
#[factory]
fn new(repo: Ref<dyn Repo>) -> Service {
Service {
repo,
greeting: RefMut::new(String::from("Hello"))
}
}
fn set_greeting(&self, msg: String) {
*self.greeting.as_mut() = msg;
}
fn print_greeting(&self) {
println!("{}", self.greeting.as_ref());
}
}
You can specify multiple modules and import them:
#[module]
struct UserModule {
#[provider]
user_service: UserService,
}
#[module]
struct RootModule {
#[import]
user_module: UserModule
}
To use providers from imported modules you should specify these providers as exported
:
#[module]
struct UserModule {
#[provider]
#[export]
user_service: UserService,
}
Also, you can specify your modules as global
then you don't have to import their directly. Just specify their only in the root
module:
#[module]
#[global]
struct UserModule {
#[provider]
#[export]
user_service: UserService,
}
#[module]
struct AccountModule {
#[provider]
#[export]
account_service: AccountService,
}
#[module]
struct RootModule {
#[import]
user_module: UserModule
#[import]
account_module: AccountModule
}
To resolve dependency cycle use Lazy
when module importing:
#[module]
struct UserModule {
#[import]
account_module: Lazy<AccountService>,
#[provider]
#[export]
user_service: UserService,
}
#[module]
struct AccountModule {
#[import]
user_module: Lazy<UserModule>,
#[provider]
#[export]
account_service: AccountService,
}
#[module]
struct RootModule {
#[import]
user_module: UserModule
#[import]
account_module: AccountModule
}
When the container is fully initialized, the system triggers events on_module_init
:
#[implements]
impl OnModuleInit for Service {
async fn on_module_init(&self) {
...
}
}
and on_module_destroy
:
#[implements]
impl OnModuleDestroy for Service {
async fn on_module_destroy(&self) {
...
}
}
The logger is used to display information about app build. To use custom logger implement ILogger
trait:
pub trait ILogger {
fn log<'a>(&self, level: LogLevel, msg: &'a str);
fn log_with_ctx<'a>(&self, level: LogLevel, msg: &'a str, ctx: &'a str);
fn set_context<'a>(&self, ctx: &'a str);
fn get_context(&self) -> String;
}
And then change build params:
let app = build::<RootModule>(
BuildParams::default().buffer_logs()
).await;
let custom_logger = app.get::<CustomLogger>().unwrap();
app.use_logger(custom_logger.cast::<dyn ILogger>().unwrap());
BlackBox DI is licensed under:
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)