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, 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 global session middleware and cookie persistence.
319    #[cfg(feature = "session")]
320    pub fn use_global_sessions(mut self, manager: SessionManager) -> Self {
321        self.global_pipeline
322            .push_middleware(SessionMiddleware::new(manager.clone()));
323        self.global_pipeline
324            .push_interceptor(SessionCookieInterceptor::new(manager));
325        self
326    }
327
328    /// Import a session module and apply its middleware/interceptor globally.
329    #[cfg(feature = "session")]
330    pub fn use_global_session_module(mut self, module: SessionModule) -> Self {
331        let manager = module.manager();
332        self = self.use_global_sessions(manager);
333        self.modules.push(Arc::new(module.global()));
334        self
335    }
336
337    /// Add an application-wide exception filter, similar to Nest's global filters.
338    pub fn use_global_filter<F>(mut self, filter: F) -> Self
339    where
340        F: ExceptionFilter,
341    {
342        self.global_pipeline.push_filter(filter);
343        self
344    }
345
346    /// Add an application-wide exception filter for selected error kinds.
347    pub fn use_global_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
348    where
349        I: IntoIterator<Item = BootErrorKind>,
350        F: ExceptionFilter,
351    {
352        self.global_pipeline.push_catch_filter(kinds, filter);
353        self
354    }
355
356    /// Add an application-wide WebSocket exception filter.
357    pub fn use_global_websocket_filter<F>(mut self, filter: F) -> Self
358    where
359        F: WebSocketExceptionFilter,
360    {
361        self.global_websocket_filters.push(Arc::new(filter));
362        self
363    }
364
365    /// Add an application-wide WebSocket exception filter for selected error kinds.
366    pub fn use_global_websocket_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
367    where
368        I: IntoIterator<Item = BootErrorKind>,
369        F: WebSocketExceptionFilter,
370    {
371        self.global_websocket_filters
372            .push(Arc::new(catch_errors(kinds, filter)));
373        self
374    }
375
376    /// Add an application-wide transport exception filter.
377    pub fn use_global_transport_filter<F>(mut self, filter: F) -> Self
378    where
379        F: TransportExceptionFilter,
380    {
381        self.global_transport_filters.push(Arc::new(filter));
382        self
383    }
384
385    /// Add an application-wide transport exception filter for selected error kinds.
386    pub fn use_global_transport_catch_filter<I, F>(mut self, kinds: I, filter: F) -> Self
387    where
388        I: IntoIterator<Item = BootErrorKind>,
389        F: TransportExceptionFilter,
390    {
391        self.global_transport_filters
392            .push(Arc::new(catch_errors(kinds, filter)));
393        self
394    }
395
396    /// Enable DTO validation for routes that carry validation metadata.
397    pub fn use_global_validation(mut self) -> Self {
398        self.global_pipeline.enable_validation();
399        self
400    }
401
402    /// Enable DTO validation with Nest-style validation options.
403    pub fn use_global_validation_options(mut self, options: ValidationOptions) -> Self {
404        self.global_pipeline.enable_validation_with_options(options);
405        self
406    }
407
408    /// Replace matching module providers before modules build controllers.
409    ///
410    /// This is primarily used by testing utilities to swap provider
411    /// implementations while preserving the application module graph.
412    pub fn override_provider(mut self, provider: ProviderDefinition) -> Self {
413        self.provider_overrides
414            .insert(provider.token().clone(), provider);
415        self
416    }
417
418    /// Replace a module by name before the application graph is registered.
419    ///
420    /// This is primarily used by testing utilities to mirror Nest's
421    /// `overrideModule(...).useModule(...)` workflow. The replacement module is
422    /// registered wherever a module with the target name appears in the import
423    /// graph.
424    pub fn override_module<M>(mut self, target_name: impl Into<String>, module: M) -> Self
425    where
426        M: Module,
427    {
428        self.module_overrides
429            .insert(target_name.into(), Arc::new(module));
430        self
431    }
432
433    /// Replace a shared module by name before the application graph is registered.
434    pub fn override_module_arc(
435        mut self,
436        target_name: impl Into<String>,
437        module: Arc<dyn Module>,
438    ) -> Self {
439        self.module_overrides.insert(target_name.into(), module);
440        self
441    }
442
443    /// Resolve module imports, providers, controllers, and routes.
444    pub fn build(self) -> Result<BootApplication> {
445        let module_ref = ModuleRef::new();
446        let global_ref = ModuleRef::new();
447        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
448        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
449        module_ref.add_visible_scope(global_ref.clone())?;
450        let mut registry =
451            ModuleRegistry::new(global_ref, self.provider_overrides, self.module_overrides);
452        let mut modules = Vec::new();
453        let mut module_instances = Vec::new();
454        let mut routes = self
455            .routes
456            .into_iter()
457            .map(|route| route.with_pipeline_prefix(&self.global_pipeline))
458            .collect::<Vec<_>>();
459        let mut gateways = self.gateways;
460        let mut message_patterns = self.message_patterns;
461
462        {
463            let mut sink = ModuleRegistrationSink {
464                modules: &mut modules,
465                module_instances: &mut module_instances,
466                routes: &mut routes,
467                gateways: &mut gateways,
468                message_patterns: &mut message_patterns,
469            };
470
471            for module in &self.modules {
472                let registered = registry.register_module(
473                    Arc::clone(module),
474                    &self.global_pipeline,
475                    &mut sink,
476                )?;
477                module_ref.add_visible_scope(registered.module_ref)?;
478            }
479        }
480        for (name, registered) in registry.registered_modules() {
481            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
482        }
483
484        let mut routes = apply_global_prefix(
485            routes,
486            self.global_prefix.as_deref(),
487            &self.global_prefix_exclusions,
488        )?;
489        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
490            .into_iter()
491            .map(|gateway| {
492                gateway
493                    .with_guard_prefix(&self.global_websocket_guards)
494                    .with_interceptor_prefix(&self.global_websocket_interceptors)
495                    .with_execution_pipeline_prefix(
496                        &self.global_execution_guards,
497                        &self.global_execution_interceptors,
498                    )
499                    .with_pipe_prefix(&self.global_websocket_pipes)
500                    .with_filter_prefix(&self.global_websocket_filters)
501                    .with_validation_prefix(
502                        self.global_pipeline.validation_enabled,
503                        self.global_pipeline.validation_options,
504                    )
505            })
506            .collect::<Vec<_>>();
507        let message_patterns = message_patterns
508            .into_iter()
509            .map(|pattern| {
510                pattern
511                    .with_guard_prefix(&self.global_transport_guards)
512                    .with_interceptor_prefix(&self.global_transport_interceptors)
513                    .with_execution_pipeline_prefix(
514                        &self.global_execution_guards,
515                        &self.global_execution_interceptors,
516                    )
517                    .with_pipe_prefix(&self.global_transport_pipes)
518                    .with_filter_prefix(&self.global_transport_filters)
519                    .with_validation_prefix(
520                        self.global_pipeline.validation_enabled,
521                        self.global_pipeline.validation_options,
522                    )
523            })
524            .collect::<Vec<_>>();
525        let documented_routes = routes.clone();
526
527        for (path, info) in self.openapi_routes {
528            let document = OpenApiDocument::from_routes(info, &documented_routes);
529            let route =
530                openapi_json_route(path, document)?.with_pipeline_prefix(&self.global_pipeline);
531            let route = apply_global_prefix_to_route(
532                route,
533                self.global_prefix.as_deref(),
534                &self.global_prefix_exclusions,
535            )?;
536            routes.push(route);
537        }
538        for ui in self.openapi_ui_routes {
539            add_openapi_ui_routes(
540                &mut routes,
541                ui,
542                &documented_routes,
543                self.global_prefix.as_deref(),
544                &self.global_prefix_exclusions,
545                &self.global_pipeline,
546            )?;
547        }
548
549        #[cfg(feature = "security")]
550        if let Some(cors_preflight) = &self.cors_preflight {
551            add_cors_preflight_routes(&mut routes, cors_preflight, &self.global_pipeline)?;
552        }
553
554        routes = routes
555            .into_iter()
556            .map(|route| route.with_default_module_ref(module_ref.clone()))
557            .collect();
558
559        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
560        validate_unique_gateways(&gateways)?;
561        validate_unique_message_patterns(&message_patterns)?;
562        validate_gateway_route_conflicts(&routes, &gateways)?;
563
564        Ok(BootApplication {
565            routes,
566            gateways,
567            message_patterns,
568            modules,
569            module_ref,
570            module_instances,
571            api_versioning: self.api_versioning,
572        })
573    }
574
575    /// Resolve modules with async provider factories, then build the application.
576    pub async fn build_async(self) -> Result<BootApplication> {
577        let module_ref = ModuleRef::new();
578        let global_ref = ModuleRef::new();
579        let lazy_module_loader = LazyModuleLoader::new(global_ref.clone());
580        global_ref.insert_arc(Arc::new(lazy_module_loader.clone()))?;
581        module_ref.add_visible_scope(global_ref.clone())?;
582        let mut registry =
583            ModuleRegistry::new(global_ref, self.provider_overrides, self.module_overrides);
584        let mut modules = Vec::new();
585        let mut module_instances = Vec::new();
586        let mut routes = self
587            .routes
588            .into_iter()
589            .map(|route| route.with_pipeline_prefix(&self.global_pipeline))
590            .collect::<Vec<_>>();
591        let mut gateways = self.gateways;
592        let mut message_patterns = self.message_patterns;
593
594        {
595            let mut sink = ModuleRegistrationSink {
596                modules: &mut modules,
597                module_instances: &mut module_instances,
598                routes: &mut routes,
599                gateways: &mut gateways,
600                message_patterns: &mut message_patterns,
601            };
602
603            for module in &self.modules {
604                let registered = registry
605                    .register_module_async(Arc::clone(module), &self.global_pipeline, &mut sink)
606                    .await?;
607                module_ref.add_visible_scope(registered.module_ref)?;
608            }
609        }
610        for (name, registered) in registry.registered_modules() {
611            lazy_module_loader.seed_module(name, registered.module_ref, registered.exports)?;
612        }
613
614        let mut routes = apply_global_prefix(
615            routes,
616            self.global_prefix.as_deref(),
617            &self.global_prefix_exclusions,
618        )?;
619        let gateways = apply_global_gateway_prefix(gateways, self.global_prefix.as_deref())?
620            .into_iter()
621            .map(|gateway| {
622                gateway
623                    .with_guard_prefix(&self.global_websocket_guards)
624                    .with_interceptor_prefix(&self.global_websocket_interceptors)
625                    .with_execution_pipeline_prefix(
626                        &self.global_execution_guards,
627                        &self.global_execution_interceptors,
628                    )
629                    .with_pipe_prefix(&self.global_websocket_pipes)
630                    .with_filter_prefix(&self.global_websocket_filters)
631                    .with_validation_prefix(
632                        self.global_pipeline.validation_enabled,
633                        self.global_pipeline.validation_options,
634                    )
635            })
636            .collect::<Vec<_>>();
637        let message_patterns = message_patterns
638            .into_iter()
639            .map(|pattern| {
640                pattern
641                    .with_guard_prefix(&self.global_transport_guards)
642                    .with_interceptor_prefix(&self.global_transport_interceptors)
643                    .with_execution_pipeline_prefix(
644                        &self.global_execution_guards,
645                        &self.global_execution_interceptors,
646                    )
647                    .with_pipe_prefix(&self.global_transport_pipes)
648                    .with_filter_prefix(&self.global_transport_filters)
649                    .with_validation_prefix(
650                        self.global_pipeline.validation_enabled,
651                        self.global_pipeline.validation_options,
652                    )
653            })
654            .collect::<Vec<_>>();
655        let documented_routes = routes.clone();
656
657        for (path, info) in self.openapi_routes {
658            let document = OpenApiDocument::from_routes(info, &documented_routes);
659            let route =
660                openapi_json_route(path, document)?.with_pipeline_prefix(&self.global_pipeline);
661            let route = apply_global_prefix_to_route(
662                route,
663                self.global_prefix.as_deref(),
664                &self.global_prefix_exclusions,
665            )?;
666            routes.push(route);
667        }
668        for ui in self.openapi_ui_routes {
669            add_openapi_ui_routes(
670                &mut routes,
671                ui,
672                &documented_routes,
673                self.global_prefix.as_deref(),
674                &self.global_prefix_exclusions,
675                &self.global_pipeline,
676            )?;
677        }
678
679        #[cfg(feature = "security")]
680        if let Some(cors_preflight) = &self.cors_preflight {
681            add_cors_preflight_routes(&mut routes, cors_preflight, &self.global_pipeline)?;
682        }
683
684        routes = routes
685            .into_iter()
686            .map(|route| route.with_default_module_ref(module_ref.clone()))
687            .collect();
688
689        validate_unique_routes(&routes, self.api_versioning.as_ref())?;
690        validate_unique_gateways(&gateways)?;
691        validate_unique_message_patterns(&message_patterns)?;
692        validate_gateway_route_conflicts(&routes, &gateways)?;
693
694        Ok(BootApplication {
695            routes,
696            gateways,
697            message_patterns,
698            modules,
699            module_ref,
700            module_instances,
701            api_versioning: self.api_versioning,
702        })
703    }
704}
705
706#[derive(Clone)]
707struct OpenApiUiRoute {
708    path: String,
709    document_path: String,
710    info: OpenApiInfo,
711}
712
713fn apply_global_gateway_prefix(
714    gateways: Vec<WebSocketGatewayDefinition>,
715    prefix: Option<&str>,
716) -> Result<Vec<WebSocketGatewayDefinition>> {
717    match prefix {
718        Some(prefix) => gateways
719            .into_iter()
720            .map(|gateway| gateway.with_path_prefix(prefix))
721            .collect(),
722        None => Ok(gateways),
723    }
724}
725
726fn openapi_json_route(path: String, document: OpenApiDocument) -> Result<RouteDefinition> {
727    RouteDefinition::get(path, move |_| {
728        let document = document.clone();
729        async move { BootResponse::json(&document) }
730    })
731    .map(RouteDefinition::hide_from_openapi)
732}
733
734fn add_openapi_ui_routes(
735    routes: &mut Vec<RouteDefinition>,
736    ui: OpenApiUiRoute,
737    documented_routes: &[RouteDefinition],
738    global_prefix: Option<&str>,
739    global_prefix_exclusions: &[MiddlewareRoute],
740    pipeline: &PipelineComponents,
741) -> Result<()> {
742    let document = OpenApiDocument::from_routes(ui.info.clone(), documented_routes);
743    let document_path = ui.document_path;
744    let ui_path = ui.path;
745    let json_route = openapi_json_route(document_path, document)?.with_pipeline_prefix(pipeline);
746    let json_route =
747        apply_global_prefix_to_route(json_route, global_prefix, global_prefix_exclusions)?;
748    let public_document_path = json_route.path().to_string();
749    let ui_route =
750        openapi_ui_route(ui_path, public_document_path, ui.info)?.with_pipeline_prefix(pipeline);
751    let ui_route = apply_global_prefix_to_route(ui_route, global_prefix, global_prefix_exclusions)?;
752    routes.push(json_route);
753    routes.push(ui_route);
754    Ok(())
755}
756
757fn openapi_ui_route(
758    path: String,
759    document_path: String,
760    info: OpenApiInfo,
761) -> Result<RouteDefinition> {
762    RouteDefinition::get(path, move |_| {
763        let document_path = document_path.clone();
764        let title = info.title.clone();
765        async move { Ok(openapi_ui_response(&title, &document_path)) }
766    })
767    .map(RouteDefinition::hide_from_openapi)
768}
769
770fn openapi_ui_response(title: &str, document_path: &str) -> BootResponse {
771    BootResponse::text_with_status(200, openapi_ui_html(title, document_path))
772        .with_header("content-type", "text/html; charset=utf-8")
773}
774
775fn openapi_ui_html(title: &str, document_path: &str) -> String {
776    format!(
777        r##"<!doctype html>
778<html lang="en">
779<head>
780  <meta charset="utf-8">
781  <title>{title}</title>
782  <meta name="viewport" content="width=device-width, initial-scale=1">
783  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
784</head>
785<body>
786  <div id="swagger-ui"></div>
787  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
788  <script>
789    window.ui = SwaggerUIBundle({{
790      url: {document_path:?},
791      dom_id: "#swagger-ui",
792      deepLinking: true,
793      presets: [SwaggerUIBundle.presets.apis],
794      layout: "BaseLayout"
795    }});
796  </script>
797</body>
798</html>"##,
799        title = escape_html(title),
800        document_path = document_path
801    )
802}
803
804fn escape_html(value: &str) -> String {
805    value
806        .replace('&', "&amp;")
807        .replace('<', "&lt;")
808        .replace('>', "&gt;")
809        .replace('"', "&quot;")
810        .replace('\'', "&#39;")
811}
812
813#[cfg(feature = "security")]
814fn add_cors_preflight_routes(
815    routes: &mut Vec<RouteDefinition>,
816    cors_preflight: &CorsPreflightRoute,
817    pipeline: &PipelineComponents,
818) -> Result<()> {
819    let existing_options = routes
820        .iter()
821        .filter(|route| route.method() == crate::HttpMethod::Options)
822        .map(RouteDefinition::path_shape)
823        .collect::<BTreeSet<_>>();
824    let mut generated = BTreeSet::new();
825    let source_routes = routes.clone();
826
827    for route in source_routes {
828        if route.method() == crate::HttpMethod::Options {
829            continue;
830        }
831
832        let path_shape = route.path_shape();
833        if existing_options.contains(&path_shape) || !generated.insert(path_shape) {
834            continue;
835        }
836
837        let preflight = RouteDefinition::options(route.path().to_string(), cors_preflight.clone())?
838            .hide_from_openapi()
839            .with_pipeline_prefix(pipeline);
840        routes.push(preflight);
841    }
842
843    Ok(())
844}
845
846fn apply_global_prefix(
847    routes: Vec<RouteDefinition>,
848    prefix: Option<&str>,
849    exclusions: &[MiddlewareRoute],
850) -> Result<Vec<RouteDefinition>> {
851    routes
852        .into_iter()
853        .map(|route| apply_global_prefix_to_route(route, prefix, exclusions))
854        .collect()
855}
856
857fn apply_global_prefix_to_route(
858    route: RouteDefinition,
859    prefix: Option<&str>,
860    exclusions: &[MiddlewareRoute],
861) -> Result<RouteDefinition> {
862    let Some(prefix) = prefix else {
863        return Ok(route);
864    };
865    if global_prefix_excludes(&route, exclusions) {
866        return Ok(route);
867    }
868    route.with_path_prefix(prefix)
869}
870
871fn global_prefix_excludes(route: &RouteDefinition, exclusions: &[MiddlewareRoute]) -> bool {
872    exclusions
873        .iter()
874        .any(|exclusion| exclusion.matches(route.method(), &[route.path().to_string()]))
875}
876
877fn validate_unique_routes(
878    routes: &[RouteDefinition],
879    versioning: Option<&ApiVersioning>,
880) -> Result<()> {
881    for (index, route) in routes.iter().enumerate() {
882        for existing in routes.iter().take(index) {
883            if existing.method() != route.method()
884                || existing.path_shape_key() != route.path_shape_key()
885                || existing.host_shape_key() != route.host_shape_key()
886            {
887                continue;
888            }
889
890            let duplicate = match versioning {
891                Some(versioning) => existing
892                    .versioning()
893                    .overlaps(route.versioning(), versioning.default_version()),
894                None => true,
895            };
896            if !duplicate {
897                continue;
898            }
899
900            let host = route
901                .host()
902                .map(|host| format!(" host {host}"))
903                .unwrap_or_default();
904            return Err(BootError::DuplicateRoute(format!(
905                "{} {}{} version {}",
906                route.method().as_str(),
907                route.path(),
908                host,
909                route.versioning()
910            )));
911        }
912    }
913
914    Ok(())
915}
916
917fn validate_unique_gateways(gateways: &[WebSocketGatewayDefinition]) -> Result<()> {
918    let mut seen = BTreeSet::new();
919
920    for gateway in gateways {
921        if !seen.insert(gateway.path_shape()) {
922            return Err(BootError::DuplicateRoute(format!("WS {}", gateway.path())));
923        }
924    }
925
926    Ok(())
927}
928
929fn validate_gateway_route_conflicts(
930    routes: &[RouteDefinition],
931    gateways: &[WebSocketGatewayDefinition],
932) -> Result<()> {
933    let get_routes = routes
934        .iter()
935        .filter(|route| route.method().matches(crate::HttpMethod::Get))
936        .map(RouteDefinition::path_shape)
937        .collect::<BTreeSet<_>>();
938
939    for gateway in gateways {
940        if get_routes.contains(&gateway.path_shape()) {
941            return Err(BootError::DuplicateRoute(format!("GET {}", gateway.path())));
942        }
943    }
944
945    Ok(())
946}
947
948fn validate_unique_message_patterns(patterns: &[MessagePatternDefinition]) -> Result<()> {
949    let mut seen = BTreeSet::new();
950
951    for pattern in patterns {
952        if !seen.insert(pattern.pattern()) {
953            return Err(BootError::DuplicateRoute(format!(
954                "message pattern {}",
955                pattern.pattern()
956            )));
957        }
958    }
959
960    Ok(())
961}