use super::builder::BootApplicationBuilder;
use crate::{
BootError, BootRequest, BootResponse, HttpAdapter, HttpMethod, Module, ModuleRef, Result,
RouteDefinition,
};
use std::collections::BTreeMap;
use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
pub struct RouteMatch<'a> {
route: &'a RouteDefinition,
params: BTreeMap<String, String>,
}
impl<'a> RouteMatch<'a> {
pub fn route(&self) -> &'a RouteDefinition {
self.route
}
pub fn params(&self) -> &BTreeMap<String, String> {
&self.params
}
pub fn param(&self, name: &str) -> Option<&str> {
self.params.get(name).map(String::as_str)
}
pub fn into_params(self) -> BTreeMap<String, String> {
self.params
}
}
impl fmt::Debug for RouteMatch<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RouteMatch")
.field("method", &self.route.method())
.field("path", &self.route.path())
.field("params", &self.params)
.finish()
}
}
#[derive(Clone)]
pub struct BootApplication {
pub(crate) routes: Vec<RouteDefinition>,
pub(crate) modules: Vec<String>,
pub(crate) module_ref: ModuleRef,
pub(crate) module_instances: Vec<Arc<dyn Module>>,
}
impl BootApplication {
pub fn builder() -> BootApplicationBuilder {
BootApplicationBuilder::new()
}
pub fn module_names(&self) -> &[String] {
&self.modules
}
pub fn routes(&self) -> &[RouteDefinition] {
&self.routes
}
pub fn route_for(&self, method: HttpMethod, path: &str) -> Option<&RouteDefinition> {
let best_specificity = best_path_specificity(&self.routes, path)?;
self.routes.iter().find(|route| {
route.matches_path_shape(path)
&& route.path_specificity().as_slice() == best_specificity.as_slice()
&& route.method() == method
})
}
pub fn route_match(&self, method: HttpMethod, path: &str) -> Result<Option<RouteMatch<'_>>> {
let Some(route) = self.route_for(method, path) else {
return Ok(None);
};
let Some(params) = route.path_params(path)? else {
return Ok(None);
};
Ok(Some(RouteMatch { route, params }))
}
pub fn allowed_methods(&self, path: &str) -> Vec<HttpMethod> {
let Some(best_specificity) = best_path_specificity(&self.routes, path) else {
return Vec::new();
};
let mut methods = Vec::new();
for route in matching_routes_with_specificity(&self.routes, path, &best_specificity) {
if !methods.contains(&route.method()) {
methods.push(route.method());
}
}
methods
}
pub fn allowed_methods_header(&self, path: &str) -> Option<String> {
let methods = self.allowed_methods(path);
if methods.is_empty() {
return None;
}
Some(
methods
.into_iter()
.map(|method| method.as_str())
.collect::<Vec<_>>()
.join(","),
)
}
pub fn module_ref(&self) -> &ModuleRef {
&self.module_ref
}
pub fn get<T>(&self) -> Result<Arc<T>>
where
T: Send + Sync + 'static,
{
self.module_ref.get::<T>()
}
pub fn get_named<T>(&self, token: &str) -> Result<Arc<T>>
where
T: Send + Sync + 'static,
{
self.module_ref.get_named::<T>(token)
}
pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>>
where
T: Send + Sync + 'static,
{
self.module_ref.get_optional::<T>()
}
pub fn get_optional_named<T>(&self, token: &str) -> Result<Option<Arc<T>>>
where
T: Send + Sync + 'static,
{
self.module_ref.get_optional_named::<T>(token)
}
pub async fn call(&self, request: BootRequest) -> Result<BootResponse> {
let path = request.path.clone();
let Some(best_specificity) = best_path_specificity(&self.routes, &path) else {
return Err(BootError::NotFound(format!(
"{} {}",
request.method.as_str(),
request.path
)));
};
if let Some(route) =
matching_route_with_specificity(&self.routes, &path, &best_specificity, |route| {
route.method() == request.method
})
{
return route.call(request).await;
}
if let Some(route) =
matching_route_with_specificity(&self.routes, &path, &best_specificity, |_| true)
{
return route.call(request).await;
}
Err(BootError::NotFound(format!(
"{} {}",
request.method.as_str(),
request.path
)))
}
pub async fn handle(&self, request: BootRequest) -> BootResponse {
match self.call(request).await {
Ok(response) => response,
Err(error) => BootResponse::from_error(&error),
}
}
pub async fn bootstrap(&self) -> Result<()> {
for module in &self.module_instances {
module
.on_application_bootstrap(self.module_ref.clone())
.await?;
}
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
for module in self.module_instances.iter().rev() {
module
.on_application_shutdown(self.module_ref.clone())
.await?;
}
Ok(())
}
pub fn into_adapter<A>(self, adapter: &A) -> Result<A::Output>
where
A: HttpAdapter,
{
adapter.build(self)
}
pub async fn serve_with<A>(self, adapter: &A, addr: SocketAddr) -> Result<()>
where
A: HttpAdapter,
{
let app_for_shutdown = self.clone();
if let Err(error) = self.bootstrap().await {
let _ = app_for_shutdown.shutdown().await;
return Err(error);
}
let serve_result = adapter.serve(self, addr).await;
let shutdown_result = app_for_shutdown.shutdown().await;
match (serve_result, shutdown_result) {
(Err(error), _) => Err(error),
(Ok(()), Err(error)) => Err(error),
(Ok(()), Ok(())) => Ok(()),
}
}
}
fn best_path_specificity(routes: &[RouteDefinition], path: &str) -> Option<Vec<u8>> {
let mut best_specificity = None;
for route in routes {
if route.matches_path_shape(path) {
let specificity = route.path_specificity();
if best_specificity
.as_ref()
.map(|best| specificity > *best)
.unwrap_or(true)
{
best_specificity = Some(specificity);
}
}
}
best_specificity
}
fn matching_routes_with_specificity<'a>(
routes: &'a [RouteDefinition],
path: &'a str,
specificity: &'a [u8],
) -> impl Iterator<Item = &'a RouteDefinition> + 'a {
routes.iter().filter(move |route| {
route.matches_path_shape(path) && route.path_specificity().as_slice() == specificity
})
}
fn matching_route_with_specificity<'a, P>(
routes: &'a [RouteDefinition],
path: &'a str,
specificity: &'a [u8],
mut predicate: P,
) -> Option<&'a RouteDefinition>
where
P: FnMut(&RouteDefinition) -> bool,
{
matching_routes_with_specificity(routes, path, specificity).find(|route| predicate(route))
}