Skip to main content

a3s_boot/app/
application.rs

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