diff --git a/Cargo.toml b/Cargo.toml index 4d5babb..a2005be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.1.1" +version = "0.1.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/lib.rs b/src/lib.rs index 7febb3a..049852b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,17 @@ //! * HTTP Headers not parsed mod body; +mod message; mod request; +mod response; mod start_line; +mod status; mod version; pub use body::HTTPBody; +pub use message::JSONMessage; pub use request::HTTPRequest; +pub use response::HTTPResponse; pub use start_line::HTTPStartLine; +pub use status::{HTTPStatusCode, HTTPStatusLine}; pub use version::HTTPVersion; diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..bd5edda --- /dev/null +++ b/src/message.rs @@ -0,0 +1,94 @@ +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, +} + +impl Default for JSONMessage { + fn default() -> Self { + JSONMessage { + message: HashMap::new(), + } + } +} + +/// try to convert `JSONMessage` into `json::JsonValue` +impl TryInto for JSONMessage { + type Error = String; + fn try_into(self) -> Result { + 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 { + 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; + } + } + } + + /// 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 = 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 = 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()); +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..6f40dbf --- /dev/null +++ b/src/response.rs @@ -0,0 +1,112 @@ +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 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(r#"{}"#).unwrap(), + } + } +} + +impl HTTPResponse { + pub fn as_500(message: Option) -> 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) -> 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 + } +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..776eded --- /dev/null +++ b/src/status.rs @@ -0,0 +1,50 @@ +use crate::HTTPVersion; + +#[derive(Debug, PartialEq)] +pub enum HTTPStatusCode { + Http200, + Http400, + Http403, + Http404, + Http500, +} + +impl Into 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 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 set_status_code(&mut self, code: HTTPStatusCode) { + self.status_code = code; + } +}