Skip to main content

a3s_boot/app/
application.rs

1use super::builder::BootApplicationBuilder;
2use crate::{
3    BootError, BootRequest, BootResponse, HttpAdapter, HttpMethod, Module, ModuleRef, Result,
4    RouteDefinition,
5};
6use std::collections::BTreeMap;
7use std::fmt;
8use std::net::SocketAddr;
9use std::sync::Arc;
10
11/// Resolved route and decoded path parameters for a method/path lookup.
12pub struct RouteMatch<'a> {
13    route: &'a RouteDefinition,
14    params: BTreeMap<String, String>,
15}
16
17impl<'a> RouteMatch<'a> {
18    pub fn route(&self) -> &'a RouteDefinition {
19        self.route
20    }
21
22    pub fn params(&self) -> &BTreeMap<String, String> {
23        &self.params
24    }
25
26    pub fn param(&self, name: &str) -> Option<&str> {
27        self.params.get(name).map(String::as_str)
28    }
29
30    pub fn into_params(self) -> BTreeMap<String, String> {
31        self.params
32    }
33}
34
35impl fmt::Debug for RouteMatch<'_> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_struct("RouteMatch")
38            .field("method", &self.route.method())
39            .field("path", &self.route.path())
40            .field("params", &self.params)
41            .finish()
42    }
43}
44
45/// Built application with a resolved module graph and framework-neutral routes.
46#[derive(Clone)]
47pub struct BootApplication {
48    pub(crate) routes: Vec<RouteDefinition>,
49    pub(crate) modules: Vec<String>,
50    pub(crate) module_ref: ModuleRef,
51    pub(crate) module_instances: Vec<Arc<dyn Module>>,
52}
53
54impl BootApplication {
55    /// Create an application builder.
56    pub fn builder() -> BootApplicationBuilder {
57        BootApplicationBuilder::new()
58    }
59
60    /// Names of modules included in the application, in registration order.
61    pub fn module_names(&self) -> &[String] {
62        &self.modules
63    }
64
65    /// Routes exposed by the application.
66    pub fn routes(&self) -> &[RouteDefinition] {
67        &self.routes
68    }
69
70    /// Route registered for a method and the most specific route shape matching a path.
71    pub fn route_for(&self, method: HttpMethod, path: &str) -> Option<&RouteDefinition> {
72        let best_specificity = best_path_specificity(&self.routes, path)?;
73        self.routes.iter().find(|route| {
74            route.matches_path_shape(path)
75                && route.path_specificity().as_slice() == best_specificity.as_slice()
76                && route.method() == method
77        })
78    }
79
80    /// Route and decoded path parameters for a method and path, when one is registered.
81    pub fn route_match(&self, method: HttpMethod, path: &str) -> Result<Option<RouteMatch<'_>>> {
82        let Some(route) = self.route_for(method, path) else {
83            return Ok(None);
84        };
85        let Some(params) = route.path_params(path)? else {
86            return Ok(None);
87        };
88        Ok(Some(RouteMatch { route, params }))
89    }
90
91    /// HTTP methods registered for the most specific route shape matching a path.
92    pub fn allowed_methods(&self, path: &str) -> Vec<HttpMethod> {
93        let Some(best_specificity) = best_path_specificity(&self.routes, path) else {
94            return Vec::new();
95        };
96
97        let mut methods = Vec::new();
98        for route in matching_routes_with_specificity(&self.routes, path, &best_specificity) {
99            if !methods.contains(&route.method()) {
100                methods.push(route.method());
101            }
102        }
103        methods
104    }
105
106    /// Comma-separated HTTP methods for an Allow header on the most specific matching path.
107    pub fn allowed_methods_header(&self, path: &str) -> Option<String> {
108        let methods = self.allowed_methods(path);
109        if methods.is_empty() {
110            return None;
111        }
112
113        Some(
114            methods
115                .into_iter()
116                .map(|method| method.as_str())
117                .collect::<Vec<_>>()
118                .join(","),
119        )
120    }
121
122    /// Provider container available to hosts and controllers.
123    pub fn module_ref(&self) -> &ModuleRef {
124        &self.module_ref
125    }
126
127    /// Resolve a typed provider from the application container.
128    pub fn get<T>(&self) -> Result<Arc<T>>
129    where
130        T: Send + Sync + 'static,
131    {
132        self.module_ref.get::<T>()
133    }
134
135    /// Resolve a named provider from the application container.
136    pub fn get_named<T>(&self, token: &str) -> Result<Arc<T>>
137    where
138        T: Send + Sync + 'static,
139    {
140        self.module_ref.get_named::<T>(token)
141    }
142
143    /// Resolve a typed provider when it is present.
144    pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>>
145    where
146        T: Send + Sync + 'static,
147    {
148        self.module_ref.get_optional::<T>()
149    }
150
151    /// Resolve a named provider when it is present.
152    pub fn get_optional_named<T>(&self, token: &str) -> Result<Option<Arc<T>>>
153    where
154        T: Send + Sync + 'static,
155    {
156        self.module_ref.get_optional_named::<T>(token)
157    }
158
159    /// Dispatch a framework-neutral request through the resolved route table.
160    pub async fn call(&self, request: BootRequest) -> Result<BootResponse> {
161        let path = request.path.clone();
162        let Some(best_specificity) = best_path_specificity(&self.routes, &path) else {
163            return Err(BootError::NotFound(format!(
164                "{} {}",
165                request.method.as_str(),
166                request.path
167            )));
168        };
169
170        if let Some(route) =
171            matching_route_with_specificity(&self.routes, &path, &best_specificity, |route| {
172                route.method() == request.method
173            })
174        {
175            return route.call(request).await;
176        }
177
178        if let Some(route) =
179            matching_route_with_specificity(&self.routes, &path, &best_specificity, |_| true)
180        {
181            return route.call(request).await;
182        }
183
184        Err(BootError::NotFound(format!(
185            "{} {}",
186            request.method.as_str(),
187            request.path
188        )))
189    }
190
191    /// Dispatch a request and convert unhandled errors into Boot HTTP responses.
192    pub async fn handle(&self, request: BootRequest) -> BootResponse {
193        match self.call(request).await {
194            Ok(response) => response,
195            Err(error) => BootResponse::from_error(&error),
196        }
197    }
198
199    /// Run async startup hooks before serving, when the host needs them.
200    pub async fn bootstrap(&self) -> Result<()> {
201        for module in &self.module_instances {
202            module
203                .on_application_bootstrap(self.module_ref.clone())
204                .await?;
205        }
206        Ok(())
207    }
208
209    /// Run async shutdown hooks in reverse registration order.
210    pub async fn shutdown(&self) -> Result<()> {
211        for module in self.module_instances.iter().rev() {
212            module
213                .on_application_shutdown(self.module_ref.clone())
214                .await?;
215        }
216        Ok(())
217    }
218
219    /// Build this application through a concrete HTTP adapter.
220    pub fn into_adapter<A>(self, adapter: &A) -> Result<A::Output>
221    where
222        A: HttpAdapter,
223    {
224        adapter.build(self)
225    }
226
227    /// Serve this application through a concrete HTTP adapter.
228    pub async fn serve_with<A>(self, adapter: &A, addr: SocketAddr) -> Result<()>
229    where
230        A: HttpAdapter,
231    {
232        let app_for_shutdown = self.clone();
233        if let Err(error) = self.bootstrap().await {
234            let _ = app_for_shutdown.shutdown().await;
235            return Err(error);
236        }
237        let serve_result = adapter.serve(self, addr).await;
238        let shutdown_result = app_for_shutdown.shutdown().await;
239
240        match (serve_result, shutdown_result) {
241            (Err(error), _) => Err(error),
242            (Ok(()), Err(error)) => Err(error),
243            (Ok(()), Ok(())) => Ok(()),
244        }
245    }
246}
247
248fn best_path_specificity(routes: &[RouteDefinition], path: &str) -> Option<Vec<u8>> {
249    let mut best_specificity = None;
250
251    for route in routes {
252        if route.matches_path_shape(path) {
253            let specificity = route.path_specificity();
254            if best_specificity
255                .as_ref()
256                .map(|best| specificity > *best)
257                .unwrap_or(true)
258            {
259                best_specificity = Some(specificity);
260            }
261        }
262    }
263
264    best_specificity
265}
266
267fn matching_routes_with_specificity<'a>(
268    routes: &'a [RouteDefinition],
269    path: &'a str,
270    specificity: &'a [u8],
271) -> impl Iterator<Item = &'a RouteDefinition> + 'a {
272    routes.iter().filter(move |route| {
273        route.matches_path_shape(path) && route.path_specificity().as_slice() == specificity
274    })
275}
276
277fn matching_route_with_specificity<'a, P>(
278    routes: &'a [RouteDefinition],
279    path: &'a str,
280    specificity: &'a [u8],
281    mut predicate: P,
282) -> Option<&'a RouteDefinition>
283where
284    P: FnMut(&RouteDefinition) -> bool,
285{
286    matching_routes_with_specificity(routes, path, specificity).find(|route| predicate(route))
287}