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    catch_errors, ApiVersioning, BootError, BootErrorKind, BootResponse, ExceptionFilter,
9    ExecutionInterceptor, Guard, Interceptor, MessagePatternDefinition, Middleware,
10    MiddlewareRoute, Module, ModuleRef, OpenApiDocument, OpenApiInfo, Pipe, ProviderDefinition,
11    ProviderToken, Result, RouteDefinition, SerializationInterceptor, TransportExceptionFilter,
12    TransportGuard, TransportInterceptor, TransportPipe, ValidationOptions,
13    WebSocketExceptionFilter, WebSocketGatewayDefinition, WebSocketGuard, WebSocketInterceptor,
14    WebSocketPipe,
15};
16#[cfg(feature = "compression")]
17use crate::{CompressionInterceptor, CompressionOptions};
18#[cfg(feature = "security")]
19use crate::{
20    CorsMiddleware, CorsOptions, CorsPreflightRoute, CorsResponseInterceptor, CsrfGuard,
21    CsrfOptions, RateLimitGuard, RateLimitOptions, RateLimitProvider, SecurityHeadersInterceptor,
22    SecurityHeadersOptions,
23};
24#[cfg(feature = "session")]
25use crate::{SessionCookieInterceptor, SessionManager, SessionMiddleware, SessionModule};
26use std::collections::{BTreeMap, BTreeSet};
27use std::sync::Arc;
28
29/// Builder for a [`BootApplication`].
30#[derive(Default)]
31pub struct BootApplicationBuilder {
32    modules: Vec<Arc<dyn Module>>,
33    routes: Vec<RouteDefinition>,
34    gateways: Vec<WebSocketGatewayDefinition>,
35    message_patterns: Vec<MessagePatternDefinition>,
36    global_pipeline: PipelineComponents,
37    global_execution_guards: Vec<Arc<dyn Guard>>,
38    global_execution_interceptors: Vec<Arc<dyn ExecutionInterceptor>>,
39    global_websocket_guards: Vec<Arc<dyn WebSocketGuard>>,
40    global_websocket_interceptors: Vec<Arc<dyn WebSocketInterceptor>>,
41    global_websocket_pipes: Vec<Arc<dyn WebSocketPipe>>,
42    global_websocket_filters: Vec<Arc<dyn WebSocketExceptionFilter>>,
43    global_transport_guards: Vec<Arc<dyn TransportGuard>>,
44    global_transport_interceptors: Vec<Arc<dyn TransportInterceptor>>,
45    global_transport_pipes: Vec<Arc<dyn TransportPipe>>,
46    global_transport_filters: Vec<Arc<dyn TransportExceptionFilter>>,
47    global_prefix: Option<String>,
48    global_prefix_exclusions: Vec<MiddlewareRoute>,
49    api_versioning: Option<ApiVersioning>,
50    openapi_routes: Vec<(String, OpenApiInfo)>,
51    openapi_ui_routes: Vec<OpenApiUiRoute>,
52    provider_overrides: BTreeMap<ProviderToken, ProviderDefinition>,
53    module_overrides: BTreeMap<String, Arc<dyn Module>>,
54    #[cfg(feature = "security")]
55    cors_preflight: Option<CorsPreflightRoute>,
56}
57
58impl BootApplicationBuilder {
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Import a root module.
64    pub fn import<M>(mut self, module: M) -> Self
65    where
66        M: Module,
67    {
68        self.modules.push(Arc::new(module));
69        self
70    }
71
72    /// Import a shared root module.
73    pub fn import_arc(mut self, module: Arc<dyn Module>) -> Self {
74        self.modules.push(module);
75        self
76    }
77
78    /// Add a framework-neutral route directly to the application shell.
79    pub fn route(mut self, route: RouteDefinition) -> Self {
80        self.routes.push(route);
81        self
82    }
83
84    /// Add a framework-neutral WebSocket gateway directly to the application shell.
85    pub fn gateway(mut self, gateway: WebSocketGatewayDefinition) -> Self {
86        self.gateways.push(gateway);
87        self
88    }
89
90    /// Add a framework-neutral microservice message pattern directly to the application shell.
91    pub fn message_pattern(mut self, pattern: MessagePatternDefinition) -> Self {
92        self.message_patterns.push(pattern);
93        self
94    }
95
96    /// Serve a generated OpenAPI document at the given path.
97    pub fn serve_openapi(mut self, path: impl Into<String>, info: OpenApiInfo) -> Self {
98        self.openapi_routes.push((path.into(), info));
99        self
100    }
101
102    /// Serve a generated OpenAPI document and a Swagger UI page for it.
103    pub fn serve_openapi_ui(
104        mut self,
105        path: impl Into<String>,
106        document_path: impl Into<String>,
107        info: OpenApiInfo,
108    ) -> Self {
109        self.openapi_ui_routes.push(OpenApiUiRoute {
110            path: path.into(),
111            document_path: document_path.into(),
112            info,
113        });
114        self
115    }
116
117    /// Prefix every route in the built application, for example `/api/v1`.
118    pub fn global_prefix(mut self, prefix: impl Into<String>) -> Self {
119        self.global_prefix = Some(prefix.into());
120        self
121    }
122
123    /// Exclude selected HTTP routes from the application-wide global prefix.
124    pub fn exclude_global_prefix<I>(mut self, routes: I) -> Self
125    where
126        I: IntoIterator<Item = MiddlewareRoute>,
127    {
128        self.global_prefix_exclusions.extend(routes);
129        self
130    }
131
132    /// Enable adapter-neutral API version matching for HTTP routes.
133    pub fn enable_api_versioning(mut self, versioning: ApiVersioning) -> Self {
134        self.api_versioning = Some(versioning);
135        self
136    }
137
138    /// Add application-wide middleware, similar to Nest middleware.
139    pub fn use_global_middleware<M>(mut self, middleware: M) -> Self
140    where
141        M: Middleware,
142    {
143        self.global_pipeline.push_middleware(middleware);
144        self
145    }
146
147    /// Add an application-wide pipe, similar to Nest's global pipes.
148    pub fn use_global_pipe<P>(mut self, pipe: P) -> Self
149    where
150        P: Pipe,
151    {
152        self.global_pipeline.push_pipe(pipe);
153        self
154    }
155
156    /// Add an application-wide WebSocket pipe.
157    pub fn use_global_websocket_pipe<P>(mut self, pipe: P) -> Self
158    where
159        P: WebSocketPipe,
160    {
161        self.global_websocket_pipes.push(Arc::new(pipe));
162        self
163    }
164
165    /// Add an application-wide WebSocket guard.
166    pub fn use_global_websocket_guard<G>(mut self, guard: G) -> Self
167    where
168        G: WebSocketGuard,
169    {
170        self.global_websocket_guards.push(Arc::new(guard));
171        self
172    }
173
174    /// Add an application-wide WebSocket interceptor.
175    pub fn use_global_websocket_interceptor<I>(mut self, interceptor: I) -> Self
176    where
177        I: WebSocketInterceptor,
178    {
179        self.global_websocket_interceptors
180            .push(Arc::new(interceptor));
181        self
182    }
183
184    /// Add an application-wide transport pipe.
185    pub fn use_global_transport_pipe<P>(mut self, pipe: P) -> Self
186    where
187        P: TransportPipe,
188    {
189        self.global_transport_pipes.push(Arc::new(pipe));
190        self
191    }
192
193    /// Add an application-wide transport guard.
194    pub fn use_global_transport_guard<G>(mut self, guard: G) -> Self
195    where
196        G: TransportGuard,
197    {
198        self.global_transport_guards.push(Arc::new(guard));
199        self
200    }
201
202    /// Add an application-wide transport interceptor.
203    pub fn use_global_transport_interceptor<I>(mut self, interceptor: I) -> Self
204    where
205        I: TransportInterceptor,
206    {
207        self.global_transport_interceptors
208            .push(Arc::new(interceptor));
209        self
210    }
211
212    /// Add an application-wide guard, similar to Nest's global guards.
213    pub fn use_global_guard<G>(mut self, guard: G) -> Self
214    where
215        G: Guard,
216    {
217        self.global_pipeline.push_guard(guard);
218        self
219    }
220
221    /// Add an application-wide protocol-neutral guard.
222    pub fn use_global_execution_guard<G>(mut self, guard: G) -> Self
223    where
224        G: Guard,
225    {
226        let guard: Arc<dyn Guard> = Arc::new(guard);
227        self.global_pipeline.push_guard_arc(Arc::clone(&guard));
228        self.global_execution_guards.push(guard);
229        self
230    }
231
232    /// Add a provider-backed authentication guard using the default auth strategy.
233    #[cfg(feature = "auth")]
234    pub fn use_global_auth(mut self) -> Self {
235        self.global_pipeline.push_guard(AuthGuard::new());
236        self
237    }
238
239    /// Add a provider-backed authentication guard using a named auth strategy.
240    #[cfg(feature = "auth")]
241    pub fn use_global_auth_strategy(mut self, strategy: impl Into<String>) -> Self {
242        self.global_pipeline
243            .push_guard(AuthGuard::new().strategy(strategy));
244        self
245    }
246
247    /// Add an application-wide interceptor, similar to Nest's global interceptors.
248    pub fn use_global_interceptor<I>(mut self, interceptor: I) -> Self
249    where
250        I: Interceptor,
251    {
252        self.global_pipeline.push_interceptor(interceptor);
253        self
254    }
255
256    /// Add an application-wide protocol-neutral interceptor.
257    pub fn use_global_execution_interceptor<I>(mut self, interceptor: I) -> Self
258    where
259        I: ExecutionInterceptor,
260    {
261        let interceptor: Arc<dyn ExecutionInterceptor> = Arc::new(interceptor);
262        self.global_pipeline
263            .push_execution_interceptor_arc(Arc::clone(&interceptor));
264        self.global_execution_interceptors.push(interceptor);
265        self
266    }
267
268    /// Add the default JSON response serialization interceptor.
269    pub fn use_global_serialization(mut self) -> Self {
270        self.global_pipeline
271            .push_interceptor(SerializationInterceptor::new());
272        self
273    }
274
275    /// Add gzip response compression for clients that send `Accept-Encoding: gzip`.
276    #[cfg(feature = "compression")]
277    pub fn use_global_compression(mut self, options: CompressionOptions) -> Self {
278        self.global_pipeline
279            .push_interceptor(CompressionInterceptor::with_options(options));
280        self
281    }
282
283    /// Add CORS handling for preflight and normal responses.
284    #[cfg(feature = "security")]
285    pub fn use_global_cors(mut self, options: CorsOptions) -> Self {
286        self.global_pipeline
287            .push_middleware(CorsMiddleware::with_options(options.clone()));
288        self.global_pipeline
289            .push_interceptor(CorsResponseInterceptor::with_options(options.clone()));
290        self.cors_preflight = Some(CorsPreflightRoute::with_options(options));
291        self
292    }
293
294    /// Add common security response headers, similar to a small Helmet setup.
295    #[cfg(feature = "security")]
296    pub fn use_global_security_headers(mut self, options: SecurityHeadersOptions) -> Self {
297        self.global_pipeline
298            .push_interceptor(SecurityHeadersInterceptor::with_options(options));
299        self
300    }
301
302    /// Add an application-wide CSRF guard for unsafe HTTP methods.
303    #[cfg(feature = "security")]
304    pub fn use_global_csrf(mut self, options: CsrfOptions) -> Self {
305        self.global_pipeline
306            .push_guard(CsrfGuard::with_options(options));
307        self
308    }
309
310    /// Add an in-memory application-wide rate limit guard.
311    #[cfg(feature = "security")]
312    pub fn use_global_rate_limit(mut self, options: RateLimitOptions) -> Self {
313        self.global_pipeline
314            .push_guard(RateLimitGuard::with_options(options));
315        self
316    }
317
318    /// Add an application-wide rate limit guard backed by a shared provider.
319    #[cfg(feature = "security")]
320    pub fn use_global_rate_limit_provider<P>(
321        mut self,
322        options: RateLimitOptions,
323        provider: P,
324    ) -> Self
325    where
326        P: RateLimitProvider,
327    {
328        self.global_pipeline
329            .push_guard(RateLimitGuard::with_provider(options, provider));
330        self
331    }
332
333    /// Add global session middleware and cookie persistence.
334    #[cfg(feature = "session")]
335    pub fn use_global_sessions(mut self, manager: SessionManager) -> Self {
336        self.global_pipeline
337            .push_middleware(SessionMiddleware::new(manager.clone()));
338        self.global_pipeline
339            .push_interceptor(SessionCookieInterceptor::new(manager));
340        self
341    }
342
343    /// Import a session module and apply its middleware/interceptor globally.
344    #[cfg(feature = "session")]
345    pub fn use_global_session_module(mut self, module: SessionModule) -> Self {
346        let manager = module.manager();
347        self = self.use_global_sessions(manager);
348        self.modules.push(Arc::new(module.global()));
349        self
350    }
351
352    /// Add an application-wide exception filter, similar to Nest's global filters.
353    pub fn use_global_filter<F>(mut self, filter: F) -> Self
354    where
355        F: ExceptionFilter,
356    {
357        self.global_pipeline.push_filter(filter);
358        self
359    }
360
361    /// Add an application-wide exception filter for selected error kinds.
362    pub fn use_global_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
363    where
364        I: IntoIterator<Item = BootErrorKind>,
365        F: ExceptionFilter,
366    {
367        self.global_pipeline.push_catch_filter(kinds, filter);
368        self
369    }
370
371    /// Add an application-wide WebSocket exception filter.
372    pub fn use_global_websocket_filter<F>(mut self, filter: F) -> Self
373    where
374        F: WebSocketExceptionFilter,
375    {
376        self.global_websocket_filters.push(Arc::new(filter));
377        self
378    }
379
380    /// Add an application-wide WebSocket exception filter for selected error kinds.
381    pub fn use_global_websocket_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
382    where
383        I: IntoIterator<Item = BootErrorKind>,
384        F: WebSocketExceptionFilter,
385    {
386        self.global_websocket_filters
387            .push(Arc::new(catch_errors(kinds, filter)));
388        self
389    }
390
391    /// Add an application-wide transport exception filter.
392    pub fn use_global_transport_filter<F>(mut self, filter: F) -> Self
393    where
394        F: TransportExceptionFilter,
395    {
396        self.global_transport_filters.push(Arc::new(filter));
397        self
398    }
399
400    /// Add an application-wide transport exception filter for selected error kinds.
401    pub fn use_global_transport_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
402    where
403        I: IntoIterator<Item = BootErrorKind>,
404        F: TransportExceptionFilter,
405    {
406        self.global_transport_filters
407            .push(Arc::new(catch_errors(kinds, filter)));
408        self
409    }
410
411    /// Enable DTO validation for routes that carry validation metadata.
412    pub fn use_global_validation(mut self) -> Self {
413        self.global_pipeline.enable_validation();
414        self
415    }
416
417    /// Enable DTO validation with Nest-style validation options.
418    pub fn use_global_validation_options(mut self, options: ValidationOptions) -> Self {
419        self.global_pipeline.enable_validation_with_options(options);
420        self
421    }
422
423    /// Replace matching module providers before modules build controllers.
424    ///
425    /// This is primarily used by testing utilities to swap provider
426    /// implementations while preserving the application module graph.
427    pub fn override_provider(mut self, provider: ProviderDefinition) -> Self {
428        self.provider_overrides
429            .insert(provider.token().clone(), provider);
430        self
431    }
432
433    /// Replace a module by name before the application graph is registered.
434    ///
435    /// This is primarily used by testing utilities to mirror Nest's
436    /// `overrideModule(...).useModule(...)` workflow. The replacement module is
437    /// registered wherever a module with the target name appears in the import
438    /// graph.
439    pub fn override_module<M>(mut self, target_name: impl Into<String>, module: M) -> Self
440    where
441        M: Module,
442    {
443        self.module_overrides
444            .insert(target_name.into(), Arc::new(module));
445        self
446    }
447
448    /// Replace a shared module by name before the application graph is registered.
449    pub fn override_module_arc(
450        mut self,
451        target_name: impl Into<String>,
452        module: Arc<dyn Module>,
453    ) -> Self {
454        self.module_overrides.insert(target_name.into(), module);
455        self
456    }
457
458    /// Resolve module imports, providers, controllers, and routes.
459    pub fn build(self) -> Result<BootApplication> {
460        let module_ref = ModuleRef::new();
461        let global_ref = ModuleRef::new();
462        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
463        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
464        module_ref.add_visible_scope(global_ref.clone())?;
465        let mut registry =
466            ModuleRegistry::new(global_ref, self.provider_overrides, self.module_overrides);
467        let mut modules = Vec::new();
468        let mut module_instances = Vec::new();
469        let mut routes = self.routes;
470        let mut gateways = self.gateways;
471        let mut message_patterns = self.message_patterns;
472
473        for module in &self.modules {
474            let registered = registry.register_module(Arc::clone(module))?;
475            module_ref.add_visible_scope(registered.module_ref)?;
476        }
477        let provider_enhancers = registry.provider_enhancers();
478        let mut effective_global_pipeline = self.global_pipeline.clone();
479        effective_global_pipeline.append(&provider_enhancers.http);
480        routes = routes
481            .into_iter()
482            .map(|route| route.with_pipeline_prefix(&effective_global_pipeline))
483            .collect();
484        {
485            let mut sink = ModuleRegistrationSink {
486                modules: &mut modules,
487                module_instances: &mut module_instances,
488                routes: &mut routes,
489                gateways: &mut gateways,
490                message_patterns: &mut message_patterns,
491            };
492            registry.finalize(&effective_global_pipeline, &mut sink)?;
493        }
494        for (name, registered) in registry.registered_modules() {
495            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
496        }
497
498        let mut routes = apply_global_prefix(
499            routes,
500            self.global_prefix.as_deref(),
501            &self.global_prefix_exclusions,
502        )?;
503        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
504            .into_iter()
505            .map(|gateway| {
506                gateway
507                    .with_provider_enhancer_prefix(&provider_enhancers)
508                    .with_guard_prefix(&self.global_websocket_guards)
509                    .with_interceptor_prefix(&self.global_websocket_interceptors)
510                    .with_execution_pipeline_prefix(
511                        &self.global_execution_guards,
512                        &self.global_execution_interceptors,
513                    )
514                    .with_pipe_prefix(&self.global_websocket_pipes)
515                    .with_filter_prefix(&self.global_websocket_filters)
516                    .with_validation_prefix(
517                        self.global_pipeline.validation_enabled,
518                        self.global_pipeline.validation_options,
519                    )
520                    .with_default_module_ref(module_ref.clone())
521            })
522            .collect::<Vec<_>>();
523        let message_patterns = message_patterns
524            .into_iter()
525            .map(|pattern| {
526                pattern
527                    .with_provider_enhancer_prefix(&provider_enhancers)
528                    .with_guard_prefix(&self.global_transport_guards)
529                    .with_interceptor_prefix(&self.global_transport_interceptors)
530                    .with_execution_pipeline_prefix(
531                        &self.global_execution_guards,
532                        &self.global_execution_interceptors,
533                    )
534                    .with_pipe_prefix(&self.global_transport_pipes)
535                    .with_filter_prefix(&self.global_transport_filters)
536                    .with_validation_prefix(
537                        self.global_pipeline.validation_enabled,
538                        self.global_pipeline.validation_options,
539                    )
540                    .with_default_module_ref(module_ref.clone())
541            })
542            .collect::<Vec<_>>();
543        let documented_routes = routes.clone();
544
545        for (path, info) in self.openapi_routes {
546            let document = OpenApiDocument::from_routes(info, &documented_routes);
547            let route = openapi_json_route(path, document)?
548                .with_pipeline_prefix(&effective_global_pipeline);
549            let route = apply_global_prefix_to_route(
550                route,
551                self.global_prefix.as_deref(),
552                &self.global_prefix_exclusions,
553            )?;
554            routes.push(route);
555        }
556        for ui in self.openapi_ui_routes {
557            add_openapi_ui_routes(
558                &mut routes,
559                ui,
560                &documented_routes,
561                self.global_prefix.as_deref(),
562                &self.global_prefix_exclusions,
563                &effective_global_pipeline,
564            )?;
565        }
566
567        #[cfg(feature = "security")]
568        if let Some(cors_preflight) = &self.cors_preflight {
569            add_cors_preflight_routes(&mut routes, cors_preflight, &effective_global_pipeline)?;
570        }
571
572        routes = routes
573            .into_iter()
574            .map(|route| route.with_default_module_ref(module_ref.clone()))
575            .collect();
576
577        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
578        validate_unique_gateways(&gateways)?;
579        validate_unique_message_patterns(&message_patterns)?;
580        validate_gateway_route_conflicts(&routes, &gateways)?;
581
582        Ok(BootApplication {
583            routes,
584            gateways,
585            message_patterns,
586            modules,
587            module_ref,
588            module_instances,
589            api_versioning: self.api_versioning,
590            global_filters: effective_global_pipeline.filters.clone(),
591        })
592    }
593
594    /// Resolve modules with async provider factories, then build the application.
595    pub async fn build_async(self) -> Result<BootApplication> {
596        let module_ref = ModuleRef::new();
597        let global_ref = ModuleRef::new();
598        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
599        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
600        module_ref.add_visible_scope(global_ref.clone())?;
601        let mut registry =
602            ModuleRegistry::new(global_ref, self.provider_overrides, self.module_overrides);
603        let mut modules = Vec::new();
604        let mut module_instances = Vec::new();
605        let mut routes = self.routes;
606        let mut gateways = self.gateways;
607        let mut message_patterns = self.message_patterns;
608
609        for module in &self.modules {
610            let registered = registry.register_module_async(Arc::clone(module)).await?;
611            module_ref.add_visible_scope(registered.module_ref)?;
612        }
613        let provider_enhancers = registry.provider_enhancers();
614        let mut effective_global_pipeline = self.global_pipeline.clone();
615        effective_global_pipeline.append(&provider_enhancers.http);
616        routes = routes
617            .into_iter()
618            .map(|route| route.with_pipeline_prefix(&effective_global_pipeline))
619            .collect();
620        {
621            let mut sink = ModuleRegistrationSink {
622                modules: &mut modules,
623                module_instances: &mut module_instances,
624                routes: &mut routes,
625                gateways: &mut gateways,
626                message_patterns: &mut message_patterns,
627            };
628            registry
629                .finalize_async(&effective_global_pipeline, &mut sink)
630                .await?;
631        }
632        for (name, registered) in registry.registered_modules() {
633            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
634        }
635
636        let mut routes = apply_global_prefix(
637            routes,
638            self.global_prefix.as_deref(),
639            &self.global_prefix_exclusions,
640        )?;
641        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
642            .into_iter()
643            .map(|gateway| {
644                gateway
645                    .with_provider_enhancer_prefix(&provider_enhancers)
646                    .with_guard_prefix(&self.global_websocket_guards)
647                    .with_interceptor_prefix(&self.global_websocket_interceptors)
648                    .with_execution_pipeline_prefix(
649                        &self.global_execution_guards,
650                        &self.global_execution_interceptors,
651                    )
652                    .with_pipe_prefix(&self.global_websocket_pipes)
653                    .with_filter_prefix(&self.global_websocket_filters)
654                    .with_validation_prefix(
655                        self.global_pipeline.validation_enabled,
656                        self.global_pipeline.validation_options,
657                    )
658                    .with_default_module_ref(module_ref.clone())
659            })
660            .collect::<Vec<_>>();
661        let message_patterns = message_patterns
662            .into_iter()
663            .map(|pattern| {
664                pattern
665                    .with_provider_enhancer_prefix(&provider_enhancers)
666                    .with_guard_prefix(&self.global_transport_guards)
667                    .with_interceptor_prefix(&self.global_transport_interceptors)
668                    .with_execution_pipeline_prefix(
669                        &self.global_execution_guards,
670                        &self.global_execution_interceptors,
671                    )
672                    .with_pipe_prefix(&self.global_transport_pipes)
673                    .with_filter_prefix(&self.global_transport_filters)
674                    .with_validation_prefix(
675                        self.global_pipeline.validation_enabled,
676                        self.global_pipeline.validation_options,
677                    )
678                    .with_default_module_ref(module_ref.clone())
679            })
680            .collect::<Vec<_>>();
681        let documented_routes = routes.clone();
682
683        for (path, info) in self.openapi_routes {
684            let document = OpenApiDocument::from_routes(info, &documented_routes);
685            let route = openapi_json_route(path, document)?
686                .with_pipeline_prefix(&effective_global_pipeline);
687            let route = apply_global_prefix_to_route(
688                route,
689                self.global_prefix.as_deref(),
690                &self.global_prefix_exclusions,
691            )?;
692            routes.push(route);
693        }
694        for ui in self.openapi_ui_routes {
695            add_openapi_ui_routes(
696                &mut routes,
697                ui,
698                &documented_routes,
699                self.global_prefix.as_deref(),
700                &self.global_prefix_exclusions,
701                &effective_global_pipeline,
702            )?;
703        }
704
705        #[cfg(feature = "security")]
706        if let Some(cors_preflight) = &self.cors_preflight {
707            add_cors_preflight_routes(&mut routes, cors_preflight, &effective_global_pipeline)?;
708        }
709
710        routes = routes
711            .into_iter()
712            .map(|route| route.with_default_module_ref(module_ref.clone()))
713            .collect();
714
715        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
716        validate_unique_gateways(&gateways)?;
717        validate_unique_message_patterns(&message_patterns)?;
718        validate_gateway_route_conflicts(&routes, &gateways)?;
719
720        Ok(BootApplication {
721            routes,
722            gateways,
723            message_patterns,
724            modules,
725            module_ref,
726            module_instances,
727            api_versioning: self.api_versioning,
728            global_filters: effective_global_pipeline.filters.clone(),
729        })
730    }
731}
732
733#[derive(Clone)]
734struct OpenApiUiRoute {
735    path: String,
736    document_path: String,
737    info: OpenApiInfo,
738}
739
740fn apply_global_gateway_prefix(
741    gateways: Vec<WebSocketGatewayDefinition>,
742    prefix: Option<&str>,
743) -> Result<Vec<WebSocketGatewayDefinition>> {
744    match prefix {
745        Some(prefix) => gateways
746            .into_iter()
747            .map(|gateway| gateway.with_path_prefix(prefix))
748            .collect(),
749        None => Ok(gateways),
750    }
751}
752
753fn openapi_json_route(path: String, document: OpenApiDocument) -> Result<RouteDefinition> {
754    RouteDefinition::get(path, move |_| {
755        let document = document.clone();
756        async move { BootResponse::json(&document) }
757    })
758    .map(RouteDefinition::hide_from_openapi)
759}
760
761fn add_openapi_ui_routes(
762    routes: &mut Vec<RouteDefinition>,
763    ui: OpenApiUiRoute,
764    documented_routes: &[RouteDefinition],
765    global_prefix: Option<&str>,
766    global_prefix_exclusions: &[MiddlewareRoute],
767    pipeline: &PipelineComponents,
768) -> Result<()> {
769    let document = OpenApiDocument::from_routes(ui.info.clone(), documented_routes);
770    let document_path = ui.document_path;
771    let ui_path = ui.path;
772    let json_route = openapi_json_route(document_path, document)?.with_pipeline_prefix(pipeline);
773    let json_route =
774        apply_global_prefix_to_route(json_route, global_prefix, global_prefix_exclusions)?;
775    let public_document_path = json_route.path().to_string();
776    let ui_route =
777        openapi_ui_route(ui_path, public_document_path, ui.info)?.with_pipeline_prefix(pipeline);
778    let ui_route = apply_global_prefix_to_route(ui_route, global_prefix, global_prefix_exclusions)?;
779    routes.push(json_route);
780    routes.push(ui_route);
781    Ok(())
782}
783
784fn openapi_ui_route(
785    path: String,
786    document_path: String,
787    info: OpenApiInfo,
788) -> Result<RouteDefinition> {
789    RouteDefinition::get(path, move |_| {
790        let document_path = document_path.clone();
791        let title = info.title.clone();
792        async move { Ok(openapi_ui_response(&title, &document_path)) }
793    })
794    .map(RouteDefinition::hide_from_openapi)
795}
796
797fn openapi_ui_response(title: &str, document_path: &str) -> BootResponse {
798    BootResponse::text_with_status(200, openapi_ui_html(title, document_path))
799        .with_header("content-type", "text/html; charset=utf-8")
800}
801
802fn openapi_ui_html(title: &str, document_path: &str) -> String {
803    format!(
804        r##"<!doctype html>
805<html lang="en">
806<head>
807  <meta charset="utf-8">
808  <title>{title}</title>
809  <meta name="viewport" content="width=device-width, initial-scale=1">
810  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
811</head>
812<body>
813  <div id="swagger-ui"></div>
814  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
815  <script>
816    window.ui = SwaggerUIBundle({{
817      url: {document_path:?},
818      dom_id: "#swagger-ui",
819      deepLinking: true,
820      presets: [SwaggerUIBundle.presets.apis],
821      layout: "BaseLayout"
822    }});
823  </script>
824</body>
825</html>"##,
826        title = escape_html(title),
827        document_path = document_path
828    )
829}
830
831fn escape_html(value: &str) -> String {
832    value
833        .replace('&', "&amp;")
834        .replace('<', "&lt;")
835        .replace('>', "&gt;")
836        .replace('"', "&quot;")
837        .replace('\'', "&#39;")
838}
839
840#[cfg(feature = "security")]
841fn add_cors_preflight_routes(
842    routes: &mut Vec<RouteDefinition>,
843    cors_preflight: &CorsPreflightRoute,
844    pipeline: &PipelineComponents,
845) -> Result<()> {
846    let existing_options = routes
847        .iter()
848        .filter(|route| route.method() == crate::HttpMethod::Options)
849        .map(RouteDefinition::path_shape)
850        .collect::<BTreeSet<_>>();
851    let mut generated = BTreeSet::new();
852    let source_routes = routes.clone();
853
854    for route in source_routes {
855        if route.method() == crate::HttpMethod::Options {
856            continue;
857        }
858
859        let path_shape = route.path_shape();
860        if existing_options.contains(&path_shape) || !generated.insert(path_shape) {
861            continue;
862        }
863
864        let preflight = RouteDefinition::options(route.path().to_string(), cors_preflight.clone())?
865            .hide_from_openapi()
866            .with_pipeline_prefix(pipeline);
867        routes.push(preflight);
868    }
869
870    Ok(())
871}
872
873fn apply_global_prefix(
874    routes: Vec<RouteDefinition>,
875    prefix: Option<&str>,
876    exclusions: &[MiddlewareRoute],
877) -> Result<Vec<RouteDefinition>> {
878    routes
879        .into_iter()
880        .map(|route| apply_global_prefix_to_route(route, prefix, exclusions))
881        .collect()
882}
883
884fn apply_global_prefix_to_route(
885    route: RouteDefinition,
886    prefix: Option<&str>,
887    exclusions: &[MiddlewareRoute],
888) -> Result<RouteDefinition> {
889    let Some(prefix) = prefix else {
890        return Ok(route);
891    };
892    if global_prefix_excludes(&route, exclusions) {
893        return Ok(route);
894    }
895    route.with_path_prefix(prefix)
896}
897
898fn global_prefix_excludes(route: &RouteDefinition, exclusions: &[MiddlewareRoute]) -> bool {
899    exclusions
900        .iter()
901        .any(|exclusion| exclusion.matches(route.method(), &[route.path().to_string()]))
902}
903
904fn validate_unique_routes(
905    routes: &[RouteDefinition],
906    versioning: Option<&ApiVersioning>,
907) -> Result<()> {
908    for (index, route) in routes.iter().enumerate() {
909        for existing in routes.iter().take(index) {
910            if existing.method() != route.method()
911                || existing.path_shape_key() != route.path_shape_key()
912                || existing.host_shape_key() != route.host_shape_key()
913            {
914                continue;
915            }
916
917            let duplicate = match versioning {
918                Some(versioning) => existing
919                    .versioning()
920                    .overlaps(route.versioning(), versioning.default_version()),
921                None => true,
922            };
923            if !duplicate {
924                continue;
925            }
926
927            let host = route
928                .host()
929                .map(|host| format!(" host {host}"))
930                .unwrap_or_default();
931            return Err(BootError::DuplicateRoute(format!(
932                "{} {}{} version {}",
933                route.method().as_str(),
934                route.path(),
935                host,
936                route.versioning()
937            )));
938        }
939    }
940
941    Ok(())
942}
943
944fn validate_unique_gateways(gateways: &[WebSocketGatewayDefinition]) -> Result<()> {
945    let mut seen = BTreeSet::new();
946
947    for gateway in gateways {
948        if !seen.insert(gateway.path_shape()) {
949            return Err(BootError::DuplicateRoute(format!("WS {}", gateway.path())));
950        }
951    }
952
953    Ok(())
954}
955
956fn validate_gateway_route_conflicts(
957    routes: &[RouteDefinition],
958    gateways: &[WebSocketGatewayDefinition],
959) -> Result<()> {
960    let get_routes = routes
961        .iter()
962        .filter(|route| route.method().matches(crate::HttpMethod::Get))
963        .map(RouteDefinition::path_shape)
964        .collect::<BTreeSet<_>>();
965
966    for gateway in gateways {
967        if get_routes.contains(&gateway.path_shape()) {
968            return Err(BootError::DuplicateRoute(format!("GET {}", gateway.path())));
969        }
970    }
971
972    Ok(())
973}
974
975fn validate_unique_message_patterns(patterns: &[MessagePatternDefinition]) -> Result<()> {
976    let mut seen = BTreeSet::new();
977
978    for pattern in patterns {
979        if !seen.insert(pattern.pattern()) {
980            return Err(BootError::DuplicateRoute(format!(
981                "message pattern {}",
982                pattern.pattern()
983            )));
984        }
985    }
986
987    Ok(())
988}