1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
//! Rullst Edge Runtime (`rullst::edge`)
//!
//! Native support for compiling and running Rullst apps on WebAssembly edge infrastructure
//! (Cloudflare Workers, Fastly Compute, AWS Lambda@Edge) abstracting Tokio/WASI differences.
use std::collections::HashMap;
use std::future::Future;
/// Environment-agnostic HTTP request payload designed for maximum compatibility.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct EdgeRequest {
/// HTTP method (e.g., "GET", "POST").
pub method: String,
/// Request URL path (e.g., "/users").
pub path: String,
/// Collection of request headers.
pub headers: HashMap<String, String>,
/// Raw request body in bytes.
pub body: Vec<u8>,
}
impl EdgeRequest {
/// Creates a new `EdgeRequest` using constructor and builder pattern for backwards compatibility.
pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
Self {
method: method.into(),
path: path.into(),
headers: HashMap::new(),
body: Vec::new(),
}
}
/// Appends a header key-value pair to the request.
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
/// Sets the raw request body.
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
}
/// Environment-agnostic HTTP response payload designed for maximum compatibility.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct EdgeResponse {
/// HTTP status code (e.g., 200, 404).
pub status: u16,
/// Collection of response headers.
pub headers: HashMap<String, String>,
/// Raw response body in bytes.
pub body: Vec<u8>,
}
impl EdgeResponse {
/// Creates a new `EdgeResponse` using constructor and builder pattern for backwards compatibility.
pub fn new(status: u16) -> Self {
Self {
status,
headers: HashMap::new(),
body: Vec::new(),
}
}
/// Appends a header key-value pair to the response.
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
/// Sets the raw response body.
pub fn with_body(mut self, body: Vec<u8>) -> Self {
self.body = body;
self
}
}
/// Environment-agnostic task spawner mapping to native Tokio or WASM local execution environments.
pub fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(future);
}
#[cfg(not(target_arch = "wasm32"))]
{
tokio::spawn(future);
}
}
/// Portable Edge server running a local Axum emulator on native, and a direct executor on WASM.
#[non_exhaustive]
pub struct EdgeServer<F> {
/// The edge HTTP request handler function.
pub handler: F,
/// Optional: Local port to bind the emulation server.
pub port: u16,
}
impl<F, Fut> EdgeServer<F>
where
F: Fn(EdgeRequest) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = EdgeResponse> + Send + 'static,
{
/// Creates a new `EdgeServer` with the specified request handler.
pub fn new(handler: F) -> Self {
Self {
handler,
port: 3000,
}
}
/// Sets the local TCP port of the emulation server.
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Serves request handling either natively as an emulator or natively in WASM edge runtimes.
#[cfg(not(target_arch = "wasm32"))]
pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
use axum::Router;
use axum::extract::Request;
use axum::routing::any;
let handler = self.handler.clone();
let app = Router::new().route(
"/*path",
any(move |req: Request| {
let handler = handler.clone();
async move {
let (parts, body) = req.into_parts();
let method = parts.method.to_string();
let path = parts.uri.path().to_string();
let mut headers = HashMap::new();
for (k, v) in parts.headers.iter() {
if let Ok(val) = v.to_str() {
headers.insert(k.as_str().to_string(), val.to_string());
}
}
let body_bytes = match axum::body::to_bytes(body, 2 * 1024 * 1024).await {
Ok(bytes) => bytes.to_vec(),
Err(_) => Vec::new(),
};
let edge_req = EdgeRequest {
method,
path,
headers,
body: body_bytes,
};
let edge_resp = handler(edge_req).await;
let mut res_builder = axum::http::Response::builder().status(edge_resp.status);
for (k, v) in edge_resp.headers.iter() {
res_builder = res_builder.header(k, v);
}
match res_builder.body(axum::body::Body::from(edge_resp.body)) {
Ok(res) => res,
Err(_) => {
let mut err_res =
axum::response::Response::new(axum::body::Body::empty());
*err_res.status_mut() = axum::http::StatusCode::INTERNAL_SERVER_ERROR;
err_res
}
}
}
}),
);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?;
println!(
"🚀 Edge local emulator running on http://localhost:{}",
self.port
);
// Spawn serving loop
axum::serve(listener, app).await?;
Ok(())
}
/// Serves request handling natively inside WASM WASI edge loops.
#[cfg(target_arch = "wasm32")]
pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
// In actual Cloudflare Workers or WASM Edge targets,
// the global handler is registered statically.
// We log execution readiness for testing.
web_sys::console::log_1(&"🚀 Rullst Edge Runtime serving on WASM target".into());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edge_request_builder() {
let req = EdgeRequest::new("POST", "/test")
.with_header("X-Foo", "bar")
.with_body(vec![1, 2, 3]);
assert_eq!(req.method, "POST");
assert_eq!(req.path, "/test");
assert_eq!(req.headers.get("X-Foo").map(|s| s.as_str()), Some("bar"));
assert_eq!(req.body, vec![1, 2, 3]);
}
#[test]
fn test_edge_response_builder() {
let res = EdgeResponse::new(201)
.with_header("Content-Type", "application/json")
.with_body(vec![123, 125]);
assert_eq!(res.status, 201);
assert_eq!(
res.headers.get("Content-Type").map(|s| s.as_str()),
Some("application/json")
);
assert_eq!(res.body, vec![123, 125]);
}
}