Skip to content

Commit

Permalink
feat!: App methods take self (#337)
Browse files Browse the repository at this point in the history
It may be useful for consumers to be able to put state on their App
implementation that they can use in the app trait. For example, I wanted
this when I was working on generating an OpenAPI client from the app's
Aide OpenAPI schema.

Also, this would be needed in order to support a builder-style approach
to building the app. This would allow us to provide a default app impl
configured with a builder-style api. We may still allow consumers to
directly use the trait impl if they want.

Closes #272
  • Loading branch information
spencewenski authored Aug 18, 2024
1 parent db5cc2e commit ad16d6b
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 44 deletions.
5 changes: 3 additions & 2 deletions examples/full/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@ impl RoadsterApp<AppState> for App {
type Cli = AppCli;
type M = Migrator;

fn metadata(_config: &AppConfig) -> RoadsterResult<AppMetadata> {
fn metadata(&self, _config: &AppConfig) -> RoadsterResult<AppMetadata> {
Ok(AppMetadata::builder()
.version(env!("VERGEN_GIT_SHA").to_string())
.build())
}

async fn provide_state(app_context: AppContext) -> RoadsterResult<AppState> {
async fn provide_state(&self, app_context: AppContext) -> RoadsterResult<AppState> {
Ok(AppState { app_context })
}

async fn services(
&self,
registry: &mut ServiceRegistry<Self, AppState>,
state: &AppState,
) -> RoadsterResult<()> {
Expand Down
5 changes: 3 additions & 2 deletions examples/leptos-ssr/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ impl RoadsterApp<AppState> for Server {
type Cli = crate::cli::AppCli;
type M = Migrator;

fn metadata(_config: &AppConfig) -> RoadsterResult<AppMetadata> {
fn metadata(&self, _config: &AppConfig) -> RoadsterResult<AppMetadata> {
Ok(AppMetadata::builder()
.version(env!("VERGEN_GIT_SHA").to_string())
.build())
}

async fn provide_state(app_context: AppContext) -> RoadsterResult<AppState> {
async fn provide_state(&self, app_context: AppContext) -> RoadsterResult<AppState> {
let leptos_config = get_configuration(None).await.map_err(|e| anyhow!(e))?;
let leptos_options = leptos_config.leptos_options.clone();
let state = AppState {
Expand All @@ -46,6 +46,7 @@ impl RoadsterApp<AppState> for Server {
}

async fn services(
&self,
registry: &mut ServiceRegistry<Self, AppState>,
state: &AppState,
) -> RoadsterResult<()> {
Expand Down
10 changes: 6 additions & 4 deletions src/app/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ pub struct AppContext {
impl AppContext {
// This method isn't used when running tests; only the mocked version is used.
#[cfg_attr(test, allow(dead_code))]
// The `A` type parameter isn't used in some feature configurations
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) async fn new<A, S>(config: AppConfig, metadata: AppMetadata) -> RoadsterResult<Self>
pub(crate) async fn new<A, S>(
#[allow(unused_variables)] app: &A,
config: AppConfig,
metadata: AppMetadata,
) -> RoadsterResult<Self>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
Expand All @@ -39,7 +41,7 @@ impl AppContext {
#[cfg(not(test))]
let context = {
#[cfg(feature = "db-sql")]
let db = sea_orm::Database::connect(A::db_connection_options(&config)?).await?;
let db = sea_orm::Database::connect(app.db_connection_options(&config)?).await?;

#[cfg(feature = "sidekiq")]
let (redis_enqueue, redis_fetch) = {
Expand Down
77 changes: 49 additions & 28 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use sea_orm_migration::MigratorTrait;
#[cfg(feature = "cli")]
use std::env;
use std::future;
use std::sync::Arc;
use tracing::{instrument, warn};

pub async fn run<A, S>(app: A) -> RoadsterResult<()>
Expand Down Expand Up @@ -78,31 +79,32 @@ where

let config = AppConfig::new(environment)?;

A::init_tracing(&config)?;
app.init_tracing(&config)?;

#[cfg(not(feature = "cli"))]
config.validate(true)?;
#[cfg(feature = "cli")]
config.validate(!roadster_cli.skip_validate_config)?;

#[cfg(not(test))]
let metadata = A::metadata(&config)?;
let metadata = app.metadata(&config)?;

// The `config.clone()` here is technically not necessary. However, without it, RustRover
// is giving a "value used after move" error when creating an actual `AppContext` below.
#[cfg(test)]
let context = AppContext::test(Some(config.clone()), None, None)?;
#[cfg(not(test))]
let context = AppContext::new::<A, S>(config, metadata).await?;
let context = AppContext::new::<A, S>(&app, config, metadata).await?;

let state = A::provide_state(context.clone()).await?;
let state = app.provide_state(context.clone()).await?;

let mut health_check_registry = HealthCheckRegistry::new(&context);
A::health_checks(&mut health_check_registry, &state).await?;
app.health_checks(&mut health_check_registry, &state)
.await?;
context.set_health_checks(health_check_registry)?;

let mut service_registry = ServiceRegistry::new(&state);
A::services(&mut service_registry, &state).await?;
app.services(&mut service_registry, &state).await?;

Ok(PreparedApp {
app,
Expand All @@ -122,29 +124,40 @@ where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
#[cfg(feature = "cli")]
let (app, roadster_cli, app_cli) = (
&prepared_app.app,
&prepared_app.roadster_cli,
&prepared_app.app_cli,
);
let state = &prepared_app.state;
let context = AppContext::from_ref(state);
let service_registry = &prepared_app.service_registry;

#[cfg(feature = "cli")]
if crate::api::cli::handle_cli(app, roadster_cli, app_cli, state).await? {
return Ok(());
{
let PreparedApp {
app,
roadster_cli,
app_cli,
..
} = &prepared_app;
if crate::api::cli::handle_cli(app, roadster_cli, app_cli, state).await? {
return Ok(());
}
}
let context = AppContext::from_ref(state);
let service_registry = &prepared_app.service_registry;

if service_registry.services.is_empty() {
warn!("No enabled services were registered, exiting.");
return Ok(());
}

#[cfg(feature = "cli")]
if crate::service::runner::handle_cli(roadster_cli, app_cli, service_registry, state).await? {
return Ok(());
{
let PreparedApp {
roadster_cli,
app_cli,
..
} = &prepared_app;
if crate::service::runner::handle_cli(roadster_cli, app_cli, service_registry, state)
.await?
{
return Ok(());
}
}

#[cfg(feature = "db-sql")]
Expand All @@ -156,7 +169,7 @@ where

crate::service::runner::before_run(service_registry, state).await?;

crate::service::runner::run(prepared_app.service_registry, state).await?;
crate::service::runner::run(prepared_app.app, prepared_app.service_registry, state).await?;

Ok(())
}
Expand All @@ -173,18 +186,18 @@ where
#[cfg(feature = "db-sql")]
type M: MigratorTrait;

fn init_tracing(config: &AppConfig) -> RoadsterResult<()> {
init_tracing(config, &Self::metadata(config)?)?;
fn init_tracing(&self, config: &AppConfig) -> RoadsterResult<()> {
init_tracing(config, &self.metadata(config)?)?;

Ok(())
}

fn metadata(_config: &AppConfig) -> RoadsterResult<AppMetadata> {
fn metadata(&self, _config: &AppConfig) -> RoadsterResult<AppMetadata> {
Ok(Default::default())
}

#[cfg(feature = "db-sql")]
fn db_connection_options(config: &AppConfig) -> RoadsterResult<ConnectOptions> {
fn db_connection_options(&self, config: &AppConfig) -> RoadsterResult<ConnectOptions> {
Ok(ConnectOptions::from(&config.database))
}

Expand All @@ -193,29 +206,37 @@ where
/// extract its [AppContext] when needed.
///
/// See the following for more details regarding [FromRef]: <https://docs.rs/axum/0.7.5/axum/extract/trait.FromRef.html>
async fn provide_state(context: AppContext) -> RoadsterResult<S>;
async fn provide_state(&self, context: AppContext) -> RoadsterResult<S>;

/// Provide the [crate::health_check::HealthCheck]s to use throughout the app.
async fn health_checks(_registry: &mut HealthCheckRegistry, _state: &S) -> RoadsterResult<()> {
async fn health_checks(
&self,
_registry: &mut HealthCheckRegistry,
_state: &S,
) -> RoadsterResult<()> {
Ok(())
}

/// Provide the [crate::service::AppService]s to run in the app.
async fn services(_registry: &mut ServiceRegistry<Self, S>, _state: &S) -> RoadsterResult<()> {
async fn services(
&self,
_registry: &mut ServiceRegistry<Self, S>,
_state: &S,
) -> RoadsterResult<()> {
Ok(())
}

/// Override to provide a custom shutdown signal. Roadster provides some default shutdown
/// signals, but it may be desirable to provide a custom signal in order to, e.g., shutdown the
/// server when a particular API is called.
async fn graceful_shutdown_signal(_state: &S) {
async fn graceful_shutdown_signal(self: Arc<Self>, _state: &S) {
let _output: () = future::pending().await;
}

/// Override to provide custom graceful shutdown logic to clean up any resources created by
/// the app. Roadster will take care of cleaning up the resources it created.
#[instrument(skip_all)]
async fn graceful_shutdown(_state: &S) -> RoadsterResult<()> {
async fn graceful_shutdown(self: Arc<Self>, _state: &S) -> RoadsterResult<()> {
Ok(())
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/service/function/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ impl RoadsterApp<AppContext> for App {
# type Cli = AppCli;
# type M = Migrator;
#
# async fn provide_state(_context: AppContext) -> RoadsterResult<AppContext> {
# async fn provide_state(&self, _context: AppContext) -> RoadsterResult<AppContext> {
# todo!()
# }
async fn services(
&self,
registry: &mut ServiceRegistry<Self, AppContext>,
context: &AppContext,
) -> RoadsterResult<()> {
Expand Down
17 changes: 10 additions & 7 deletions src/service/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use anyhow::anyhow;
use axum::extract::FromRef;
use itertools::Itertools;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -83,6 +84,7 @@ where
}

pub(crate) async fn run<A, S>(
app: A,
service_registry: ServiceRegistry<A, S>,
state: &S,
) -> RoadsterResult<()>
Expand All @@ -91,6 +93,7 @@ where
AppContext: FromRef<S>,
A: App<S>,
{
let app = Arc::new(app);
let cancel_token = CancellationToken::new();
let mut join_set = JoinSet::new();

Expand All @@ -106,13 +109,13 @@ where

// Task to clean up resources when gracefully shutting down.
{
let context = state.clone();
let cancel_token = cancel_token.clone();
let app_graceful_shutdown = {
let context = context.clone();
Box::pin(async move { A::graceful_shutdown(&context).await })
let state = state.clone();
let app = app.clone();
Box::pin(async move { app.graceful_shutdown(&state).await })
};
let context = AppContext::from_ref(&context);
let context = AppContext::from_ref(state);
join_set.spawn(Box::pin(async move {
cancel_on_error(
cancel_token.clone(),
Expand All @@ -128,10 +131,10 @@ where
}
// Task to listen for the signal to gracefully shutdown, and trigger other tasks to stop.
{
let context = state.clone();
let app_graceful_shutdown_signal = {
let context = context.clone();
Box::pin(async move { A::graceful_shutdown_signal(&context).await })
let context = state.clone();
let app = app.clone();
Box::pin(async move { app.graceful_shutdown_signal(&context).await })
};
let graceful_shutdown_signal =
graceful_shutdown_signal(cancel_token.clone(), app_graceful_shutdown_signal);
Expand Down

0 comments on commit ad16d6b

Please sign in to comment.