Skip to main content

nova_boot/
runtime.rs

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
39/// The main application container.
40///
41/// `NovaApp` holds framework-level configuration, the application state, and
42/// registered plugins. Construct with `NovaApp::new(name, port, state)` and
43/// call `.add_plugin(...)` to register plugins before `run()`.
44pub 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
56/// A route contributed via the `inventory` macro by attribute macros.
57///
58/// Each route is a static descriptor with a path, method and a handler
59/// constructor function used at startup to register the route into the
60/// application router.
61pub 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        // Step 1: Initialize all plugins
95        for plugin in &self.plugins {
96            println!("🔌 Loading plugin: {}", plugin.name());
97            plugin.on_init().await;
98        }
99
100        // Step 2: Build base router as Router<()> with framework routes
101        let mut base: Router<()> = self.router.clone();
102
103        // Step 3: Collect and deduplicate inventory routes
104        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        // Step 4: Register inventory routes
125        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        // Step 5: Inject application state as an Extension layer
132        // Handlers use `Extension<S>` or a wrapper to access state.
133        base = base.layer(axum::Extension(self.state.clone()));
134
135        // Step 6: Let plugins extend the router
136        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    /// Run plugin shutdown hooks in reverse order.
145    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    /// Run the server on plain HTTP.
154    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    /// Run the server with TLS (HTTPS).
171    #[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    /// TLS support is only available when the `tls` feature is enabled.
201    #[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}