Skip to main content

windjammer_runtime/platform/native/
http.rs

1/// Native implementation of std::http using axum
2use 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
15// ============================================================================
16// HTTP CLIENT (re-export from net.rs)
17// ============================================================================
18
19pub use super::net::{get, post, Request, Response};
20
21// Implement put and delete here
22pub 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// ============================================================================
45// HTTP SERVER IMPLEMENTATION (axum-based)
46// ============================================================================
47
48/// HTTP Server Request (received by server)
49#[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/// HTTP Server Response (sent by server)
58#[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/// HTTP Server
125#[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        // Use tokio runtime to run the async server
141        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    // Create a catch-all router that handles all methods and paths
155    let app = Router::new().fallback(move |req: AxumRequest| {
156        let handler = handler.clone();
157        async move { handle_request(req, handler).await }
158    });
159
160    // Parse address
161    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    // Start axum server
170    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    // Convert axum request to Windjammer request
186    let method = axum_req.method().to_string();
187    let path = axum_req.uri().path().to_string();
188
189    // Extract headers
190    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    // Extract body
198    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    // Create Windjammer request
213    let req = ServerRequest {
214        method: method.clone(),
215        path: path.clone(),
216        headers,
217        body,
218    };
219
220    // Call user's handler (pass by reference to match Windjammer's API)
221    let response = handler(&req);
222
223    // Log the request
224    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 Windjammer response to axum response
237    convert_to_axum_response(response)
238}
239
240fn convert_to_axum_response(response: ServerResponse) -> AxumResponse {
241    // Map status code
242    let status =
243        StatusCode::from_u16(response.status as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
244
245    // Build body
246    let body = if let Some(binary) = response.binary_body {
247        Body::from(binary)
248    } else {
249        Body::from(response.body)
250    };
251
252    // Build response with proper headers
253    let mut builder = AxumResponse::builder().status(status);
254
255    // Add all custom headers
256    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}