use axum::Router;
use axum::extract::DefaultBodyLimit;
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
use std::sync::Arc;
use crate::fallback::{fallback_404, fallback_405};
use crate::state::{AroState, AroStateBuilder};
type StartupHook = Box<
dyn FnOnce(
Arc<AroState>,
) -> Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error>>>>>
+ Send,
>;
struct UserLayer {
apply: Box<dyn Fn(Router) -> Router + Send + Sync>,
}
pub struct App {
router: Router,
stateful_router: Option<Router<AroState>>,
state: Option<AroState>,
builder: AroStateBuilder,
default_middleware: bool,
body_limit: Option<usize>,
user_layers: Vec<UserLayer>,
startup_hooks: Vec<StartupHook>,
}
impl App {
pub fn new() -> Self {
Self {
router: Router::new(),
stateful_router: None,
state: None,
builder: AroState::builder(),
default_middleware: true,
body_limit: None,
user_layers: Vec::new(),
startup_hooks: Vec::new(),
}
}
pub fn routes(mut self, router: Router) -> Self {
self.router = self.router.merge(router);
self
}
pub fn state(mut self, state: AroState) -> Self {
self.state = Some(state);
self
}
pub fn register<T: ?Sized + 'static>(mut self, service: Arc<T>) -> Self
where
Arc<T>: Send + Sync + Clone + 'static,
{
self.builder = self.builder.register::<T>(service);
self
}
pub fn routes_with_state(mut self, router: Router<AroState>) -> Self {
self.stateful_router = Some(match self.stateful_router {
Some(existing) => existing.merge(router),
None => router,
});
self
}
pub fn layer<L>(mut self, layer: L) -> Self
where
L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
L::Service:
tower::Service<axum::http::Request<axum::body::Body>> + Clone + Send + Sync + 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Response:
axum::response::IntoResponse + 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Error:
Into<std::convert::Infallible> + 'static,
<L::Service as tower::Service<axum::http::Request<axum::body::Body>>>::Future:
Send + 'static,
{
self.user_layers.push(UserLayer {
apply: Box::new(move |router| router.layer(layer.clone())),
});
self
}
pub fn body_limit(mut self, max_bytes: usize) -> Self {
self.body_limit = Some(max_bytes);
self
}
pub fn config<T: serde::de::DeserializeOwned + Send + Sync + 'static>(
self,
) -> Result<Self, crate::config::ConfigError> {
let cfg: T = crate::config::load_config()?;
Ok(self.register::<T>(Arc::new(cfg)))
}
pub fn on_startup<F>(mut self, hook: F) -> Self
where
F: FnOnce(
Arc<AroState>,
)
-> Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error>>>>>
+ Send
+ 'static,
{
self.startup_hooks.push(Box::new(hook));
self
}
pub fn timeout(self, duration: std::time::Duration) -> Self {
self.layer(crate::middleware::timeout(duration))
}
pub fn without_defaults(mut self) -> Self {
self.default_middleware = false;
self
}
pub fn build(self) -> Router {
let (router, _state, _hooks) = self.into_parts();
router
}
pub async fn build_with_hooks(self) -> Result<Router, Box<dyn std::error::Error>> {
let (router, state, hooks) = self.into_parts();
let shared_state = Arc::new(state);
for hook in hooks {
hook(Arc::clone(&shared_state)).await?;
}
Ok(router)
}
pub async fn serve(self, addr: SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
let app = self.build_with_hooks().await?;
let listener = TcpListener::bind(addr).await?;
tracing::info!("listening on {}", addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
Ok(())
}
fn into_parts(self) -> (Router, AroState, Vec<StartupHook>) {
let mut router = self.router;
let state = match self.state {
Some(external) => external.merge(self.builder.build()),
None => self.builder.build(),
};
if let Some(stateful) = self.stateful_router {
router = router.merge(stateful.with_state(state.clone()));
}
router = router
.fallback(fallback_404)
.method_not_allowed_fallback(fallback_405);
if let Some(max_bytes) = self.body_limit {
router = router.layer(DefaultBodyLimit::max(max_bytes));
}
if self.default_middleware {
router = router.layer(TraceLayer::new_for_http());
}
for user_layer in self.user_layers {
router = (user_layer.apply)(router);
}
(router, state, self.startup_hooks)
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
#[expect(
clippy::expect_used,
reason = "signal handler installation failure is unrecoverable"
)]
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
tracing::info!("shutdown signal received, starting graceful shutdown");
}