roadster/app/
roadster_app.rs

1#[cfg(feature = "cli")]
2use crate::api::cli::RunCommand;
3use crate::app::context::AppContext;
4use crate::app::metadata::AppMetadata;
5use crate::app::{run, App};
6use crate::config::environment::Environment;
7use crate::config::AppConfig;
8#[cfg(feature = "db-sql")]
9use crate::db::migration::Migrator;
10use crate::error::RoadsterResult;
11use crate::health::check::registry::HealthCheckRegistry;
12use crate::health::check::HealthCheck;
13use crate::lifecycle::registry::LifecycleHandlerRegistry;
14use crate::lifecycle::AppLifecycleHandler;
15use crate::service::registry::ServiceRegistry;
16use crate::service::AppService;
17use crate::util::empty::Empty;
18use anyhow::anyhow;
19use async_trait::async_trait;
20use axum_core::extract::FromRef;
21use config::AsyncSource;
22#[cfg(feature = "db-sea-orm")]
23use sea_orm::ConnectOptions;
24use std::future;
25use std::future::Future;
26use std::pin::Pin;
27use std::sync::{Arc, Mutex};
28
29type StateBuilder<S> = dyn Send + Sync + Fn(AppContext) -> RoadsterResult<S>;
30type TracingInitializer = dyn Send + Sync + Fn(&AppConfig) -> RoadsterResult<()>;
31type AsyncConfigSourceProvider =
32    dyn Send + Sync + Fn(&Environment) -> RoadsterResult<Box<dyn AsyncSource + Send + Sync>>;
33type MetadataProvider = dyn Send + Sync + Fn(&AppConfig) -> RoadsterResult<AppMetadata>;
34#[cfg(feature = "db-sea-orm")]
35type DbConnOptionsProvider = dyn Send + Sync + Fn(&AppConfig) -> RoadsterResult<ConnectOptions>;
36#[cfg(any(
37    feature = "db-diesel-postgres-pool",
38    feature = "db-diesel-mysql-pool",
39    feature = "db-diesel-sqlite-pool"
40))]
41type DieselConnectionCustomizer<C> = Box<dyn r2d2::CustomizeConnection<C, diesel::r2d2::Error>>;
42#[cfg(any(
43    feature = "db-diesel-postgres-pool",
44    feature = "db-diesel-mysql-pool",
45    feature = "db-diesel-sqlite-pool"
46))]
47type DieselConnectionCustomizerProvider<C> =
48    dyn Send + Sync + Fn(&AppConfig) -> RoadsterResult<DieselConnectionCustomizer<C>>;
49#[cfg(any(
50    feature = "db-diesel-postgres-pool-async",
51    feature = "db-diesel-mysql-pool-async"
52))]
53type DieselAsyncConnectionCustomizer<C> =
54    Box<dyn bb8_8::CustomizeConnection<C, diesel_async::pooled_connection::PoolError>>;
55#[cfg(any(
56    feature = "db-diesel-postgres-pool-async",
57    feature = "db-diesel-mysql-pool-async"
58))]
59type DieselAsyncConnectionCustomizerProvider<C> =
60    dyn Send + Sync + Fn(&AppConfig) -> RoadsterResult<DieselAsyncConnectionCustomizer<C>>;
61#[cfg(feature = "db-sql")]
62type MigratorProvider<S> = dyn Send + Sync + Fn(&S) -> RoadsterResult<Box<dyn Migrator<S>>>;
63type LifecycleHandlers<A, S> = Vec<Box<dyn AppLifecycleHandler<A, S>>>;
64type LifecycleHandlerProviders<A, S> =
65    Vec<Box<dyn Send + Sync + Fn(&mut LifecycleHandlerRegistry<A, S>, &S) -> RoadsterResult<()>>>;
66type HealthCheckProviders<S> =
67    Vec<Box<dyn Send + Sync + Fn(&mut HealthCheckRegistry, &S) -> RoadsterResult<()>>>;
68type Services<A, S> = Vec<Box<dyn AppService<A, S>>>;
69type ServiceProviders<A, S> = Vec<
70    Box<
71        dyn Send
72            + Sync
73            + for<'a> Fn(
74                &'a mut ServiceRegistry<A, S>,
75                &'a S,
76            ) -> Pin<Box<dyn 'a + Send + Future<Output = RoadsterResult<()>>>>,
77    >,
78>;
79type GracefulShutdownSignalProvider<S> =
80    Option<Box<dyn Send + Sync + Fn(&S) -> Pin<Box<dyn Send + Future<Output = ()>>>>>;
81
82/// Inner state shared between both the [`RoadsterApp`] and [`RoadsterAppBuilder`].
83struct Inner<
84    S,
85    #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync = Empty,
86    #[cfg(not(feature = "cli"))] Cli: 'static = Empty,
87> where
88    S: 'static + Clone + Send + Sync,
89    AppContext: FromRef<S>,
90{
91    state_provider: Option<Box<StateBuilder<S>>>,
92    tracing_initializer: Option<Box<TracingInitializer>>,
93    async_config_source_providers: Vec<Box<AsyncConfigSourceProvider>>,
94    metadata: Option<AppMetadata>,
95    metadata_provider: Option<Box<MetadataProvider>>,
96    #[cfg(feature = "db-sea-orm")]
97    sea_orm_conn_options: Option<ConnectOptions>,
98    #[cfg(feature = "db-sea-orm")]
99    sea_orm_conn_options_provider: Option<Box<DbConnOptionsProvider>>,
100    #[cfg(feature = "db-diesel-postgres-pool")]
101    diesel_pg_connection_customizer_provider:
102        Option<Box<DieselConnectionCustomizerProvider<crate::db::DieselPgConn>>>,
103    #[cfg(feature = "db-diesel-mysql-pool")]
104    diesel_mysql_connection_customizer_provider:
105        Option<Box<DieselConnectionCustomizerProvider<crate::db::DieselMysqlConn>>>,
106    #[cfg(feature = "db-diesel-sqlite-pool")]
107    diesel_sqlite_connection_customizer_provider:
108        Option<Box<DieselConnectionCustomizerProvider<crate::db::DieselSqliteConn>>>,
109    #[cfg(feature = "db-diesel-postgres-pool-async")]
110    diesel_pg_async_connection_customizer_provider:
111        Option<Box<DieselAsyncConnectionCustomizerProvider<crate::db::DieselPgConnAsync>>>,
112    #[cfg(feature = "db-diesel-mysql-pool-async")]
113    diesel_mysql_async_connection_customizer_provider:
114        Option<Box<DieselAsyncConnectionCustomizerProvider<crate::db::DieselMysqlConnAsync>>>,
115    #[cfg(feature = "db-sql")]
116    migrator_providers: Vec<Box<MigratorProvider<S>>>,
117    health_checks: Vec<Arc<dyn HealthCheck>>,
118    health_check_providers: HealthCheckProviders<S>,
119    graceful_shutdown_signal_provider: GracefulShutdownSignalProvider<S>,
120    lifecycle_handler_providers: LifecycleHandlerProviders<RoadsterApp<S, Cli>, S>,
121    service_providers: ServiceProviders<RoadsterApp<S, Cli>, S>,
122}
123
124impl<
125        S,
126        #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync,
127        #[cfg(not(feature = "cli"))] Cli: 'static,
128    > Inner<S, Cli>
129where
130    S: 'static + Clone + Send + Sync,
131    AppContext: FromRef<S>,
132{
133    fn new() -> Self {
134        Self {
135            state_provider: Default::default(),
136            tracing_initializer: Default::default(),
137            async_config_source_providers: Default::default(),
138            metadata: Default::default(),
139            metadata_provider: Default::default(),
140            #[cfg(feature = "db-sea-orm")]
141            sea_orm_conn_options: Default::default(),
142            #[cfg(feature = "db-sea-orm")]
143            sea_orm_conn_options_provider: Default::default(),
144            #[cfg(feature = "db-diesel-postgres-pool")]
145            diesel_pg_connection_customizer_provider: Default::default(),
146            #[cfg(feature = "db-diesel-mysql-pool")]
147            diesel_mysql_connection_customizer_provider: Default::default(),
148            #[cfg(feature = "db-diesel-sqlite-pool")]
149            diesel_sqlite_connection_customizer_provider: Default::default(),
150            #[cfg(feature = "db-diesel-postgres-pool-async")]
151            diesel_pg_async_connection_customizer_provider: Default::default(),
152            #[cfg(feature = "db-diesel-mysql-pool-async")]
153            diesel_mysql_async_connection_customizer_provider: Default::default(),
154            #[cfg(feature = "db-sql")]
155            migrator_providers: Default::default(),
156            health_checks: Default::default(),
157            health_check_providers: Default::default(),
158            graceful_shutdown_signal_provider: Default::default(),
159            lifecycle_handler_providers: Default::default(),
160            service_providers: Default::default(),
161        }
162    }
163
164    fn tracing_initializer(
165        &mut self,
166        tracing_initializer: impl 'static + Send + Sync + Fn(&AppConfig) -> RoadsterResult<()>,
167    ) {
168        self.tracing_initializer = Some(Box::new(tracing_initializer));
169    }
170
171    fn add_async_config_source_provider(
172        &mut self,
173        async_config_source_provider: impl 'static
174            + Send
175            + Sync
176            + Fn(&Environment) -> RoadsterResult<Box<dyn AsyncSource + Send + Sync>>,
177    ) {
178        self.async_config_source_providers
179            .push(Box::new(async_config_source_provider));
180    }
181
182    fn set_metadata(&mut self, metadata: AppMetadata) {
183        self.metadata = Some(metadata);
184    }
185
186    fn metadata_provider(
187        &mut self,
188        metadata_provider: impl 'static + Send + Sync + Fn(&AppConfig) -> RoadsterResult<AppMetadata>,
189    ) {
190        self.metadata_provider = Some(Box::new(metadata_provider));
191    }
192
193    #[cfg(feature = "db-sea-orm")]
194    fn sea_orm_conn_options(&mut self, sea_orm_conn_options: ConnectOptions) {
195        self.sea_orm_conn_options = Some(sea_orm_conn_options);
196    }
197
198    #[cfg(feature = "db-sea-orm")]
199    fn sea_orm_conn_options_provider(
200        &mut self,
201        sea_orm_conn_options_provider: impl 'static
202            + Send
203            + Sync
204            + Fn(&AppConfig) -> RoadsterResult<ConnectOptions>,
205    ) {
206        self.sea_orm_conn_options_provider = Some(Box::new(sea_orm_conn_options_provider));
207    }
208
209    fn state_provider(
210        &mut self,
211        builder: impl 'static + Send + Sync + Fn(AppContext) -> RoadsterResult<S>,
212    ) {
213        self.state_provider = Some(Box::new(builder));
214    }
215
216    #[cfg(feature = "db-diesel-postgres-pool")]
217    fn diesel_pg_connection_customizer_provider(
218        &mut self,
219        connection_customizer: impl 'static
220            + Send
221            + Sync
222            + Fn(
223                &AppConfig,
224            ) -> RoadsterResult<
225                Box<dyn r2d2::CustomizeConnection<crate::db::DieselPgConn, diesel::r2d2::Error>>,
226            >,
227    ) {
228        self.diesel_pg_connection_customizer_provider = Some(Box::new(connection_customizer));
229    }
230
231    #[cfg(feature = "db-diesel-mysql-pool")]
232    fn diesel_mysql_connection_customizer_provider(
233        &mut self,
234        connection_customizer: impl 'static
235            + Send
236            + Sync
237            + Fn(
238                &AppConfig,
239            ) -> RoadsterResult<
240                Box<dyn r2d2::CustomizeConnection<crate::db::DieselMysqlConn, diesel::r2d2::Error>>,
241            >,
242    ) {
243        self.diesel_mysql_connection_customizer_provider = Some(Box::new(connection_customizer));
244    }
245
246    #[cfg(feature = "db-diesel-sqlite-pool")]
247    fn diesel_sqlite_connection_customizer_provider(
248        &mut self,
249        connection_customizer: impl 'static
250            + Send
251            + Sync
252            + Fn(
253                &AppConfig,
254            ) -> RoadsterResult<
255                Box<
256                    dyn r2d2::CustomizeConnection<crate::db::DieselSqliteConn, diesel::r2d2::Error>,
257                >,
258            >,
259    ) {
260        self.diesel_sqlite_connection_customizer_provider = Some(Box::new(connection_customizer));
261    }
262
263    #[cfg(feature = "db-diesel-postgres-pool-async")]
264    fn diesel_pg_async_connection_customizer_provider(
265        &mut self,
266        connection_customizer: impl 'static
267            + Send
268            + Sync
269            + Fn(
270                &AppConfig,
271            ) -> RoadsterResult<
272                Box<
273                    dyn bb8_8::CustomizeConnection<
274                        crate::db::DieselPgConnAsync,
275                        diesel_async::pooled_connection::PoolError,
276                    >,
277                >,
278            >,
279    ) {
280        self.diesel_pg_async_connection_customizer_provider = Some(Box::new(connection_customizer));
281    }
282
283    #[cfg(feature = "db-diesel-mysql-pool-async")]
284    fn diesel_mysql_async_connection_customizer_provider(
285        &mut self,
286        connection_customizer: impl 'static
287            + Send
288            + Sync
289            + Fn(
290                &AppConfig,
291            ) -> RoadsterResult<
292                Box<
293                    dyn bb8_8::CustomizeConnection<
294                        crate::db::DieselMysqlConnAsync,
295                        diesel_async::pooled_connection::PoolError,
296                    >,
297                >,
298            >,
299    ) {
300        self.diesel_mysql_async_connection_customizer_provider =
301            Some(Box::new(connection_customizer));
302    }
303
304    #[cfg(feature = "db-sql")]
305    fn add_migrator_provider(
306        &mut self,
307        migrator_provider: impl 'static + Send + Sync + Fn(&S) -> RoadsterResult<Box<dyn Migrator<S>>>,
308    ) {
309        self.migrator_providers.push(Box::new(migrator_provider))
310    }
311
312    fn add_health_check(&mut self, health_check: impl 'static + HealthCheck) {
313        self.health_checks.push(Arc::new(health_check));
314    }
315
316    fn add_health_check_provider(
317        &mut self,
318        health_check_provider: impl 'static
319            + Send
320            + Sync
321            + Fn(&mut HealthCheckRegistry, &S) -> RoadsterResult<()>,
322    ) {
323        self.health_check_providers
324            .push(Box::new(health_check_provider));
325    }
326
327    fn provide_graceful_shutdown_signal(
328        &mut self,
329        graceful_shutdown_signal_provider: impl 'static
330            + Send
331            + Sync
332            + Fn(&S) -> Pin<Box<dyn Send + Future<Output = ()>>>,
333    ) {
334        self.graceful_shutdown_signal_provider = Some(Box::new(graceful_shutdown_signal_provider));
335    }
336
337    fn init_tracing(&self, config: &AppConfig) -> RoadsterResult<()> {
338        if let Some(tracing_initializer) = self.tracing_initializer.as_ref() {
339            tracing_initializer(config)
340        } else {
341            crate::tracing::init_tracing(config, &self.get_metadata(config)?)
342        }
343    }
344
345    fn get_metadata(&self, config: &AppConfig) -> RoadsterResult<AppMetadata> {
346        if let Some(metadata) = self.metadata.as_ref() {
347            Ok(metadata.clone())
348        } else if let Some(metadata_provider) = self.metadata_provider.as_ref() {
349            metadata_provider(config)
350        } else {
351            Ok(Default::default())
352        }
353    }
354
355    #[cfg(feature = "db-sea-orm")]
356    fn sea_orm_connection_options(&self, config: &AppConfig) -> RoadsterResult<ConnectOptions> {
357        if let Some(sea_orm_conn_options) = self.sea_orm_conn_options.as_ref() {
358            Ok(sea_orm_conn_options.clone())
359        } else if let Some(sea_orm_conn_options_provider) =
360            self.sea_orm_conn_options_provider.as_ref()
361        {
362            sea_orm_conn_options_provider(config)
363        } else {
364            Ok(ConnectOptions::from(&config.database))
365        }
366    }
367
368    async fn provide_state(&self, context: AppContext) -> RoadsterResult<S> {
369        let state_provider = self
370            .state_provider
371            .as_ref()
372            .ok_or_else(|| anyhow!("State builder missing"))?;
373        state_provider(context)
374    }
375
376    async fn health_checks(
377        &self,
378        registry: &mut HealthCheckRegistry,
379        state: &S,
380    ) -> RoadsterResult<()> {
381        for health_check in self.health_checks.iter() {
382            registry.register_arc(health_check.clone())?;
383        }
384        for provider in self.health_check_providers.iter() {
385            provider(registry, state)?;
386        }
387        Ok(())
388    }
389
390    async fn graceful_shutdown_signal(&self, state: &S) {
391        if let Some(signal) = self.graceful_shutdown_signal_provider.as_ref() {
392            signal(state).await;
393        } else {
394            let _output: () = future::pending().await;
395        }
396    }
397}
398
399pub struct RoadsterApp<
400    S,
401    #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync = Empty,
402    #[cfg(not(feature = "cli"))] Cli: 'static = Empty,
403> where
404    S: Clone + Send + Sync + 'static,
405    AppContext: FromRef<S>,
406{
407    inner: Inner<S, Cli>,
408    async_config_sources: Mutex<Vec<Box<dyn AsyncSource + Send + Sync>>>,
409    #[cfg(feature = "db-sea-orm")]
410    sea_orm_migrator: Mutex<Option<Box<dyn Migrator<S>>>>,
411    #[cfg(feature = "db-diesel")]
412    diesel_migrator: Mutex<Option<Box<dyn Migrator<S>>>>,
413    #[cfg(feature = "db-sql")]
414    migrators: Mutex<Vec<Box<dyn Migrator<S>>>>,
415    #[cfg(feature = "db-diesel-postgres-pool")]
416    diesel_pg_connection_customizer:
417        Mutex<Option<DieselConnectionCustomizer<crate::db::DieselPgConn>>>,
418    #[cfg(feature = "db-diesel-mysql-pool")]
419    diesel_mysql_connection_customizer:
420        Mutex<Option<DieselConnectionCustomizer<crate::db::DieselMysqlConn>>>,
421    #[cfg(feature = "db-diesel-sqlite-pool")]
422    diesel_sqlite_connection_customizer:
423        Mutex<Option<DieselConnectionCustomizer<crate::db::DieselSqliteConn>>>,
424    #[cfg(feature = "db-diesel-postgres-pool-async")]
425    diesel_pg_async_connection_customizer:
426        Mutex<Option<DieselAsyncConnectionCustomizer<crate::db::DieselPgConnAsync>>>,
427    #[cfg(feature = "db-diesel-mysql-pool-async")]
428    diesel_mysql_async_connection_customizer:
429        Mutex<Option<DieselAsyncConnectionCustomizer<crate::db::DieselMysqlConnAsync>>>,
430    // Interior mutability pattern -- this allows us to keep the handler reference as a
431    // Box, which helps with single ownership and ensuring we only register a handler once.
432    lifecycle_handlers: Mutex<LifecycleHandlers<RoadsterApp<S, Cli>, S>>,
433    // Interior mutability pattern -- this allows us to keep the service reference as a
434    // Box, which helps with single ownership and ensuring we only register a service once.
435    services: Mutex<Services<RoadsterApp<S, Cli>, S>>,
436}
437
438pub struct RoadsterAppBuilder<
439    S,
440    #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync = Empty,
441    #[cfg(not(feature = "cli"))] Cli: 'static = Empty,
442> where
443    S: Clone + Send + Sync + 'static,
444    AppContext: FromRef<S>,
445{
446    inner: Inner<S, Cli>,
447    async_config_sources: Vec<Box<dyn AsyncSource + Send + Sync>>,
448    #[cfg(feature = "db-sea-orm")]
449    sea_orm_migrator: Option<Box<dyn Migrator<S>>>,
450    #[cfg(feature = "db-diesel")]
451    diesel_migrator: Option<Box<dyn Migrator<S>>>,
452    #[cfg(feature = "db-sql")]
453    migrators: Vec<Box<dyn Migrator<S>>>,
454    #[cfg(feature = "db-diesel-postgres-pool")]
455    diesel_pg_connection_customizer: Option<DieselConnectionCustomizer<crate::db::DieselPgConn>>,
456    #[cfg(feature = "db-diesel-mysql-pool")]
457    diesel_mysql_connection_customizer:
458        Option<DieselConnectionCustomizer<crate::db::DieselMysqlConn>>,
459    #[cfg(feature = "db-diesel-sqlite-pool")]
460    diesel_sqlite_connection_customizer:
461        Option<DieselConnectionCustomizer<crate::db::DieselSqliteConn>>,
462    #[cfg(feature = "db-diesel-postgres-pool-async")]
463    diesel_pg_async_connection_customizer:
464        Option<DieselAsyncConnectionCustomizer<crate::db::DieselPgConnAsync>>,
465    #[cfg(feature = "db-diesel-mysql-pool-async")]
466    diesel_mysql_async_connection_customizer:
467        Option<DieselAsyncConnectionCustomizer<crate::db::DieselMysqlConnAsync>>,
468    lifecycle_handlers: LifecycleHandlers<RoadsterApp<S, Cli>, S>,
469    services: Services<RoadsterApp<S, Cli>, S>,
470}
471
472impl<
473        S,
474        #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync,
475        #[cfg(not(feature = "cli"))] Cli: 'static,
476    > RoadsterApp<S, Cli>
477where
478    S: Clone + Send + Sync + 'static,
479    AppContext: FromRef<S>,
480{
481    /// Create a new [`RoadsterAppBuilder`] to use to build the [`RoadsterApp`].
482    pub fn builder() -> RoadsterAppBuilder<S, Cli> {
483        RoadsterAppBuilder::new()
484    }
485
486    /// Utility method to run the [`RoadsterApp`].
487    ///
488    /// Note: RustRover doesn't seem to recognize this method in some cases. You can also run the
489    /// [`RoadsterApp`] using [`crate::app::run`] directly instead.
490    pub async fn run(self) -> RoadsterResult<()> {
491        run::run(self).await?;
492
493        Ok(())
494    }
495}
496
497impl<
498        S,
499        #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync,
500        #[cfg(not(feature = "cli"))] Cli: 'static,
501    > Default for RoadsterAppBuilder<S, Cli>
502where
503    S: 'static + Clone + Send + Sync,
504    AppContext: FromRef<S>,
505{
506    fn default() -> Self {
507        Self::new()
508    }
509}
510
511impl<
512        S,
513        #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync,
514        #[cfg(not(feature = "cli"))] Cli: 'static,
515    > RoadsterAppBuilder<S, Cli>
516where
517    S: 'static + Clone + Send + Sync,
518    AppContext: FromRef<S>,
519{
520    pub fn new() -> Self {
521        Self {
522            inner: Inner::new(),
523            async_config_sources: Default::default(),
524            #[cfg(feature = "db-sea-orm")]
525            sea_orm_migrator: Default::default(),
526            #[cfg(feature = "db-diesel")]
527            diesel_migrator: Default::default(),
528            #[cfg(feature = "db-sql")]
529            migrators: Default::default(),
530            #[cfg(feature = "db-diesel-postgres-pool")]
531            diesel_pg_connection_customizer: Default::default(),
532            #[cfg(feature = "db-diesel-mysql-pool")]
533            diesel_mysql_connection_customizer: Default::default(),
534            #[cfg(feature = "db-diesel-sqlite-pool")]
535            diesel_sqlite_connection_customizer: Default::default(),
536            #[cfg(feature = "db-diesel-postgres-pool-async")]
537            diesel_pg_async_connection_customizer: Default::default(),
538            #[cfg(feature = "db-diesel-mysql-pool-async")]
539            diesel_mysql_async_connection_customizer: Default::default(),
540            lifecycle_handlers: Default::default(),
541            services: Default::default(),
542        }
543    }
544
545    /// Add an async config source ([`AsyncSource`]). Useful to load configs/secrets from an
546    /// external service, e.g., AWS or GCS secrets manager services.
547    pub fn add_async_config_source(mut self, source: impl AsyncSource + Send + 'static) -> Self {
548        self.async_config_sources.push(Box::new(source));
549        self
550    }
551
552    /// Add an async config source ([`AsyncSource`]). Useful to load configs/secrets from an
553    /// external service, e.g., AWS or GCS secrets manager services.
554    pub fn add_async_config_source_provider(
555        mut self,
556        source_provider: impl 'static
557            + Send
558            + Sync
559            + Fn(&Environment) -> RoadsterResult<Box<dyn AsyncSource + Send + Sync>>,
560    ) -> Self {
561        self.inner.add_async_config_source_provider(source_provider);
562        self
563    }
564
565    /// Provide the logic to initialize tracing for the [`RoadsterApp`].
566    pub fn tracing_initializer(
567        mut self,
568        tracing_initializer: impl 'static + Send + Sync + Fn(&AppConfig) -> RoadsterResult<()>,
569    ) -> Self {
570        self.inner.tracing_initializer(tracing_initializer);
571        self
572    }
573
574    /// Provide the [`AppMetadata`] for the [`RoadsterApp`].
575    pub fn metadata(mut self, metadata: AppMetadata) -> Self {
576        self.inner.set_metadata(metadata);
577        self
578    }
579
580    /// Provide the logic to build the [`AppMetadata`] for the [`RoadsterApp`].
581    pub fn metadata_provider(
582        mut self,
583        metadata_provider: impl 'static + Send + Sync + Fn(&AppConfig) -> RoadsterResult<AppMetadata>,
584    ) -> Self {
585        self.inner.metadata_provider(metadata_provider);
586        self
587    }
588
589    /// Provide the [`ConnectOptions`] for the [`RoadsterApp`].
590    #[cfg(feature = "db-sea-orm")]
591    pub fn sea_orm_conn_options(mut self, sea_orm_conn_options: ConnectOptions) -> Self {
592        self.inner.sea_orm_conn_options(sea_orm_conn_options);
593        self
594    }
595
596    /// Provide the logic to build the [`ConnectOptions`] for the [`RoadsterApp`].
597    #[cfg(feature = "db-sea-orm")]
598    pub fn sea_orm_conn_options_provider(
599        mut self,
600        sea_orm_conn_options_provider: impl 'static
601            + Send
602            + Sync
603            + Fn(&AppConfig) -> RoadsterResult<ConnectOptions>,
604    ) -> Self {
605        self.inner
606            .sea_orm_conn_options_provider(sea_orm_conn_options_provider);
607        self
608    }
609
610    #[cfg(feature = "db-diesel-postgres-pool")]
611    pub fn diesel_pg_connection_customizer(
612        mut self,
613        connection_customizer: impl 'static
614            + r2d2::CustomizeConnection<crate::db::DieselPgConn, diesel::r2d2::Error>,
615    ) -> Self {
616        self.diesel_pg_connection_customizer = Some(Box::new(connection_customizer));
617        self
618    }
619
620    #[cfg(feature = "db-diesel-postgres-pool")]
621    pub fn diesel_pg_connection_customizer_provider(
622        mut self,
623        connection_customizer: impl 'static
624            + Send
625            + Sync
626            + Fn(
627                &AppConfig,
628            ) -> RoadsterResult<
629                Box<dyn r2d2::CustomizeConnection<crate::db::DieselPgConn, diesel::r2d2::Error>>,
630            >,
631    ) -> Self {
632        self.inner
633            .diesel_pg_connection_customizer_provider(connection_customizer);
634        self
635    }
636
637    #[cfg(feature = "db-diesel-mysql-pool")]
638    pub fn diesel_mysql_connection_customizer(
639        mut self,
640        connection_customizer: impl 'static
641            + r2d2::CustomizeConnection<crate::db::DieselMysqlConn, diesel::r2d2::Error>,
642    ) -> Self {
643        self.diesel_mysql_connection_customizer = Some(Box::new(connection_customizer));
644        self
645    }
646
647    #[cfg(feature = "db-diesel-mysql-pool")]
648    pub fn diesel_mysql_connection_customizer_provider(
649        mut self,
650        connection_customizer: impl 'static
651            + Send
652            + Sync
653            + Fn(
654                &AppConfig,
655            ) -> RoadsterResult<
656                Box<dyn r2d2::CustomizeConnection<crate::db::DieselMysqlConn, diesel::r2d2::Error>>,
657            >,
658    ) -> Self {
659        self.inner
660            .diesel_mysql_connection_customizer_provider(connection_customizer);
661        self
662    }
663
664    #[cfg(feature = "db-diesel-sqlite-pool")]
665    pub fn diesel_sqlite_connection_customizer(
666        mut self,
667        connection_customizer: impl 'static
668            + r2d2::CustomizeConnection<crate::db::DieselSqliteConn, diesel::r2d2::Error>,
669    ) -> Self {
670        self.diesel_sqlite_connection_customizer = Some(Box::new(connection_customizer));
671        self
672    }
673
674    #[cfg(feature = "db-diesel-sqlite-pool")]
675    pub fn diesel_sqlite_connection_customizer_provider(
676        mut self,
677        connection_customizer: impl 'static
678            + Send
679            + Sync
680            + Fn(
681                &AppConfig,
682            ) -> RoadsterResult<
683                Box<
684                    dyn r2d2::CustomizeConnection<crate::db::DieselSqliteConn, diesel::r2d2::Error>,
685                >,
686            >,
687    ) -> Self {
688        self.inner
689            .diesel_sqlite_connection_customizer_provider(connection_customizer);
690        self
691    }
692
693    #[cfg(feature = "db-diesel-postgres-pool-async")]
694    pub fn diesel_pg_async_connection_customizer(
695        mut self,
696        connection_customizer: impl 'static
697            + bb8_8::CustomizeConnection<
698                crate::db::DieselPgConnAsync,
699                diesel_async::pooled_connection::PoolError,
700            >,
701    ) -> Self {
702        self.diesel_pg_async_connection_customizer = Some(Box::new(connection_customizer));
703        self
704    }
705
706    #[cfg(feature = "db-diesel-postgres-pool-async")]
707    pub fn diesel_pg_async_connection_customizer_provider(
708        mut self,
709        connection_customizer: impl 'static
710            + Send
711            + Sync
712            + Fn(
713                &AppConfig,
714            ) -> RoadsterResult<
715                Box<
716                    dyn bb8_8::CustomizeConnection<
717                        crate::db::DieselPgConnAsync,
718                        diesel_async::pooled_connection::PoolError,
719                    >,
720                >,
721            >,
722    ) -> Self {
723        self.inner
724            .diesel_pg_async_connection_customizer_provider(connection_customizer);
725        self
726    }
727
728    #[cfg(feature = "db-diesel-mysql-pool-async")]
729    pub fn diesel_mysql_async_connection_customizer(
730        mut self,
731        connection_customizer: impl 'static
732            + bb8_8::CustomizeConnection<
733                crate::db::DieselMysqlConnAsync,
734                diesel_async::pooled_connection::PoolError,
735            >,
736    ) -> Self {
737        self.diesel_mysql_async_connection_customizer = Some(Box::new(connection_customizer));
738        self
739    }
740
741    #[cfg(feature = "db-diesel-mysql-pool-async")]
742    pub fn diesel_mysql_async_connection_customizer_provider(
743        mut self,
744        connection_customizer: impl 'static
745            + Send
746            + Sync
747            + Fn(
748                &AppConfig,
749            ) -> RoadsterResult<
750                Box<
751                    dyn bb8_8::CustomizeConnection<
752                        crate::db::DieselMysqlConnAsync,
753                        diesel_async::pooled_connection::PoolError,
754                    >,
755                >,
756            >,
757    ) -> Self {
758        self.inner
759            .diesel_mysql_async_connection_customizer_provider(connection_customizer);
760        self
761    }
762
763    /// Provide the logic to build the custom state for the [`RoadsterApp`].
764    pub fn state_provider(
765        mut self,
766        builder: impl 'static + Send + Sync + Fn(AppContext) -> RoadsterResult<S>,
767    ) -> Self {
768        self.inner.state_provider(builder);
769        self
770    }
771
772    /// Add the diesel migrator [`sea_orm_migration::MigratorTrait`] to run on app start up
773    /// (if the `database.auto-migrate` config field is set to `true`)
774    ///
775    /// Note: SeaORM migrations expect all of the applied migrations to be available
776    /// to the provided migrator, so only a single SeaORM migrator is allowed.
777    #[cfg(feature = "db-sea-orm")]
778    pub fn sea_orm_migrator(
779        mut self,
780        migrator: impl 'static + Sync + sea_orm_migration::MigratorTrait,
781    ) -> Self {
782        self.sea_orm_migrator = Some(Box::new(
783            crate::db::migration::sea_orm::SeaOrmMigrator::new(migrator),
784        ));
785        self
786    }
787
788    /// Add the diesel migrator [`diesel::migration::MigrationSource`] to run on app start up
789    /// (if the `database.auto-migrate` config field is set to `true`)
790    ///
791    /// Note: Diesel migrations expect all of the applied migrations to be available
792    /// to the provided migrator, so only a single Diesel migrator is allowed.
793    #[cfg(feature = "db-diesel")]
794    pub fn diesel_migrator<C>(
795        mut self,
796        migrator: impl 'static + Send + Sync + diesel::migration::MigrationSource<C::Backend>,
797    ) -> Self
798    where
799        C: 'static
800            + diesel::connection::Connection
801            + Send
802            + diesel_migrations::MigrationHarness<C::Backend>,
803    {
804        self.diesel_migrator = Some(Box::new(
805            crate::db::migration::diesel::DieselMigrator::<C>::new(migrator),
806        ));
807        self
808    }
809
810    /// Add a [`Migrator`] to run on app start up (if the `database.auto-migrate` config field is
811    /// set to `true`).
812    ///
813    /// Note: SeaORM and Diesel migrations expect all of the applied migrations to be available
814    /// to the provided migrator, so multiple SeaORM or Diesel migrators should not be provided
815    /// via this method.
816    #[cfg(feature = "db-sql")]
817    pub fn add_migrator(mut self, migrator: impl Migrator<S> + 'static) -> Self {
818        self.migrators.push(Box::new(migrator));
819        self
820    }
821
822    /// Add a [`MigratorProvider`] that provides a [`Migrator`] to run on app start up
823    /// (if the `database.auto-migrate` config field is set to `true`).
824    ///
825    /// This is useful compared to [`Self::add_migrator`] if the [`Migrator`] implementation
826    /// needs access to the app state for any reason.
827    ///
828    /// Note: SeaORM and Diesel migrations expect all of the applied migrations to be available
829    /// to the provided migrator, so multiple SeaORM or Diesel migrators should not be provided
830    /// via this method.
831    #[cfg(feature = "db-sql")]
832    pub fn add_migrator_provider(
833        mut self,
834        migrator_provider: impl 'static + Send + Sync + Fn(&S) -> RoadsterResult<Box<dyn Migrator<S>>>,
835    ) -> Self {
836        self.inner.add_migrator_provider(migrator_provider);
837        self
838    }
839
840    /// Add a [`AppLifecycleHandler`] for the [`RoadsterApp`].
841    ///
842    /// This method can be called multiple times to register multiple handlers.
843    pub fn add_lifecycle_handler(
844        mut self,
845        lifecycle_handler: impl 'static + AppLifecycleHandler<RoadsterApp<S, Cli>, S>,
846    ) -> Self {
847        self.lifecycle_handlers.push(Box::new(lifecycle_handler));
848        self
849    }
850
851    /// Provide the logic to register [`AppLifecycleHandler`]s for the [`RoadsterApp`].
852    ///
853    /// This method can be called multiple times to register multiple handlers in separate
854    /// callbacks.
855    pub fn add_lifecycle_handler_provider(
856        mut self,
857        lifecycle_handler_provider: impl 'static
858            + Send
859            + Sync
860            + Fn(&mut LifecycleHandlerRegistry<RoadsterApp<S, Cli>, S>, &S) -> RoadsterResult<()>,
861    ) -> Self {
862        self.inner
863            .lifecycle_handler_providers
864            .push(Box::new(lifecycle_handler_provider));
865        self
866    }
867
868    /// Add a [`HealthCheck`] for the [`RoadsterApp`].
869    ///
870    /// This method can be called multiple times to register multiple health checks.
871    pub fn add_health_check(mut self, health_check: impl 'static + HealthCheck) -> Self {
872        self.inner.add_health_check(health_check);
873        self
874    }
875
876    /// Provide the logic to register [`HealthCheck`]s for the [`RoadsterApp`].
877    ///
878    /// This method can be called multiple times to register multiple health checks in separate
879    /// callbacks.
880    pub fn add_health_check_provider(
881        mut self,
882        health_check_provider: impl 'static
883            + Send
884            + Sync
885            + Fn(&mut HealthCheckRegistry, &S) -> RoadsterResult<()>,
886    ) -> Self {
887        self.inner.add_health_check_provider(health_check_provider);
888        self
889    }
890
891    /// Add a [`AppService`] for the [`RoadsterApp`].
892    ///
893    /// This method can be called multiple times to register multiple services.
894    pub fn add_service(
895        mut self,
896        service: impl 'static + AppService<RoadsterApp<S, Cli>, S>,
897    ) -> Self {
898        self.services.push(Box::new(service));
899        self
900    }
901
902    /// Provide the logic to register [`AppService`]s for the [`RoadsterApp`].
903    ///
904    /// This method can be called multiple times to register multiple services in separate
905    /// callbacks.
906    pub fn add_service_provider(
907        mut self,
908        service_provider: impl 'static
909            + Send
910            + Sync
911            + for<'a> Fn(
912                &'a mut ServiceRegistry<RoadsterApp<S, Cli>, S>,
913                &'a S,
914            ) -> Pin<Box<dyn 'a + Send + Future<Output = RoadsterResult<()>>>>,
915    ) -> Self {
916        self.inner
917            .service_providers
918            .push(Box::new(service_provider));
919        self
920    }
921
922    /// Provide a custom signal to listen for in order to shutdown the [`RoadsterApp`].
923    pub fn graceful_shutdown_signal_provider(
924        mut self,
925        graceful_shutdown_signal_provider: impl 'static
926            + Send
927            + Sync
928            + Fn(&S) -> Pin<Box<dyn Send + Future<Output = ()>>>,
929    ) -> Self {
930        self.inner
931            .provide_graceful_shutdown_signal(graceful_shutdown_signal_provider);
932        self
933    }
934
935    /// Build the [`RoadsterApp`] from this [`RoadsterAppBuilder`].
936    pub fn build(self) -> RoadsterApp<S, Cli> {
937        RoadsterApp {
938            inner: self.inner,
939            async_config_sources: Mutex::new(self.async_config_sources),
940            #[cfg(feature = "db-sea-orm")]
941            sea_orm_migrator: Mutex::new(self.sea_orm_migrator),
942            #[cfg(feature = "db-diesel")]
943            diesel_migrator: Mutex::new(self.diesel_migrator),
944            #[cfg(feature = "db-sql")]
945            migrators: Mutex::new(self.migrators),
946            #[cfg(feature = "db-diesel-postgres-pool")]
947            diesel_pg_connection_customizer: Mutex::new(self.diesel_pg_connection_customizer),
948            #[cfg(feature = "db-diesel-mysql-pool")]
949            diesel_mysql_connection_customizer: Mutex::new(self.diesel_mysql_connection_customizer),
950            #[cfg(feature = "db-diesel-sqlite-pool")]
951            diesel_sqlite_connection_customizer: Mutex::new(
952                self.diesel_sqlite_connection_customizer,
953            ),
954            #[cfg(feature = "db-diesel-postgres-pool-async")]
955            diesel_pg_async_connection_customizer: Mutex::new(
956                self.diesel_pg_async_connection_customizer,
957            ),
958            #[cfg(feature = "db-diesel-mysql-pool-async")]
959            diesel_mysql_async_connection_customizer: Mutex::new(
960                self.diesel_mysql_async_connection_customizer,
961            ),
962            lifecycle_handlers: Mutex::new(self.lifecycle_handlers),
963            services: Mutex::new(self.services),
964        }
965    }
966}
967
968#[async_trait]
969impl<
970        S,
971        #[cfg(feature = "cli")] Cli: 'static + clap::Args + RunCommand<RoadsterApp<S, Cli>, S> + Send + Sync,
972        #[cfg(not(feature = "cli"))] Cli: 'static,
973    > App<S> for RoadsterApp<S, Cli>
974where
975    S: Clone + Send + Sync + 'static,
976    AppContext: FromRef<S>,
977{
978    type Cli = Cli;
979
980    fn async_config_sources(
981        &self,
982        environment: &Environment,
983    ) -> RoadsterResult<Vec<Box<dyn AsyncSource + Send + Sync>>> {
984        let mut async_config_sources = self
985            .async_config_sources
986            .lock()
987            .map_err(crate::error::Error::from)?;
988
989        let mut sources: Vec<Box<dyn AsyncSource + Send + Sync>> = Default::default();
990        for source in async_config_sources.drain(..) {
991            sources.push(source);
992        }
993
994        for provider in self.inner.async_config_source_providers.iter() {
995            let source = provider(environment)?;
996            sources.push(source);
997        }
998
999        Ok(sources)
1000    }
1001
1002    fn init_tracing(&self, config: &AppConfig) -> RoadsterResult<()> {
1003        self.inner.init_tracing(config)
1004    }
1005
1006    fn metadata(&self, config: &AppConfig) -> RoadsterResult<AppMetadata> {
1007        self.inner.get_metadata(config)
1008    }
1009
1010    #[cfg(feature = "db-sea-orm")]
1011    fn sea_orm_connection_options(&self, config: &AppConfig) -> RoadsterResult<ConnectOptions> {
1012        self.inner.sea_orm_connection_options(config)
1013    }
1014
1015    async fn provide_state(&self, context: AppContext) -> RoadsterResult<S> {
1016        self.inner.provide_state(context).await
1017    }
1018
1019    #[cfg(feature = "db-diesel-postgres-pool")]
1020    fn diesel_pg_connection_customizer(
1021        &self,
1022        config: &AppConfig,
1023    ) -> RoadsterResult<
1024        Box<dyn r2d2::CustomizeConnection<crate::db::DieselPgConn, diesel::r2d2::Error>>,
1025    > {
1026        let mut connection_customizer = self
1027            .diesel_pg_connection_customizer
1028            .lock()
1029            .map_err(crate::error::Error::from)?;
1030
1031        if let Some(connection_customizer) = connection_customizer.take() {
1032            return Ok(connection_customizer);
1033        }
1034
1035        if let Some(connection_customizer_provider) =
1036            self.inner.diesel_pg_connection_customizer_provider.as_ref()
1037        {
1038            return connection_customizer_provider(config);
1039        };
1040
1041        Ok(Box::new(r2d2::NopConnectionCustomizer))
1042    }
1043
1044    #[cfg(feature = "db-diesel-mysql-pool")]
1045    fn diesel_mysql_connection_customizer(
1046        &self,
1047        config: &AppConfig,
1048    ) -> RoadsterResult<
1049        Box<dyn r2d2::CustomizeConnection<crate::db::DieselMysqlConn, diesel::r2d2::Error>>,
1050    > {
1051        let mut connection_customizer = self
1052            .diesel_mysql_connection_customizer
1053            .lock()
1054            .map_err(crate::error::Error::from)?;
1055
1056        if let Some(connection_customizer) = connection_customizer.take() {
1057            return Ok(connection_customizer);
1058        }
1059
1060        if let Some(connection_customizer_provider) = self
1061            .inner
1062            .diesel_mysql_connection_customizer_provider
1063            .as_ref()
1064        {
1065            return connection_customizer_provider(config);
1066        };
1067
1068        Ok(Box::new(r2d2::NopConnectionCustomizer))
1069    }
1070
1071    #[cfg(feature = "db-diesel-sqlite-pool")]
1072    fn diesel_sqlite_connection_customizer(
1073        &self,
1074        config: &AppConfig,
1075    ) -> RoadsterResult<
1076        Box<dyn r2d2::CustomizeConnection<crate::db::DieselSqliteConn, diesel::r2d2::Error>>,
1077    > {
1078        let mut connection_customizer = self
1079            .diesel_sqlite_connection_customizer
1080            .lock()
1081            .map_err(crate::error::Error::from)?;
1082
1083        if let Some(connection_customizer) = connection_customizer.take() {
1084            return Ok(connection_customizer);
1085        }
1086
1087        if let Some(connection_customizer_provider) = self
1088            .inner
1089            .diesel_sqlite_connection_customizer_provider
1090            .as_ref()
1091        {
1092            return connection_customizer_provider(config);
1093        };
1094
1095        Ok(Box::new(r2d2::NopConnectionCustomizer))
1096    }
1097
1098    #[cfg(feature = "db-diesel-postgres-pool-async")]
1099    fn diesel_pg_async_connection_customizer(
1100        &self,
1101        config: &AppConfig,
1102    ) -> RoadsterResult<
1103        Box<
1104            dyn bb8_8::CustomizeConnection<
1105                crate::db::DieselPgConnAsync,
1106                diesel_async::pooled_connection::PoolError,
1107            >,
1108        >,
1109    > {
1110        let mut connection_customizer = self
1111            .diesel_pg_async_connection_customizer
1112            .lock()
1113            .map_err(crate::error::Error::from)?;
1114
1115        if let Some(connection_customizer) = connection_customizer.take() {
1116            return Ok(connection_customizer);
1117        }
1118
1119        if let Some(connection_customizer_provider) = self
1120            .inner
1121            .diesel_pg_async_connection_customizer_provider
1122            .as_ref()
1123        {
1124            return connection_customizer_provider(config);
1125        };
1126
1127        Ok(Box::new(Empty))
1128    }
1129
1130    #[cfg(feature = "db-diesel-mysql-pool-async")]
1131    fn diesel_mysql_async_connection_customizer(
1132        &self,
1133        config: &AppConfig,
1134    ) -> RoadsterResult<
1135        Box<
1136            dyn bb8_8::CustomizeConnection<
1137                crate::db::DieselMysqlConnAsync,
1138                diesel_async::pooled_connection::PoolError,
1139            >,
1140        >,
1141    > {
1142        let mut connection_customizer = self
1143            .diesel_mysql_async_connection_customizer
1144            .lock()
1145            .map_err(crate::error::Error::from)?;
1146
1147        if let Some(connection_customizer) = connection_customizer.take() {
1148            return Ok(connection_customizer);
1149        }
1150
1151        if let Some(connection_customizer_provider) = self
1152            .inner
1153            .diesel_mysql_async_connection_customizer_provider
1154            .as_ref()
1155        {
1156            return connection_customizer_provider(config);
1157        };
1158
1159        Ok(Box::new(Empty))
1160    }
1161
1162    #[cfg(feature = "db-sql")]
1163    fn migrators(&self, state: &S) -> RoadsterResult<Vec<Box<dyn Migrator<S>>>> {
1164        let mut result = Vec::new();
1165
1166        #[cfg(feature = "db-sea-orm")]
1167        {
1168            let mut sea_orm_migrator = self
1169                .sea_orm_migrator
1170                .lock()
1171                .map_err(crate::error::Error::from)?;
1172            if let Some(sea_orm_migrator) = sea_orm_migrator.take() {
1173                result.push(sea_orm_migrator);
1174            }
1175        }
1176
1177        #[cfg(feature = "db-diesel")]
1178        {
1179            let mut diesel_migrator = self
1180                .diesel_migrator
1181                .lock()
1182                .map_err(crate::error::Error::from)?;
1183            if let Some(diesel_migrator) = diesel_migrator.take() {
1184                result.push(diesel_migrator);
1185            }
1186        }
1187
1188        let mut migrators = self.migrators.lock().map_err(crate::error::Error::from)?;
1189        for migrator in migrators.drain(..) {
1190            result.push(migrator);
1191        }
1192
1193        for migrator_provider in self.inner.migrator_providers.iter() {
1194            result.push(migrator_provider(state)?);
1195        }
1196
1197        Ok(result)
1198    }
1199
1200    async fn lifecycle_handlers(
1201        &self,
1202        registry: &mut LifecycleHandlerRegistry<Self, S>,
1203        state: &S,
1204    ) -> RoadsterResult<()> {
1205        {
1206            let mut lifecycle_handlers = self
1207                .lifecycle_handlers
1208                .lock()
1209                .map_err(crate::error::Error::from)?;
1210            for lifecycle_handler in lifecycle_handlers.drain(..) {
1211                registry.register_boxed(lifecycle_handler)?;
1212            }
1213        }
1214
1215        for provider in self.inner.lifecycle_handler_providers.iter() {
1216            provider(registry, state)?;
1217        }
1218        Ok(())
1219    }
1220
1221    async fn health_checks(
1222        &self,
1223        registry: &mut HealthCheckRegistry,
1224        state: &S,
1225    ) -> RoadsterResult<()> {
1226        self.inner.health_checks(registry, state).await
1227    }
1228
1229    async fn services(
1230        &self,
1231        registry: &mut ServiceRegistry<Self, S>,
1232        state: &S,
1233    ) -> RoadsterResult<()> {
1234        {
1235            let mut services = self.services.lock().map_err(crate::error::Error::from)?;
1236            for service in services.drain(..) {
1237                registry.register_boxed(service)?;
1238            }
1239        }
1240
1241        for provider in self.inner.service_providers.iter() {
1242            provider(registry, state).await?;
1243        }
1244        Ok(())
1245    }
1246
1247    async fn graceful_shutdown_signal(self: Arc<Self>, state: &S) {
1248        self.inner.graceful_shutdown_signal(state).await
1249    }
1250}