#[cfg(feature = "cli")]
use crate::api::cli::parse_cli;
#[cfg(feature = "cli")]
use crate::api::cli::roadster::RoadsterCli;
use crate::app::App;
use crate::app::context::AppContext;
use crate::config::environment::Environment;
use crate::config::{AppConfig, AppConfigOptions, ConfigOverrideSource};
#[cfg(feature = "db-sql")]
use crate::db::migration::Migrator;
use crate::error::RoadsterResult;
use crate::health::check::registry::HealthCheckRegistry;
use crate::lifecycle::registry::LifecycleHandlerRegistry;
use crate::service::registry::ServiceRegistry;
use axum_core::extract::FromRef;
use std::marker::PhantomData;
use std::path::PathBuf;
#[non_exhaustive]
pub struct PreparedApp<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
#[cfg(feature = "cli")]
pub cli: Option<PreparedAppCli<A, S>>,
pub app: A,
pub state: S,
#[cfg(feature = "db-sql")]
pub migrators: Vec<Box<dyn Migrator<S>>>,
pub service_registry: ServiceRegistry<S>,
pub lifecycle_handler_registry: LifecycleHandlerRegistry<A, S>,
}
#[non_exhaustive]
pub struct PreparedAppCli<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
#[cfg(feature = "cli")]
pub roadster_cli: RoadsterCli,
#[cfg(feature = "cli")]
pub app_cli: A::Cli,
pub(crate) _app: PhantomData<A>,
pub(crate) _state: PhantomData<S>,
}
#[derive(Default, Debug, bon::Builder)]
#[non_exhaustive]
pub struct PrepareOptions {
#[builder(field)]
pub config_sources: Vec<Box<dyn config::Source + Send + Sync>>,
pub env: Option<Environment>,
#[builder(default = true)]
pub parse_cli: bool,
pub config_dir: Option<PathBuf>,
pub config: Option<AppConfig>,
}
impl<S: prepare_options_builder::State> PrepareOptionsBuilder<S> {
pub fn config_sources(
mut self,
config_sources: Vec<Box<dyn config::Source + Send + Sync>>,
) -> Self {
self.config_sources.extend(config_sources);
self
}
pub fn add_config_source(
mut self,
source: impl config::Source + Send + Sync + 'static,
) -> Self {
self.config_sources.push(Box::new(source));
self
}
pub fn add_config_source_boxed(
mut self,
source: Box<dyn config::Source + Send + Sync>,
) -> Self {
self.config_sources.push(source);
self
}
}
impl PrepareOptions {
pub fn test() -> Self {
PrepareOptions::builder()
.env(Environment::Test)
.parse_cli(false)
.build()
}
pub fn with_config_override(mut self, name: String, value: config::Value) -> Self {
self.config_sources.push(Box::new(
ConfigOverrideSource::builder()
.name(name)
.value(value)
.build(),
));
self
}
pub fn with_config(mut self, config: AppConfig) -> Self {
self.config = Some(config);
self
}
}
pub async fn prepare<A, S>(app: A, options: PrepareOptions) -> RoadsterResult<PreparedApp<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Send + Sync + 'static,
{
prepare_from_cli_and_state(build_cli_and_state(app, options).await?).await
}
#[allow(clippy::disallowed_macros)]
pub(crate) async fn build_cli_and_state<A, S>(
app: A,
options: PrepareOptions,
) -> RoadsterResult<CliAndState<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Send + Sync + 'static,
{
#[cfg(feature = "cli")]
let (roadster_cli, app_cli) = if options.parse_cli {
let (roadster_cli, app_cli) = parse_cli::<A, S, _, _>(std::env::args_os())?;
(Some(roadster_cli), Some(app_cli))
} else {
(None, None)
};
#[cfg(feature = "cli")]
let environment = roadster_cli
.as_ref()
.and_then(|cli| cli.environment.clone())
.or(options.env);
#[cfg(not(feature = "cli"))]
let environment: Option<Environment> = options.env;
let environment = if let Some(environment) = environment {
println!("Using environment: {environment:?}");
environment
} else {
Environment::new()?
};
#[cfg(feature = "cli")]
let config_dir = roadster_cli
.as_ref()
.and_then(|cli| cli.config_dir.clone())
.or(options.config_dir);
#[cfg(not(feature = "cli"))]
let config_dir: Option<std::path::PathBuf> = options.config_dir;
let async_config_sources = app.async_config_sources(&environment)?;
let app_config_options = AppConfigOptions::builder()
.environment(environment)
.maybe_config_dir(config_dir)
.config_sources(options.config_sources);
let app_config_options = async_config_sources
.into_iter()
.fold(app_config_options, |app_config_options, source| {
app_config_options.add_async_source_boxed(source)
})
.build();
let config = if let Some(config) = options.config {
config
} else {
AppConfig::new_with_options(app_config_options).await?
};
app.init_tracing(&config)?;
#[cfg(not(feature = "cli"))]
config.validate(true)?;
#[cfg(feature = "cli")]
config.validate(
!roadster_cli
.as_ref()
.map(|cli| cli.skip_validate_config)
.unwrap_or_default(),
)?;
let state = build_state(&app, config).await?;
Ok(CliAndState {
app,
#[cfg(feature = "cli")]
roadster_cli,
#[cfg(feature = "cli")]
app_cli,
state,
})
}
pub(crate) async fn build_state<A, S>(app: &A, config: AppConfig) -> RoadsterResult<S>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Send + Sync + 'static,
{
#[cfg(not(test))]
let metadata = app.metadata(&config)?;
let mut extension_registry = Default::default();
app.provide_context_extensions(&config, &mut extension_registry)
.await?;
#[cfg(test)]
let context = AppContext::test(Some(config.clone()), None, None)?;
#[cfg(not(test))]
let context = AppContext::new::<A, S>(app, config, metadata, extension_registry).await?;
app.provide_state(context).await
}
pub(crate) async fn prepare_from_cli_and_state<A, S>(
cli_and_state: CliAndState<A, S>,
) -> RoadsterResult<PreparedApp<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Send + Sync + 'static,
{
let CliAndState {
app,
#[cfg(feature = "cli")]
roadster_cli,
#[cfg(feature = "cli")]
app_cli,
state,
} = cli_and_state;
let PreparedAppWithoutCli {
app,
state,
#[cfg(feature = "db-sql")]
migrators,
service_registry,
lifecycle_handler_registry,
} = prepare_without_cli(app, state).await?;
#[cfg(feature = "cli")]
let cli = if let Some((roadster_cli, app_cli)) = roadster_cli.zip(app_cli) {
Some(PreparedAppCli {
roadster_cli,
app_cli,
_app: Default::default(),
_state: Default::default(),
})
} else {
None
};
Ok(PreparedApp {
#[cfg(feature = "cli")]
cli,
app,
#[cfg(feature = "db-sql")]
migrators,
state,
service_registry,
lifecycle_handler_registry,
})
}
#[non_exhaustive]
pub struct PreparedAppWithoutCli<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
pub app: A,
pub state: S,
#[cfg(feature = "db-sql")]
pub migrators: Vec<Box<dyn Migrator<S>>>,
pub service_registry: ServiceRegistry<S>,
pub lifecycle_handler_registry: LifecycleHandlerRegistry<A, S>,
}
pub(crate) async fn prepare_without_cli<A, S>(
app: A,
state: S,
) -> RoadsterResult<PreparedAppWithoutCli<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Send + Sync + 'static,
{
let context = AppContext::from_ref(&state);
#[cfg(feature = "db-sql")]
let migrators = app.migrators(&state)?;
let mut lifecycle_handler_registry = LifecycleHandlerRegistry::new(&state);
app.lifecycle_handlers(&mut lifecycle_handler_registry, &state)
.await?;
let mut health_check_registry = HealthCheckRegistry::new(&context);
app.health_checks(&mut health_check_registry, &state)
.await?;
context.set_health_checks(health_check_registry)?;
let mut service_registry = ServiceRegistry::new(&state);
app.services(&mut service_registry, &state).await?;
Ok(PreparedAppWithoutCli {
app,
state,
#[cfg(feature = "db-sql")]
migrators,
service_registry,
lifecycle_handler_registry,
})
}
#[non_exhaustive]
pub(crate) struct CliAndState<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
pub app: A,
#[cfg(feature = "cli")]
pub roadster_cli: Option<RoadsterCli>,
#[cfg(feature = "cli")]
pub app_cli: Option<A::Cli>,
pub state: S,
}
#[cfg(test)]
mod tests {
use crate::app::prepare::PrepareOptions;
use insta::assert_debug_snapshot;
#[test]
fn prepare_options_test() {
let options = PrepareOptions::test();
assert_debug_snapshot!(options);
}
}