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 {
type Error = roadster::error::Error;
fn name(&self) -> String {
"example".to_string()
}
fn enabled(&self) -> bool {
true
}
async fn check(&self) -> Result<CheckResponse, Self::Error> {
// 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()
}
Custom state for crate authors via extensions
Crate authors may need custom context not available in the
AppContext. Roadster provides a context
extension mechanism via the ExtensionRegistry, which is initialized by app authors in the
App#provide_context_extensions or RoadsterAppBuilder#context_extension_provider methods. Context provided in these
methods can then be retrieved using the AppContext#get_extension method. This is similar to the Axum's
Extension mechanism.
pub fn app_with_context_extension() -> RoadsterApp<AppContext> {
RoadsterApp::builder()
// Use the default `AppContext` for this example
.state_provider(Ok)
// Register custom data to be added to the `AppContext`.
.context_extension_provider(|_config, registry| {
Box::pin(async move {
registry.register("Custom String context".to_string())?;
Ok(())
})
})
.build()
}