Skip to main content

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}