use crate::{HttpConfig, HttpError, HttpResult};
use elif_core::{Container, app_config::AppConfigTrait};
use axum::{
Router,
routing::{get, IntoMakeService},
extract::State,
response::Json,
};
use serde_json::{json, Value};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::signal;
use tower::ServiceBuilder;
use tower_http::{
trace::TraceLayer,
timeout::TimeoutLayer,
limit::RequestBodyLimitLayer,
};
use tracing::{info, warn, error};
#[derive(Clone)]
pub struct ServerState {
pub container: Arc<Container>,
pub http_config: HttpConfig,
}
pub struct HttpServer {
app: IntoMakeService<Router<ServerState>>,
addr: SocketAddr,
config: HttpConfig,
container: Arc<Container>,
}
pub struct HttpServerBuilder {
container: Option<Arc<Container>>,
http_config: Option<HttpConfig>,
router: Option<Router<ServerState>>,
}
impl HttpServerBuilder {
pub fn new() -> Self {
Self {
container: None,
http_config: None,
router: None,
}
}
pub fn container(mut self, container: Arc<Container>) -> Self {
self.container = Some(container);
self
}
pub fn http_config(mut self, config: HttpConfig) -> Self {
self.http_config = Some(config);
self
}
pub fn router(mut self, router: Router<ServerState>) -> Self {
self.router = Some(router);
self
}
pub fn build(self) -> HttpResult<HttpServer> {
let container = self.container
.ok_or_else(|| HttpError::config("Container is required"))?;
let http_config = match self.http_config {
Some(config) => config,
None => HttpConfig::from_env()?,
};
http_config.validate()?;
let server = HttpServer::new(container, http_config, self.router)?;
Ok(server)
}
}
impl Default for HttpServerBuilder {
fn default() -> Self {
Self::new()
}
}
impl HttpServer {
pub fn new(
container: Arc<Container>,
http_config: HttpConfig,
custom_router: Option<Router<ServerState>>,
) -> HttpResult<Self> {
let app_config = container.config();
let addr = format!("{}:{}", app_config.server.host, app_config.server.port)
.parse::<SocketAddr>()
.map_err(|e| HttpError::config(format!("Invalid server address: {}", e)))?;
let state = ServerState {
container: container.clone(),
http_config: http_config.clone(),
};
let mut app = Router::new()
.route(&http_config.health_check_path, get(health_check))
.with_state(state);
if let Some(custom_router) = custom_router {
app = app.merge(custom_router);
}
let middleware_stack = ServiceBuilder::new()
.layer(RequestBodyLimitLayer::new(http_config.max_request_size))
.layer(TimeoutLayer::new(http_config.request_timeout()));
if http_config.enable_tracing {
app = app.layer(TraceLayer::new_for_http());
}
app = app.layer(middleware_stack);
let app = app.into_make_service();
Ok(Self {
app,
addr,
config: http_config,
container,
})
}
pub async fn run(self) -> HttpResult<()> {
info!(
"Starting HTTP server on {} with config: {:?}",
self.addr, self.config
);
self.container.validate()
.map_err(|e| HttpError::startup(format!("Container validation failed: {}", e)))?;
let listener = tokio::net::TcpListener::bind(self.addr).await
.map_err(|e| HttpError::startup(format!("Failed to bind to {}: {}", self.addr, e)))?;
info!("HTTP server listening on {}", self.addr);
let server = axum::serve(listener, self.app)
.with_graceful_shutdown(shutdown_signal());
if let Err(e) = server.await {
error!("Server error: {}", e);
return Err(HttpError::startup(format!("Server failed: {}", e)));
}
info!("HTTP server stopped gracefully");
Ok(())
}
pub async fn run_with_shutdown<F>(self, shutdown: F) -> HttpResult<()>
where
F: std::future::Future<Output = ()> + Send + 'static,
{
info!(
"Starting HTTP server on {} with custom shutdown handler",
self.addr
);
self.container.validate()
.map_err(|e| HttpError::startup(format!("Container validation failed: {}", e)))?;
let listener = tokio::net::TcpListener::bind(self.addr).await
.map_err(|e| HttpError::startup(format!("Failed to bind to {}: {}", self.addr, e)))?;
info!("HTTP server listening on {}", self.addr);
let server = axum::serve(listener, self.app)
.with_graceful_shutdown(shutdown);
if let Err(e) = server.await {
error!("Server error: {}", e);
return Err(HttpError::startup(format!("Server failed: {}", e)));
}
info!("HTTP server stopped gracefully");
Ok(())
}
pub fn config(&self) -> &HttpConfig {
&self.config
}
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn container(&self) -> Arc<Container> {
self.container.clone()
}
}
async fn health_check(State(state): State<ServerState>) -> Result<Json<Value>, HttpError> {
let container = &state.container;
let database = container.database();
let db_healthy = database.is_connected();
if !db_healthy {
warn!("Health check failed: database not connected");
return Err(HttpError::health_check("Database connection unavailable"));
}
let app_config = container.config();
let response = json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
"version": "0.1.0",
"environment": format!("{:?}", app_config.environment),
"services": {
"database": if db_healthy { "healthy" } else { "unhealthy" }
}
});
Ok(Json(response))
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
info!("Received Ctrl+C, initiating graceful shutdown");
},
_ = terminate => {
info!("Received terminate signal, initiating graceful shutdown");
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use elif_core::container::test_implementations::*;
use std::sync::Arc;
use tokio_test;
fn create_test_container() -> Arc<Container> {
let config = Arc::new(create_test_config());
let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
Container::builder()
.config(config)
.database(database)
.build()
.unwrap()
.into()
}
#[test]
fn test_http_server_builder() {
let container = create_test_container();
let http_config = HttpConfig::default();
let server = HttpServerBuilder::new()
.container(container)
.http_config(http_config)
.build();
assert!(server.is_ok());
let server = server.unwrap();
assert_eq!(server.addr().port(), 8080);
}
#[test]
fn test_http_server_builder_missing_container() {
let result = HttpServerBuilder::new().build();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HttpError::ConfigError { .. }));
}
#[test]
fn test_server_config_access() {
let container = create_test_container();
let http_config = HttpConfig::default();
let server = HttpServerBuilder::new()
.container(container.clone())
.http_config(http_config.clone())
.build()
.unwrap();
assert_eq!(server.config().request_timeout_secs, http_config.request_timeout_secs);
assert_eq!(server.container().config().name, "test-app");
}
#[tokio::test]
async fn test_health_check_handler() {
let container = create_test_container();
let state = ServerState {
container,
http_config: HttpConfig::default(),
};
let result = health_check(State(state)).await;
assert!(result.is_ok());
let response = result.unwrap();
let status = response.0.get("status").and_then(|v| v.as_str()).unwrap();
assert_eq!(status, "healthy");
}
#[test]
fn test_invalid_server_address() {
let container = create_test_container();
let mut app_config = create_test_config();
app_config.server.host = "invalid-host".to_string();
let config_arc = Arc::new(app_config);
let database = Arc::new(TestDatabase::new()) as Arc<dyn elif_core::DatabaseConnection>;
let invalid_container = Container::builder()
.config(config_arc)
.database(database)
.build()
.unwrap();
let result = HttpServer::new(
Arc::new(invalid_container),
HttpConfig::default(),
None
);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HttpError::ConfigError { .. }));
}
}