elif_http/server.rs
1//! # Elif HTTP Server
2//!
3//! A NestJS-like HTTP server that provides a clean, intuitive API while using Axum under the hood.
4//! Users interact only with framework types - Axum is completely abstracted away.
5
6use crate::{
7 HttpConfig, HttpError, HttpResult,
8 ElifRouter,
9 MiddlewarePipeline, Middleware,
10};
11use elif_core::Container;
12use std::net::SocketAddr;
13use std::sync::Arc;
14use tokio::signal;
15use tracing::{info, warn};
16
17/// The main HTTP server - NestJS-like experience
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use elif_http::{Server, HttpConfig};
23/// use elif_core::{Container, container::test_implementations::*};
24/// use std::sync::Arc;
25///
26/// #[tokio::main]
27/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28/// let config = Arc::new(create_test_config());
29/// let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
30///
31/// let container = Container::builder()
32/// .config(config)
33/// .database(database)
34/// .build()?;
35///
36/// let server = Server::new(container, HttpConfig::default())?;
37/// server.listen("0.0.0.0:3000").await?;
38///
39/// Ok(())
40/// }
41/// ```
42pub struct Server {
43 container: Arc<Container>,
44 config: HttpConfig,
45 router: Option<ElifRouter>,
46 middleware: MiddlewarePipeline,
47}
48
49impl Server {
50 /// Create a new server instance
51 pub fn new(container: Container, config: HttpConfig) -> HttpResult<Self> {
52 Ok(Self {
53 container: Arc::new(container),
54 config,
55 router: None,
56 middleware: MiddlewarePipeline::new(),
57 })
58 }
59
60 /// Create a new server with existing Arc<Container>
61 pub fn with_container(container: Arc<Container>, config: HttpConfig) -> HttpResult<Self> {
62 Ok(Self {
63 container,
64 config,
65 router: None,
66 middleware: MiddlewarePipeline::new(),
67 })
68 }
69
70 /// Set custom routes using framework router
71 ///
72 /// # Example
73 ///
74 /// ```rust,no_run
75 /// use elif_http::{Server, ElifRouter, HttpConfig};
76 /// use elif_core::{Container, container::test_implementations::*};
77 /// use std::sync::Arc;
78 ///
79 /// # async fn get_users() -> &'static str { "users" }
80 /// # async fn create_user() -> &'static str { "created" }
81 ///
82 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
83 /// let config = Arc::new(create_test_config());
84 /// let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
85 ///
86 /// let container = Container::builder()
87 /// .config(config)
88 /// .database(database)
89 /// .build()?;
90 ///
91 /// let mut server = Server::new(container, HttpConfig::default())?;
92 ///
93 /// let router = ElifRouter::new()
94 /// .get("/users", get_users)
95 /// .post("/users", create_user);
96 ///
97 /// server.use_router(router);
98 /// # Ok(())
99 /// # }
100 /// ```
101 pub fn use_router(&mut self, router: ElifRouter) -> &mut Self {
102 self.router = Some(router);
103 self
104 }
105
106 /// Add middleware to the server
107 ///
108 /// # Example
109 ///
110 /// ```rust,no_run
111 /// use elif_http::{Server, HttpConfig};
112 /// use elif_core::{Container, container::test_implementations::*};
113 /// use std::sync::Arc;
114 ///
115 /// # struct LoggingMiddleware;
116 /// # impl LoggingMiddleware {
117 /// # fn default() -> Self { LoggingMiddleware }
118 /// # }
119 /// # impl elif_http::Middleware for LoggingMiddleware {}
120 ///
121 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
122 /// let config = Arc::new(create_test_config());
123 /// let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
124 ///
125 /// let container = Container::builder()
126 /// .config(config)
127 /// .database(database)
128 /// .build()?;
129 ///
130 /// let mut server = Server::new(container, HttpConfig::default())?;
131 /// server.use_middleware(LoggingMiddleware::default());
132 /// # Ok(())
133 /// # }
134 /// ```
135 pub fn use_middleware<M>(&mut self, middleware: M) -> &mut Self
136 where
137 M: Middleware + 'static,
138 {
139 self.middleware = std::mem::take(&mut self.middleware).add(middleware);
140 self
141 }
142
143 /// Start the server on the specified address
144 ///
145 /// # Example
146 ///
147 /// ```rust,no_run
148 /// # use elif_http::{Server, HttpConfig};
149 /// # use elif_core::{Container, container::test_implementations::*};
150 /// # use std::sync::Arc;
151 /// #
152 /// # #[tokio::main]
153 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
154 /// # let config = Arc::new(create_test_config());
155 /// # let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
156 /// #
157 /// # let container = Container::builder()
158 /// # .config(config)
159 /// # .database(database)
160 /// # .build()?;
161 /// #
162 /// # let server = Server::new(container, HttpConfig::default())?;
163 /// server.listen("0.0.0.0:3000").await?;
164 /// # Ok(())
165 /// # }
166 /// ```
167 pub async fn listen<A: Into<String>>(self, addr: A) -> HttpResult<()> {
168 let addr_str = addr.into();
169 let socket_addr: SocketAddr = addr_str.parse()
170 .map_err(|e| HttpError::config(format!("Invalid address '{}': {}", addr_str, e)))?;
171
172 self.listen_on(socket_addr).await
173 }
174
175 /// Start the server on the specified SocketAddr
176 pub async fn listen_on(self, addr: SocketAddr) -> HttpResult<()> {
177 info!("🚀 Starting Elif server on {}", addr);
178 info!("📋 Health check endpoint: {}", self.config.health_check_path);
179
180 // Build the internal router
181 let axum_router = self.build_internal_router().await?;
182
183 // Create TCP listener
184 let listener = tokio::net::TcpListener::bind(addr)
185 .await
186 .map_err(|e| HttpError::startup(format!("Failed to bind to {}: {}", addr, e)))?;
187
188 info!("✅ Server listening on {}", addr);
189 info!("🔧 Framework: Elif.rs (Axum under the hood)");
190
191 // Serve with graceful shutdown
192 axum::serve(listener, axum_router.into_make_service_with_connect_info::<SocketAddr>())
193 .with_graceful_shutdown(shutdown_signal())
194 .await
195 .map_err(|e| HttpError::internal(format!("Server error: {}", e)))?;
196
197 info!("🛑 Server shut down gracefully");
198 Ok(())
199 }
200
201 /// Build the internal Axum router (hidden from users)
202 async fn build_internal_router(self) -> HttpResult<axum::Router> {
203 let container = self.container.clone();
204 let config = self.config.clone();
205
206 // Create health check handler with captured context
207 let health_container = container.clone();
208 let health_config = config.clone();
209 let health_handler = move || {
210 let container = health_container.clone();
211 let config = health_config.clone();
212 async move {
213 health_check(container, config).await
214 }
215 };
216
217 // Start with framework router
218 let mut router = if let Some(user_router) = self.router {
219 user_router
220 } else {
221 ElifRouter::new()
222 };
223
224 // Add health check route
225 router = router.get(&config.health_check_path, health_handler);
226
227 // Convert to Axum router
228 Ok(router.into_axum_router())
229 }
230}
231
232/// Default health check handler
233pub async fn health_check(_container: Arc<Container>, _config: HttpConfig) -> axum::response::Json<serde_json::Value> {
234 use serde_json::json;
235
236 let response = json!({
237 "status": "healthy",
238 "framework": "Elif.rs",
239 "version": env!("CARGO_PKG_VERSION"),
240 "timestamp": std::time::SystemTime::now()
241 .duration_since(std::time::UNIX_EPOCH)
242 .unwrap()
243 .as_secs(),
244 "server": {
245 "ready": true,
246 "uptime": "N/A"
247 }
248 });
249
250 axum::response::Json(response)
251}
252
253/// Graceful shutdown signal handler
254async fn shutdown_signal() {
255 let ctrl_c = async {
256 signal::ctrl_c()
257 .await
258 .expect("Failed to install Ctrl+C handler");
259 };
260
261 #[cfg(unix)]
262 let terminate = async {
263 signal::unix::signal(signal::unix::SignalKind::terminate())
264 .expect("Failed to install signal handler")
265 .recv()
266 .await;
267 };
268
269 #[cfg(not(unix))]
270 let terminate = std::future::pending::<()>();
271
272 tokio::select! {
273 _ = ctrl_c => {
274 warn!("📡 Received Ctrl+C, shutting down gracefully...");
275 },
276 _ = terminate => {
277 warn!("📡 Received terminate signal, shutting down gracefully...");
278 },
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use elif_core::{
286 container::test_implementations::*,
287 app_config::AppConfigTrait,
288 };
289
290 fn create_test_container() -> Arc<Container> {
291 let config = Arc::new(create_test_config());
292 let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
293
294 Container::builder()
295 .config(config)
296 .database(database)
297 .build()
298 .unwrap()
299 .into()
300 }
301
302 #[test]
303 fn test_server_creation() {
304 let container = create_test_container();
305 let config = HttpConfig::default();
306
307 let server = Server::with_container(container, config);
308 assert!(server.is_ok());
309 }
310
311 #[test]
312 fn test_server_with_arc_container() {
313 let container = create_test_container();
314 let config = HttpConfig::default();
315
316 let server = Server::with_container(container, config);
317 assert!(server.is_ok());
318 }
319
320 #[tokio::test]
321 async fn test_health_check_handler() {
322 let container = create_test_container();
323 let config = HttpConfig::default();
324
325 let response = health_check(container, config).await;
326 // Test that response is properly formatted JSON
327 assert!(response.0.get("status").is_some());
328 assert_eq!(response.0["status"], "healthy");
329 }
330
331 #[test]
332 fn test_invalid_address() {
333 let container = create_test_container();
334 let config = HttpConfig::default();
335 let server = Server::with_container(container, config).unwrap();
336
337 // This should be tested with an actual tokio runtime in integration tests
338 // For now, we just verify the server can be created
339 }
340}