use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::net::TcpStream;
use crate::status::{HttpStatus, StatusArg};
pub struct Response<'a> {
stream: &'a mut TcpStream,
headers: HashMap<String, String>,
status: HttpStatus,
status_code: u16,
status_reason: String,
sent: bool,
}
impl<'a> Response<'a> {
pub fn new(stream: &'a mut TcpStream) -> Response<'a> {
let mut headers = HashMap::new();
headers.insert("HTTP-Server-Powered-By".to_string(), "rxpress".to_string());
Response {
stream,
headers,
status: HttpStatus::OK,
status_code: 200,
status_reason: "OK".to_string(),
sent: false,
}
}
pub fn status<'b, T: Into<StatusArg<'b>>>(&mut self, arg: T) -> &mut Self {
match arg.into() {
StatusArg::Enum(e) => {
self.status = e;
self.status_code = e.code();
self.status_reason = HttpStatus::reason(self.status_code).to_string();
}
StatusArg::Code(code) => {
self.status_code = code;
self.status_reason = HttpStatus::reason(code).to_string();
}
StatusArg::CodeReason(code, reason) => {
self.status_code = code;
self.status_reason = reason.to_string();
}
}
self
}
pub fn set_header(&mut self, key: &str, value: &str) -> &mut Self {
self.headers.insert(key.to_string(), value.to_string());
self
}
pub fn send(&mut self, msg: &str) {
if self.sent {
eprintln!(
"[rxpress warning!]: response already sent, ignoring subsequent send() call."
);
return;
}
self.sent = true; self.set_header("Content-Type", "text/plain");
self.write_response(msg.as_bytes());
}
pub fn json(&mut self, msg: &str) {
if self.sent {
eprintln!(
"[rxpress warning!]: response already sent, ignoring subsequent json() call."
);
return;
}
self.sent = true; self.set_header("Content-Type", "application/json");
self.write_response(msg.as_bytes());
}
pub fn html(&mut self, body: &str) {
if self.sent {
eprintln!(
"[rxpress warning!]: response already sent, ignoring subsequent html() call."
);
return;
}
self.sent = true; self.set_header("Content-Type", "text/html; charset=utf-8");
self.write_response(body.as_bytes());
}
pub fn html_file(&mut self, path: &str) {
if self.sent {
eprintln!(
"[rxpress warning!]: response already sent, ignoring subsequent html_file() call."
);
return;
}
self.sent = true; self.set_header("Content-Type", "text/html; charset=utf-8");
match fs::read_to_string(path) {
Ok(content) => {
self.write_response(content.as_bytes());
}
Err(_) => {
let body = &format!(
"<h2>Internal Server Error</h2>\n<p>No file found on {}</p>",
path
);
self.status(HttpStatus::InternalServerError);
self.write_response(body.as_bytes());
}
}
}
fn write_response(&mut self, msg: &[u8]) {
let headers = self
.headers
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<String>>()
.join("\r\n");
let res = format!(
"HTTP/1.1 {} {}\r\n{}\r\nContent-Length: {}\r\n\r\n",
self.status_code,
self.status_reason,
headers,
msg.len()
);
self.stream.write_all(res.as_bytes()).unwrap();
self.stream.write_all(msg).unwrap();
self.stream.flush().unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{TcpListener, TcpStream};
fn tcp_pair() -> (TcpStream, TcpStream) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let client = TcpStream::connect(addr).unwrap();
let server = listener.accept().unwrap().0;
(client, server)
}
#[test]
fn test_set_header() {
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.set_header("X-Test", "123");
assert_eq!(res.headers.get("X-Test"), Some(&"123".to_string()));
}
#[test]
fn test_status_with_enum() {
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.status(HttpStatus::Forbidden);
assert_eq!(res.status_code, 403);
assert_eq!(res.status_reason, "Forbidden");
}
#[test]
fn test_status_with_custom_code_and_reason() {
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.status((499, "Custom Reason"));
assert_eq!(res.status_code, 499);
assert_eq!(res.status_reason, "Custom Reason");
}
#[test]
fn test_send_and_json_set_content_type() {
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.send("hello");
assert_eq!(
res.headers.get("Content-Type"),
Some(&"text/plain".to_string())
);
let (_c2, mut s2) = tcp_pair();
let mut res2 = Response::new(&mut s2);
res2.json(r#"{"msg":"ok"}"#);
assert_eq!(
res2.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_html_sets_content_type() {
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.html("<h1>Test</h1>");
assert!(res.sent);
assert_eq!(
res.headers.get("Content-Type"),
Some(&"text/html; charset=utf-8".to_string())
);
}
#[test]
fn test_html_file_success_and_failure() {
let tmp_file = "test_html_file.html";
fs::write(tmp_file, "<h1>Hello</h1>").unwrap();
let (_c, mut s) = tcp_pair();
let mut res = Response::new(&mut s);
res.html_file(tmp_file);
assert!(res.sent);
assert_eq!(
res.headers.get("Content-Type"),
Some(&"text/html; charset=utf-8".to_string())
);
fs::remove_file(tmp_file).unwrap();
let (_c2, mut s2) = tcp_pair();
let mut res2 = Response::new(&mut s2);
res2.html_file("missing_file.html");
assert!(res2.sent);
assert_eq!(res2.status_code, 500);
}
}