torus-http 0.2.9

Toy RUSttp - aka Torus is a toy project synchronous http server library written with 0 dependencies
Documentation
//! Exports a trait for generating http responses, with a simple default
//! implementation for string bodies.
//!
//! # Example:
//!
//! ```rust
//! use torus_http::response::{HttpResponse, Response};
//! let response = HttpResponse::new().set_body("hey there").insert_header("Cool-Header", "so cool");
//! let response = "hello".to_response();
//! ```

use std::collections::HashMap;

use crate::status::{HttpStatus, ServerErrorResponse};

/// Trait that allows things to be sent back from the server
pub trait Response {
    fn to_response(&self) -> HttpResponse;
}

impl Response for &str {
    fn to_response(&self) -> HttpResponse {
        HttpResponse::new_body((*self).to_string(), HttpStatus::default())
    }
}

impl Response for String {
    fn to_response(&self) -> HttpResponse {
        HttpResponse::new_body(self.clone(), HttpStatus::default())
    }
}

impl<S: Response> Response for Option<S> {
    fn to_response(&self) -> HttpResponse {
        match self {
            Some(e) => e.to_response(),
            None => HttpResponse::new().set_status(HttpStatus::ServerError(
                ServerErrorResponse::InternalServerError,
            )),
        }
    }
}

impl<S: Response, E: Response> Response for Result<S, E> {
    fn to_response(&self) -> HttpResponse {
        match self {
            Ok(s) => s.to_response().set_status(HttpStatus::default()),
            Err(e) => e
                .to_response()
                .set_status(ServerErrorResponse::InternalServerError.into()),
        }
    }
}

impl Response for HttpResponse {
    // TODO: make this not need to clone, can't just take ownership since dyn
    fn to_response(&self) -> HttpResponse {
        self.clone()
    }
}

/// Struct that contains all the information that will be sent to the client
#[derive(Eq, PartialEq, Clone, Debug, Default)]
pub struct HttpResponse {
    pub headers: HashMap<String, String>,
    pub status: HttpStatus,
    pub body: Option<String>,
}

impl HttpResponse {
    #[must_use]
    pub fn new_body(body: String, status: HttpStatus) -> Self {
        let mut headers: HashMap<String, String> = HashMap::new();
        headers.insert("Content-Length".into(), body.chars().count().to_string());
        Self {
            headers,
            status,
            body: Some(body),
        }
    }

    #[must_use]
    pub fn insert_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.insert(key.into(), value.into());
        self
    }

    #[must_use]
    pub fn set_body(mut self, body: impl Into<String>) -> Self {
        let body = body.into();
        let body_len = body.chars().count();
        self.body.replace(body);
        self.headers
            .insert("Content-Length".into(), body_len.to_string());
        self
    }

    #[must_use]
    pub fn set_status(mut self, status: HttpStatus) -> Self {
        self.status = status;
        self
    }

    #[must_use]
    pub fn new() -> Self {
        Self {
            headers: HashMap::new(),
            status: HttpStatus::default(),
            body: None,
        }
    }

    pub(crate) fn into_bytes(self) -> Vec<u8> {
        self.into_string().into_bytes()
    }

    pub(crate) fn into_string(self) -> String {
        use std::fmt::Write;

        let headers = self.headers.iter().fold(String::new(), |mut acc, (k, v)| {
            acc.write_fmt(core::format_args!("{k}: {v}\r\n"))
                .expect("if this fails fuck clippy");
            acc
        });

        format!(
            "HTTP/1.1 {}\r\n{headers}\r\n{}",
            self.status,
            self.body.unwrap_or_default()
        )
    }
}