webio 0.2.0-alpha

A minimalist async web framework for Rust with a zero-dependency philosophy.
Documentation
//! # WebIO 🦀
//! 
//! A minimalist, high-performance async web framework for Rust built with a **zero-dependency philosophy**.
//! 
//! ## Why WebIO?
//! Most Rust web frameworks rely on heavy dependency trees (`tokio`, `hyper`, `serde`). **WebIO** 
//! explores the power of the Rust `std` library to provide a fully functional async engine 
//! with a tiny binary footprint and rapid compilation times.

#![doc = include_str!("../README.md")]

use std::{
    collections::HashMap,
    future::Future,
    io::{Read, Write, BufWriter},
    net::{TcpListener, TcpStream, Shutdown},
    pin::Pin,
    sync::Arc,
    time::{Instant, SystemTime, UNIX_EPOCH},
    task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
};

// --- Core Traits & Types ---

/// Enables streaming response bodies to support large data transfers without 
/// high memory overhead. This is crucial for keeping memory usage low in a zero-dep environment.
pub trait BodyStream: Send {
    /// Returns the next chunk of bytes. Returns `None` when the stream is exhausted.
    fn next_chunk(&mut self) -> Option<Vec<u8>>; 
}

impl BodyStream for Vec<u8> {
    fn next_chunk(&mut self) -> Option<Vec<u8>> {
        if self.is_empty() { None } else { Some(std::mem::take(self)) }
    }
}

/// A conversion trait to abstract different types of response data.
/// This allows the `.body()` method to accept `String`, `&str`, or `Vec<u8>` seamlessly.
pub trait IntoBytes { fn into_bytes(self) -> Vec<u8>; }
impl IntoBytes for String { fn into_bytes(self) -> Vec<u8> { self.into_bytes() } }
impl IntoBytes for &str { fn into_bytes(self) -> Vec<u8> { self.as_bytes().to_vec() } }
impl IntoBytes for Vec<u8> { fn into_bytes(self) -> Vec<u8> { self } }

/// Represents an incoming HTTP request.
/// Designed for simplicity, containing parsed methods, paths, and headers.
pub struct Req { 
    pub method: String, 
    pub path: String, 
    pub body: String,
    pub headers: HashMap<String, String> 
}

/// Wrapper for URL path parameters (e.g., `<id>` in a route).
pub struct Params(pub HashMap<String, String>);

/// Standard HTTP Status Codes. Using a `u16` representation ensures
/// compatibility with the HTTP protocol while providing type-safe common codes.
#[derive(Copy, Clone)]
#[repr(u16)]
pub enum StatusCode { 
    Ok = 200, 
    Unauthorized = 401, 
    Forbidden = 403, 
    NotFound = 404 
}

/// The outgoing HTTP response. 
/// Uses a `Box<dyn BodyStream>` to allow for flexible, memory-efficient body types.
pub struct Reply {
    pub status: u16,
    pub headers: HashMap<String, String>,
    pub body: Box<dyn BodyStream>,
}

impl Reply {
    /// Creates a new response with a specific status code.
    pub fn new(status: StatusCode) -> Self {
        Self { status: status as u16, headers: HashMap::new(), body: Box::new(Vec::new()) }
    }

    /// Builder pattern method to add headers to the response.
    pub fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.insert(key.to_string(), value.to_string());
        self
    }

    /// Sets the response body. Accepts any type implementing [`IntoBytes`].
    pub fn body<T: IntoBytes>(mut self, data: T) -> Self {
        self.body = Box::new(data.into_bytes());
        self
    }
}

/// Type alias for a pinned, thread-safe future. 
/// Necessary for handling async route logic without an external runtime.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// Represents a route handler. 
/// Receives the Request and Path Params, returning an async Response.
pub type Handler = Box<dyn Fn(Req, Params) -> BoxFuture<'static, Reply> + Send + Sync>;

/// Middleware signature for early-exit logic (e.g., Auth or Logging).
pub type Middleware = Box<dyn Fn(&str) -> Option<Reply> + Send + Sync>;

/// Supported HTTP Methods.
pub enum Method { GET, POST }
pub use Method::*;

// --- WebIo Engine ---

/// The central Application controller.
/// Manages routing, middleware, and the internal TCP lifecycle.
pub struct WebIo {
    routes: Vec<(String, String, Handler)>,
    mw: Option<Middleware>,
    handlers_404: HashMap<String, Handler>,
}

impl WebIo {
    /// Initializes a new WebIo instance with an empty routing table.
    pub fn new() -> Self { 
        Self { 
            routes: Vec::new(), 
            mw: None, 
            handlers_404: HashMap::new() 
        } 
    }

