use crate::http::{ requests::{Request, RequestStatus, URL}, responses::{Response, UnitOrBoxedError}, }; use std::{ collections::HashMap, net::{Shutdown, TcpListener}, }; type HandlerFn = fn(Request) -> Response<'static>; // Collection of handlers for requests pub struct Handlers { matchers: HashMap, } impl Handlers { /// Create a new Handlers struct to handle requests /// /// !! This struct does nothing until you add handlers to it (add_handler) and then bind it to a TcpListener !! pub fn new() -> Self { Handlers { matchers: HashMap::new(), } } /// Add a request handler /// /// !! Be sure to bind this Handlers struct to a TcpListener to actually handle requests !! /// /// path: Path to match (no trailing /) /// handler: Function to handle the request. Must return a Response pub fn add_handler(&mut self, path: &str, handler: HandlerFn) { self.matchers.insert(path.to_string(), handler); } /// Bind these handlers to a listener in order to handle incoming requests /// You will need to pass in a TcpListener /// /// !! Call this *after* adding all your handlers with add_handler !! /// /// listener: TcpListener to bind to pub fn bind(&self, listener: TcpListener) { for stream in listener.incoming() { match stream { Ok(stream) => { let request = match Request::parse_stream(stream) { RequestStatus::Ok(req) => req, RequestStatus::MalformedHTTP(stream) => { stream.shutdown(Shutdown::Both).unwrap_or_else(|_| { eprintln!("Failed to close malformed HTTP stream") }); return; } }; self.handle_req(request) .unwrap_or_else(|_| eprintln!("Failed to send handle request")); } Err(e) => { eprintln!("Failed to establish connection: {}", e); return; } } } } fn match_handler(&self, url: &URL) -> Option { 'matching_loop: for (path, handler) in self.matchers.iter() { // Exact match if path == &url.path { return Some(*handler); }; // Segment matching let url_segements = url.path.split('/').collect::>(); let path_segments = path.split('/').collect::>(); // If the URL has more segments than the path, it can't match // or if the path has no segments, it can't match if (url_segements.len() != path_segments.len()) || (path_segments.len() == 0) { continue; } // Check each segment of the url for (url_segment, path_segment) in url_segements.iter().zip(path_segments.iter()) { if path_segment.starts_with('[') { // e.g. /path/[id] if path_segment.ends_with(']') { continue; } // e.g. /path/prefix[id].suffix let prefix = path_segment.split('[').collect::>()[0]; let suffix = path_segment.split(']').collect::>()[1]; // begins_with and starts_with always return true on empty strings if url_segment.starts_with(prefix) && url_segment.ends_with(suffix) { continue; } break 'matching_loop; } if url_segment == path_segment { continue; } break 'matching_loop; } return Some(*handler); } None } fn handle_req(&self, req: Request) -> UnitOrBoxedError { match self.match_handler(&req.url) { Some(handler) => handler(req).send()?, None => { return Response::new(req, 404, "Not Found").send(); } }; Ok(()) } }