1use crate::traits::NovaPlugin;
2use axum::Json;
3use axum::routing::MethodRouter;
4use axum::routing::get;
5use axum::{Router, serve};
6use serde_json::json;
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use tokio::net::TcpListener;
10use tracing::info;
11
12async fn framework_health() -> Json<serde_json::Value> {
13 Json(json!({"status": "healthy", "service": "nova"}))
14}
15
16async fn shutdown_signal() {
17 let ctrl_c = async {
18 tokio::signal::ctrl_c()
19 .await
20 .expect("failed to install Ctrl+C signal handler");
21 };
22
23 #[cfg(unix)]
24 let terminate = async {
25 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
26 .expect("failed to install SIGTERM signal handler")
27 .recv()
28 .await;
29 };
30
31 #[cfg(not(unix))]
32 let terminate = std::future::pending::<()>();
33
34 tokio::select! {
35 _ = ctrl_c => {},
36 _ = terminate => {},
37 }
38}
39
40pub struct NovaApp<S = ()>
46where
47 S: Clone + Send + Sync + 'static,
48{
49 name: &'static str,
50 port: u16,
51 router: Router<()>,
52 address: SocketAddr,
53 state: S,
54 plugins: Vec<Box<dyn NovaPlugin>>,
55}
56
57pub struct NovaRoute {
63 pub path: &'static str,
64 pub method: &'static str,
65 pub handler: fn() -> MethodRouter<()>,
66}
67
68inventory::collect!(NovaRoute);
69
70type RouteRegistry = HashMap<(&'static str, &'static str), fn() -> MethodRouter<()>>;
71
72impl<S> NovaApp<S>
73where
74 S: Clone + Send + Sync + 'static,
75{
76 pub fn new(name: &'static str, port: u16, state: S) -> Self {
77 let router = Router::<()>::new().route("/health", get(framework_health));
78
79 Self {
80 name,
81 port,
82 router,
83 address: format!("0.0.0.0:{port}").parse().expect("Invalid address"),
84 state,
85 plugins: Vec::new(),
86 }
87 }
88
89 pub fn add_plugin<P: NovaPlugin + 'static>(mut self, plugin: P) -> Self {
90 self.plugins.push(Box::new(plugin));
91 self
92 }
93
94 async fn build_router(&self) -> Router<()> {
95 for plugin in &self.plugins {
97 info!("🔌 Loading plugin: {}", plugin.name());
98 plugin.on_init().await;
99 }
100
101 let mut base: Router<()> = self.router.clone();
103
104 let mut route_map: RouteRegistry = HashMap::new();
106 for route in inventory::iter::<NovaRoute> {
107 if route.path == "/health" {
108 tracing::warn!(
109 "Route {} {} conflicts with built-in health check and will be overridden",
110 route.method,
111 route.path
112 );
113 continue;
114 }
115 let key = (route.method, route.path);
116 if route_map.insert(key, route.handler).is_some() {
117 tracing::warn!(
118 "Duplicate route detected, overriding: {} {}",
119 route.method,
120 route.path
121 );
122 }
123 }
124
125 for ((method, path), handler) in route_map.into_iter() {
127 info!("📡 Registering {} route: {}", method, path);
128 let method_router: MethodRouter<()> = (handler)();
129 base = base.route(path, method_router);
130 }
131
132 base = base.layer(axum::Extension(self.state.clone()));
135
136 for plugin in &self.plugins {
138 info!("🔌 Injecting state for: {}", plugin.name());
139 base = plugin.extend_router(base);
140 }
141
142 base
143 }
144
145 async fn shutdown(&self) {
147 info!("🛑 {} shutting down", self.name);
148 for plugin in self.plugins.iter().rev() {
149 info!("🔌 Stopping plugin: {}", plugin.name());
150 plugin.on_shutdown().await;
151 }
152 }
153
154 pub async fn run(self) {
156 let final_router: Router<()> = self.build_router().await;
157
158 info!("🚀 {} starting on port {}", self.name, self.port);
159 let listener = TcpListener::bind(&self.address)
160 .await
161 .expect("Failed to bind server socket");
162
163 serve(listener, final_router)
164 .with_graceful_shutdown(shutdown_signal())
165 .await
166 .expect("Server failed to start");
167
168 self.shutdown().await;
169 }
170
171 #[cfg(feature = "tls")]
173 pub async fn run_tls(self, cert_pem: &[u8], key_pem: &[u8]) {
174 use axum_server::Handle;
175 use axum_server::tls_rustls::RustlsConfig;
176
177 let final_router: Router<()> = self.build_router().await;
178 let handle = Handle::new();
179 let shutdown_handle = handle.clone();
180
181 let config = RustlsConfig::from_pem(cert_pem.to_vec(), key_pem.to_vec())
182 .await
183 .expect("invalid TLS certificate or key");
184
185 info!("🔒 {} starting on port {} with TLS", self.name, self.port);
186
187 tokio::spawn(async move {
188 shutdown_signal().await;
189 shutdown_handle.shutdown();
190 });
191
192 axum_server::bind_rustls(self.address, config)
193 .handle(handle.clone())
194 .serve(final_router.into_make_service())
195 .await
196 .expect("Server failed to start");
197
198 self.shutdown().await;
199 }
200
201 #[cfg(not(feature = "tls"))]
203 pub async fn run_tls(self, _cert_pem: &[u8], _key_pem: &[u8]) {
204 panic!("TLS support requires enabling the `tls` feature");
205 }
206}