    /// Registers a global middleware function. 
    /// If the middleware returns `Some(Reply)`, the request cycle ends early.
    pub fn use_mw<F>(&mut self, f: F) where F: Fn(&str) -> Option<Reply> + Send + Sync + 'static {
        self.mw = Some(Box::new(f));
    }

    /// Configures custom 404 handlers. 
    /// It intelligently detects `Content-Type` to serve JSON or HTML based on the client's `Accept` header.
    pub fn on_404<F, Fut>(&mut self, handler: F) 
    where F: Fn(Req, Params) -> Fut + Send + Sync + 'static, Fut: Future<Output = Reply> + Send + 'static,
    {
        let h: Handler = Box::new(move |r, p| Box::pin(handler(r, p)));
         // Sniffing the handler's default content type to categorize it
        let dummy_req = Req { method: "".into(), path: "".into(), body: "".into(), headers: HashMap::new() };
        let sniff = execute(h(dummy_req, Params(HashMap::new())));
        
        let ct = sniff.headers.get("Content-Type").cloned().unwrap_or_default();
        if ct.contains("json") {
            self.handlers_404.insert("json".to_string(), h);
        } else {
            self.handlers_404.insert("html".to_string(), h);
        }
    }

    // Defines a route with a specific method and path.
    /// Supports dynamic segments using `<name>` syntax (e.g., `/user/<id>`).
    pub fn route<F, Fut>(&mut self, method: Method, path: &str, handler: F)
    where F: Fn(Req, Params) -> Fut + Send + Sync + 'static, Fut: Future<Output = Reply> + Send + 'static,
    {
        let m = match method { GET => "GET", POST => "POST" }.to_string();
        self.routes.push((m, path.to_string(), Box::new(move |r, p| Box::pin(handler(r, p)))));
    }

    /// Starts the blocking TCP listener loop. 
    /// Spawns a new thread per connection to maintain responsiveness.
    pub async fn run(self, host: &str, port: &str) {
        let listener = TcpListener::bind(format!("{}:{}", host, port)).expect("Bind failed");
        println!("🦀 WebIo Live: http://{}:{}", host, port);
        let app = Arc::new(self);
        for stream in listener.incoming() {
            if let Ok(s) = stream {
                let a = Arc::clone(&app);
                // Multi-threaded execution model for high availability
                std::thread::spawn(move || execute(a.handle_connection(s)));
            }
        }
    }

    /// Internal logic to parse HTTP raw text into structured [`Req`] data and route it.
    async fn handle_connection(&self, mut stream: TcpStream) {
        let start_time = Instant::now();
        let _ = stream.set_nodelay(true); // Optimizes for small packets/latency
        let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(150)));

        let mut buffer = [0; 4096];
        let n = match stream.read(&mut buffer) { Ok(n) if n > 0 => n, _ => return };
        let header_str = String::from_utf8_lossy(&buffer[..n]);
        
        let mut lines = header_str.lines();
        let parts: Vec<&str> = lines.next().unwrap_or("").split_whitespace().collect();
        if parts.len() < 2 || parts[1] == "/favicon.ico" { return; }

        let (method, full_path) = (parts[0], parts[1]);

        let mut headers = HashMap::new();
        for line in lines {
            if line.is_empty() { break; }
            if let Some((k, v)) = line.split_once(": ") {
                headers.insert(k.to_lowercase(), v.to_string());
            }
        }

        // Execution Logic:
        // 1. Check Middleware for early returns.
        // 2. Parse Route segments and extract parameters.
        // 3. Fallback to Smart 404 if no route matches.

        // --- 1. Middleware ---
        if let Some(ref mw_func) = self.mw {
            if let Some(early_reply) = mw_func(full_path) {
                self.finalize(stream, early_reply, method, full_path, start_time).await;
                return;
            }
        }

        // --- 2. Router ---
        let path_only = full_path.split('?').next().unwrap_or("/");
        let mut final_params = HashMap::new();
        let mut active_handler: Option<&Handler> = None;
        let path_segments: Vec<&str> = path_only.split('/').filter(|s| !s.is_empty()).collect();

        for (r_method, r_path, handler) in &self.routes {
            if r_method != method { continue; }
            let route_segments: Vec<&str> = r_path.split('/').filter(|s| !s.is_empty()).collect();
            if route_segments.len() == path_segments.len() {
                let mut matches = true;
                let mut temp_params = HashMap::new();
                for (r_seg, p_seg) in route_segments.iter().zip(path_segments.iter()) {
                    if r_seg.starts_with('<') && r_seg.ends_with('>') {
                        temp_params.insert(r_seg[1..r_seg.len()-1].to_string(), p_seg.to_string());
                    } else if r_seg != p_seg { matches = false; break; }
                }
                if matches { final_params = temp_params; active_handler = Some(handler); break; }
            }
        }

        let req = Req { method: method.to_string(), path: full_path.to_string(), body: String::new(), headers };
        
        let reply = if let Some(handler) = active_handler {
            handler(req, Params(final_params)).await
        } else {
            // --- 3. SMART 404: Default JSON, HTML for Browsers ---
            let accept = req.headers.get("accept").cloned().unwrap_or_default();
            let h_404 = if accept.contains("text/html") {
                self.handlers_404.get("html") 
            } else {
                self.handlers_404.get("json") 
            };

            if let Some(h) = h_404 {
                h(req, Params(HashMap::new())).await
            } else {
                Reply::new(StatusCode::NotFound).body("404 Not Found")
            }
        };

        self.finalize(stream, reply, method, full_path, start_time).await;
    }

    /// Finalizes the HTTP response by writing headers and body chunks to the [`TcpStream`].
    /// Uses [`BufWriter`] to minimize expensive syscalls during network I/O.
    /// 
    /// ### Performance Analysis:
    /// In local environments, WebIo consistently achieves response times in the 
    /// **50µs - 150µs** range (e.g., `[00:50:18] GET / -> 200 (56.9µs)`).
    /// 
    /// **How we achieve this speed:**
    /// 1. **Zero Runtime Overhead:** Unlike frameworks that use complex task stealing 
    ///    and global schedulers, WebIo uses a direct-poll executor that adds nearly zero 
    ///    latency to the future resolution.
    /// 2. **BufWriter Optimization:** We use a high-capacity (64KB) [`BufWriter`] to 
    ///    batch syscalls. This minimizes the "context switch" tax between the 
    ///    application and the OS kernel.
    /// 3. **No-Copy Routing:** Our router uses segment-matching on slices where possible, 
    ///    reducing heap allocations during the request lifecycle.
    /// 4. **No-Delay Sockets:** By setting `TCP_NODELAY`, we bypass the Nagle algorithm, 
    ///    ensuring that small HTTP headers are sent immediately.
    async fn finalize(&self, stream: TcpStream, reply: Reply, method: &str, path: &str, start: Instant) {
        {
            // We use a large buffer to ensure that headers and initial chunks
            // are sent in a single syscall (packet), drastically reducing latency.
            let mut writer = BufWriter::with_capacity(65536, &stream);
            
            // HTTP/1.1 Chunked Transfer Encoding allows us to start sending data 
            // without knowing the total Content-Length upfront.
            let mut head = format!(
                "HTTP/1.1 {} OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n", reply.status
            );

            for (k, v) in &reply.headers { 
                head.push_str(&format!("{}: {}\r\n", k, v)); 
            }
            head.push_str("\r\n");

            let _ = writer.write_all(head.as_bytes());

            // Stream the body in chunks to maintain a low memory profile.
            // This prevents loading the entire response into RAM.
            let mut b = reply.body;
            while let Some(data) = b.next_chunk() {
                // Chunk format: [size in hex]\r\n[data]\r\n
                let _ = writer.write_all(format!("{:X}\r\n", data.len()).as_bytes());
                let _ = writer.write_all(&data);
                let _ = writer.write_all(b"\r\n");
            }

            // Final zero-sized chunk to signal end of stream
            let _ = writer.write_all(b"0\r\n\r\n");
            let _ = writer.flush();
        }
        
        // --- High-Resolution Performance Logging ---
        // We calculate the precise duration from the moment the TCP stream was accepted
        // until the final byte of the chunked response is flushed.
        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
        println!(
            "[{:02}:{:02}:{:02}] {} {} -> {} ({:?})", 
            (now/3600)%24, (now/60)%60, now%60, 
            method, path, reply.status, start.elapsed() // Direct high-resolution measurement
        );

        // Terminate the connection immediately to free up OS resources and 
        // prevent 'hanging' connections in high-concurrency benchmarks.
        let _ = stream.shutdown(Shutdown::Both);
    }
}

