async-injector

Documentation Crates Actions Status

Asynchronous dependency injection for Rust.

This crate provides a dependency injection system that can be used to reactively reconfigure you're application while it's running. Reactive in this case refers to the application being reconfigured as-the-value changes, and not for other typical scenarios such as when it's being restarted.

Values are provided as [Stream]s of updates that can be subscribed to as necessary throughout your application.

Examples

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 update.

This is available as the fake_database example and you can run it yourself using: sh cargo run --example fake_database

```rust use tokio::time;

[derive(Clone)]

struct Database;

[tokio::main]

async fn main() { let injector = asyncinjector::Injector::new(); let (mut databasestream, mut database) = injector.stream::().await;

// Insert the database dependency in a different task in the background.
tokio::spawn({
    let injector = injector.clone();

    async move {
        time::sleep(time::Duration::from_secs(2)).await;
        injector.update(Database).await;
    }
});

assert!(database.is_none());
// Every update to the stored type will be streamed, allowing you to
// react to it.
database = database_stream.recv().await;
assert!(database.is_some());

} ```

The [Injector] provides a structured broadcast system of updates, that can integrate cleanly into asynchronous contexts.

With a bit of glue, this means that your application can be reconfigured without restarting it. Providing a 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 under different names. 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. This will otherwise cause an error to be raised as it's being injected.

```rust use async_injector::Key; use serde::Serialize; use std::{error::Error, time::Duration}; use tokio::time;

[derive(Serialize)]

enum Tag { One, Two, }

[tokio::main]

async fn main() -> Result<(), Box> { let injector = async_injector::Injector::new(); let one = Key::::tagged(Tag::One)?; let two = Key::::tagged(Tag::Two)?;

tokio::spawn({
    let injector = injector.clone();
    let one = one.clone();

    async move {
        let mut interval = time::interval(Duration::from_secs(1));

        for i in 0u32.. {
            interval.tick().await;
            injector.update_key(&one, i).await;
        }
    }
});

tokio::spawn({
    let injector = injector.clone();
    let two = two.clone();

    async move {
        let mut interval = time::interval(Duration::from_secs(1));

        for i in 0u32.. {
            interval.tick().await;
            injector.update_key(&two, i * 2).await;
        }
    }
});

let (mut one_stream, mut one) = injector.stream_key(one).await;
let (mut two_stream, mut two) = injector.stream_key(two).await;

println!("one: {:?}", one);
println!("two: {:?}", two);

loop {
    tokio::select! {
        update = one_stream.recv() => {
            one = update;
            println!("one: {:?}", one);
        }
        update = two_stream.recv() => {
            two = update;
            println!("two: {:?}", two);
        }
    }
}

} ```

The Provider derive

The following showcases how the [Provider] derive can be used to conveniently wait for groups of dependencies to become available.

Below we're waiting for two database parameters to become updated: * url * connection_limit

Also note how they're being update asynchronously in a background thread to simulate being updated "somewhere else". This could be an update event caused by a multitude of things, like a configuration change in a frontend.

```rust use async_injector::{Key, Provider, Injector}; use serde::Serialize; use std::error::Error; use std::time::Duration;

/// Fake database connection.

[derive(Clone, Debug, PartialEq, Eq)]

struct Database { url: String, connection_limit: u32, }

/// Provider that describes how to construct a database.

[derive(Serialize)]

pub enum Tag { DatabaseUrl, ConnectionLimit, }

/// A group of database params to wait for until they become available.

[derive(Provider)]

struct DatabaseParams { #[dependency(tag = "Tag::DatabaseUrl")] url: String, #[dependency(tag = "Tag::ConnectionLimit")] connection_limit: u32, }

async fn updatedbparams(injector: Injector, dburl: Key, connectionlimit: Key) { tokio::time::sleep(Duration::from_secs(2));

injector.update_key(&db_url, String::from("example.com")).await;
injector.update_key(&connection_limit, 5).await;

}

/// Fake service that runs for two seconds with a configured database. async fn service(db: Database) { tokio::time::sleep(Duration::from_secs(2)); }

[tokio::test]

async fn testprovider() -> Result<(), Box> { let dburl = Key::::tagged(Tag::DatabaseUrl)?; let connection_limit = Key::::tagged(Tag::ConnectionLimit)?;

let injector = Injector::new();

/// Set up asynchronous task that updates the parameters in the background.
tokio::spawn(update_db_params(injector.clone(), db_url, connection_limit));

let provider = DatabaseParams::new(&injector).await?;

loop {
    /// Wait until database is configured.
    let database = loop {
        if let Some(update) = provider.update().await {
            break Database {
                url: update.url,
                connection_limit: update.connection_limit,
            };
        }
    };

    assert_eq!(
        new_database,
        Database {
            url: String::from("example.com"),
            connection_limit: 5
        }
    );

    loop {
        tokio::select! {
            _ = service(database.clone()) => {
                break;
            }
            update = provider.update().await {
                match update {
                    None => break,
                }
            }
        }
    }
}

Ok(())

} ```