aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock43
-rw-r--r--Cargo.toml1
-rw-r--r--codecrafters.yml2
-rw-r--r--src/extractor.rs44
-rw-r--r--src/http_types.rs89
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs268
-rw-r--r--src/request.rs75
-rw-r--r--src/response.rs53
-rw-r--r--src/router.rs62
10 files changed, 510 insertions, 129 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0f65e0a..931bb35 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "anyhow"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -101,6 +110,7 @@ dependencies = [
"itertools",
"nom",
"pretty_assertions",
+ "regex",
"thiserror",
"tokio",
]
@@ -132,9 +142,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "minimal-lexical"
@@ -258,6 +268,35 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 321f7f7..6a19fc8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ thiserror = "1.0.38" # error handling
tokio = { version = "1.23.0", features = ["full"] } # async networking
nom = "7.1.3" # parser combinators
itertools = "0.11.0" # General iterator helpers
+regex = "1.10.4"
[dev-dependencies]
pretty_assertions = "1.3.0" # nicer looking assertions
diff --git a/codecrafters.yml b/codecrafters.yml
index 92afff5..47b84d8 100644
--- a/codecrafters.yml
+++ b/codecrafters.yml
@@ -2,7 +2,7 @@
#
# These can be VERY verbose, so we suggest turning them off
# unless you really need them.
-debug: false
+debug: true
# Use this to change the Rust version used to run your code
# on Codecrafters.
diff --git a/src/extractor.rs b/src/extractor.rs
new file mode 100644
index 0000000..edc7b2e
--- /dev/null
+++ b/src/extractor.rs
@@ -0,0 +1,44 @@
+use regex::{escape, Regex};
+
+pub fn build_regex_from_path(path_template: &str) -> Regex {
+ // Escape literal parts of the path to safely convert to regex
+ let mut regex_string = "^".to_string();
+ for component in path_template.split('/') {
+ if component.starts_with(':') {
+ // Replace placeholder with regex to capture alphanumeric, underscores, or hyphens
+ regex_string.push_str("/([a-zA-Z0-9_-]+)");
+ } else if !component.is_empty() {
+ // Escape and add literal components to the regex
+ regex_string.push('/');
+ regex_string.push_str(&escape(component));
+ }
+ }
+ regex_string.push_str("/?$");
+
+ // Compile the regex
+ Regex::new(&regex_string).unwrap()
+}
+
+// pub fn match_path(method: &Method) -> String {
+// use Method::*;
+// match method {
+// Get(route) => {
+// match re.captures(route) {
+// Some(caps) => {
+// println!("Matched route: {}", route);
+// // Iterate over the captures to extract the path segments and parameters
+// for cap in caps.iter().flatten().skip(1) {
+// println!("Segment: {}", cap.as_str());
+// }
+// "empty".to_string()
+// }
+// None => {
+// println!("No match for route: {}", route);
+// "empty none".to_string()
+// }
+// }
+// }
+// Post(_) => todo!(),
+// Put(_) => todo!(),
+// }
+// }
diff --git a/src/http_types.rs b/src/http_types.rs
index a652e6c..a196f25 100644
--- a/src/http_types.rs
+++ b/src/http_types.rs
@@ -10,14 +10,25 @@ pub enum StatusCode {
NotFound,
}
-type Endpoint = String;
-type Target = String;
+type Route = String;
-#[derive(Debug)]
-pub enum HTTPMethod {
- Get((Endpoint, Target)),
- Post((Endpoint, Target)),
- Put((Endpoint, Target)),
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum Method {
+ Get(Route),
+ Post(Route),
+ Put(Route),
+}
+
+pub fn get(route: &str) -> Method {
+ Method::Get(route.to_string())
+}
+
+pub fn post(route: &str) -> Method {
+ Method::Post(route.to_string())
+}
+
+pub fn put(route: &str) -> Method {
+ Method::Put(route.to_string())
}
impl From<StatusCode> for String {
@@ -31,20 +42,43 @@ impl From<StatusCode> for String {
}
}
-impl From<HTTPMethod> for String {
- fn from(val: HTTPMethod) -> Self {
- use HTTPMethod::*;
+impl From<Method> for String {
+ fn from(val: Method) -> Self {
+ use Method::*;
match val {
- Get((endpoint, target)) => "GET".to_string() + &endpoint + &target,
- Post((endpoint, target)) => "POST".to_string() + &endpoint + &target,
- Put((endpoint, target)) => "PUT".to_string() + &endpoint + &target,
+ Get(route) => "GET ".to_string() + &route,
+ Post(route) => "POST ".to_string() + &route,
+ Put(route) => "PUT ".to_string() + &route,
+ }
+ }
+}
+impl From<&Method> for String {
+ fn from(val: &Method) -> Self {
+ use Method::*;
+ match val {
+ Get(route) => "GET ".to_string() + &route,
+ Post(route) => "POST ".to_string() + &route,
+ Put(route) => "PUT ".to_string() + &route,
}
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Headers(pub HashMap<String, String>);
+impl From<Vec<String>> for Headers {
+ fn from(value: Vec<String>) -> Self {
+ let mut header_map = HashMap::new();
+ for header in value.iter().filter(|val| !val.is_empty()) {
+ let (key, val) = header
+ .split_once(": ")
+ .expect("Should be splitable by :<space>");
+ header_map.insert(key.to_string(), val.to_string());
+ }
+ Headers(header_map)
+ }
+}
+
impl From<&[&str]> for Headers {
fn from(value: &[&str]) -> Self {
let mut header_map = HashMap::new();
@@ -58,16 +92,29 @@ impl From<&[&str]> for Headers {
}
}
-impl From<&str> for HTTPMethod {
+impl From<String> for Method {
+ fn from(val: String) -> Self {
+ use Method::*;
+ let request_line = val.split(' ').collect_vec();
+ let (method, route) = (request_line[0], request_line[1]);
+ match method {
+ "GET" => Get(route.to_string()),
+ "POST" => Post(route.to_string()),
+ _ => {
+ eprintln!("{method} Not Supported Yet");
+ unreachable!()
+ }
+ }
+ }
+}
+impl From<&str> for Method {
fn from(val: &str) -> Self {
- use HTTPMethod::*;
+ use Method::*;
let request_line = val.split(' ').collect_vec();
- let (method, info) = (request_line[0], request_line[1]);
- let info = info.chars().skip(1).collect::<String>() + &"/";
- let (endpoint, target) = info.split_once("/").expect("Should be splitable by /");
+ let (method, route) = (request_line[0], request_line[1]);
match method {
- "GET" => Get((endpoint.to_string(), target.to_string())),
- "POST" => Post((endpoint.to_string(), target.to_string())),
+ "GET" => Get(route.to_string()),
+ "POST" => Post(route.to_string()),
_ => {
eprintln!("{method} Not Supported Yet");
unreachable!()
diff --git a/src/lib.rs b/src/lib.rs
index 4667e1d..f9749a7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,5 @@
+pub mod extractor;
pub mod http_types;
pub mod request;
pub mod response;
+pub mod router;
diff --git a/src/main.rs b/src/main.rs
index 1ad6f26..ccdaa37 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,128 @@
#![allow(unused)]
+use http_server_starter_rust::router::Router;
use itertools::Itertools;
use std::collections::HashMap;
+use std::fs::File;
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
-use std::{str, thread};
+use std::str::Utf8Error;
+use std::sync::{Arc, Mutex};
+use std::{str, thread, usize};
-use http_server_starter_rust::http_types::*;
use http_server_starter_rust::request::*;
use http_server_starter_rust::response::*;
+use http_server_starter_rust::{extractor, http_types::*};
-fn handle_client(mut stream: TcpStream) {
+fn read_file_as_bytes(path: &str) -> io::Result<Vec<u8>> {
+ // Open the file in read-only mode
+ let mut file = File::open(path)?;
+
+ // Create a buffer to hold the file contents
+ let mut buffer = Vec::new();
+
+ // Read the file contents into the buffer
+ file.read_to_end(&mut buffer)?;
+
+ // Return the buffer
+ Ok(buffer)
+}
+
+fn handle_echo(request: &Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ let mut headers = HashMap::new();
+ // Extract the route regardless of the variant
+ let mut echo_string = "".to_string();
+ let route = match request.method() {
+ Method::Get(route) | Method::Post(route) | Method::Put(route) => route,
+ };
+
+ for ch in route.chars().skip(1).skip_while(|&ch| ch != '/').skip(1) {
+ echo_string.push(ch);
+ }
+ if echo_string.chars().last().unwrap() == '/' {
+ echo_string.pop();
+ }
+ let len = echo_string.len().to_string();
+ headers.insert("Content-Type".to_string(), "text/plain".to_string());
+ headers.insert("Content-Length".to_string(), len);
+ let body = echo_string;
+ Response::new(
+ "1.1".to_string(),
+ StatusCode::Ok,
+ Some(Headers(headers)),
+ Some(body),
+ )
+}
+
+fn handle_files(request: &Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ // Extract the route regardless of the variant
+ let mut file = "".to_string();
+ let route = match request.method() {
+ Method::Get(route) | Method::Post(route) | Method::Put(route) => route,
+ };
+
+ let mut directory = ctx.unwrap().get(&"dir".to_string()).unwrap().clone();
+ directory.pop(); // remove last slash
+
+ for ch in route.chars().skip(1).skip_while(|&ch| ch != '/') {
+ file.push(ch);
+ }
+ if file.chars().last().unwrap() == '/' {
+ file.pop();
+ }
+ let len = file.len().to_string();
+
+ let full_path = &(directory + &file);
+ dbg!(full_path);
+
+ match read_file_as_bytes(full_path) {
+ Ok(bytes) => {
+ let mut headers = HashMap::new();
+ headers.insert(
+ "Content-Type".to_string(),
+ "application/octet-stream".to_string(),
+ );
+ headers.insert("Content-Length".to_string(), bytes.len().to_string());
+ let body = String::from_utf8(bytes).unwrap();
+ Response::new(
+ "1.1".to_string(),
+ StatusCode::Ok,
+ Some(Headers(headers)),
+ Some(body),
+ )
+ }
+ Err(_) => Response::new("1.1".to_string(), StatusCode::NotFound, None, None),
+ }
+}
+
+fn handle_user_agent(request: &Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ let mut headers = HashMap::new();
+ let user_agent = request.get_tag("User-Agent".to_string());
+ let len = user_agent.len().to_string();
+ headers.insert("Content-Type".to_string(), "text/plain".to_string());
+ headers.insert("Content-Length".to_string(), len);
+ let body = user_agent.to_string();
+ Response::new(
+ "1.1".to_string(),
+ StatusCode::Ok,
+ Some(Headers(headers)),
+ Some(body),
+ )
+ .into()
+}
+
+fn handle_success(request: &Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ Response::new("1.1".to_string(), StatusCode::Ok, None, None).into()
+}
+
+fn handle_not_found(request: Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into()
+}
+
+fn serve(
+ mut stream: TcpStream,
+ router: Arc<Mutex<Router>>,
+ ctx: Arc<Mutex<HashMap<String, String>>>,
+) -> io::Result<usize> {
// Buffer to store the data received from the client
let mut buffer = [0; 512];
@@ -17,107 +130,78 @@ fn handle_client(mut stream: TcpStream) {
match stream.read(&mut buffer) {
Ok(_) => {
// Convert buffer to a string and print the received data
- if let Ok(request) = str::from_utf8(&buffer) {
- println!("Received request: {}", request);
- let request = Request::from(request);
- println!("Request after parsing: {:?}", request);
- let succses: String =
- Response::new("1.1".to_string(), StatusCode::Ok, None, None).into();
- let succses = succses.as_bytes();
-
- let not_found: String =
- Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into();
- let not_found = not_found.as_bytes();
-
- match request.method {
- HTTPMethod::Get((endpoint, target)) => {
- match (endpoint.as_str(), target.as_str()) {
- ("", "") => match stream.write(succses) {
- Ok(_) => {
- println!("Response sent successfully");
- }
- Err(e) => eprintln!("Failed to send response: {}", e),
- },
- ("echo", target) => {
- let mut headers = HashMap::new();
- headers
- .insert("Content-Type".to_string(), "text/plain".to_string());
- headers.insert(
- "Content-Length".to_string(),
- (target.len() - 1).to_string(),
- );
- let body = target[0..target.len() - 1].to_string();
- let response: String = Response::new(
- "1.1".to_string(),
- StatusCode::Ok,
- Some(Headers(headers)),
- Some(body),
- )
- .into();
- dbg!(&response);
- let response = response.as_bytes();
-
- match stream.write(response) {
- Ok(_) => {
- println!("Response sent successfully");
- println!("Hello echo");
- }
- Err(e) => eprintln!("Failed to send response: {}", e),
- }
- }
- ("user-agent", _) => {
- let mut headers = HashMap::new();
- let user_agent: String =
- request.headers.unwrap().0.get("User-Agent").unwrap().into();
- headers
- .insert("Content-Type".to_string(), "text/plain".to_string());
- headers.insert(
- "Content-Length".to_string(),
- user_agent.len().to_string(),
- );
- let body = user_agent;
- let response: String = Response::new(
- "1.1".to_string(),
- StatusCode::Ok,
- Some(Headers(headers)),
- Some(body),
- )
- .into();
- dbg!(&response);
- let response = response.as_bytes();
-
- match stream.write(response) {
- Ok(_) => {
- println!("Response sent successfully");
- println!("Hello user-agent");
- }
- Err(e) => eprintln!("Failed to send response: {}", e),
- }
- }
- _ => match stream.write(not_found) {
- Ok(_) => println!("Response sent successfully"),
- Err(e) => eprintln!("Failed to send response: {}", e),
- },
- }
- }
- HTTPMethod::Post(_target) => todo!(),
- HTTPMethod::Put(_target) => todo!(),
+ match str::from_utf8(&buffer) {
+ Ok(request) => {
+ use Method::*;
+ println!("Received request:\n{}", request);
+ let request_lines: Vec<&str> = request.split("\r\n").collect();
+ let request = Request::from(request_lines);
+ let request_string: String = (&request).into();
+
+ println!("Request after parsing:\n{}", request_string);
+ dbg!(&request.method);
+
+ let response: String = {
+ let router = router.lock().unwrap();
+ let ctx = ctx.lock().unwrap();
+ router.handle(&request, Some(&ctx)).into()
+ };
+ stream.write(response.as_bytes())
}
+ Err(_) => todo!(),
}
}
- Err(e) => {
- eprintln!("Failed to read from stream: {}", e);
- }
+ Err(_) => todo!(),
}
}
fn main() -> io::Result<()> {
+ // Collect the command-line arguments
+ let args: Vec<String> = std::env::args().collect();
+
+ let mut dir = "".to_string();
+
+ let ctx = Arc::new(Mutex::new(HashMap::new()));
+
+ // Check if the correct number of arguments are provided
+ if args.len() == 3 {
+ // Parse the arguments
+ if args[1] == "--directory" {
+ dir += &args[2];
+ println!("Directory: {}", dir);
+ } else {
+ eprintln!("Unknown argument: {}", args[1]);
+ eprintln!("Usage: {} --directory <path>", args[0]);
+ return Ok(());
+ }
+ } else {
+ eprintln!("Usage: {} --directory <path>", args[0]);
+ }
+
let listener = TcpListener::bind("127.0.0.1:4221").unwrap();
+ let router = Arc::new(Mutex::new(Router::new()));
+ ctx.lock().unwrap().insert("dir".to_string(), dir);
+
+ {
+ let mut router = router.lock().unwrap();
+ router
+ .route(get("/"), handle_success)
+ .route(get("/echo/:var/"), handle_echo)
+ .route(get("/user-agent/"), handle_user_agent)
+ .route(get("/files/:file/"), handle_files);
+ }
+
for stream in listener.incoming() {
match stream {
Ok(stream) => {
- thread::spawn(move || handle_client(stream));
+ let router = Arc::clone(&router);
+ let ctx = Arc::clone(&ctx);
+ thread::spawn(move || {
+ if let Err(e) = serve(stream, router, ctx) {
+ eprintln!("Failed to serve connection: {}", e);
+ }
+ });
}
Err(e) => {
eprintln!("error: {}", e);
diff --git a/src/request.rs b/src/request.rs
index 8728e79..27a41f5 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -1,14 +1,16 @@
+use std::collections::HashMap;
+
use crate::http_types::*;
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Request {
- pub method: HTTPMethod,
+ pub method: Method,
pub headers: Option<Headers>,
body: Option<String>,
}
impl Request {
- fn new(method: HTTPMethod, headers: Headers, body: String) -> Self {
+ fn new(method: Method, headers: Headers, body: String) -> Self {
let headers = if headers.0.len() == 0 {
None
} else {
@@ -21,19 +23,31 @@ impl Request {
body,
}
}
+
+ pub fn get_tag(&self, key: String) -> String {
+ self.headers.as_ref().unwrap().0.get(&key).unwrap().clone()
+ }
+
+ pub fn method(&self) -> &Method {
+ &self.method
+ }
+
+ pub fn headers(&self) -> &Option<Headers> {
+ &self.headers
+ }
+
+ pub fn body(&self) -> &Option<String> {
+ &self.body
+ }
}
-impl From<&str> for Request {
- fn from(val: &str) -> Self {
- let request: Vec<&str> = val.split("\r\n").collect();
- match &request[..] {
+impl From<Vec<&str>> for Request {
+ fn from(value: Vec<&str>) -> Self {
+ match &value[..] {
[request_line, headers @ .., body] => {
- let (method, headers, body) = (
- HTTPMethod::from(*request_line),
- Headers::from(headers),
- body.to_string(),
- );
- Request::new(method, headers, body)
+ let (method, headers, body) =
+ (Method::from(*request_line), Headers::from(headers), body);
+ Request::new(method, headers, (*body).to_string())
}
_ => {
unreachable!();
@@ -41,3 +55,38 @@ impl From<&str> for Request {
}
}
}
+
+impl<'a> Into<String> for Request {
+ fn into(self) -> String {
+ let method = String::from(self.method);
+ let (method, endpoint) = method.split_once(" ").unwrap();
+ let status_line = format!("{} {} HTTP/1.1", method, endpoint);
+ let headers = self
+ .headers
+ .unwrap_or(Headers(HashMap::new()))
+ .0
+ .iter()
+ .map(|(key, value)| format!("{key}: {value}\r\n"))
+ .collect::<String>();
+ let body = self.body.unwrap_or("".to_string());
+ format!("{status_line}\r\n{headers}\r\n{body}")
+ }
+}
+
+impl Into<String> for &Request {
+ fn into(self) -> String {
+ let method = String::from(self.method.clone());
+ let (method, endpoint) = method.split_once(" ").unwrap();
+ let status_line = format!("{} {} HTTP/1.1", method, endpoint);
+ let headers = self
+ .headers()
+ .clone()
+ .unwrap_or(Headers(HashMap::new()))
+ .0
+ .iter()
+ .map(|(key, value)| format!("{key}: {value}\r\n"))
+ .collect::<String>();
+ let body = self.body.clone().unwrap_or("".to_string());
+ format!("{status_line}\r\n{headers}\r\n{body}")
+ }
+}
diff --git a/src/response.rs b/src/response.rs
index 4219afd..c3540d2 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -38,3 +38,56 @@ impl Into<String> for Response {
format!("{status_line}\r\n{headers}\r\n{body}")
}
}
+
+impl Into<Response> for &str {
+ fn into(self) -> Response {
+ let mut lines = self.lines();
+
+ // Parse the status line
+ let status_line = lines.next().unwrap_or_default();
+ let mut status_line_parts = status_line.split_whitespace();
+
+ let version = status_line_parts.next().unwrap_or_default().to_string();
+ let status_code = status_line_parts.next().unwrap_or_default();
+
+ let status = match status_code {
+ "200" => StatusCode::Ok,
+ "404" => StatusCode::NotFound,
+ _ => StatusCode::Ok,
+ };
+
+ // Parse headers
+ let mut headers_map = HashMap::new();
+ while let Some(line) = lines.next() {
+ if line.is_empty() {
+ break;
+ }
+ let mut parts = line.splitn(2, ':');
+ let key = parts.next().unwrap_or_default().trim();
+ let value = parts.next().unwrap_or_default().trim();
+ headers_map.insert(key.to_string(), value.to_string());
+ }
+ let headers = if headers_map.is_empty() {
+ None
+ } else {
+ Some(Headers(headers_map))
+ };
+
+ // Parse body
+ let body: Option<String> = {
+ let remaining_lines: Vec<&str> = lines.collect();
+ if remaining_lines.is_empty() {
+ None
+ } else {
+ Some(remaining_lines.join("\n"))
+ }
+ };
+
+ Response {
+ version,
+ status,
+ headers,
+ body,
+ }
+ }
+}
diff --git a/src/router.rs b/src/router.rs
new file mode 100644
index 0000000..1d8e6f2
--- /dev/null
+++ b/src/router.rs
@@ -0,0 +1,62 @@
+use crate::{
+ extractor::build_regex_from_path,
+ http_types::{Method, StatusCode},
+ request::Request,
+ response::Response,
+};
+use regex::Regex;
+use std::collections::HashMap;
+
+type Handler = fn(&Request, Option<&HashMap<String, String>>) -> Response;
+type Routes = HashMap<Method, Handler>;
+
+pub struct Router {
+ routes: Routes,
+}
+
+impl Router {
+ // Create a new Router
+ pub fn new() -> Self {
+ Router {
+ routes: HashMap::new(),
+ }
+ }
+
+ pub fn routes(&self) -> &Routes {
+ &self.routes
+ }
+
+ // Add a route to the router
+ pub fn route(&mut self, method: Method, handler: Handler) -> &mut Self {
+ use Method::*;
+
+ let method_string = match &method {
+ Get(s) | Post(s) | Put(s) => s,
+ };
+
+ let re = build_regex_from_path(method_string.as_str());
+ let meth = Get(re.to_string());
+ dbg!(&meth);
+ self.routes.insert(meth, handler);
+ self
+ }
+
+ // Handle incoming requests
+ pub fn handle(&self, request: &Request, ctx: Option<&HashMap<String, String>>) -> Response {
+ use Method::*;
+ let method_string = match &request.method {
+ Get(s) | Post(s) | Put(s) => s,
+ };
+ for (method, handler) in self.routes() {
+ let route_method = match method {
+ Get(s) | Post(s) | Put(s) => s.as_str(),
+ };
+ let re = Regex::new(route_method).unwrap();
+ dbg!(&re, method_string);
+ if re.is_match(method_string) {
+ return handler(request, ctx);
+ }
+ }
+ Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into()
+ }
+}