use crate::traits::NovaPlugin;
use axum::Json;
use axum::routing::MethodRouter;
use axum::routing::get;
use axum::{Router, serve};
use serde_json::json;
use std::collections::HashMap;
use std::net::SocketAddr;
use tokio::net::TcpListener;
async fn framework_health() -> Json<serde_json::Value> {
Json(json!({"status": "healthy", "service": "nova"}))
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C signal handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
pub struct NovaApp<S = ()>
where
S: Clone + Send + Sync + 'static,
{
name: &'static str,
port: u16,
router: Router<()>,
address: SocketAddr,
state: S,
plugins: Vec<Box<dyn NovaPlugin>>,
}
pub struct NovaRoute {
pub path: &'static str,
pub method: &'static str,
pub handler: fn() -> MethodRouter<()>,
}
inventory::collect!(NovaRoute);
type RouteRegistry = HashMap<(&'static str, &'static str), fn() -> MethodRouter<()>>;
impl<S> NovaApp<S>
where
S: Clone + Send + Sync + 'static,
{
pub fn new(name: &'static str, port: u16, state: S) -> Self {
let router = Router::<()>::new().route("/health", get(framework_health));
Self {
name,
port,
router,
address: format!("0.0.0.0:{port}").parse().expect("Invalid address"),
state,
plugins: Vec::new(),
}
}
pub fn add_plugin<P: NovaPlugin + 'static>(mut self, plugin: P) -> Self {
self.plugins.push(Box::new(plugin));
self
}
async fn build_router(&self) -> Router<()> {
for plugin in &self.plugins {
println!("🔌 Loading plugin: {}", plugin.name());
plugin.on_init().await;
}
let mut base: Router<()> = self.router.clone();
let mut route_map: RouteRegistry = HashMap::new();
for route in inventory::iter::<NovaRoute> {
if route.path == "/health" {
tracing::warn!(
"Route {} {} conflicts with built-in health check and will be overridden",
route.method,
route.path
);
continue;
}
let key = (route.method, route.path);
if route_map.insert(key, route.handler).is_some() {
tracing::warn!(
"Duplicate route detected, overriding: {} {}",
route.method,
route.path
);
}
}
for ((method, path), handler) in route_map.into_iter() {
println!("📡 Registering {} route: {}", method, path);
let method_router: MethodRouter<()> = (handler)();
base = base.route(path, method_router);
}
base = base.layer(axum::Extension(self.state.clone()));
for plugin in &self.plugins {
println!("🔌 Injecting state for: {}", plugin.name());
base = plugin.extend_router(base);
}
base
}
async fn shutdown(&self) {
println!("🛑 {} shutting down", self.name);
for plugin in self.plugins.iter().rev() {
println!("🔌 Stopping plugin: {}", plugin.name());
plugin.on_shutdown().await;
}
}
pub async fn run(self) {
let final_router: Router<()> = self.build_router().await;
println!("🚀 {} starting on port {}", self.name, self.port);
let listener = TcpListener::bind(&self.address)
.await
.expect("Failed to bind server socket");
serve(listener, final_router)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("Server failed to start");
self.shutdown().await;
}
#[cfg(feature = "tls")]
pub async fn run_tls(self, cert_pem: &[u8], key_pem: &[u8]) {
use axum_server::Handle;
use axum_server::tls_rustls::RustlsConfig;
let final_router: Router<()> = self.build_router().await;
let handle = Handle::new();
let shutdown_handle = handle.clone();
let config = RustlsConfig::from_pem(cert_pem.to_vec(), key_pem.to_vec())
.await
.expect("invalid TLS certificate or key");
println!("🔒 {} starting on port {} with TLS", self.name, self.port);
tokio::spawn(async move {
shutdown_signal().await;
shutdown_handle.shutdown();
});
axum_server::bind_rustls(self.address, config)
.handle(handle.clone())
.serve(final_router.into_make_service())
.await
.expect("Server failed to start");
self.shutdown().await;
}
#[cfg(not(feature = "tls"))]
pub async fn run_tls(self, _cert_pem: &[u8], _key_pem: &[u8]) {
panic!("TLS support requires enabling the `tls` feature");
}
}