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