Compare commits
	
		
			No commits in common. "master" and "v0.1.0" have entirely different histories.
		
	
	
		
	
		
| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "http" | name = "http" | ||||||
| version = "0.1.6" | version = "0.1.0" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							| @ -1,27 +1,3 @@ | |||||||
| # http | # http | ||||||
| 
 | 
 | ||||||
| A basic Rust library to parse an HTTP request and build HTTP response. | A basic Rust lib to parse an HTTP request and build HTTP response. | ||||||
| 
 |  | ||||||
| **NOTE**: only few parts of the specification has been implemented and only JSON body are allowed. |  | ||||||
| 
 |  | ||||||
| ## Integration |  | ||||||
| Get the latest version: |  | ||||||
| ```toml |  | ||||||
| http = { git = "https://gitea.thegux.fr/rmanach/http" } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Or get a specific version: |  | ||||||
| ```toml |  | ||||||
| http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.2" } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Documentation |  | ||||||
| ```bash |  | ||||||
| cargo doc -r --no-deps --open |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Launch unit tests |  | ||||||
| ```bash |  | ||||||
| cargo test |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -7,8 +7,8 @@ pub struct HTTPBody { | |||||||
|     data: JsonValue, |     data: JsonValue, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// HTTPBody represents an HTTP request body.
 | /// HTTPBody represents an HTTP request body
 | ||||||
| /// For simplicity, only JSON body is allowed.
 | /// for simplicity, only JSON body is allowed
 | ||||||
| impl HTTPBody { | impl HTTPBody { | ||||||
|     fn new(data: JsonValue) -> HTTPBody { |     fn new(data: JsonValue) -> HTTPBody { | ||||||
|         HTTPBody { data } |         HTTPBody { data } | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/lib.rs
									
									
									
									
									
								
							| @ -1,26 +1,18 @@ | |||||||
| //! **http** library is a light HTTP parser and builder.
 | //! http parses the request according to the HTTP message specifications
 | ||||||
|  | //! it also includes `HTTPResponse` to build an HTTPResponse
 | ||||||
| //!
 | //!
 | ||||||
| //! It parses the request according to the HTTP message specifications and includes `HTTPResponse` to build an HTTP Response.
 | //! see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 | ||||||
|  | //! NOTE: only few parts of the specification has been implemented
 | ||||||
| //!
 | //!
 | ||||||
| //! See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages> for more details.
 | //! * Only json body allowed
 | ||||||
| //!
 |  | ||||||
| //! NOTE: only few parts of the specification has been implemented.
 |  | ||||||
| //!
 |  | ||||||
| //! * Only JSON body allowed
 |  | ||||||
| //! * HTTP Headers not parsed
 | //! * HTTP Headers not parsed
 | ||||||
| 
 | 
 | ||||||
| mod body; | mod body; | ||||||
| mod message; |  | ||||||
| mod request; | mod request; | ||||||
| mod response; |  | ||||||
| mod start_line; | mod start_line; | ||||||
| mod status; |  | ||||||
| mod version; | mod version; | ||||||
| 
 | 
 | ||||||
| pub use body::HTTPBody; | pub use body::HTTPBody; | ||||||
| pub use message::JSONMessage; |  | ||||||
| pub use request::HTTPRequest; | pub use request::HTTPRequest; | ||||||
| pub use response::HTTPResponse; |  | ||||||
| pub use start_line::HTTPStartLine; | pub use start_line::HTTPStartLine; | ||||||
| pub use status::{HTTPStatusCode, HTTPStatusLine}; |  | ||||||
| pub use version::HTTPVersion; | pub use version::HTTPVersion; | ||||||
|  | |||||||
| @ -1,94 +0,0 @@ | |||||||
| use json::JsonValue; |  | ||||||
| use std::collections::HashMap; |  | ||||||
| 
 |  | ||||||
| const JSON_DELIMITER: &'static str = ","; |  | ||||||
| 
 |  | ||||||
| /// JSONMessage aims to build a JSON object from an `HashMap`
 |  | ||||||
| pub struct JSONMessage { |  | ||||||
|     message: HashMap<String, String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for JSONMessage { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         JSONMessage { |  | ||||||
|             message: HashMap::new(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// try to convert `JSONMessage` into `json::JsonValue`
 |  | ||||||
| impl TryInto<JsonValue> for JSONMessage { |  | ||||||
|     type Error = String; |  | ||||||
|     fn try_into(self) -> Result<JsonValue, Self::Error> { |  | ||||||
|         let message = format!(r#"{{{}}}"#, self.build_json()); |  | ||||||
|         match json::parse(&message) { |  | ||||||
|             Ok(r) => Ok(r), |  | ||||||
|             Err(e) => Err(format!( |  | ||||||
|                 "unable to parse the HTTPMessage correctly: {}, err={}", |  | ||||||
|                 message, e |  | ||||||
|             )), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl JSONMessage { |  | ||||||
|     pub fn put(&mut self, key: &str, value: &str) { |  | ||||||
|         self.message.insert(key.to_string(), value.to_string()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // associated function to build an HTTPMessage error
 |  | ||||||
|     pub fn error(message: &str) -> Option<JsonValue> { |  | ||||||
|         let mut http_message = JSONMessage::default(); |  | ||||||
|         http_message.put("error", message); |  | ||||||
| 
 |  | ||||||
|         match message.try_into() { |  | ||||||
|             Ok(m) => Some(m), |  | ||||||
|             Err(e) => { |  | ||||||
|                 log::error!( |  | ||||||
|                     "unable to parse the message: {} into JSON, err={}", |  | ||||||
|                     message, |  | ||||||
|                     e |  | ||||||
|                 ); |  | ||||||
|                 return None; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// build_json loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER`
 |  | ||||||
|     fn build_json(self) -> String { |  | ||||||
|         let unstruct: Vec<String> = self |  | ||||||
|             .message |  | ||||||
|             .keys() |  | ||||||
|             .map(|k| format!(r#""{}":{:?}"#, k, self.message.get(k).unwrap())) |  | ||||||
|             .collect(); |  | ||||||
|         let joined = unstruct.join(JSON_DELIMITER); |  | ||||||
|         joined |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_message() { |  | ||||||
|     let mut message = JSONMessage::default(); |  | ||||||
|     message.put("email", "toto@toto.fr"); |  | ||||||
|     message.put("password", "tata"); |  | ||||||
| 
 |  | ||||||
|     let mut json_result: Result<JsonValue, String> = message.try_into(); |  | ||||||
|     assert!(json_result.is_ok()); |  | ||||||
| 
 |  | ||||||
|     let mut json = json_result.unwrap(); |  | ||||||
|     assert!(json.has_key("email")); |  | ||||||
|     assert!(json.has_key("password")); |  | ||||||
| 
 |  | ||||||
|     let empty_message = JSONMessage::default(); |  | ||||||
|     json_result = empty_message.try_into(); |  | ||||||
|     assert!(json_result.is_ok()); |  | ||||||
| 
 |  | ||||||
|     json = json_result.unwrap(); |  | ||||||
|     assert_eq!("{}", json.dump().to_string()); |  | ||||||
| 
 |  | ||||||
|     let mut bad_message = JSONMessage::default(); |  | ||||||
|     bad_message.put("\"", ""); |  | ||||||
| 
 |  | ||||||
|     json_result = bad_message.try_into(); |  | ||||||
|     assert!(json_result.is_err()); |  | ||||||
| } |  | ||||||
| @ -16,14 +16,14 @@ pub struct HTTPRequest<'a> { | |||||||
| 
 | 
 | ||||||
| impl<'a> HTTPRequest<'a> { | impl<'a> HTTPRequest<'a> { | ||||||
|     /// get_request_parts splits correctly the incoming request in order to get:
 |     /// get_request_parts splits correctly the incoming request in order to get:
 | ||||||
|     /// * start line
 |     /// * start_line
 | ||||||
|     /// * headers
 |     /// * headers
 | ||||||
|     /// * data (if exists)
 |     /// * data (if exists)
 | ||||||
|     fn get_request_parts(request: &str) -> Result<RequestParts, &str> { |     fn get_request_parts(request: &str) -> Result<RequestParts, &str> { | ||||||
|         let mut request_parts: VecDeque<&str> = request.split(HTTP_REQUEST_SEPARATOR).collect(); |         let mut request_parts: VecDeque<&str> = request.split(HTTP_REQUEST_SEPARATOR).collect(); | ||||||
| 
 | 
 | ||||||
|         if request_parts.len() < 3 { |         if request_parts.len() < 3 { | ||||||
|             return Err("request has not enough informations to be correctly parsed"); |             return Err("request has no enough informations to be correctly parsed"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let start_line = request_parts.pop_front().unwrap(); |         let start_line = request_parts.pop_front().unwrap(); | ||||||
| @ -55,7 +55,7 @@ impl<'a> HTTPRequest<'a> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// get_body_value retrieves JSON value in `HTTPBody`
 |     /// get_bodyèvalue retrieves JSON value in `HTTPBody`
 | ||||||
|     pub fn get_body_value(&self, key: &str) -> Option<String> { |     pub fn get_body_value(&self, key: &str) -> Option<String> { | ||||||
|         match self.body { |         match self.body { | ||||||
|             Some(ref b) => match &b.get_data() { |             Some(ref b) => match &b.get_data() { | ||||||
| @ -66,14 +66,6 @@ impl<'a> HTTPRequest<'a> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// get_body returns the body data as JsonValue
 |  | ||||||
|     pub fn get_body(&self) -> Option<&JsonValue> { |  | ||||||
|         match self.body { |  | ||||||
|             Some(ref b) => Some(b.get_data()), |  | ||||||
|             None => None, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_target(&self) -> &str { |     pub fn get_target(&self) -> &str { | ||||||
|         self.start_line.get_target() |         self.start_line.get_target() | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										117
									
								
								src/response.rs
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								src/response.rs
									
									
									
									
									
								
							| @ -1,117 +0,0 @@ | |||||||
| use json::JsonValue; |  | ||||||
| 
 |  | ||||||
| use crate::{HTTPStatusCode, HTTPStatusLine}; |  | ||||||
| 
 |  | ||||||
| const UNEXPECTED_ERROR: &'static str = r#"{"error":"unexpected error occurred"}"#; |  | ||||||
| const NOT_FOUND_ERROR: &'static str = r#"{"error":"url not found"}"#; |  | ||||||
| const FORBIDDEN_ERROR: &'static str = r#"{"error":"url forbidden"}"#; |  | ||||||
| const BAD_REQUEST_ERROR: &'static str = r#"{"error":"bad request"}"#; |  | ||||||
| 
 |  | ||||||
| const STATUS_OK: &'static str = r#"{"status":"ok"}"#; |  | ||||||
| 
 |  | ||||||
| /// HTTPResponse represents an HTTP response (headers are not parsed)
 |  | ||||||
| ///
 |  | ||||||
| /// NOTE: for simplicity, only JSON body are allowed
 |  | ||||||
| pub struct HTTPResponse { |  | ||||||
|     status_line: HTTPStatusLine, |  | ||||||
|     body: JsonValue, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPResponse { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         let body: String = json::stringify(self.body.clone()); |  | ||||||
|         let status_line: String = self.status_line.into(); |  | ||||||
|         format!( |  | ||||||
|             "{}\r\nContent-Type: application/json\r\nContent-Length:{}\r\n\r\n{}", |  | ||||||
|             status_line, |  | ||||||
|             body.len(), |  | ||||||
|             body |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPResponse { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         HTTPResponse { |  | ||||||
|             status_line: HTTPStatusLine::default(), |  | ||||||
|             body: json::parse("{}").unwrap(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPResponse { |  | ||||||
|     pub fn get_status_code(&self) -> HTTPStatusCode { |  | ||||||
|         self.status_line.get_status_code() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_500(message: Option<json::JsonValue>) -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http500); |  | ||||||
| 
 |  | ||||||
|         response.body = { |  | ||||||
|             match message { |  | ||||||
|                 Some(m) => m, |  | ||||||
|                 None => json::parse(UNEXPECTED_ERROR).unwrap(), |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_404() -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http404); |  | ||||||
|         response.body = json::parse(NOT_FOUND_ERROR).unwrap(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_403() -> Self { |  | ||||||
|         let mut response = HTTPResponse { |  | ||||||
|             status_line: HTTPStatusLine::default(), |  | ||||||
|             body: json::parse(FORBIDDEN_ERROR).unwrap(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http403); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_400() -> Self { |  | ||||||
|         let mut response = HTTPResponse { |  | ||||||
|             status_line: HTTPStatusLine::default(), |  | ||||||
|             body: json::parse(BAD_REQUEST_ERROR).unwrap(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http400); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_200(message: Option<json::JsonValue>) -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http200); |  | ||||||
| 
 |  | ||||||
|         response.body = { |  | ||||||
|             match message { |  | ||||||
|                 Some(m) => m, |  | ||||||
|                 None => json::parse(STATUS_OK).unwrap(), |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,54 +0,0 @@ | |||||||
| use crate::HTTPVersion; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, PartialEq, Clone, Copy)] |  | ||||||
| pub enum HTTPStatusCode { |  | ||||||
|     Http200, |  | ||||||
|     Http400, |  | ||||||
|     Http403, |  | ||||||
|     Http404, |  | ||||||
|     Http500, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPStatusCode { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         match self { |  | ||||||
|             Self::Http200 => "200".to_string(), |  | ||||||
|             Self::Http400 => "400".to_string(), |  | ||||||
|             Self::Http404 => "404".to_string(), |  | ||||||
|             Self::Http403 => "403".to_string(), |  | ||||||
|             Self::Http500 => "500".to_string(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct HTTPStatusLine { |  | ||||||
|     version: HTTPVersion, |  | ||||||
|     status_code: HTTPStatusCode, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPStatusLine { |  | ||||||
|     fn default() -> HTTPStatusLine { |  | ||||||
|         HTTPStatusLine { |  | ||||||
|             version: HTTPVersion::Http1_1, |  | ||||||
|             status_code: HTTPStatusCode::Http400, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPStatusLine { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         let version: String = self.version.into(); |  | ||||||
|         let status_code: String = self.status_code.into(); |  | ||||||
|         format! {"{} {}", version, status_code} |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPStatusLine { |  | ||||||
|     pub fn get_status_code(&self) -> HTTPStatusCode { |  | ||||||
|         self.status_code |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn set_status_code(&mut self, code: HTTPStatusCode) { |  | ||||||
|         self.status_code = code; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user