Skip to main content

a3s_boot/app/
application.rs

1use super::builder::BootApplicationBuilder;
2use crate::pipeline::PipelineComponent;
3use crate::versioning::ApiVersionCandidate;
4use crate::{
5    ApiVersioning, BootError, BootRequest, BootResponse, ContextIdFactory, DiscoveryService,
6    ExceptionFilter, ExecutionContext, HttpAdapter, HttpMethod, LazyModuleLoader,
7    MessagePatternDefinition, MessageTransport, Module, ModuleRef, OpenApiDocument, OpenApiInfo,
8    ProviderToken, Reflector, Result, RouteDefinition, SerializationOptions, TransportMessage,
9    TransportReply, WebSocketGatewayDefinition,
10};
11use std::collections::BTreeMap;
12use std::fmt;
13use std::net::SocketAddr;
14use std::sync::Arc;
15
16#[derive(Clone)]
17pub(crate) struct ModuleInstance {
18    pub module: Arc<dyn Module>,
19    pub module_ref: ModuleRef,
20    pub imports: Vec<String>,
21    pub exports: Vec<ProviderToken>,
22    pub is_global: bool,
23    pub route_prefix: Option<String>,
24}
25
26/// Resolved route and decoded path parameters for a method/path lookup.
27pub struct RouteMatch<'a> {
28    route: &'a RouteDefinition,
29    params: BTreeMap<String, String>,
30}
31
32impl<'a> RouteMatch<'a> {
33    pub fn route(&self) -> &'a RouteDefinition {
34        self.route
35    }
36
37    pub fn params(&self) -> &BTreeMap<String, String> {
38        &self.params
39    }
40
41    pub fn param(&self, name: &str) -> Option<&str> {
42        self.params.get(name).map(String::as_str)
43    }
44
45    pub fn into_params(self) -> BTreeMap<String, String> {
46        self.params
47    }
48}
49
50impl fmt::Debug for RouteMatch<'_> {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.debug_struct("RouteMatch")
53            .field("method", &self.route.method())
54            .field("path", &self.route.path())
55            .field("params", &self.params)
56            .finish()
57    }
58}
59
60/// Built application with a resolved module graph and framework-neutral routes.
61#[derive(Clone)]
62pub struct BootApplication {
63    pub(crate) routes: Vec<RouteDefinition>,
64    pub(crate) gateways: Vec<WebSocketGatewayDefinition>,
65    pub(crate) message_patterns: Vec<MessagePatternDefinition>,
66    pub(crate) modules: Vec<String>,
67    pub(crate) module_ref: ModuleRef,
68    pub(crate) module_instances: Vec<ModuleInstance>,
69    pub(crate) api_versioning: Option<ApiVersioning>,
70    pub(crate) global_filters: Vec<PipelineComponent<dyn ExceptionFilter>>,
71}
72
73impl BootApplication {
74    /// Create an application builder.
75    pub fn builder() -> BootApplicationBuilder {
76        BootApplicationBuilder::new()
77    }
78
79    /// Names of modules included in the application, in registration order.
80    pub fn module_names(&self) -> &[String] {
81        &self.modules
82    }
83
84    /// Routes exposed by the application.
85    pub fn routes(&self) -> &[RouteDefinition] {
86        &self.routes
87    }
88
89    /// API versioning configuration used for HTTP route matching, when enabled.
90    pub fn api_versioning(&self) -> Option<&ApiVersioning> {
91        self.api_versioning.as_ref()
92    }
93
94    /// WebSocket gateways exposed by the application.
95    pub fn gateways(&self) -> &[WebSocketGatewayDefinition] {
96        &self.gateways
97    }
98
99    /// Microservice message patterns exposed by the application.
100    pub fn message_patterns(&self) -> &[MessagePatternDefinition] {
101        &self.message_patterns
102    }
103
104    /// Build a discovery snapshot for modules, routes, gateways, and message patterns.
105    pub fn discovery(&self) -> Result<DiscoveryService> {
106        DiscoveryService::from_app(self)
107    }
108
109    /// Build a discovery snapshot and metadata reflector.
110    pub fn reflector(&self) -> Result<Reflector> {
111        Reflector::from_app(self)
112    }
113
114    /// Generate an OpenAPI 3 document from the resolved route table.
115    pub fn openapi(&self, info: OpenApiInfo) -> OpenApiDocument {
116        OpenApiDocument::from_routes(info, &self.routes)
117    }
118
119    /// Route registered for a method and the most specific route shape matching a path.
120    pub fn route_for(&self, method: HttpMethod, path: &str) -> Option<&RouteDefinition> {
121        let candidates = self.path_candidates(path);
122        self.route_for_candidates(method, &candidates)
123            .map(|matched| matched.route)
124    }
125
126    /// Route and decoded path parameters for a method and path, when one is registered.
127    pub fn route_match(&self, method: HttpMethod, path: &str) -> Result<Option<RouteMatch<'_>>> {
128        let candidates = self.path_candidates(path);
129        let Some(matched) = self.route_for_candidates(method, &candidates) else {
130            return Ok(None);
131        };
132        let Some(params) = matched.route.path_params(&matched.path)? else {
133            return Ok(None);
134        };
135        Ok(Some(RouteMatch {
136            route: matched.route,
137            params,
138        }))
139    }
140
141    pub fn gateway_for(&self, path: &str) -> Option<&WebSocketGatewayDefinition> {
142        self.gateways
143            .iter()
144            .find(|gateway| gateway.matches_path(path))
145    }
146
147    pub fn message_pattern_for(&self, pattern: &str) -> Option<&MessagePatternDefinition> {
148        self.message_patterns
149            .iter()
150            .find(|definition| definition.pattern() == pattern)
151    }
152
153    /// HTTP methods registered for the most specific route shape matching a path.
154    pub fn allowed_methods(&self, path: &str) -> Vec<HttpMethod> {
155        let candidates = self.path_candidates(path);
156        let Some((candidate_index, best_specificity)) =
157            best_path_specificity(&self.routes, &candidates, self.api_versioning.as_ref())
158        else {
159            return Vec::new();
160        };
161        let candidate = &candidates[candidate_index];
162
163        let mut methods = Vec::new();
164        let mut has_wildcard = false;
165        for route in &self.routes {
166            if !route_matches_candidate(
167                route,
168                candidate,
169                &best_specificity,
170                self.api_versioning.as_ref(),
171            ) {
172                continue;
173            }
174
175            if route.method().is_wildcard() {
176                has_wildcard = true;
177                continue;
178            }
179
180            if !methods.contains(&route.method()) {
181                methods.push(route.method());
182            }
183        }
184        if has_wildcard {
185            return HttpMethod::standard_methods().to_vec();
186        }
187        methods
188    }
189
190    /// Comma-separated HTTP methods for an Allow header on the most specific matching path.
191    pub fn allowed_methods_header(&self, path: &str) -> Option<String> {
192        let methods = self.allowed_methods(path);
193        if methods.is_empty() {
194            return None;
195        }
196
197        Some(
198            methods
199                .into_iter()
200                .map(|method| method.as_str())
201                .collect::<Vec<_>>()
202                .join(","),
203        )
204    }
205
206    /// Provider container available to hosts and controllers.
207    pub fn module_ref(&self) -> &ModuleRef {
208        &self.module_ref
209    }
210
211    /// Lazy module loader available through the application provider graph.
212    pub fn lazy_module_loader(&self) -> Result<Arc<LazyModuleLoader>> {
213        self.get::<LazyModuleLoader>()
214    }
215
216    /// Resolve a typed provider from the application container.
217    pub fn get<T>(&self) -> Result<Arc<T>>
218    where
219        T: Send + Sync + 'static,
220    {
221        self.module_ref.get::<T>()
222    }
223
224    /// Resolve a named provider from the application container.
225    pub fn get_named<T>(&self, token: &str) -> Result<Arc<T>>
226    where
227        T: Send + Sync + 'static,
228    {
229        self.module_ref.get_named::<T>(token)
230    }
231
232    /// Resolve a typed provider when it is present.
233    pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>>
234    where
235        T: Send + Sync + 'static,
236    {
237        self.module_ref.get_optional::<T>()
238    }
239
240    /// Resolve a named provider when it is present.
241    pub fn get_optional_named<T>(&self, token: &str) -> Result<Option<Arc<T>>>
242    where
243        T: Send + Sync + 'static,
244    {
245        self.module_ref.get_optional_named::<T>(token)
246    }
247
248    /// Dispatch a framework-neutral request through the resolved route table.
249    pub async fn call(&self, request: BootRequest) -> Result<BootResponse> {
250        let candidates = self.request_candidates(&request);
251        let host = request.host();
252        let Some((candidate_index, best_specificity)) = best_request_specificity(
253            &self.routes,
254            &candidates,
255            self.api_versioning.as_ref(),
256            host,
257        ) else {
258            let error =
259                BootError::NotFound(format!("{} {}", request.method.as_str(), request.path));
260            return self.handle_global_error(request, error).await;
261        };
262        let candidate = &candidates[candidate_index];
263
264        if let Some(route) = matching_route_with_specificity(
265            &self.routes,
266            candidate,
267            &best_specificity,
268            self.api_versioning.as_ref(),
269            host,
270            |route| route.method() == request.method,
271        ) {
272            let request = request.with_matched_path(candidate.path.clone());
273            return route.call(request).await;
274        }
275
276        if let Some(route) = matching_route_with_specificity(
277            &self.routes,
278            candidate,
279            &best_specificity,
280            self.api_versioning.as_ref(),
281            host,
282            |route| route.method().matches(request.method),
283        ) {
284            let request = request.with_matched_path(candidate.path.clone());
285            return route.call(request).await;
286        }
287
288        if let Some(route) = matching_route_with_specificity(
289            &self.routes,
290            candidate,
291            &best_specificity,
292            self.api_versioning.as_ref(),
293            host,
294            |_| true,
295        ) {
296            let request = request.with_matched_path(candidate.path.clone());
297            return route.call(request).await;
298        }
299
300        let error = BootError::NotFound(format!("{} {}", request.method.as_str(), request.path));
301        self.handle_global_error(request, error).await
302    }
303
304    async fn handle_global_error(
305        &self,
306        request: BootRequest,
307        error: BootError,
308    ) -> Result<BootResponse> {
309        let context_id = ContextIdFactory::create();
310        let request = request.with_module_ref(self.module_ref.context_scope(&context_id));
311        let context = ExecutionContext::new(
312            request.clone(),
313            request.path.clone(),
314            None,
315            None,
316            SerializationOptions::default(),
317            BTreeMap::new(),
318        );
319        for filter in self.global_filters.iter().rev() {
320            let filter = filter.resolve(&context_id)?;
321            if let Some(response) = filter
322                .catch(context.clone(), error.clone_for_filter())
323                .await?
324            {
325                return Ok(response);
326            }
327        }
328        Err(error)
329    }
330
331    /// Dispatch a request and convert unhandled errors into Boot HTTP responses.
332    pub async fn handle(&self, request: BootRequest) -> BootResponse {
333        match self.call(request).await {
334            Ok(response) => response,
335            Err(error) => BootResponse::from_error(&error),
336        }
337    }
338
339    /// Dispatch a microservice transport message through the resolved pattern table.
340    pub async fn dispatch_message(
341        &self,
342        message: TransportMessage,
343    ) -> Result<Option<TransportReply>> {
344        let pattern = message.pattern.clone();
345        let Some(definition) = self.message_pattern_for(&pattern) else {
346            return Err(BootError::NotFound(format!("message pattern {pattern}")));
347        };
348        definition.dispatch(message).await
349    }
350
351    /// Dispatch an event-only transport message and ignore any handler reply.
352    pub async fn emit_message(&self, message: TransportMessage) -> Result<()> {
353        self.dispatch_message(message).await.map(|_| ())
354    }
355
356    /// Run async startup hooks before serving, when the host needs them.
357    pub async fn bootstrap(&self) -> Result<()> {
358        for instance in &self.module_instances {
359            instance.module_ref.bootstrap_local_providers().await?;
360            instance
361                .module
362                .on_application_bootstrap(instance.module_ref.clone())
363                .await?;
364        }
365        for gateway in &self.gateways {
366            gateway.after_init().await?;
367        }
368        Ok(())
369    }
370
371    /// Run async shutdown hooks in reverse registration order.
372    pub async fn shutdown(&self) -> Result<()> {
373        self.shutdown_inner(None).await
374    }
375
376    /// Run async shutdown hooks with a signal label visible to signal-aware hooks.
377    pub async fn shutdown_with_signal(&self, signal: impl Into<String>) -> Result<()> {
378        self.shutdown_inner(Some(signal.into())).await
379    }
380
381    async fn shutdown_inner(&self, signal: Option<String>) -> Result<()> {
382        for instance in self.module_instances.iter().rev() {
383            instance
384                .module
385                .on_module_destroy(instance.module_ref.clone(), signal.clone())
386                .await?;
387            instance
388                .module_ref
389                .destroy_local_providers(signal.clone())
390                .await?;
391        }
392        for instance in self.module_instances.iter().rev() {
393            instance
394                .module
395                .before_application_shutdown(instance.module_ref.clone(), signal.clone())
396                .await?;
397            instance
398                .module_ref
399                .before_application_shutdown_local_providers(signal.clone())
400                .await?;
401        }
402        for instance in self.module_instances.iter().rev() {
403            instance
404                .module
405                .on_application_shutdown_with_signal(instance.module_ref.clone(), signal.clone())
406                .await?;
407            instance
408                .module_ref
409                .shutdown_local_providers(signal.clone())
410                .await?;
411        }
412        Ok(())
413    }
414
415    /// Build this application through a concrete HTTP adapter.
416    pub fn into_adapter<A>(self, adapter: &A) -> Result<A::Output>
417    where
418        A: HttpAdapter,
419    {
420        adapter.build(self)
421    }
422
423    /// Build this application through a concrete message transport.
424    pub fn into_message_transport<T>(self, transport: &T) -> Result<T::Output>
425    where
426        T: MessageTransport,
427    {
428        transport.build(self)
429    }
430
431    /// Serve this application through a concrete HTTP adapter.
432    pub async fn serve_with<A>(self, adapter: &A, addr: SocketAddr) -> Result<()>
433    where
434        A: HttpAdapter,
435    {
436        let app_for_shutdown = self.clone();
437        if let Err(error) = self.bootstrap().await {
438            let _ = app_for_shutdown.shutdown().await;
439            return Err(error);
440        }
441        let serve_result = adapter.serve(self, addr).await;
442        let shutdown_result = app_for_shutdown.shutdown().await;
443
444        match (serve_result, shutdown_result) {
445            (Err(error), _) => Err(error),
446            (Ok(()), Err(error)) => Err(error),
447            (Ok(()), Ok(())) => Ok(()),
448        }
449    }
450
451    fn request_candidates(&self, request: &BootRequest) -> Vec<ApiVersionCandidate> {
452        match self.api_versioning.as_ref() {
453            Some(versioning) => versioning.request_candidates(request),
454            None => vec![ApiVersionCandidate {
455                path: request.path.clone(),
456                version: None,
457            }],
458        }
459    }
460
461    fn path_candidates(&self, path: &str) -> Vec<ApiVersionCandidate> {
462        match self.api_versioning.as_ref() {
463            Some(versioning) => versioning.path_candidates(path),
464            None => vec![ApiVersionCandidate {
465                path: path.to_string(),
466                version: None,
467            }],
468        }
469    }
470
471    fn route_for_candidates<'a>(
472        &'a self,
473        method: HttpMethod,
474        candidates: &[ApiVersionCandidate],
475    ) -> Option<MatchedRoute<'a>> {
476        let (candidate_index, best_specificity) =
477            best_path_specificity(&self.routes, candidates, self.api_versioning.as_ref())?;
478        let candidate = &candidates[candidate_index];
479        let exact = self.routes.iter().find(|route| {
480            route_matches_candidate(
481                route,
482                candidate,
483                &best_specificity,
484                self.api_versioning.as_ref(),
485            ) && route.method() == method
486        });
487        let route = exact.or_else(|| {
488            self.routes.iter().find(|route| {
489                route_matches_candidate(
490                    route,
491                    candidate,
492                    &best_specificity,
493                    self.api_versioning.as_ref(),
494                ) && route.method().matches(method)
495            })
496        })?;
497
498        Some(MatchedRoute {
499            route,
500            path: candidate.path.clone(),
501        })
502    }
503}
504
505struct MatchedRoute<'a> {
506    route: &'a RouteDefinition,
507    path: String,
508}
509
510fn best_path_specificity(
511    routes: &[RouteDefinition],
512    candidates: &[ApiVersionCandidate],
513    versioning: Option<&ApiVersioning>,
514) -> Option<(usize, Vec<u8>)> {
515    let mut best = None;
516
517    for (candidate_index, candidate) in candidates.iter().enumerate() {
518        for route in routes {
519            if route_matches_path_and_version(route, candidate, versioning) {
520                let specificity = route.path_specificity();
521                if best
522                    .as_ref()
523                    .map(|(_, best_specificity)| specificity > *best_specificity)
524                    .unwrap_or(true)
525                {
526                    best = Some((candidate_index, specificity));
527                }
528            }
529        }
530    }
531
532    best
533}
534
535fn matching_route_with_specificity<'a, P>(
536    routes: &'a [RouteDefinition],
537    candidate: &ApiVersionCandidate,
538    specificity: &[u8],
539    versioning: Option<&ApiVersioning>,
540    host: Option<&str>,
541    mut predicate: P,
542) -> Option<&'a RouteDefinition>
543where
544    P: FnMut(&RouteDefinition) -> bool,
545{
546    routes.iter().find(|route| {
547        route_matches_request_candidate(route, candidate, specificity, versioning, host)
548            && predicate(route)
549    })
550}
551
552fn best_request_specificity(
553    routes: &[RouteDefinition],
554    candidates: &[ApiVersionCandidate],
555    versioning: Option<&ApiVersioning>,
556    host: Option<&str>,
557) -> Option<(usize, Vec<u8>)> {
558    let mut best = None;
559
560    for (candidate_index, candidate) in candidates.iter().enumerate() {
561        for route in routes {
562            if route_matches_path_version_and_host(route, candidate, versioning, host) {
563                let specificity = request_specificity(route);
564                if best
565                    .as_ref()
566                    .map(|(_, best_specificity)| specificity > *best_specificity)
567                    .unwrap_or(true)
568                {
569                    best = Some((candidate_index, specificity));
570                }
571            }
572        }
573    }
574
575    best
576}
577
578fn route_matches_candidate(
579    route: &RouteDefinition,
580    candidate: &ApiVersionCandidate,
581    specificity: &[u8],
582    versioning: Option<&ApiVersioning>,
583) -> bool {
584    route_matches_path_and_version(route, candidate, versioning)
585        && route.path_specificity().as_slice() == specificity
586}
587
588fn route_matches_request_candidate(
589    route: &RouteDefinition,
590    candidate: &ApiVersionCandidate,
591    specificity: &[u8],
592    versioning: Option<&ApiVersioning>,
593    host: Option<&str>,
594) -> bool {
595    route_matches_path_version_and_host(route, candidate, versioning, host)
596        && request_specificity(route).as_slice() == specificity
597}
598
599fn route_matches_path_and_version(
600    route: &RouteDefinition,
601    candidate: &ApiVersionCandidate,
602    versioning: Option<&ApiVersioning>,
603) -> bool {
604    if !route.matches_path_shape(&candidate.path) {
605        return false;
606    }
607
608    match versioning {
609        Some(versioning) => route
610            .versioning()
611            .matches(candidate.version.as_deref(), versioning.default_version()),
612        None => true,
613    }
614}
615
616fn route_matches_path_version_and_host(
617    route: &RouteDefinition,
618    candidate: &ApiVersionCandidate,
619    versioning: Option<&ApiVersioning>,
620    host: Option<&str>,
621) -> bool {
622    route_matches_path_and_version(route, candidate, versioning) && route.matches_host(host)
623}
624
625fn request_specificity(route: &RouteDefinition) -> Vec<u8> {
626    let mut specificity = route.path_specificity();
627    specificity.push(2);
628    specificity.extend(route.host_specificity());
629    specificity
630}