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 HealthCheck
s, and most HealthCheck
s 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()
}