38 releases

0.19.3 Dec 6, 2023
0.19.1 Apr 14, 2023
0.18.5 Mar 22, 2023
0.18.2 Feb 7, 2022
0.1.0 Jul 30, 2019

#454 in Asynchronous

27 downloads per month

MIT/Apache

53KB
505 lines

async-injector

github crates.io docs.rs build status

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.


Usage

Add async-injector to your Cargo.toml.

[dependencies]
async-injector = "0.19.3"

Example

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.


Injecting multiple things of the same type

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.


A simple greeter

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

The Provider derive

The 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>
}

Fixed arguments to 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(())
}

Dependencies

~4–10MB
~90K SLoC