From f2cdc940374a2eef7cdae9a34c90a52e527a0874 Mon Sep 17 00:00:00 2001 From: omagdy7 Date: Thu, 6 Jun 2024 22:22:58 +0300 Subject: refactor: Changed the method struct signatrue and added a new general Endpoint struct for better ergonomic api --- src/handlers.rs | 149 +++++++++++++++++++++++++++++++++++++++++++++++ src/http_types.rs | 87 ++++++++++++++++++---------- src/lib.rs | 1 + src/main.rs | 170 ++---------------------------------------------------- src/request.rs | 16 ++--- src/router.rs | 76 +++++++++++++----------- src/server.rs | 2 +- 7 files changed, 263 insertions(+), 238 deletions(-) create mode 100644 src/handlers.rs (limited to 'src') diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..30b10e2 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,149 @@ +use crate::http_types::*; +use crate::request::*; +use crate::response::*; +use crate::utils::*; +use std::collections::HashMap; + +pub fn handle_echo(request: &Request, ctx: Option<&HashMap>) -> Response { + let mut headers = HashMap::new(); + let mut echo_string = "".to_string(); + let route = request.endpoint.route.clone(); + + let mut body = vec![]; + + 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(); + } + + if let Some(encoding) = request.get_tag("Accept-Encoding") { + if encoding.contains("gzip") { + headers.insert("Content-Encoding".to_string(), "gzip".to_string()); + match encode_gzip_string(echo_string.as_str()) { + Ok(encoded_bytes) => { + println!("In succses"); + let len = encoded_bytes.len(); + body = encoded_bytes; + headers.insert("Content-Length".to_string(), len.to_string()); + } + Err(err) => { + println!("In error {}", &echo_string); + println!("Error: {err}"); + } + } + } else { + let len = echo_string.len(); + headers.insert("Content-Length".to_string(), len.to_string()); + body = echo_string.as_bytes().to_owned(); + } + } else { + let len = echo_string.len(); + headers.insert("Content-Length".to_string(), len.to_string()); + body = echo_string.as_bytes().to_owned(); + } + + headers.insert("Content-Type".to_string(), "text/plain".to_string()); + + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) +} + +pub fn handle_post_files(request: &Request, ctx: Option<&HashMap>) -> Response { + // Extract the route regardless of the variant + let mut file = "".to_string(); + let route = request.endpoint.route.clone(); + + 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); + let bytes = request.body().as_ref().unwrap(); + let body = bytes.as_bytes(); + + match save_bytes_to_file(body, full_path) { + Ok(bytes) => Response::new("1.1".to_string(), StatusCode::Created, None, None), + Err(err) => { + println!("Error: {err}"); + Response::new("1.1".to_string(), StatusCode::NotFound, None, None) + } + } +} + +pub fn handle_files(request: &Request, ctx: Option<&HashMap>) -> Response { + // Extract the route regardless of the variant + let mut file = "".to_string(); + let route = request.endpoint.route.clone(); + + 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); + + 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 = bytes; + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) + } + Err(_) => Response::new("1.1".to_string(), StatusCode::NotFound, None, None), + } +} + +pub fn handle_user_agent(request: &Request, ctx: Option<&HashMap>) -> Response { + let mut headers = HashMap::new(); + if let Some(user_agent) = request.get_tag("User-Agent") { + 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.as_bytes().to_owned(); + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) + } else { + println!("User-Agent isn't present in headers"); + Response::new("1.1".to_string(), StatusCode::BadRequest, None, None) + } +} + +pub fn handle_success(request: &Request, ctx: Option<&HashMap>) -> Response { + Response::new("1.1".to_string(), StatusCode::Ok, None, None).into() +} + +pub fn handle_not_found(request: Request, ctx: Option<&HashMap>) -> Response { + Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() +} diff --git a/src/http_types.rs b/src/http_types.rs index 79bf015..44a9039 100644 --- a/src/http_types.rs +++ b/src/http_types.rs @@ -15,21 +15,48 @@ type Route = String; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Method { - Get(Route), - Post(Route), - Put(Route), + GET, + POST, + PUT, } -pub fn get(route: &str) -> Method { - Method::Get(route.to_string()) +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Endpoint { + pub method: Method, + pub route: Route, } -pub fn post(route: &str) -> Method { - Method::Post(route.to_string()) +impl Endpoint { + fn get(route: &str) -> Self { + Self { + method: Method::GET, + route: route.to_string(), + } + } + fn post(route: &str) -> Self { + Self { + method: Method::POST, + route: route.to_string(), + } + } + fn put(route: &str) -> Self { + Self { + method: Method::PUT, + route: route.to_string(), + } + } } -pub fn put(route: &str) -> Method { - Method::Put(route.to_string()) +pub fn get(route: &str) -> Endpoint { + Endpoint::get(route) +} + +pub fn post(route: &str) -> Endpoint { + Endpoint::post(route) +} + +pub fn put(route: &str) -> Endpoint { + Endpoint::put(route) } impl From for String { @@ -44,23 +71,23 @@ impl From for String { } } -impl From 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, +impl From for String { + fn from(val: Endpoint) -> Self { + use Method as M; + match (val.method, val.route) { + (M::GET, route) => "GET ".to_string() + &route, + (M::POST, route) => "GET ".to_string() + &route, + (M::PUT, route) => "GET ".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, +impl From<&Endpoint> for String { + fn from(val: &Endpoint) -> Self { + use Method as M; + match (&val.method, &val.route) { + (M::GET, route) => "GET ".to_string() + &route, + (M::POST, route) => "GET ".to_string() + &route, + (M::PUT, route) => "GET ".to_string() + &route, } } } @@ -94,14 +121,13 @@ impl From<&[&str]> for Headers { } } -impl From for Method { +impl From for Endpoint { 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()), + "GET" => Endpoint::get(route), + "POST" => Endpoint::post(route), _ => { eprintln!("{method} Not Supported Yet"); unreachable!() @@ -109,14 +135,13 @@ impl From for Method { } } } -impl From<&str> for Method { +impl From<&str> for Endpoint { fn from(val: &str) -> 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()), + "GET" => Endpoint::get(route), + "POST" => Endpoint::post(route), _ => { eprintln!("{method} Not Supported Yet"); unreachable!() diff --git a/src/lib.rs b/src/lib.rs index 9d58875..582b67e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod extractor; +pub mod handlers; pub mod http_types; pub mod request; pub mod response; diff --git a/src/main.rs b/src/main.rs index 250f94b..1b08605 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,170 +1,10 @@ -#![allow(unused)] +use http_server_starter_rust::handlers::*; +use http_server_starter_rust::http_types::{get, post}; use http_server_starter_rust::router::Router; -use itertools::Itertools; -use nom::AsBytes; -use std::collections::HashMap; -use std::fs::File; -use std::io::{self, Read, Write}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream}; -use std::str::Utf8Error; -use std::sync::{Arc, Mutex}; -use std::{str, thread, usize}; - -use http_server_starter_rust::request::*; -use http_server_starter_rust::response::*; use http_server_starter_rust::server::*; -use http_server_starter_rust::utils::*; -use http_server_starter_rust::{extractor, http_types::*}; - -fn handle_echo(request: &Request, ctx: Option<&HashMap>) -> Response { - let mut headers = HashMap::new(); - let mut echo_string = "".to_string(); - let route = match request.method() { - Method::Get(route) | Method::Post(route) | Method::Put(route) => route, - }; - - let mut body = vec![]; - - 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(); - } - - if let Some(encoding) = request.get_tag("Accept-Encoding") { - if encoding.contains("gzip") { - headers.insert("Content-Encoding".to_string(), "gzip".to_string()); - match encode_gzip_string(echo_string.as_str()) { - Ok(encoded_bytes) => { - println!("In succses"); - let len = encoded_bytes.len(); - body = encoded_bytes; - headers.insert("Content-Length".to_string(), len.to_string()); - } - Err(err) => { - println!("In error {}", &echo_string); - println!("Error: {err}"); - } - } - } else { - let len = echo_string.len(); - headers.insert("Content-Length".to_string(), len.to_string()); - body = echo_string.as_bytes().to_owned(); - } - } else { - let len = echo_string.len(); - headers.insert("Content-Length".to_string(), len.to_string()); - body = echo_string.as_bytes().to_owned(); - } - - headers.insert("Content-Type".to_string(), "text/plain".to_string()); - - Response::new( - "1.1".to_string(), - StatusCode::Ok, - Some(Headers(headers)), - Some(body), - ) -} - -fn handle_post_files(request: &Request, ctx: Option<&HashMap>) -> 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); - let bytes = request.body().as_ref().unwrap(); - let body = bytes.as_bytes(); - - match save_bytes_to_file(body, full_path) { - Ok(bytes) => Response::new("1.1".to_string(), StatusCode::Created, None, None), - Err(err) => { - println!("Error: {err}"); - Response::new("1.1".to_string(), StatusCode::NotFound, None, None) - } - } -} - -fn handle_files(request: &Request, ctx: Option<&HashMap>) -> 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); - - 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 = bytes; - 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>) -> Response { - let mut headers = HashMap::new(); - if let Some(user_agent) = request.get_tag("User-Agent") { - 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.as_bytes().to_owned(); - Response::new( - "1.1".to_string(), - StatusCode::Ok, - Some(Headers(headers)), - Some(body), - ) - } else { - println!("User-Agent isn't present in headers"); - Response::new("1.1".to_string(), StatusCode::BadRequest, None, None) - } -} - -fn handle_success(request: &Request, ctx: Option<&HashMap>) -> Response { - Response::new("1.1".to_string(), StatusCode::Ok, None, None).into() -} - -fn handle_not_found(request: Request, ctx: Option<&HashMap>) -> Response { - Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() -} +use std::collections::HashMap; +use std::io::{self}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; fn main() -> io::Result<()> { // Collect the command-line arguments diff --git a/src/request.rs b/src/request.rs index 7c8f43c..1858d5a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -4,13 +4,13 @@ use crate::http_types::*; #[derive(Debug, Clone)] pub struct Request { - pub method: Method, + pub endpoint: Endpoint, pub headers: Option, body: Option, } impl Request { - fn new(method: Method, headers: Headers, body: String) -> Self { + fn new(method: Endpoint, headers: Headers, body: String) -> Self { let headers = if headers.0.len() == 0 { None } else { @@ -18,7 +18,7 @@ impl Request { }; let body = if body.is_empty() { None } else { Some(body) }; Request { - method, + endpoint: method, headers, body, } @@ -28,8 +28,8 @@ impl Request { self.headers.as_ref().unwrap().0.get(&key.to_string()) } - pub fn method(&self) -> &Method { - &self.method + pub fn endpoint(&self) -> &Endpoint { + &self.endpoint } pub fn headers(&self) -> &Option { @@ -46,7 +46,7 @@ impl From> for Request { match &value[..] { [request_line, headers @ .., body] => { let (method, headers, body) = - (Method::from(*request_line), Headers::from(headers), body); + (Endpoint::from(*request_line), Headers::from(headers), body); if let Some(content_length) = headers.0.get("Content-Length") { let content_length = content_length .parse::() @@ -65,7 +65,7 @@ impl From> for Request { impl<'a> Into for Request { fn into(self) -> String { - let method = String::from(self.method); + let method = String::from(self.endpoint); let (method, endpoint) = method.split_once(" ").unwrap(); let status_line = format!("{} {} HTTP/1.1", method, endpoint); let headers = self @@ -82,7 +82,7 @@ impl<'a> Into for Request { impl Into for &Request { fn into(self) -> String { - let method = String::from(self.method.clone()); + let method = String::from(self.endpoint.clone()); let (method, endpoint) = method.split_once(" ").unwrap(); let status_line = format!("{} {} HTTP/1.1", method, endpoint); let headers = self diff --git a/src/router.rs b/src/router.rs index 7b1313b..ff9a893 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,6 +1,6 @@ use crate::{ extractor::build_regex_from_path, - http_types::{Method, StatusCode}, + http_types::{get, post, put, Endpoint, Method, StatusCode}, request::Request, response::Response, }; @@ -8,7 +8,7 @@ use regex::Regex; use std::collections::HashMap; type Handler = fn(&Request, Option<&HashMap>) -> Response; -type Routes = HashMap; +type Routes = HashMap; pub struct Router { routes: Routes, @@ -27,53 +27,63 @@ impl Router { } // Add a route to the router - pub fn route(&mut self, method: Method, handler: Handler) -> &mut Self { - use Method::*; - match method { - Get(route) => { + pub fn route(&mut self, endpoint: Endpoint, handler: Handler) -> &mut Self { + use Method as M; + match (endpoint.method, endpoint.route) { + (M::GET, route) => { let re = build_regex_from_path(&route); - let meth = Get(re.to_string()); - self.routes.insert(meth, handler); + let epoint = get(re.as_str()); + self.routes.insert(epoint, handler); + } + (M::POST, route) => { + let re = build_regex_from_path(&route); + let epoint = post(re.as_str()); + self.routes.insert(epoint, handler); } - Post(route) => { + (M::PUT, route) => { let re = build_regex_from_path(&route); - let meth = Post(re.to_string()); + let meth = put(re.as_str()); self.routes.insert(meth, handler); } - Put(_) => todo!(), } self } // Handle incoming requests pub fn handle(&self, request: &Request, ctx: Option<&HashMap>) -> Response { - use Method::*; - match &request.method { - Get(request_method) => { - for (method, handler) in self.routes() { - if let Get(method_string) = method { - let re = Regex::new(method_string).unwrap(); - // dbg!(&re, request_method); - if re.is_match(request_method) { - return handler(request, ctx); - } + use Method as M; + for (endpoint, handler) in self.routes() { + let repoint = &request.endpoint; + match ( + &repoint.method, + &repoint.route, + &endpoint.method, + &endpoint.route, + ) { + (M::GET, request_route, M::GET, endpoint_regex) => { + let re = Regex::new(&endpoint_regex).unwrap(); + // dbg!(&re, request_method); + if re.is_match(&request_route) { + return handler(request, ctx); } } - Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() - } - Post(request_method) => { - for (method, handler) in self.routes() { - if let Post(method_string) = method { - let re = Regex::new(method_string).unwrap(); - // dbg!(&re, request_method); - if re.is_match(request_method) { - return handler(request, ctx); - } + (M::POST, request_route, M::POST, endpoint_regex) => { + let re = Regex::new(&endpoint_regex).unwrap(); + // dbg!(&re, request_method); + if re.is_match(&request_route) { + return handler(request, ctx); + } + } + (M::PUT, request_route, M::PUT, endpoint_regex) => { + let re = Regex::new(&endpoint_regex).unwrap(); + // dbg!(&re, request_method); + if re.is_match(&request_route) { + return handler(request, ctx); } } - Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() + _ => {} } - Put(_) => todo!(), } + Response::new("1.1".to_string(), StatusCode::NotFound, None, None) } } diff --git a/src/server.rs b/src/server.rs index 9c60d33..6c0eeb1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -40,7 +40,7 @@ impl Server { let request_string: String = (&request).into(); println!("Request after parsing:\n{}", request_string); - dbg!(&request.method); + dbg!(&request.endpoint); let response: Vec = router.handle(&request, ctx).into(); stream.write(response.as_bytes()) -- cgit v1.2.3