use crate::application::*;
use crate::controller::{RouteRegistrar, WebControllerRegistrar};
use crate::response::JsonResponse;
use axum::{Router, extract::Request, middleware::Next};
use std::any::TypeId;
use std::collections::HashMap;
use sword_core::*;
use sword_layers::prelude::*;
pub(crate) struct WebApplicationRouter {
pub state: State,
pub config: Config,
pub layer_stack: LayerStack<State>,
pub controller_registry: ControllerRegistry,
pub web_config: WebApplicationConfig,
}
impl WebApplicationRouter {
pub fn build(&mut self) -> Router<State> {
let extensions = inventory::iter::<WebExtensionRegistrar>()
.map(|entry| entry.extension)
.collect::<Vec<&'static dyn WebExtension>>();
let extension_ctx = WebExtensionContext {
state: self.state.clone(),
config: self.config.clone(),
controller_map: self.controller_registry.snapshot(),
};
for extension in &extensions {
extension.init_state(&extension_ctx);
}
let mut api_router = Router::new();
api_router = self.apply_web_controllers(api_router);
api_router = self.apply_web_layers(api_router);
api_router = api_router.route(
"/health",
axum::routing::get(|| async { JsonResponse::Ok().message("healthy") }),
);
#[cfg(feature = "swagger-ui")]
let (mut api_router, openapi_urls) = self.apply_openapi_specs(api_router);
for registrar in inventory::iter::<sword_layers::SwordServiceRegistrar>() {
tracing::info!(target: "sword.layers", name = registrar.name, "Registering service");
(registrar.display)(&self.config);
(registrar.register)(&self.config)(&mut api_router as &mut dyn std::any::Any);
}
let mut router = if let Some(prefix) = &self.web_config.router_prefix {
Router::new().nest(prefix, api_router)
} else {
api_router
};
#[cfg(feature = "swagger-ui")]
{
router = self.apply_openapi_swagger_ui(router, &openapi_urls);
}
for extension in extensions {
router = extension.extend_router(&extension_ctx, router);
}
router = router.layer(CookieManagerLayer::new());
router = self.layer_stack.apply(router);
router = router.layer(NotFoundLayer);
router
}
fn apply_web_controllers(&mut self, mut router: Router<State>) -> Router<State> {
let controller_registrars = inventory::iter::<WebControllerRegistrar>()
.map(|reg| (reg.controller_id, reg))
.collect::<HashMap<TypeId, &WebControllerRegistrar>>();
let mut routes_by_controller: HashMap<TypeId, Vec<&RouteRegistrar>> = HashMap::new();
for route in inventory::iter::<RouteRegistrar>() {
routes_by_controller
.entry(route.controller_id)
.or_default()
.push(route);
}
for controller_id in self.controller_registry.get_by_kind(Controller::Web) {
let controller_registrar = controller_registrars
.get(&controller_id)
.copied()
.unwrap_or_else(|| {
sword_error! {
title: "Controller metadata not found",
reason: "No WebControllerRegistrar entry was found for controller",
context: {
"controller_id" => format!("{controller_id:?}"),
"source" => "WebRouter::apply_http_controllers",
},
hints: ["This usually indicates a controller macro expansion issue"],
}
});
(controller_registrar.build)(&self.state);
let controller_routes = routes_by_controller
.get(&controller_id)
.cloned()
.unwrap_or_default();
if controller_routes.is_empty() {
sword_error! {
title: "Controller has no registered routes",
reason: "No RouteRegistrar entries were found for controller",
context: {
"controller_id" => format!("{controller_id:?}"),
"source" => "WebRouter::apply_http_controllers",
},
hints: ["This usually indicates a controller macro expansion issue"],
}
}
let mut controller_router = Router::new();
for route in controller_routes {
let route_handler = (route.handler)(self.state.clone());
controller_router = controller_router.route(route.path, route_handler);
}
match controller_registrar.controller_path {
"/" => {
router = router.merge(controller_router);
}
_ => {
router = router.nest(controller_registrar.controller_path, controller_router);
}
}
}
router
}
fn apply_web_layers(&self, mut router: Router<State>) -> Router<State> {
let body_limit_config = self.web_config.body_limit.clone();
router = router.layer(BodyLimitLayer::new(&body_limit_config));
if self.web_config.request_timeout.enabled {
router = router.layer(TimeoutLayer::from(self.web_config.request_timeout.clone()));
router = router.layer(RequestTimeoutResponseLayer::new());
}
router = router.layer(axum::middleware::from_fn(
move |mut req: Request, next: Next| async move {
req.extensions_mut()
.insert(BodyLimitValue(body_limit_config.max_size.parsed));
next.run(req).await
},
));
router = router.layer(RequestIdLayer::new());
router
}
#[cfg(feature = "swagger-ui")]
fn apply_openapi_specs(&self, mut router: Router<State>) -> (Router<State>, Vec<String>) {
let Some(openapi) = &self.web_config.openapi else {
return (router, vec![]);
};
let mut urls: Vec<String> = Vec::new();
let prefix = self.web_config.router_prefix.as_deref().unwrap_or("");
for spec_path in &openapi.spec_file_paths {
let file_extension = spec_path
.rsplit('.')
.next()
.unwrap_or_default()
.to_lowercase();
let spec_file_content_type = match file_extension.as_str() {
"yaml" | "yml" => "application/x-yaml",
"json" => "application/json",
ext => {
sword_error! {
title: "Unsupported OpenAPI spec file type",
reason: "The OpenAPI spec file has an unsupported extension",
context: {
"spec_path" => spec_path.clone(),
"file_extension" => ext.to_string(),
"source" => "WebRouter::apply_openapi",
},
hints: ["Supported extensions are .yaml, .yml, and .json"],
fatal: false,
}
continue;
}
};
let file_content = match std::fs::read_to_string(spec_path) {
Ok(c) => c,
Err(err) => {
sword_error! {
title: "Failed to read OpenAPI spec file",
reason: "An error occurred while reading the OpenAPI spec file",
context: {
"spec_path" => spec_path.clone(),
"error" => err.to_string(),
"source" => "WebRouter::apply_openapi",
},
hints: ["Ensure the file exists and is readable"],
fatal: false,
}
continue;
}
};
let filename = spec_path.rsplit('/').next().unwrap_or("openapi.yaml");
let route_path = format!("/openapi/{filename}");
router = router.route(
&route_path,
axum::routing::get(move || async move {
(
[("Content-Type", spec_file_content_type)],
file_content.clone(),
)
}),
);
urls.push(format!("{prefix}{route_path}"));
}
(router, urls)
}
#[cfg(feature = "swagger-ui")]
fn apply_openapi_swagger_ui(&self, router: Router<State>, urls: &[String]) -> Router<State> {
use utoipa_swagger_ui::{Config, SwaggerUi};
if urls.is_empty() {
return router;
}
let prefix = self.web_config.router_prefix.as_deref().unwrap_or("");
let docs_path = format!("{prefix}/docs");
router.merge(SwaggerUi::new(docs_path).config(Config::new(urls.to_vec())))
}
}