Skip to main content

hooch_http/
app.rs

1//! # Hooch HTTP Server Application
2//!
3//! This module implements a simple asynchronous HTTP server built on the Hooch async runtime.
4//! It supports customizable middleware and routing, enabling developers to process HTTP requests
5//! and dispatch them to appropriate handlers based on URI patterns and HTTP methods.
6//!
7//! ## Features
8//!
9//! - **Middleware Support:**  
10//!   Middleware functions can be registered to process incoming HTTP requests. They can modify
11//!   requests or short-circuit further processing by returning an immediate HTTP response.
12//!
13//! - **Routing:**  
14//!   Routes can be defined with parameterized URI patterns and HTTP method matching. The router
15//!   matches incoming requests to routes and invokes the corresponding asynchronous handler.
16//!
17//! - **Asynchronous I/O:**  
18//!   The server uses `HoochTcpListener` and `HoochTcpStream` to handle TCP connections asynchronously,
19//!   ensuring scalable and non-blocking I/O operations.
20//!
21//! - **Static Lifetime Management:**  
22//!   Middleware and route handlers are required to have a `'static` lifetime. To satisfy this, the
23//!   middleware and route vectors are leaked during the build process.
24//!
25//! ## Usage
26//!
27//! Use the [`HoochAppBuilder`] to configure the server's address, middleware, and routes. Once
28//! configured, call the `build` method to create a [`HoochApp`] instance, and then invoke its `serve`
29//! method to start accepting connections.
30//!
31//! ### Example
32//!
33//! ```rust
34//! use hooch_http::{HoochAppBuilder, HttpResponseBuilder, HttpMethod, Middleware};
35//!
36//! # async {
37//! let mut app = HoochAppBuilder::new("127.0.0.1:8080").unwrap();
38//!
39//! // Add middleware that logs incoming requests
40//! app.add_middleware(|req, socket| async move {
41//!     println!("Incoming request from {}: {:?}", socket, req);
42//!     Middleware::Continue(req)
43//! });
44//!
45//! // Add a simple GET route for "/hello"
46//! app.add_route("/hello", HttpMethod::GET, |req, params| async move {
47//!     HttpResponseBuilder::ok().body("Hello, world!".to_string()).build()
48//! });
49//!
50//! let app = app.build();
51//! app.serve().await;
52//! # };
53//! ```
54//!
55//! This module is ideal for applications requiring a lightweight, customizable HTTP server with minimal
56//! runtime dependencies and asynchronous processing.
57use std::{
58    future::Future,
59    io,
60    net::{SocketAddr, ToSocketAddrs},
61    pin::Pin,
62};
63
64use futures::FutureExt;
65
66use hooch::{
67    net::{HoochTcpListener, HoochTcpStream},
68    spawner::Spawner,
69};
70
71use crate::{
72    request::HttpRequest, response::HttpResponse, HttpMethod, HttpResponseBuilder, Params, Uri,
73};
74
75/// A future that will eventually resolve to a [`Middleware`] result.
76type MiddlewareFuture = Pin<Box<dyn Future<Output = Middleware> + Send>>;
77
78/// A boxed middleware function. It takes an HTTP request and the client's socket address,
79/// and returns a [`MiddlewareFuture`] that resolves to either a modified request or a short-circuited response.
80type MiddlewareFn = Box<dyn Fn(HttpRequest<'static>, SocketAddr) -> MiddlewareFuture + Send + Sync>;
81
82/// A future that will eventually resolve to an [`HttpResponse`].
83type RouterFuture = Pin<Box<dyn Future<Output = HttpResponse> + Send>>;
84
85/// A boxed router function. It accepts an HTTP request and route parameters,
86/// and returns a [`RouterFuture`] resolving to an [`HttpResponse`].
87type RouterFn = Box<dyn Fn(HttpRequest<'static>, Params<'static>) -> RouterFuture + Send + Sync>;
88
89/// Enum representing the outcome of middleware processing.
90#[derive(Debug)]
91pub enum Middleware {
92    /// Continue processing the request, possibly with modifications.
93    Continue(HttpRequest<'static>),
94    /// Short-circuit further processing by immediately returning this response.
95    ShortCircuit(HttpResponse),
96}
97
98/// Structure representing a single route with its associated HTTP method, path, and handler.
99pub struct Route {
100    /// The asynchronous handler function for this route.
101    fut: RouterFn,
102    /// The HTTP method that this route responds to.
103    method: HttpMethod,
104    /// The URI pattern against which requests are matched.
105    path: &'static str,
106}
107
108/// Builder for configuring and creating a [`HoochApp`] instance.
109///
110/// The builder collects middleware and routes, then consumes itself to create a static instance
111/// of the application. Note that middleware and routes are leaked to achieve a `'static` lifetime.
112pub struct HoochAppBuilder {
113    addr: SocketAddr,
114    middleware: Vec<MiddlewareFn>,
115    router: Vec<Route>,
116}
117
118impl HoochAppBuilder {
119    /// Creates a new `HoochAppBuilder` from an address that implements [`ToSocketAddrs`].
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the provided address cannot be resolved.
124    pub fn new(addr: impl ToSocketAddrs) -> io::Result<Self> {
125        let addr = addr
126            .to_socket_addrs()?
127            .next()
128            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no address resolved"))?;
129
130        Ok(Self {
131            addr,
132            middleware: Vec::new(),
133            router: Vec::new(),
134        })
135    }
136
137    /// Adds a middleware function to the application.
138    ///
139    /// The middleware is a function that receives an HTTP request and the client's socket address,
140    /// and returns a [`MiddlewareFuture`] indicating whether to continue processing or short-circuit.
141    pub fn add_middleware<Fut, F>(&mut self, middleware: F)
142    where
143        Fut: Future<Output = Middleware> + Send + 'static,
144        F: Fn(HttpRequest<'static>, SocketAddr) -> Fut + Send + Sync + 'static,
145    {
146        self.middleware.push(Box::new(move |req, socket| {
147            Box::pin(middleware(req, socket))
148        }));
149    }
150
151    /// Adds a new route to the application.
152    ///
153    /// The route is specified by a URI pattern, an HTTP method, and a handler function.
154    /// The handler receives the request and extracted route parameters, and returns a [`RouterFuture`].
155    pub fn add_route<FutRoute, FnRoute>(
156        &mut self,
157        path: &'static str,
158        method: HttpMethod,
159        route: FnRoute,
160    ) where
161        FnRoute: Fn(HttpRequest<'static>, Params<'static>) -> FutRoute + Sync + Send + 'static,
162        FutRoute: Future<Output = HttpResponse> + Send + 'static,
163    {
164        let route = Route {
165            fut: Box::new(move |req, params| route(req, params).boxed()),
166            method,
167            path,
168        };
169        self.router.push(route);
170    }
171
172    /// Consumes the builder and returns a [`HoochApp`] instance.
173    ///
174    /// This function leaks the middleware and route vectors in order to provide them with a `'static` lifetime,
175    /// which is required by the async runtime.
176    pub fn build(self) -> HoochApp {
177        let middleware_ptr: &'static Vec<MiddlewareFn> = Box::leak(Box::new(self.middleware));
178        let route_ptr: &'static Vec<Route> = Box::leak(Box::new(self.router));
179        HoochApp {
180            addr: self.addr,
181            middleware: middleware_ptr,
182            routes: route_ptr,
183        }
184    }
185}
186
187/// A simple HTTP server built on the Hooch async runtime.
188///
189/// `HoochApp` listens for incoming TCP connections, processes HTTP requests through a series of middleware,
190/// matches requests to routes, and returns serialized HTTP responses.
191pub struct HoochApp {
192    addr: SocketAddr,
193    middleware: &'static Vec<MiddlewareFn>,
194    routes: &'static Vec<Route>,
195}
196
197impl HoochApp {
198    /// Starts the HTTP server and begins accepting incoming connections.
199    ///
200    /// The server binds to the configured address, and for each accepted connection,
201    /// it spawns an asynchronous task to handle the stream.
202    pub async fn serve(&self) {
203        let listener = HoochTcpListener::bind(self.addr).await.unwrap();
204        let middleware_ptr: &'static Vec<MiddlewareFn> = self.middleware;
205        let route_ptr: &'static Vec<Route> = self.routes;
206
207        while let Ok((stream, socket)) = listener.accept().await {
208            println!("Received connection from {:?}", socket);
209            Spawner::spawn(async move {
210                Self::handle_stream(stream, socket, middleware_ptr, route_ptr).await;
211            });
212        }
213    }
214
215    /// Handles a single TCP stream.
216    ///
217    /// This method reads the HTTP request from the stream, applies all middleware in sequence,
218    /// and then routes the request to the appropriate handler based on HTTP method and URI matching.
219    /// If a middleware short-circuits the processing or no matching route is found, an appropriate
220    /// HTTP response is sent back immediately.
221    ///
222    /// # Arguments
223    ///
224    /// * `stream` - The TCP stream representing the client connection.
225    /// * `socket_addr` - The client's socket address.
226    /// * `middleware_fns` - A slice of middleware functions to process the request.
227    /// * `routes` - A slice of defined routes to match against the request.
228    async fn handle_stream(
229        mut stream: HoochTcpStream,
230        socket_addr: SocketAddr,
231        middleware_fns: &'static [MiddlewareFn],
232        routes: &'static [Route],
233    ) {
234        let mut buffer = [0; 1024 * 100];
235        let bytes_read = stream.read(&mut buffer).await.unwrap();
236
237        // Parse the raw bytes into an HTTP request.
238        let http_request = HttpRequest::from_bytes(&buffer[..bytes_read]);
239
240        // SAFETY: Transmute the lifetime of the request to 'static since the buffer is no longer used.
241        let mut http_request: HttpRequest<'static> = unsafe { std::mem::transmute(http_request) };
242
243        // Process middleware sequentially. If any middleware returns a ShortCircuit,
244        // send its response immediately without further processing.
245        for mid in middleware_fns.iter() {
246            let middleware = mid(http_request, socket_addr).await;
247            match middleware {
248                Middleware::Continue(req) => {
249                    http_request = req;
250                }
251                Middleware::ShortCircuit(response) => {
252                    return Self::handle_http_response(response, stream).await;
253                }
254            }
255        }
256
257        // Iterate through routes to find a match for the request's URI and HTTP method.
258        for route in routes.iter() {
259            // SAFETY: Transmute the URI lifetime to 'static for matching within this async context.
260            let uri: &Uri<'static> = unsafe { std::mem::transmute(http_request.uri()) };
261            if let Some(param) = uri.is_match(route.path) {
262                if route.method == http_request.method() {
263                    let response = (route.fut)(http_request, param).await;
264                    return Self::handle_http_response(response, stream).await;
265                }
266            }
267        }
268
269        // If no matching route is found, respond with a 404 Not Found.
270        Self::handle_http_response(HttpResponseBuilder::not_found().build(), stream).await;
271    }
272
273    /// Serializes an [`HttpResponse`] and writes it to the TCP stream.
274    ///
275    /// This method converts the response into a byte vector and writes it to the stream,
276    /// sending the complete HTTP response back to the client.
277    ///
278    /// # Arguments
279    ///
280    /// * `http_response` - The response to serialize and send.
281    /// * `stream` - The TCP stream to write the response to.
282    async fn handle_http_response(http_response: HttpResponse, mut stream: HoochTcpStream) {
283        let mut buffer = Vec::with_capacity(std::mem::size_of_val(&http_response));
284        buffer = http_response.serialize(buffer);
285        stream.write(&buffer).await.unwrap();
286    }
287}