elif_http/
simple_stateful_server.rs1use crate::{HttpConfig, HttpError, HttpResult};
7use elif_core::Container;
8use axum::{
9 Router,
10 routing::get,
11 response::Json,
12};
13use serde_json::{json, Value};
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tokio::signal;
17use tracing::{info, warn};
18
19pub struct SimpleStatefulHttpServer {
21 router: Router,
22 addr: SocketAddr,
23}
24
25impl SimpleStatefulHttpServer {
26 pub fn new(container: Arc<Container>, config: HttpConfig) -> HttpResult<Self> {
28 let app_config = container.config();
29 let addr = format!("{}:{}", app_config.server.host, app_config.server.port)
30 .parse::<SocketAddr>()
31 .map_err(|e| HttpError::config(format!("Invalid server address: {}", e)))?;
32
33 let health_container = container.clone();
35 let health_config = config.clone();
36 let health_handler = move || {
37 let container = health_container.clone();
38 let config = health_config.clone();
39 async move {
40 health_check_with_di(container, config).await
41 }
42 };
43
44 let router = Router::new()
46 .route(&config.health_check_path, get(health_handler));
47
48 Ok(Self { router, addr })
49 }
50
51 pub async fn run(self) -> HttpResult<()> {
53 info!("Starting simple stateful HTTP server on {}", self.addr);
54
55 let listener = tokio::net::TcpListener::bind(self.addr)
56 .await
57 .map_err(|e| HttpError::startup(format!("Failed to bind to {}: {}", self.addr, e)))?;
58
59 info!("Simple stateful HTTP server listening on {}", self.addr);
60
61 axum::serve(listener, self.router)
62 .with_graceful_shutdown(shutdown_signal())
63 .await
64 .map_err(|e| HttpError::startup(format!("Server failed: {}", e)))?;
65
66 info!("Simple stateful HTTP server stopped gracefully");
67 Ok(())
68 }
69}
70
71async fn health_check_with_di(container: Arc<Container>, config: HttpConfig) -> Json<Value> {
73 let database = container.database();
75 let db_healthy = database.is_connected();
76
77 let app_config = container.config();
78 let response = json!({
79 "status": if db_healthy { "healthy" } else { "degraded" },
80 "timestamp": chrono::Utc::now().to_rfc3339(),
81 "version": "0.1.0",
82 "environment": format!("{:?}", app_config.environment),
83 "server": "simple-stateful",
84 "services": {
85 "database": if db_healthy { "healthy" } else { "unhealthy" },
86 "container": "healthy"
87 },
88 "config": {
89 "request_timeout": config.request_timeout_secs,
90 "health_check_path": config.health_check_path,
91 "tracing_enabled": config.enable_tracing
92 }
93 });
94
95 if !db_healthy {
96 warn!("Health check degraded: database not connected");
97 }
98
99 Json(response)
100}
101
102async fn shutdown_signal() {
104 let ctrl_c = async {
105 signal::ctrl_c()
106 .await
107 .expect("failed to install Ctrl+C handler");
108 };
109
110 #[cfg(unix)]
111 let terminate = async {
112 signal::unix::signal(signal::unix::SignalKind::terminate())
113 .expect("failed to install signal handler")
114 .recv()
115 .await;
116 };
117
118 #[cfg(not(unix))]
119 let terminate = std::future::pending::<()>();
120
121 tokio::select! {
122 _ = ctrl_c => {
123 info!("Received Ctrl+C, initiating graceful shutdown");
124 },
125 _ = terminate => {
126 info!("Received terminate signal, initiating graceful shutdown");
127 },
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use elif_core::container::test_implementations::*;
135
136 fn create_test_container() -> Arc<Container> {
137 let config = Arc::new(create_test_config());
138 let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
139
140 Container::builder()
141 .config(config)
142 .database(database)
143 .build()
144 .unwrap()
145 .into()
146 }
147
148 #[test]
149 fn test_simple_stateful_server_creation() {
150 let container = create_test_container();
151 let config = HttpConfig::default();
152
153 let server = SimpleStatefulHttpServer::new(container, config);
154 assert!(server.is_ok());
155
156 let server = server.unwrap();
157 assert_eq!(server.addr.port(), 8080);
158 }
159
160 #[tokio::test]
161 async fn test_health_check_with_di() {
162 let container = create_test_container();
163 let config = HttpConfig::default();
164
165 let result = health_check_with_di(container, config).await;
166 let value = result.0;
167
168 assert_eq!(value.get("status").and_then(|v| v.as_str()).unwrap(), "healthy");
169 assert_eq!(value.get("server").and_then(|v| v.as_str()).unwrap(), "simple-stateful");
170
171 assert!(value.get("services").is_some());
173 assert!(value.get("config").is_some());
174 }
175}