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