Crates.io | async-injector |
lib.rs | async-injector |
version | 0.19.3 |
source | src |
created_at | 2019-07-30 18:15:51.855138 |
updated_at | 2023-12-06 14:27:30.764706 |
description | Reactive dependency injection for Rust. |
homepage | https://github.com/udoprog/async-injector |
repository | https://github.com/udoprog/async-injector |
max_upload_size | |
id | 153015 |
size | 84,219 |
Asynchronous dependency injection for Rust.
This library provides the glue which allows for building robust decoupled applications that can be reconfigured dynamically while they are running.
For a real world example of how this is used, see OxidizeBot
for which
it was written.
Add async-injector
to your Cargo.toml
.
[dependencies]
async-injector = "0.19.3"
In the following we'll showcase the injection of a fake Database
. The
idea here would be that if something about the database connection changes,
a new instance of Database
would be created and cause the application to
reconfigure itself.
use async_injector::{Key, Injector, Provider};
#[derive(Debug, Clone)]
struct Database;
#[derive(Debug, Provider)]
struct Service {
#[dependency]
database: Database,
}
async fn service(injector: Injector) -> Result<(), Box<dyn std::error::Error>> {
let mut provider = Service::provider(&injector).await?;
let Service { database } = provider.wait().await;
println!("Service got initial database {database:?}!");
let Service { database } = provider.wait().await;
println!("Service got new database {database:?}!");
Ok(())
}
Note: This is available as the
database
example:cargo run --example database
The Injector
above provides a structured broadcasting system that allows
for configuration updates to be cleanly integrated into asynchronous
contexts. The update itself is triggered by some other component that is
responsible for constructing the Database
instance.
Building up the components of your application like this means that it can be reconfigured without restarting it. Providing a much richer user experience.
In the previous section you might've noticed that the injected value was
solely discriminated by its type: Database
. In this example we'll show how
Key
can be used to tag values of the same type with different names to
discriminate them. This can be useful when dealing with overly generic types
like String
.
The tag used must be serializable with serde
. It must also not use any
components which cannot be hashed, like f32
and f64
.
The following example showcases the use of Key
to injector two different
values into an asynchronous greeter
.
use async_injector::{Key, Injector};
async fn greeter(injector: Injector) -> Result<(), Box<dyn std::error::Error>> {
let name = Key::<String>::tagged("name")?;
let fun = Key::<String>::tagged("fun")?;
let (mut name_stream, mut name) = injector.stream_key(name).await;
let (mut fun_stream, mut fun) = injector.stream_key(fun).await;
loop {
tokio::select! {
update = name_stream.recv() => {
name = update;
}
update = fun_stream.recv() => {
fun = update;
}
}
let (Some(name), Some(fun)) = (&name, &fun) else {
continue;
};
println!("Hi {name}! I see you do \"{fun}\" for fun!");
return Ok(());
}
}
Note: you can run this using:
cargo run --example greeter
The loop above can be implemented more easily using the Provider
derive,
so let's do that.
use async_injector::{Injector, Provider};
#[derive(Provider)]
struct Dependencies {
#[dependency(tag = "name")]
name: String,
#[dependency(tag = "fun")]
fun: String,
}
async fn greeter(injector: Injector) -> Result<(), Box<dyn std::error::Error>> {
let mut provider = Dependencies::provider(&injector).await?;
let Dependencies { name, fun } = provider.wait().await;
println!("Hi {name}! I see you do \"{fun}\" for fun!");
Ok(())
}
Note: you can run this using:
cargo run --example greeter_provider
Provider
deriveThe Provider
derive can be used to conveniently implement the mechanism
necessary to wait for a specific set of dependencies to become available.
It builds a companion structure next to the type being provided called
<name>Provider
which in turn implements the following set of methods:
use async_injector::{Error, Injector};
impl Dependencies {
/// Construct a new provider.
async fn provider(injector: &Injector) -> Result<DependenciesProvider, Error>
}
struct DependenciesProvider {
/* private fields */
}
impl DependenciesProvider {
/// Try to construct the current value. Returns [None] unless all
/// required dependencies are available.
fn build(&mut self) -> Option<Dependencies>
/// Wait until we can successfully build the complete provided
/// value.
async fn wait(&mut self) -> Dependencies
/// Wait until the provided value has changed. Either some
/// dependencies are no longer available at which it returns `None`,
/// or all dependencies are available after which we return the
/// build value.
async fn wait_for_update(&mut self) -> Option<Dependencies>
}
Provider
Any arguments which do not have the #[dependency]
attribute are known as
"fixed" arguments. These must be passed in when calling the provider
constructor. They can also be used during tag construction.
use async_injector::{Injector, Key, Provider};
#[derive(Provider)]
struct Dependencies {
name_tag: &'static str,
#[dependency(tag = name_tag)]
name: String,
}
async fn greeter(injector: Injector) -> Result<(), Box<dyn std::error::Error>> {
let mut provider = Dependencies::provider(&injector, "name").await?;
let Dependencies { name, .. } = provider.wait().await;
println!("Hi {name}!");
Ok(())
}