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