/// A lightweight, blocking executor that drives a [`Future`] to completion.
/// 
/// ### Why this exists:
/// In a standard Rust project, you would use `tokio::run` or `async_std::main`. 
/// Since **WebIO** forbids external dependencies, we use this custom executor. 
/// It leverages a "no-op" waker and a `yield_now` loop to poll futures 
/// until they return [`Poll::Ready`].
///
/// ### Performance Note:
/// This executor is designed for simplicity. It uses a spin-loop with 
/// `std::thread::yield_now()`, which is efficient for I/O-bound web tasks 
/// but may cause higher CPU usage than an interrupt-driven epoll/kqueue reactor.
pub fn execute<F: Future>(mut future: F) -> F::Output {
    // Pin the future to the stack. This is safe because the future 
    // does not move for the duration of this function call.
    let mut future = unsafe { Pin::new_unchecked(&mut future) };
    
    // Define a virtual table (VTable) for a manual Waker.
    // Since we are polling in a loop, the waker methods (clone, wake, drop) 
    // don't need to do anything—the loop will naturally re-poll.
    static VTABLE: RawWakerVTable = RawWakerVTable::new(
        |p| RawWaker::new(p, &VTABLE), // Clone
        |_| {},                                             // Wake
        |_| {},                                             // Wake by ref      
        |_| {});                                            // Drop
    
    // Create a raw waker from our VTable.
    let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
    
    let mut cx = Context::from_waker(&waker);
    loop {
        // Poll the future exactly once
        match future.as_mut().poll(&mut cx) { 
            Poll::Ready(v) => return v,
            // Relinquish the current thread's time slice to the OS.
            // This prevents the CPU from hitting 100% usage while waiting for I/O.
            Poll::Pending => std::thread::yield_now() 
        }
    }
}