From 8d8cfe33fa4621e448f7fde61f3814170d25325b Mon Sep 17 00:00:00 2001 From: Compositr Date: Fri, 1 Nov 2024 21:14:47 +1100 Subject: [PATCH] feat: dynmaic request path handling --- src/handlers.rs | 82 +++++++++++++++++++++++++++++++++++++------- src/http/requests.rs | 16 +++++++++ 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 8b5e103..b2dbbe1 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,5 +1,5 @@ use crate::http::{ - requests::{Request, RequestStatus}, + requests::{Request, RequestStatus, URL}, responses::{Response, UnitOrBoxedError}, }; use std::{ @@ -7,17 +7,19 @@ use std::{ net::{Shutdown, TcpListener}, }; +type HandlerFn = fn(Request) -> Response<'static>; + // Collection of handlers for requests -pub struct Handlers { - matchers: HashMap, +pub struct Handlers { + matchers: HashMap, } -impl Handlers -where - FnT: Fn(Request) -> Response<'static>, -{ +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:: { + Handlers { matchers: HashMap::new(), } } @@ -28,7 +30,7 @@ where /// /// 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: FnT) { + pub fn add_handler(&mut self, path: &str, handler: HandlerFn) { self.matchers.insert(path.to_string(), handler); } @@ -63,10 +65,64 @@ where } } - fn handle_req(&self, req: Request) -> UnitOrBoxedError { - match self.matchers.get(&req.url.path) { - Some(handler) => (*handler)(req).send(), - None => Response::new(req, 404, "Not Found").send(), + 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(()) } } diff --git a/src/http/requests.rs b/src/http/requests.rs index 21e80f1..b085f6c 100644 --- a/src/http/requests.rs +++ b/src/http/requests.rs @@ -39,6 +39,22 @@ impl URL { Some(URL { path, query, raw }) } + + /// Utility function to get a path segment by index + /// + /// This is a very simple function, and its implementation is fairly naïve + /// + /// index: Index of the path segment to get + /// + /// # Examples + /// + /// ``` + /// let url = URL::new("/path/to/resource").unwrap(); + /// assert_eq!(url.get_path_segment(1), Some("to")); + /// ``` + pub fn get_path_segment(&self, index: usize) -> Option<&str> { + self.path.split('/').nth(index + 1) + } } pub struct Request {