Skip to main content

a3s_boot/app/
builder.rs

1use super::application::BootApplication;
2use super::lazy::LazyModuleLoader;
3use super::registration::{ModuleRegistrationSink, ModuleRegistry};
4use crate::pipeline::PipelineComponents;
5#[cfg(feature = "auth")]
6use crate::AuthGuard;
7use crate::{
8    ApiVersioning, BootError, BootErrorKind, BootResponse, ExceptionFilter, ExecutionInterceptor,
9    Guard, Interceptor, MessagePatternDefinition, Middleware, Module, ModuleRef, OpenApiDocument,
10    OpenApiInfo, Pipe, ProviderDefinition, ProviderToken, Result, RouteDefinition,
11    SerializationInterceptor, WebSocketGatewayDefinition,
12};
13#[cfg(feature = "compression")]
14use crate::{CompressionInterceptor, CompressionOptions};
15#[cfg(feature = "security")]
16use crate::{
17    CorsMiddleware, CorsOptions, CorsPreflightRoute, CorsResponseInterceptor, CsrfGuard,
18    CsrfOptions, RateLimitGuard, RateLimitOptions, SecurityHeadersInterceptor,
19    SecurityHeadersOptions,
20};
21#[cfg(feature = "session")]
22use crate::{SessionCookieInterceptor, SessionManager, SessionMiddleware, SessionModule};
23use std::collections::{BTreeMap, BTreeSet};
24use std::sync::Arc;
25
26/// Builder for a [`BootApplication`].
27#[derive(Default)]
28pub struct BootApplicationBuilder {
29    modules: Vec<Arc<dyn Module>>,
30    routes: Vec<RouteDefinition>,
31    gateways: Vec<WebSocketGatewayDefinition>,
32    message_patterns: Vec<MessagePatternDefinition>,
33    global_pipeline: PipelineComponents,
34    global_execution_guards: Vec<Arc<dyn Guard>>,
35    global_execution_interceptors: Vec<Arc<dyn ExecutionInterceptor>>,
36    global_prefix: Option<String>,
37    api_versioning: Option<ApiVersioning>,
38    openapi_routes: Vec<(String, OpenApiInfo)>,
39    provider_overrides: BTreeMap<ProviderToken, ProviderDefinition>,
40    #[cfg(feature = "security")]
41    cors_preflight: Option<CorsPreflightRoute>,
42}
43
44impl BootApplicationBuilder {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Import a root module.
50    pub fn import<M>(mut self, module: M) -> Self
51    where
52        M: Module,
53    {
54        self.modules.push(Arc::new(module));
55        self
56    }
57
58    /// Import a shared root module.
59    pub fn import_arc(mut self, module: Arc<dyn Module>) -> Self {
60        self.modules.push(module);
61        self
62    }
63
64    /// Add a framework-neutral route directly to the application shell.
65    pub fn route(mut self, route: RouteDefinition) -> Self {
66        self.routes.push(route);
67        self
68    }
69
70    /// Add a framework-neutral WebSocket gateway directly to the application shell.
71    pub fn gateway(mut self, gateway: WebSocketGatewayDefinition) -> Self {
72        self.gateways.push(gateway);
73        self
74    }
75
76    /// Add a framework-neutral microservice message pattern directly to the application shell.
77    pub fn message_pattern(mut self, pattern: MessagePatternDefinition) -> Self {
78        self.message_patterns.push(pattern);
79        self
80    }
81
82    /// Serve a generated OpenAPI document at the given path.
83    pub fn serve_openapi(mut self, path: impl Into<String>, info: OpenApiInfo) -> Self {
84        self.openapi_routes.push((path.into(), info));
85        self
86    }
87
88    /// Prefix every route in the built application, for example `/api/v1`.
89    pub fn global_prefix(mut self, prefix: impl Into<String>) -> Self {
90        self.global_prefix = Some(prefix.into());
91        self
92    }
93
94    /// Enable adapter-neutral API version matching for HTTP routes.
95    pub fn enable_api_versioning(mut self, versioning: ApiVersioning) -> Self {
96        self.api_versioning = Some(versioning);
97        self
98    }
99
100    /// Add application-wide middleware, similar to Nest middleware.
101    pub fn use_global_middleware<M>(mut self, middleware: M) -> Self
102    where
103        M: Middleware,
104    {
105        self.global_pipeline.push_middleware(middleware);
106        self
107    }
108
109    /// Add an application-wide pipe, similar to Nest's global pipes.
110    pub fn use_global_pipe<P>(mut self, pipe: P) -> Self
111    where
112        P: Pipe,
113    {
114        self.global_pipeline.push_pipe(pipe);
115        self
116    }
117
118    /// Add an application-wide guard, similar to Nest's global guards.
119    pub fn use_global_guard<G>(mut self, guard: G) -> Self
120    where
121        G: Guard,
122    {
123        self.global_pipeline.push_guard(guard);
124        self
125    }
126
127    /// Add an application-wide protocol-neutral guard.
128    pub fn use_global_execution_guard<G>(mut self, guard: G) -> Self
129    where
130        G: Guard,
131    {
132        let guard: Arc<dyn Guard> = Arc::new(guard);
133        self.global_pipeline.push_guard_arc(Arc::clone(&guard));
134        self.global_execution_guards.push(guard);
135        self
136    }
137
138    /// Add a provider-backed authentication guard using the default auth strategy.
139    #[cfg(feature = "auth")]
140    pub fn use_global_auth(mut self) -> Self {
141        self.global_pipeline.push_guard(AuthGuard::new());
142        self
143    }
144
145    /// Add a provider-backed authentication guard using a named auth strategy.
146    #[cfg(feature = "auth")]
147    pub fn use_global_auth_strategy(mut self, strategy: impl Into<String>) -> Self {
148        self.global_pipeline
149            .push_guard(AuthGuard::new().strategy(strategy));
150        self
151    }
152
153    /// Add an application-wide interceptor, similar to Nest's global interceptors.
154    pub fn use_global_interceptor<I>(mut self, interceptor: I) -> Self
155    where
156        I: Interceptor,
157    {
158        self.global_pipeline.push_interceptor(interceptor);
159        self
160    }
161
162    /// Add an application-wide protocol-neutral interceptor.
163    pub fn use_global_execution_interceptor<I>(mut self, interceptor: I) -> Self
164    where
165        I: ExecutionInterceptor,
166    {
167        let interceptor: Arc<dyn ExecutionInterceptor> = Arc::new(interceptor);
168        self.global_pipeline
169            .push_execution_interceptor_arc(Arc::clone(&interceptor));
170        self.global_execution_interceptors.push(interceptor);
171        self
172    }
173
174    /// Add the default JSON response serialization interceptor.
175    pub fn use_global_serialization(mut self) -> Self {
176        self.global_pipeline
177            .push_interceptor(SerializationInterceptor::new());
178        self
179    }
180
181    /// Add gzip response compression for clients that send `Accept-Encoding: gzip`.
182    #[cfg(feature = "compression")]
183    pub fn use_global_compression(mut self, options: CompressionOptions) -> Self {
184        self.global_pipeline
185            .push_interceptor(CompressionInterceptor::with_options(options));
186        self
187    }
188
189    /// Add CORS handling for preflight and normal responses.
190    #[cfg(feature = "security")]
191    pub fn use_global_cors(mut self, options: CorsOptions) -> Self {
192        self.global_pipeline
193            .push_middleware(CorsMiddleware::with_options(options.clone()));
194        self.global_pipeline
195            .push_interceptor(CorsResponseInterceptor::with_options(options.clone()));
196        self.cors_preflight = Some(CorsPreflightRoute::with_options(options));
197        self
198    }
199
200    /// Add common security response headers, similar to a small Helmet setup.
201    #[cfg(feature = "security")]
202    pub fn use_global_security_headers(mut self, options: SecurityHeadersOptions) -> Self {
203        self.global_pipeline
204            .push_interceptor(SecurityHeadersInterceptor::with_options(options));
205        self
206    }
207
208    /// Add an application-wide CSRF guard for unsafe HTTP methods.
209    #[cfg(feature = "security")]
210    pub fn use_global_csrf(mut self, options: CsrfOptions) -> Self {
211        self.global_pipeline
212            .push_guard(CsrfGuard::with_options(options));
213        self
214    }
215
216    /// Add an in-memory application-wide rate limit guard.
217    #[cfg(feature = "security")]
218    pub fn use_global_rate_limit(mut self, options: RateLimitOptions) -> Self {
219        self.global_pipeline
220            .push_guard(RateLimitGuard::with_options(options));
221        self
222    }
223
224    /// Add global session middleware and cookie persistence.
225    #[cfg(feature = "session")]
226    pub fn use_global_sessions(mut self, manager: SessionManager) -> Self {
227        self.global_pipeline
228            .push_middleware(SessionMiddleware::new(manager.clone()));
229        self.global_pipeline
230            .push_interceptor(SessionCookieInterceptor::new(manager));
231        self
232    }
233
234    /// Import a session module and apply its middleware/interceptor globally.
235    #[cfg(feature = "session")]
236    pub fn use_global_session_module(mut self, module: SessionModule) -> Self {
237        let manager = module.manager();
238        self = self.use_global_sessions(manager);
239        self.modules.push(Arc::new(module));
240        self
241    }
242
243    /// Add an application-wide exception filter, similar to Nest's global filters.
244    pub fn use_global_filter<F>(mut self, filter: F) -> Self
245    where
246        F: ExceptionFilter,
247    {
248        self.global_pipeline.push_filter(filter);
249        self
250    }
251
252    /// Add an application-wide exception filter for selected error kinds.
253    pub fn use_global_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
254    where
255        I: IntoIterator<Item = BootErrorKind>,
256        F: ExceptionFilter,
257    {
258        self.global_pipeline.push_catch_filter(kinds, filter);
259        self
260    }
261
262    /// Enable DTO validation for routes that carry validation metadata.
263    pub fn use_global_validation(mut self) -> Self {
264        self.global_pipeline.enable_validation();
265        self
266    }
267
268    /// Replace matching module providers before modules build controllers.
269    ///
270    /// This is primarily used by testing utilities to swap provider
271    /// implementations while preserving the application module graph.
272    pub fn override_provider(mut self, provider: ProviderDefinition) -> Self {
273        self.provider_overrides
274            .insert(provider.token().clone(), provider);
275        self
276    }
277
278    /// Resolve module imports, providers, controllers, and routes.
279    pub fn build(self) -> Result<BootApplication> {
280        let module_ref = ModuleRef::new();
281        let global_ref = ModuleRef::new();
282        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
283        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
284        module_ref.add_visible_scope(global_ref.clone())?;
285        let mut registry = ModuleRegistry::new(global_ref, self.provider_overrides);
286        let mut modules = Vec::new();
287        let mut module_instances = Vec::new();
288        let mut routes = self
289            .routes
290            .into_iter()
291            .map(|route| route.with_pipeline_prefix(&self.global_pipeline))
292            .collect::<Vec<_>>();
293        let mut gateways = self.gateways;
294        let mut message_patterns = self.message_patterns;
295
296        {
297            let mut sink = ModuleRegistrationSink {
298                modules: &mut modules,
299                module_instances: &mut module_instances,
300                routes: &mut routes,
301                gateways: &mut gateways,
302                message_patterns: &mut message_patterns,
303            };
304
305            for module in &self.modules {
306                let registered = registry.register_module(
307                    Arc::clone(module),
308                    &self.global_pipeline,
309                    &mut sink,
310                )?;
311                module_ref.add_visible_scope(registered.module_ref)?;
312            }
313        }
314        for (name, registered) in registry.registered_modules() {
315            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
316        }
317
318        let mut routes = apply_global_prefix(routes, self.global_prefix.as_deref())?;
319        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
320            .into_iter()
321            .map(|gateway| {
322                gateway.with_execution_pipeline_prefix(
323                    &self.global_execution_guards,
324                    &self.global_execution_interceptors,
325                )
326            })
327            .collect::<Vec<_>>();
328        let message_patterns = message_patterns
329            .into_iter()
330            .map(|pattern| {
331                pattern.with_execution_pipeline_prefix(
332                    &self.global_execution_guards,
333                    &self.global_execution_interceptors,
334                )
335            })
336            .collect::<Vec<_>>();
337        let documented_routes = routes.clone();
338
339        for (path, info) in self.openapi_routes {
340            let document = OpenApiDocument::from_routes(info, &documented_routes);
341            let route =
342                openapi_json_route(path, document)?.with_pipeline_prefix(&self.global_pipeline);
343            let route = match self.global_prefix.as_deref() {
344                Some(prefix) => route.with_path_prefix(prefix)?,
345                None => route,
346            };
347            routes.push(route);
348        }
349
350        #[cfg(feature = "security")]
351        if let Some(cors_preflight) = &self.cors_preflight {
352            add_cors_preflight_routes(&mut routes, cors_preflight, &self.global_pipeline)?;
353        }
354
355        routes = routes
356            .into_iter()
357            .map(|route| route.with_default_module_ref(module_ref.clone()))
358            .collect();
359
360        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
361        validate_unique_gateways(&gateways)?;
362        validate_unique_message_patterns(&message_patterns)?;
363        validate_gateway_route_conflicts(&routes, &gateways)?;
364
365        Ok(BootApplication {
366            routes,
367            gateways,
368            message_patterns,
369            modules,
370            module_ref,
371            module_instances,
372            api_versioning: self.api_versioning,
373        })
374    }
375
376    /// Resolve modules with async provider factories, then build the application.
377    pub async fn build_async(self) -> Result<BootApplication> {
378        let module_ref = ModuleRef::new();
379        let global_ref = ModuleRef::new();
380        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
381        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
382        module_ref.add_visible_scope(global_ref.clone())?;
383        let mut registry = ModuleRegistry::new(global_ref, self.provider_overrides);
384        let mut modules = Vec::new();
385        let mut module_instances = Vec::new();
386        let mut routes = self
387            .routes
388            .into_iter()
389            .map(|route| route.with_pipeline_prefix(&self.global_pipeline))
390            .collect::<Vec<_>>();
391        let mut gateways = self.gateways;
392        let mut message_patterns = self.message_patterns;
393
394        {
395            let mut sink = ModuleRegistrationSink {
396                modules: &mut modules,
397                module_instances: &mut module_instances,
398                routes: &mut routes,
399                gateways: &mut gateways,
400                message_patterns: &mut message_patterns,
401            };
402
403            for module in &self.modules {
404                let registered = registry
405                    .register_module_async(Arc::clone(module), &self.global_pipeline, &mut sink)
406                    .await?;
407                module_ref.add_visible_scope(registered.module_ref)?;
408            }
409        }
410        for (name, registered) in registry.registered_modules() {
411            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
412        }
413
414        let mut routes = apply_global_prefix(routes, self.global_prefix.as_deref())?;
415        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
416            .into_iter()
417            .map(|gateway| {
418                gateway.with_execution_pipeline_prefix(
419                    &self.global_execution_guards,
420                    &self.global_execution_interceptors,
421                )
422            })
423            .collect::<Vec<_>>();
424        let message_patterns = message_patterns
425            .into_iter()
426            .map(|pattern| {
427                pattern.with_execution_pipeline_prefix(
428                    &self.global_execution_guards,
429                    &self.global_execution_interceptors,
430                )
431            })
432            .collect::<Vec<_>>();
433        let documented_routes = routes.clone();
434
435        for (path, info) in self.openapi_routes {
436            let document = OpenApiDocument::from_routes(info, &documented_routes);
437            let route =
438                openapi_json_route(path, document)?.with_pipeline_prefix(&self.global_pipeline);
439            let route = match self.global_prefix.as_deref() {
440                Some(prefix) => route.with_path_prefix(prefix)?,
441                None => route,
442            };
443            routes.push(route);
444        }
445
446        #[cfg(feature = "security")]
447        if let Some(cors_preflight) = &self.cors_preflight {
448            add_cors_preflight_routes(&mut routes, cors_preflight, &self.global_pipeline)?;
449        }
450
451        routes = routes
452            .into_iter()
453            .map(|route| route.with_default_module_ref(module_ref.clone()))
454            .collect();
455
456        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
457        validate_unique_gateways(&gateways)?;
458        validate_unique_message_patterns(&message_patterns)?;
459        validate_gateway_route_conflicts(&routes, &gateways)?;
460
461        Ok(BootApplication {
462            routes,
463            gateways,
464            message_patterns,
465            modules,
466            module_ref,
467            module_instances,
468            api_versioning: self.api_versioning,
469        })
470    }
471}
472
473fn apply_global_gateway_prefix(
474    gateways: Vec<WebSocketGatewayDefinition>,
475    prefix: Option<&str>,
476) -> Result<Vec<WebSocketGatewayDefinition>> {
477    match prefix {
478        Some(prefix) => gateways
479            .into_iter()
480            .map(|gateway| gateway.with_path_prefix(prefix))
481            .collect(),
482        None => Ok(gateways),
483    }
484}
485
486fn openapi_json_route(path: String, document: OpenApiDocument) -> Result<RouteDefinition> {
487    RouteDefinition::get(path, move |_| {
488        let document = document.clone();
489        async move { BootResponse::json(&document) }
490    })
491    .map(RouteDefinition::hide_from_openapi)
492}
493
494#[cfg(feature = "security")]
495fn add_cors_preflight_routes(
496    routes: &mut Vec<RouteDefinition>,
497    cors_preflight: &CorsPreflightRoute,
498    pipeline: &PipelineComponents,
499) -> Result<()> {
500    let existing_options = routes
501        .iter()
502        .filter(|route| route.method() == crate::HttpMethod::Options)
503        .map(RouteDefinition::path_shape)
504        .collect::<BTreeSet<_>>();
505    let mut generated = BTreeSet::new();
506    let source_routes = routes.clone();
507
508    for route in source_routes {
509        if route.method() == crate::HttpMethod::Options {
510            continue;
511        }
512
513        let path_shape = route.path_shape();
514        if existing_options.contains(&path_shape) || !generated.insert(path_shape) {
515            continue;
516        }
517
518        let preflight = RouteDefinition::options(route.path().to_string(), cors_preflight.clone())?
519            .hide_from_openapi()
520            .with_pipeline_prefix(pipeline);
521        routes.push(preflight);
522    }
523
524    Ok(())
525}
526
527fn apply_global_prefix(
528    routes: Vec<RouteDefinition>,
529    prefix: Option<&str>,
530) -> Result<Vec<RouteDefinition>> {
531    match prefix {
532        Some(prefix) => routes
533            .into_iter()
534            .map(|route| route.with_path_prefix(prefix))
535            .collect(),
536        None => Ok(routes),
537    }
538}
539
540fn validate_unique_routes(
541    routes: &[RouteDefinition],
542    versioning: Option<&ApiVersioning>,
543) -> Result<()> {
544    for (index, route) in routes.iter().enumerate() {
545        for existing in routes.iter().take(index) {
546            if existing.method() != route.method()
547                || existing.path_shape_key() != route.path_shape_key()
548                || existing.host_shape_key() != route.host_shape_key()
549            {
550                continue;
551            }
552
553            let duplicate = match versioning {
554                Some(versioning) => existing
555                    .versioning()
556                    .overlaps(route.versioning(), versioning.default_version()),
557                None => true,
558            };
559            if !duplicate {
560                continue;
561            }
562
563            let host = route
564                .host()
565                .map(|host| format!(" host {host}"))
566                .unwrap_or_default();
567            return Err(BootError::DuplicateRoute(format!(
568                "{} {}{} version {}",
569                route.method().as_str(),
570                route.path(),
571                host,
572                route.versioning()
573            )));
574        }
575    }
576
577    Ok(())
578}
579
580fn validate_unique_gateways(gateways: &[WebSocketGatewayDefinition]) -> Result<()> {
581    let mut seen = BTreeSet::new();
582
583    for gateway in gateways {
584        if !seen.insert(gateway.path_shape()) {
585            return Err(BootError::DuplicateRoute(format!("WS {}", gateway.path())));
586        }
587    }
588
589    Ok(())
590}
591
592fn validate_gateway_route_conflicts(
593    routes: &[RouteDefinition],
594    gateways: &[WebSocketGatewayDefinition],
595) -> Result<()> {
596    let get_routes = routes
597        .iter()
598        .filter(|route| route.method().matches(crate::HttpMethod::Get))
599        .map(RouteDefinition::path_shape)
600        .collect::<BTreeSet<_>>();
601
602    for gateway in gateways {
603        if get_routes.contains(&gateway.path_shape()) {
604            return Err(BootError::DuplicateRoute(format!("GET {}", gateway.path())));
605        }
606    }
607
608    Ok(())
609}
610
611fn validate_unique_message_patterns(patterns: &[MessagePatternDefinition]) -> Result<()> {
612    let mut seen = BTreeSet::new();
613
614    for pattern in patterns {
615        if !seen.insert(pattern.pattern()) {
616            return Err(BootError::DuplicateRoute(format!(
617                "message pattern {}",
618                pattern.pattern()
619            )));
620        }
621    }
622
623    Ok(())
624}