App context

The AppContext is the core container for shared state in Roadster. It can be used as the Axum Router state directly, or you can define your own state type that can be used by both Roadster and Axum by implementing FromRef.

use axum::extract::FromRef;
use roadster::app::context::AppContext;

#[derive(Clone, FromRef)]
pub struct CustomState {
    pub context: AppContext,
    pub custom_field: String,
}

Provide and ProvideRef traits

The Provide and ProvideRef traits allow getting an instance of T from the implementing type. AppContext implements this for various types it contains. This allows a method to specify the type it requires, then the caller of the method can determine how to provide the type. This is a similar concept to dependency injection (DI) in frameworks like Java Spring, though this is far from a full DI system.

This is useful, for example, to allow mocking the DB connection in tests. Your DB operation method would declare a parameter of type ProvideRef<DataBaseConnection>, then your application code would provide the AppContext to the method, and your tests could provide a mocked ProvideRef instance that returns a mock DB connection. Note that mocking the DB comes with its own set of trade-offs, for example, it may not exactly match the behavior of an actual DB that's used in production. Consider testing against an actual DB instead of mocking, e.g., by using test containers.

Mocked implementations of the traits are provided if the testing-mocks feature is enabled.

pub async fn check_db_health(context: AppContext) -> RoadsterResult<()> {
    // `ping_db` can be called with `AppContext` in order to use the actual `DatabaseConnection`
    // in production
    ping_db(context).await
}

/// Example app method that takes a [`ProvideRef`] in order to allow using a mocked
/// [`DatabaseConnection`] in tests.
async fn ping_db(db: impl ProvideRef<DatabaseConnection>) -> RoadsterResult<()> {
    db.provide().ping().await?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use roadster::app::context::MockProvideRef;
    use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};

    #[tokio::test]
    async fn db_ping() {
        let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
        let mut db_provider = MockProvideRef::<DatabaseConnection>::new();
        db_provider.expect_provide().return_const(db);

        super::ping_db(db_provider).await.unwrap();
    }
}

See also:

Weak reference

In some cases, it can be useful to have a weak reference to the AppContext state in order to prevent reference cycles for things that are included in the AppContext but also need a reference to the AppContext. For example, the AppContext keeps a reference to the HealthChecks, and most HealthChecks need to use the AppContext.

To get a weak reference to the AppContext's state, use AppContext#downgrade to get a new instance of AppContextWeak.


pub struct ExampleHealthCheck {
    // Prevent reference cycle because the `ExampleHealthCheck` is also stored in the `AppContext`
    context: AppContextWeak,
}

#[async_trait]
impl HealthCheck for ExampleHealthCheck {
    fn name(&self) -> String {
        "example".to_string()
    }

    fn enabled(&self) -> bool {
        true
    }

    async fn check(&self) -> RoadsterResult<CheckResponse> {
        // Upgrade the `AppContext` in order to use it
        let _context = self
            .context
            .upgrade()
            .ok_or_else(|| anyhow!("Could not upgrade AppContextWeak"))?;

        Ok(CheckResponse::builder()
            .status(Status::Ok)
            .latency(Duration::from_secs(0))
            .build())
    }
}

pub fn build_app() -> App {
    RoadsterApp::builder()
        .state_provider(|context| {
            Ok(CustomState {
                context,
                custom_field: "Custom Field".to_string(),
            })
        })
        .add_health_check_provider(|registry, state| {
            // Downgrade the context before providing it to the `ExampleHealthCheck`
            let context = AppContext::from_ref(state).downgrade();
            registry.register(ExampleHealthCheck { context })
        })
        .build()
}

See also