windjammer_runtime/platform/native/
http.rs1use axum::{
3 body::Body,
4 extract::Request as AxumRequest,
5 http::{HeaderName, HeaderValue, StatusCode},
6 response::{IntoResponse, Response as AxumResponse},
7 Router,
8};
9use std::net::SocketAddr;
10use std::str::FromStr;
11use std::sync::Arc;
12
13pub type HttpResult<T> = Result<T, String>;
14
15pub use super::net::{get, post, Request, Response};
20
21pub fn put(url: String, body: String) -> HttpResult<Response> {
23 Request {
24 url,
25 method: "PUT".to_string(),
26 headers: Vec::new(),
27 body: Some(body),
28 timeout: None,
29 }
30 .send()
31}
32
33pub fn delete(url: String) -> HttpResult<Response> {
34 Request {
35 url,
36 method: "DELETE".to_string(),
37 headers: Vec::new(),
38 body: None,
39 timeout: None,
40 }
41 .send()
42}
43
44#[derive(Debug, Clone)]
50pub struct ServerRequest {
51 pub method: String,
52 pub path: String,
53 pub headers: Vec<(String, String)>,
54 pub body: String,
55}
56
57#[derive(Debug, Clone)]
59pub struct ServerResponse {
60 pub status: i64,
61 pub headers: Vec<(String, String)>,
62 pub body: String,
63 pub binary_body: Option<Vec<u8>>,
64}
65
66impl ServerResponse {
67 pub fn new(status: i64, body: String) -> Self {
68 Self {
69 status,
70 headers: Vec::new(),
71 body,
72 binary_body: None,
73 }
74 }
75
76 pub fn binary(status: i64, data: Vec<u8>) -> Self {
77 Self {
78 status,
79 headers: Vec::new(),
80 body: String::new(),
81 binary_body: Some(data),
82 }
83 }
84
85 pub fn html(body: String) -> Self {
86 Self {
87 status: 200,
88 headers: vec![(
89 "Content-Type".to_string(),
90 "text/html; charset=utf-8".to_string(),
91 )],
92 body,
93 binary_body: None,
94 }
95 }
96
97 pub fn json(body: String) -> Self {
98 Self {
99 status: 200,
100 headers: vec![("Content-Type".to_string(), "application/json".to_string())],
101 body,
102 binary_body: None,
103 }
104 }
105
106 pub fn error(status: i64, message: String) -> Self {
107 Self {
108 status,
109 headers: vec![(
110 "Content-Type".to_string(),
111 "text/plain; charset=utf-8".to_string(),
112 )],
113 body: message,
114 binary_body: None,
115 }
116 }
117
118 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
119 self.headers.push((key.into(), value.into()));
120 self
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct Server {
127 pub address: String,
128 pub port: i64,
129}
130
131impl Server {
132 pub fn new(address: String, port: i64) -> Self {
133 Self { address, port }
134 }
135
136 pub fn serve<F>(self, handler: F) -> HttpResult<()>
137 where
138 F: Fn(&ServerRequest) -> ServerResponse + Send + Sync + 'static,
139 {
140 let runtime = tokio::runtime::Runtime::new()
142 .map_err(|e| format!("Failed to create Tokio runtime: {}", e))?;
143
144 runtime.block_on(async { serve_with_axum(self.address, self.port, handler).await })
145 }
146}
147
148async fn serve_with_axum<F>(address: String, port: i64, handler: F) -> HttpResult<()>
149where
150 F: Fn(&ServerRequest) -> ServerResponse + Send + Sync + 'static,
151{
152 let handler = Arc::new(handler);
153
154 let app = Router::new().fallback(move |req: AxumRequest| {
156 let handler = handler.clone();
157 async move { handle_request(req, handler).await }
158 });
159
160 let addr = format!("{}:{}", address, port);
162 let socket_addr: SocketAddr = addr
163 .parse()
164 .map_err(|e| format!("Invalid address {}: {}", addr, e))?;
165
166 println!("🚀 Windjammer server (axum) listening on http://{}", addr);
167 println!("📍 Press Ctrl+C to stop");
168
169 let listener = tokio::net::TcpListener::bind(socket_addr)
171 .await
172 .map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
173
174 axum::serve(listener, app)
175 .await
176 .map_err(|e| format!("Server error: {}", e))?;
177
178 Ok(())
179}
180
181async fn handle_request<F>(axum_req: AxumRequest, handler: Arc<F>) -> impl IntoResponse
182where
183 F: Fn(&ServerRequest) -> ServerResponse,
184{
185 let method = axum_req.method().to_string();
187 let path = axum_req.uri().path().to_string();
188
189 let mut headers = Vec::new();
191 for (key, value) in axum_req.headers() {
192 if let Ok(value_str) = value.to_str() {
193 headers.push((key.to_string(), value_str.to_string()));
194 }
195 }
196
197 let body_bytes = match axum::body::to_bytes(axum_req.into_body(), usize::MAX).await {
199 Ok(bytes) => bytes,
200 Err(e) => {
201 eprintln!("❌ Failed to read request body: {}", e);
202 return (
203 StatusCode::BAD_REQUEST,
204 format!("Failed to read body: {}", e),
205 )
206 .into_response();
207 }
208 };
209
210 let body = String::from_utf8_lossy(&body_bytes).to_string();
211
212 let req = ServerRequest {
214 method: method.clone(),
215 path: path.clone(),
216 headers,
217 body,
218 };
219
220 let response = handler(&req);
222
223 let status_emoji = if response.status >= 200 && response.status < 300 {
225 "✅"
226 } else if response.status >= 400 {
227 "❌"
228 } else {
229 "📤"
230 };
231 println!(
232 "{} {} {} -> {}",
233 status_emoji, method, path, response.status
234 );
235
236 convert_to_axum_response(response)
238}
239
240fn convert_to_axum_response(response: ServerResponse) -> AxumResponse {
241 let status =
243 StatusCode::from_u16(response.status as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
244
245 let body = if let Some(binary) = response.binary_body {
247 Body::from(binary)
248 } else {
249 Body::from(response.body)
250 };
251
252 let mut builder = AxumResponse::builder().status(status);
254
255 for (key, value) in response.headers {
257 if let (Ok(header_name), Ok(header_value)) =
258 (HeaderName::from_str(&key), HeaderValue::from_str(&value))
259 {
260 builder = builder.header(header_name, header_value);
261 }
262 }
263
264 builder.body(body).unwrap_or_else(|e| {
265 eprintln!("❌ Failed to build response: {}", e);
266 (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response()
267 })
268}