webio/lib.rs
1//! # WebIO 🦀
2//!
3//! A minimalist, high-performance async web framework for Rust built with a **zero-dependency philosophy**.
4//!
5//! ## Why WebIO?
6//! Most Rust web frameworks rely on heavy dependency trees (`tokio`, `hyper`, `serde`). **WebIO**
7//! explores the power of the Rust `std` library to provide a fully functional async engine
8//! with a tiny binary footprint and rapid compilation times.
9
10#![doc = include_str!("../README.md")]
11
12use std::{
13 collections::HashMap,
14 future::Future,
15 io::{Read, Write, BufWriter},
16 net::{TcpListener, TcpStream, Shutdown},
17 pin::Pin,
18 sync::Arc,
19 time::{Instant, SystemTime, UNIX_EPOCH},
20 task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
21};
22
23// --- Core Traits & Types ---
24
25/// Enables streaming response bodies to support large data transfers without
26/// high memory overhead. This is crucial for keeping memory usage low in a zero-dep environment.
27pub trait BodyStream: Send {
28 /// Returns the next chunk of bytes. Returns `None` when the stream is exhausted.
29 fn next_chunk(&mut self) -> Option<Vec<u8>>;
30}
31
32impl BodyStream for Vec<u8> {
33 fn next_chunk(&mut self) -> Option<Vec<u8>> {
34 if self.is_empty() { None } else { Some(std::mem::take(self)) }
35 }
36}
37
38/// A conversion trait to abstract different types of response data.
39/// This allows the `.body()` method to accept `String`, `&str`, or `Vec<u8>` seamlessly.
40pub trait IntoBytes { fn into_bytes(self) -> Vec<u8>; }
41impl IntoBytes for String { fn into_bytes(self) -> Vec<u8> { self.into_bytes() } }
42impl IntoBytes for &str { fn into_bytes(self) -> Vec<u8> { self.as_bytes().to_vec() } }
43impl IntoBytes for Vec<u8> { fn into_bytes(self) -> Vec<u8> { self } }
44
45/// Represents an incoming HTTP request.
46/// Designed for simplicity, containing parsed methods, paths, and headers.
47pub struct Req {
48 pub method: String,
49 pub path: String,
50 pub body: String,
51 pub headers: HashMap<String, String>
52}
53
54/// Wrapper for URL path parameters (e.g., `<id>` in a route).
55pub struct Params(pub HashMap<String, String>);
56
57/// Standard HTTP Status Codes. Using a `u16` representation ensures
58/// compatibility with the HTTP protocol while providing type-safe common codes.
59#[derive(Copy, Clone)]
60#[repr(u16)]
61pub enum StatusCode {
62 Ok = 200,
63 Unauthorized = 401,
64 Forbidden = 403,
65 NotFound = 404
66}
67
68/// The outgoing HTTP response.
69/// Uses a `Box<dyn BodyStream>` to allow for flexible, memory-efficient body types.
70pub struct Reply {
71 pub status: u16,
72 pub headers: HashMap<String, String>,
73 pub body: Box<dyn BodyStream>,
74}
75
76impl Reply {
77 /// Creates a new response with a specific status code.
78 pub fn new(status: StatusCode) -> Self {
79 Self { status: status as u16, headers: HashMap::new(), body: Box::new(Vec::new()) }
80 }
81
82 /// Builder pattern method to add headers to the response.
83 pub fn header(mut self, key: &str, value: &str) -> Self {
84 self.headers.insert(key.to_string(), value.to_string());
85 self
86 }
87
88 /// Sets the response body. Accepts any type implementing [`IntoBytes`].
89 pub fn body<T: IntoBytes>(mut self, data: T) -> Self {
90 self.body = Box::new(data.into_bytes());
91 self
92 }
93}
94
95/// Type alias for a pinned, thread-safe future.
96/// Necessary for handling async route logic without an external runtime.
97pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
98
99/// Represents a route handler.
100/// Receives the Request and Path Params, returning an async Response.
101pub type Handler = Box<dyn Fn(Req, Params) -> BoxFuture<'static, Reply> + Send + Sync>;
102
103/// Middleware signature for early-exit logic (e.g., Auth or Logging).
104pub type Middleware = Box<dyn Fn(&str) -> Option<Reply> + Send + Sync>;
105
106/// Supported HTTP Methods.
107pub enum Method { GET, POST }
108pub use Method::*;
109
110// --- WebIo Engine ---
111
112/// The central Application controller.
113/// Manages routing, middleware, and the internal TCP lifecycle.
114pub struct WebIo {
115 routes: Vec<(String, String, Handler)>,
116 mw: Option<Middleware>,
117 handlers_404: HashMap<String, Handler>,
118}
119
120impl WebIo {
121 /// Initializes a new WebIo instance with an empty routing table.
122 pub fn new() -> Self {
123 Self {
124 routes: Vec::new(),
125 mw: None,
126 handlers_404: HashMap::new()
127 }
128 }
129
130 /// Registers a global middleware function.
131 /// If the middleware returns `Some(Reply)`, the request cycle ends early.
132 pub fn use_mw<F>(&mut self, f: F) where F: Fn(&str) -> Option<Reply> + Send + Sync + 'static {
133 self.mw = Some(Box::new(f));
134 }
135
136 /// Configures custom 404 handlers.
137 /// It intelligently detects `Content-Type` to serve JSON or HTML based on the client's `Accept` header.
138 pub fn on_404<F, Fut>(&mut self, handler: F)
139 where F: Fn(Req, Params) -> Fut + Send + Sync + 'static, Fut: Future<Output = Reply> + Send + 'static,
140 {
141 let h: Handler = Box::new(move |r, p| Box::pin(handler(r, p)));
142 // Sniffing the handler's default content type to categorize it
143 let dummy_req = Req { method: "".into(), path: "".into(), body: "".into(), headers: HashMap::new() };
144 let sniff = execute(h(dummy_req, Params(HashMap::new())));
145
146 let ct = sniff.headers.get("Content-Type").cloned().unwrap_or_default();
147 if ct.contains("json") {
148 self.handlers_404.insert("json".to_string(), h);
149 } else {
150 self.handlers_404.insert("html".to_string(), h);
151 }
152 }
153
154 // Defines a route with a specific method and path.
155 /// Supports dynamic segments using `<name>` syntax (e.g., `/user/<id>`).
156 pub fn route<F, Fut>(&mut self, method: Method, path: &str, handler: F)
157 where F: Fn(Req, Params) -> Fut + Send + Sync + 'static, Fut: Future<Output = Reply> + Send + 'static,
158 {
159 let m = match method { GET => "GET", POST => "POST" }.to_string();
160 self.routes.push((m, path.to_string(), Box::new(move |r, p| Box::pin(handler(r, p)))));
161 }
162
163 /// Starts the blocking TCP listener loop.
164 /// Spawns a new thread per connection to maintain responsiveness.
165 pub async fn run(self, host: &str, port: &str) {
166 let listener = TcpListener::bind(format!("{}:{}", host, port)).expect("Bind failed");
167 println!("🦀 WebIo Live: http://{}:{}", host, port);
168 let app = Arc::new(self);
169 for stream in listener.incoming() {
170 if let Ok(s) = stream {
171 let a = Arc::clone(&app);
172 // Multi-threaded execution model for high availability
173 std::thread::spawn(move || execute(a.handle_connection(s)));
174 }
175 }
176 }
177
178 /// Internal logic to parse HTTP raw text into structured [`Req`] data and route it.
179 async fn handle_connection(&self, mut stream: TcpStream) {
180 let start_time = Instant::now();
181 let _ = stream.set_nodelay(true); // Optimizes for small packets/latency
182 let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(150)));
183
184 let mut buffer = [0; 4096];
185 let n = match stream.read(&mut buffer) { Ok(n) if n > 0 => n, _ => return };
186 let header_str = String::from_utf8_lossy(&buffer[..n]);
187
188 let mut lines = header_str.lines();
189 let parts: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
190 if parts.len() < 2 || parts[1] == "/favicon.ico" { return; }
191
192 let (method, full_path) = (parts[0], parts[1]);
193
194 let mut headers = HashMap::new();
195 for line in lines {
196 if line.is_empty() { break; }
197 if let Some((k, v)) = line.split_once(": ") {
198 headers.insert(k.to_lowercase(), v.to_string());
199 }
200 }
201
202 // Execution Logic:
203 // 1. Check Middleware for early returns.
204 // 2. Parse Route segments and extract parameters.
205 // 3. Fallback to Smart 404 if no route matches.
206
207 // --- 1. Middleware ---
208 if let Some(ref mw_func) = self.mw {
209 if let Some(early_reply) = mw_func(full_path) {
210 self.finalize(stream, early_reply, method, full_path, start_time).await;
211 return;
212 }
213 }
214
215 // --- 2. Router ---
216 let path_only = full_path.split('?').next().unwrap_or("/");
217 let mut final_params = HashMap::new();
218 let mut active_handler: Option<&Handler> = None;
219 let path_segments: Vec<&str> = path_only.split('/').filter(|s| !s.is_empty()).collect();
220
221 for (r_method, r_path, handler) in &self.routes {
222 if r_method != method { continue; }
223 let route_segments: Vec<&str> = r_path.split('/').filter(|s| !s.is_empty()).collect();
224 if route_segments.len() == path_segments.len() {
225 let mut matches = true;
226 let mut temp_params = HashMap::new();
227 for (r_seg, p_seg) in route_segments.iter().zip(path_segments.iter()) {
228 if r_seg.starts_with('<') && r_seg.ends_with('>') {
229 temp_params.insert(r_seg[1..r_seg.len()-1].to_string(), p_seg.to_string());
230 } else if r_seg != p_seg { matches = false; break; }
231 }
232 if matches { final_params = temp_params; active_handler = Some(handler); break; }
233 }
234 }
235
236 let req = Req { method: method.to_string(), path: full_path.to_string(), body: String::new(), headers };
237
238 let reply = if let Some(handler) = active_handler {
239 handler(req, Params(final_params)).await
240 } else {
241 // --- 3. SMART 404: Default JSON, HTML for Browsers ---
242 let accept = req.headers.get("accept").cloned().unwrap_or_default();
243 let h_404 = if accept.contains("text/html") {
244 self.handlers_404.get("html")
245 } else {
246 self.handlers_404.get("json")
247 };
248
249 if let Some(h) = h_404 {
250 h(req, Params(HashMap::new())).await
251 } else {
252 Reply::new(StatusCode::NotFound).body("404 Not Found")
253 }
254 };
255
256 self.finalize(stream, reply, method, full_path, start_time).await;
257 }
258
259 /// Finalizes the HTTP response by writing headers and body chunks to the [`TcpStream`].
260 /// Uses [`BufWriter`] to minimize expensive syscalls during network I/O.
261 ///
262 /// ### Performance Analysis:
263 /// In local environments, WebIo consistently achieves response times in the
264 /// **50µs - 150µs** range (e.g., `[00:50:18] GET / -> 200 (56.9µs)`).
265 ///
266 /// **How we achieve this speed:**
267 /// 1. **Zero Runtime Overhead:** Unlike frameworks that use complex task stealing
268 /// and global schedulers, WebIo uses a direct-poll executor that adds nearly zero
269 /// latency to the future resolution.
270 /// 2. **BufWriter Optimization:** We use a high-capacity (64KB) [`BufWriter`] to
271 /// batch syscalls. This minimizes the "context switch" tax between the
272 /// application and the OS kernel.
273 /// 3. **No-Copy Routing:** Our router uses segment-matching on slices where possible,
274 /// reducing heap allocations during the request lifecycle.
275 /// 4. **No-Delay Sockets:** By setting `TCP_NODELAY`, we bypass the Nagle algorithm,
276 /// ensuring that small HTTP headers are sent immediately.
277 async fn finalize(&self, stream: TcpStream, reply: Reply, method: &str, path: &str, start: Instant) {
278 {
279 // We use a large buffer to ensure that headers and initial chunks
280 // are sent in a single syscall (packet), drastically reducing latency.
281 let mut writer = BufWriter::with_capacity(65536, &stream);
282
283 // HTTP/1.1 Chunked Transfer Encoding allows us to start sending data
284 // without knowing the total Content-Length upfront.
285 let mut head = format!(
286 "HTTP/1.1 {} OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n", reply.status
287 );
288
289 for (k, v) in &reply.headers {
290 head.push_str(&format!("{}: {}\r\n", k, v));
291 }
292 head.push_str("\r\n");
293
294 let _ = writer.write_all(head.as_bytes());
295
296 // Stream the body in chunks to maintain a low memory profile.
297 // This prevents loading the entire response into RAM.
298 let mut b = reply.body;
299 while let Some(data) = b.next_chunk() {
300 // Chunk format: [size in hex]\r\n[data]\r\n
301 let _ = writer.write_all(format!("{:X}\r\n", data.len()).as_bytes());
302 let _ = writer.write_all(&data);
303 let _ = writer.write_all(b"\r\n");
304 }
305
306 // Final zero-sized chunk to signal end of stream
307 let _ = writer.write_all(b"0\r\n\r\n");
308 let _ = writer.flush();
309 }
310
311 // --- High-Resolution Performance Logging ---
312 // We calculate the precise duration from the moment the TCP stream was accepted
313 // until the final byte of the chunked response is flushed.
314 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
315 println!(
316 "[{:02}:{:02}:{:02}] {} {} -> {} ({:?})",
317 (now/3600)%24, (now/60)%60, now%60,
318 method, path, reply.status, start.elapsed() // Direct high-resolution measurement
319 );
320
321 // Terminate the connection immediately to free up OS resources and
322 // prevent 'hanging' connections in high-concurrency benchmarks.
323 let _ = stream.shutdown(Shutdown::Both);
324 }
325}
326
327/// A lightweight, blocking executor that drives a [`Future`] to completion.
328///
329/// ### Why this exists:
330/// In a standard Rust project, you would use `tokio::run` or `async_std::main`.
331/// Since **WebIO** forbids external dependencies, we use this custom executor.
332/// It leverages a "no-op" waker and a `yield_now` loop to poll futures
333/// until they return [`Poll::Ready`].
334///
335/// ### Performance Note:
336/// This executor is designed for simplicity. It uses a spin-loop with
337/// `std::thread::yield_now()`, which is efficient for I/O-bound web tasks
338/// but may cause higher CPU usage than an interrupt-driven epoll/kqueue reactor.
339pub fn execute<F: Future>(mut future: F) -> F::Output {
340 // Pin the future to the stack. This is safe because the future
341 // does not move for the duration of this function call.
342 let mut future = unsafe { Pin::new_unchecked(&mut future) };
343
344 // Define a virtual table (VTable) for a manual Waker.
345 // Since we are polling in a loop, the waker methods (clone, wake, drop)
346 // don't need to do anything—the loop will naturally re-poll.
347 static VTABLE: RawWakerVTable = RawWakerVTable::new(
348 |p| RawWaker::new(p, &VTABLE), // Clone
349 |_| {}, // Wake
350 |_| {}, // Wake by ref
351 |_| {}); // Drop
352
353 // Create a raw waker from our VTable.
354 let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
355
356 let mut cx = Context::from_waker(&waker);
357 loop {
358 // Poll the future exactly once
359 match future.as_mut().poll(&mut cx) {
360 Poll::Ready(v) => return v,
361 // Relinquish the current thread's time slice to the OS.
362 // This prevents the CPU from hitting 100% usage while waiting for I/O.
363 Poll::Pending => std::thread::yield_now()
364 }
365 }
366}