use crate::{Body, Method};
use serde_json::Value;
use std::{
collections::HashMap,
fmt::{Display, Formatter},
};
const CRLF: &str = "\r\n";
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum StatusCode {
Ok,
NotFound,
BadRequest,
MethodNotAllowed,
}
impl Display for StatusCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Ok => "200 OK",
Self::NotFound => "404 Not Found",
Self::BadRequest => "400 Bad Request",
Self::MethodNotAllowed => "405 Method Not Allowed",
}
)
}
}
#[derive(Debug, Clone)]
pub struct Response {
pub status_code: StatusCode,
pub headers: HashMap<String, String>,
pub body: Body,
}
impl Response {
#[must_use]
pub fn ok() -> Self {
Self {
body: Body::None,
status_code: StatusCode::Ok,
headers: HashMap::new(),
}
}
#[must_use]
pub fn text(body: &str) -> Self {
Self::ok()
.header("Content-Type", "text/plain")
.body(Body::Text(body.to_string()))
}
#[must_use]
pub fn json(body: &Value) -> Self {
Self::ok()
.header("Content-Type", "application/json")
.body(Body::Json(body.clone()))
}
#[must_use]
pub fn not_found() -> Self {
Self::text("Not Found").status(StatusCode::NotFound)
}
#[must_use]
pub fn invalid_request() -> Self {
Self::text("Invalid Request").status(StatusCode::BadRequest)
}
#[must_use]
pub fn method_not_allowed(methods: &[Method]) -> Self {
let mut methods = methods
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>();
methods.sort();
Self::text("Method Not Allowed")
.status(StatusCode::MethodNotAllowed)
.header("Allow", &methods.join(", "))
}
#[must_use]
pub fn status(&mut self, code: StatusCode) -> Self {
self.status_code = code;
self.clone()
}
#[must_use]
pub fn header(&mut self, name: &str, value: &str) -> Self {
self.headers.insert(name.into(), value.into());
self.clone()
}
#[must_use]
pub fn body(&mut self, body: Body) -> Self {
self.body = body;
self.clone()
}
}
impl ToString for Response {
fn to_string(&self) -> String {
let mut str_response = String::new();
str_response.push_str(&format!("HTTP/1.1 {}{CRLF}", self.status_code));
for (name, value) in &self.headers {
str_response.push_str(&format!("{name}: {value}{CRLF}"));
}
str_response.push_str(CRLF);
str_response.push_str(&self.body.to_string());
str_response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_to_string() {
let response = Response::text("Hello, World!");
assert_eq!(
response.to_string(),
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, World!"
);
}
}