feat: dynmaic request path handling

This commit is contained in:
Compositr 2024-11-01 21:14:47 +11:00
parent e8a30fb919
commit 8d8cfe33fa
Signed by: compositr
GPG key ID: 91E3DE20129A0B4A
2 changed files with 85 additions and 13 deletions

View file

@ -1,5 +1,5 @@
use crate::http::{ use crate::http::{
requests::{Request, RequestStatus}, requests::{Request, RequestStatus, URL},
responses::{Response, UnitOrBoxedError}, responses::{Response, UnitOrBoxedError},
}; };
use std::{ use std::{
@ -7,17 +7,19 @@ use std::{
net::{Shutdown, TcpListener}, net::{Shutdown, TcpListener},
}; };
type HandlerFn = fn(Request) -> Response<'static>;
// Collection of handlers for requests // Collection of handlers for requests
pub struct Handlers<FnT> { pub struct Handlers {
matchers: HashMap<String, FnT>, matchers: HashMap<String, HandlerFn>,
} }
impl<FnT> Handlers<FnT> impl Handlers {
where /// Create a new Handlers struct to handle requests
FnT: Fn(Request) -> Response<'static>, ///
{ /// !! This struct does nothing until you add handlers to it (add_handler) and then bind it to a TcpListener !!
pub fn new() -> Self { pub fn new() -> Self {
Handlers::<FnT> { Handlers {
matchers: HashMap::new(), matchers: HashMap::new(),
} }
} }
@ -28,7 +30,7 @@ where
/// ///
/// path: Path to match (no trailing /) /// path: Path to match (no trailing /)
/// handler: Function to handle the request. Must return a Response /// 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); self.matchers.insert(path.to_string(), handler);
} }
@ -63,10 +65,64 @@ where
} }
} }
fn match_handler(&self, url: &URL) -> Option<HandlerFn> {
'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::<Vec<&str>>();
let path_segments = path.split('/').collect::<Vec<&str>>();
// 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::<Vec<_>>()[0];
let suffix = path_segment.split(']').collect::<Vec<_>>()[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 { fn handle_req(&self, req: Request) -> UnitOrBoxedError {
match self.matchers.get(&req.url.path) { match self.match_handler(&req.url) {
Some(handler) => (*handler)(req).send(), Some(handler) => handler(req).send()?,
None => Response::new(req, 404, "Not Found").send(), None => {
} return Response::new(req, 404, "Not Found").send();
}
};
Ok(())
} }
} }

View file

@ -39,6 +39,22 @@ impl URL {
Some(URL { path, query, raw }) 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 { pub struct Request {