use std::sync::Arc;
use std::time::Duration;
use actix_web::{web, App as ActixApp, HttpServer, HttpResponse, middleware::Logger};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppState {
Created,
Starting,
Running,
Stopping,
Stopped,
}
#[derive(Clone)]
pub struct ServerControl {
state: Arc<RwLock<AppState>>,
shutdown_trigger: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
}
impl ServerControl {
fn new() -> Self {
Self {
state: Arc::new(RwLock::new(AppState::Created)),
shutdown_trigger: Arc::new(RwLock::new(None)),
}
}
async fn set_state(&self, s: AppState) {
let mut guard = self.state.write().await;
*guard = s;
}
async fn current_state(&self) -> AppState {
*self.state.read().await
}
fn trigger_shutdown(&self) {
if let Some(tx) = self.shutdown_trigger.try_write().ok().and_then(|mut g| g.take()) {
let _ = tx.send(());
}
}
}
pub struct App {
host: String,
port: u16,
shutdown_timeout: Duration,
configure_fn: Option<Arc<dyn Fn(&mut web::ServiceConfig) + Send + Sync>>,
control: Arc<RwLock<Option<ServerControl>>>,
}
impl App {
pub fn new() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
shutdown_timeout: Duration::from_secs(30),
configure_fn: None,
control: Arc::new(RwLock::new(None)),
}
}
pub fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
self.shutdown_timeout = timeout;
self
}
pub fn configure<F>(mut self, f: F) -> Self
where
F: Fn(&mut web::ServiceConfig) + Send + Sync + 'static,
{
self.configure_fn = Some(Arc::new(f));
self
}
pub async fn run(self) -> std::io::Result<()> {
let addr = format!("{}:{}", self.host, self.port);
let control = ServerControl::new();
control.set_state(AppState::Starting).await;
{
let mut c = self.control.write().await;
*c = Some(control.clone());
}
tracing::info!("Starting Kegani server on {}", addr);
let state = control.state.clone();
let configure_fn = self.configure_fn.clone();
let server = HttpServer::new(move || {
let state = state.clone();
ActixApp::new()
.wrap(Logger::default())
.app_data(web::Data::new(state))
.configure(|cfg: &mut web::ServiceConfig| {
cfg.service(web::scope("/api-docs")
.route("/openapi.json", web::get().to(openapi_json_handler))
.route("/swagger-ui", web::get().to(swagger_ui_handler))
.route("/redoc", web::get().to(redoc_handler))
);
if let Some(ref f) = configure_fn {
f(cfg);
}
})
.default_service(web::route().to(not_found_handler))
})
.bind(&addr)?
.run();
control.set_state(AppState::Running).await;
let _ = server.await;
control.set_state(AppState::Stopped).await;
tracing::info!("Server stopped");
Ok(())
}
pub async fn serve(self) -> std::io::Result<()> {
let addr = format!("{}:{}", self.host, self.port);
let control = ServerControl::new();
control.set_state(AppState::Starting).await;
{
let mut c = self.control.write().await;
*c = Some(control.clone());
}
let state = control.state.clone();
let configure_fn = self.configure_fn.clone();
let _server = HttpServer::new(move || {
let state = state.clone();
ActixApp::new()
.wrap(Logger::default())
.app_data(web::Data::new(state))
.configure(|cfg: &mut web::ServiceConfig| {
cfg.service(web::scope("/api-docs")
.route("/openapi.json", web::get().to(openapi_json_handler))
.route("/swagger-ui", web::get().to(swagger_ui_handler))
.route("/redoc", web::get().to(redoc_handler))
);
if let Some(ref f) = configure_fn {
f(cfg);
}
})
.default_service(web::route().to(not_found_handler))
})
.bind(&addr)?
.run();
let _ = _server;
control.set_state(AppState::Running).await;
Ok(())
}
pub async fn stop(&self) {
if let Some(ref ctrl) = *self.control.read().await {
ctrl.set_state(AppState::Stopping).await;
ctrl.trigger_shutdown();
}
}
pub async fn state(&self) -> AppState {
if let Some(ref ctrl) = *self.control.read().await {
ctrl.current_state().await
} else {
AppState::Created
}
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
async fn not_found_handler() -> HttpResponse {
HttpResponse::NotFound().json(serde_json::json!({
"error": "NOT_FOUND",
"message": "The requested resource was not found"
}))
}
async fn openapi_json_handler() -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.json(serde_json::json!({
"openapi": "3.0.3",
"info": { "title": "Kegani API", "version": "1.0.0" },
"paths": {}
}))
}
async fn swagger_ui_handler() -> HttpResponse {
let html = r#"<!DOCTYPE html>
<html>
<head>
<title>Kegani API — Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
SwaggerUIBundle({
url: "/api-docs/openapi.json",
dom_id: "swagger-ui",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: "BaseLayout"
});
};
</script>
</body>
</html>"#;
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
async fn redoc_handler() -> HttpResponse {
let html = r#"<!DOCTYPE html>
<html>
<head>
<title>Kegani API — ReDoc</title>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
<redoc spec-url="/api-docs/openapi.json"></redoc>
<script src="https://unpkg.com/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>"#;
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_creation() {
let app = App::new();
assert_eq!(app.host, "0.0.0.0");
assert_eq!(app.port, 8080);
}
#[test]
fn test_app_builder() {
let app = App::new().host("127.0.0.1").port(3000);
assert_eq!(app.host, "127.0.0.1");
assert_eq!(app.port, 3000);
}
}