use axum::{
Router,
routing::any,
extract::Request as AxumRequest,
response::{Response as AxumResponse, IntoResponse},
body::Body,
http::{StatusCode, HeaderMap, HeaderName, HeaderValue, Method},
};
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct HttpServerRequest {
pub method: String,
pub path: String,
pub headers: Vec<(String, String)>,
pub body: String,
}
pub type ServerRequest = HttpServerRequest;
#[derive(Debug, Clone)]
pub struct HttpServerResponse {
pub status: i64,
pub headers: Vec<(String, String)>,
pub body: String,
pub binary_body: Option<Vec<u8>>,
}
pub type ServerResponse = HttpServerResponse;
impl HttpServerResponse {
pub fn new(status: i64, body: String) -> Self {
Self {
status,
headers: vec![],
body,
binary_body: None,
}
}
pub fn html(body: String) -> Self {
Self {
status: 200,
headers: vec![("Content-Type".to_string(), "text/html; charset=utf-8".to_string())],
body,
binary_body: None,
}
}
pub fn json(body: String) -> Self {
Self {
status: 200,
headers: vec![("Content-Type".to_string(), "application/json".to_string())],
body,
binary_body: None,
}
}
pub fn binary(status: i64, data: Vec<u8>) -> Self {
Self {
status,
headers: vec![],
body: String::new(),
binary_body: Some(data),
}
}
pub fn error(status: i64, message: String) -> Self {
Self {
status,
headers: vec![("Content-Type".to_string(), "text/plain; charset=utf-8".to_string())],
body: message,
binary_body: None,
}
}
pub fn header(mut self, key: String, value: String) -> Self {
self.headers.push((key, value));
self
}
}
#[derive(Debug, Clone)]
pub struct Server {
pub address: String,
pub port: i64,
}
impl Server {
pub fn new(address: String, port: i64) -> Self {
Self { address, port }
}
pub fn serve<F>(self, handler: F) -> Result<(), String>
where
F: Fn(ServerRequest) -> ServerResponse + Send + Sync + 'static,
{
windjammer_http_serve(self.address, self.port, handler)
}
}
#[tokio::main]
pub async fn windjammer_http_serve<F>(
address: String,
port: i64,
handler: F,
) -> Result<(), String>
where
F: Fn(HttpServerRequest) -> HttpServerResponse + Send + Sync + 'static,
{
let handler = Arc::new(handler);
let app = Router::new().fallback(move |req: AxumRequest| {
let handler = handler.clone();
async move {
handle_request(req, handler).await
}
});
let addr = format!("{}:{}", address, port);
let socket_addr: SocketAddr = addr.parse()
.map_err(|e| format!("Invalid address {}: {}", addr, e))?;
println!("🚀 Windjammer server (axum) listening on http://{}", addr);
println!("📍 Press Ctrl+C to stop");
let listener = tokio::net::TcpListener::bind(socket_addr).await
.map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
axum::serve(listener, app).await
.map_err(|e| format!("Server error: {}", e))?;
Ok(())
}
async fn handle_request<F>(
axum_req: AxumRequest,
handler: Arc<F>,
) -> impl IntoResponse
where
F: Fn(HttpServerRequest) -> HttpServerResponse,
{
let method = axum_req.method().to_string();
let path = axum_req.uri().path().to_string();
let mut headers = Vec::new();
for (key, value) in axum_req.headers() {
if let Ok(value_str) = value.to_str() {
headers.push((key.to_string(), value_str.to_string()));
}
}
let body_bytes = match axum::body::to_bytes(axum_req.into_body(), usize::MAX).await {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("❌ Failed to read request body: {}", e);
return (
StatusCode::BAD_REQUEST,
format!("Failed to read body: {}", e),
).into_response();
}
};
let body = String::from_utf8_lossy(&body_bytes).to_string();
let req = HttpServerRequest {
method: method.clone(),
path: path.clone(),
headers,
body,
};
let response = handler(req);
let status_emoji = if response.status >= 200 && response.status < 300 {
"✅"
} else if response.status >= 400 {
"❌"
} else {
"📤"
};
println!("{} {} {} -> {}", status_emoji, method, path, response.status);
convert_to_axum_response(response)
}
fn convert_to_axum_response(response: HttpServerResponse) -> AxumResponse {
let status = StatusCode::from_u16(response.status as u16)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body = if let Some(binary) = response.binary_body {
Body::from(binary)
} else {
Body::from(response.body)
};
let mut builder = AxumResponse::builder().status(status);
for (key, value) in response.headers {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::from_str(&key),
HeaderValue::from_str(&value),
) {
builder = builder.header(header_name, header_value);
}
}
builder.body(body).unwrap_or_else(|e| {
eprintln!("❌ Failed to build response: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response()
})
}