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}