francoisgib_webserver 1.0.3

HTTP Webserver
Documentation
mod test;
use std::str::FromStr;

use chrono::{DateTime, Utc};
use smallvec::SmallVec;
use strum_macros::{Display, EnumString};

/// Represents standard HTTP headers used in web requests and responses.
///
/// This enum provides a type-safe way to represent common HTTP headers, with support
/// for serialization and deserialization through the `strum_macros` crate.
///
/// # Features
/// - Implements `Display` for converting to strings (outputs Train-Case header names).
/// - Implements `EnumString` for parsing from strings (case-insensitive).
/// - Provides `PartialEq` for equality comparison between headers.
/// - Includes debug formatting with `Debug`.
///
/// # Example
/// ```
/// use std::str::FromStr;
/// use francoisgib_webserver::http::headers::HttpHeader;
///
/// // Create an enum variant directly
/// let header = HttpHeader::ContentType;
///
/// // Convert to string (outputs in Train-Case)
/// assert_eq!(header.to_string(), "Content-Type");
///
/// // Parse from string (case-insensitive)
/// let parsed = HttpHeader::from_str("content-type").unwrap();
/// assert_eq!(parsed, HttpHeader::ContentType);
///
/// // Case insensitivity
/// let parsed = HttpHeader::from_str("Content-Type").unwrap();
/// assert_eq!(parsed, HttpHeader::ContentType);
/// ```
///
/// # Notes
/// - String representation uses Train-Case (e.g., "Content-Type") due to the
///   `#[strum(serialize_all = "Train-Case")]` attribute.
/// - String parsing is case-insensitive due to the `ascii_case_insensitive` attribute.
/// - Uses the `strum_macros` crate for string serialization and deserialization.
#[derive(Debug, Display, EnumString, PartialEq)]
#[strum(serialize_all = "Train-Case", ascii_case_insensitive)]
pub enum HttpHeader {
    Server,
    Date,
    ContentType,
    ContentLength,
    UserAgent,
    Authorization,
    Accept,
    Host,
    Connection,
    SetCookie,
    TransferEncoding,
    Range,
}

#[derive(Debug, PartialEq)]
pub enum HttpHeaderValue {
    Server(String),
    Date(DateTime<Utc>),
    ContentType(ContentType),
    ContentLength(u64),
    UserAgent(String),
    Authorization(String),
    Accept(SmallVec<[ContentType; 1]>),
    Host(String),
    Connection(Connection),
    SetCookie(SmallVec<[(String, String); 1]>),
    TransferEncoding(TransferEncoding),
    Range((u16, u16)),
}

#[derive(Debug)]
pub struct HeaderEntry {
    pub name: HttpHeader,
    pub value: HttpHeaderValue,
}

impl HeaderEntry {
    pub fn new(name: HttpHeader, value: HttpHeaderValue) -> Self {
        HeaderEntry { name, value }
    }
}

impl ToString for HeaderEntry {
    fn to_string(&self) -> String {
        let name = self.name.to_string();
        let value = match &self.value {
            HttpHeaderValue::ContentType(val) => val.to_string(),

            HttpHeaderValue::ContentLength(val) => val.to_string(),

            HttpHeaderValue::UserAgent(val) => val.clone(),

            HttpHeaderValue::Authorization(val) => val.clone(),

            HttpHeaderValue::Accept(vals) => vals
                .iter()
                .map(|v| v.to_string())
                .collect::<Vec<String>>()
                .join(", "),

            HttpHeaderValue::Host(val) => val.to_owned(),

            HttpHeaderValue::Connection(val) => val.to_string(),

            HttpHeaderValue::SetCookie(kv) => kv
                .iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect::<Vec<String>>()
                .join("; "),

            HttpHeaderValue::TransferEncoding(val) => val.to_string(),

            HttpHeaderValue::Range((begin, end)) => format!("bytes={}-{}", begin, end),

            HttpHeaderValue::Date(date) => date.to_rfc2822(),

            HttpHeaderValue::Server(server) => server.to_owned(),
        };

        format!("{}: {}", name, value)
    }
}

impl FromStr for HeaderEntry {
    type Err = String;

    fn from_str(header: &str) -> Result<Self, Self::Err> {
        let (name, value) = header
            .split_once(':')
            .map(|(name, value)| (name.trim(), value.trim()))
            .ok_or_else(|| format!("Invalid header format: {}", header))?;

        let header =
            HttpHeader::from_str(name).map_err(|_| format!("Invalid header name: {}", name))?;
        let value = match header {
            HttpHeader::ContentType => {
                let content_type = ContentType::from_str(value)
                    .map_err(|_| format!("Invalid content type: {}", value))?;
                HttpHeaderValue::ContentType(content_type)
            }

            HttpHeader::ContentLength => {
                let length = value
                    .parse::<u64>()
                    .map_err(|_| format!("Invalid content length: {}", value))?;
                HttpHeaderValue::ContentLength(length)
            }

            HttpHeader::Connection => {
                let connection = Connection::from_str(value)
                    .map_err(|_| format!("Invalid connection type: {}", value))?;
                HttpHeaderValue::Connection(connection)
            }

            HttpHeader::TransferEncoding => {
                let encoding = TransferEncoding::from_str(value)
                    .map_err(|_| format!("Invalid transfer encoding: {}", value))?;
                HttpHeaderValue::TransferEncoding(encoding)
            }

            HttpHeader::Host => HttpHeaderValue::Host(value.to_string()),

            HttpHeader::Accept => HttpHeaderValue::Accept(
                value
                    .split(',')
                    .map(|x| x.trim())
                    .filter_map(|x| ContentType::from_str(x).ok())
                    .collect::<SmallVec<[ContentType; 1]>>(),
            ),

            HttpHeader::UserAgent => HttpHeaderValue::UserAgent(value.to_string()),

            HttpHeader::Authorization => HttpHeaderValue::Authorization(value.to_string()),

            HttpHeader::SetCookie => HttpHeaderValue::SetCookie(parse_set_cookie(value)),

            HttpHeader::Range => {
                let range = parse_range_header(value);
                if range.is_none() {
                    return Err("Invalid range header.".to_string());
                }
                HttpHeaderValue::Range(range.unwrap())
            }

            _ => return Err("Not implemented.".to_owned()),
        };
        Ok(HeaderEntry {
            name: header,
            value,
        })
    }
}

#[derive(Debug, Display, EnumString, PartialEq, Clone, Copy)]
pub enum ContentType {
    #[strum(serialize = "application/json", serialize = "json")]
    ApplicationJson,

    #[strum(serialize = "application/xml", serialize = "xml")]
    ApplicationXml,

    #[strum(serialize = "application/octet-stream")]
    ApplicationOctetStream,

    #[strum(serialize = "text/html", serialize = "html")]
    TextHtml,

    #[strum(serialize = "text/plain", serialize = "txt")]
    TextPlain,

    #[strum(serialize = "text/css", serialize = "css")]
    TextCss,

    #[strum(
        serialize = "text/javascript",
        serialize = "application/javascript",
        serialize = "js"
    )]
    TextJavascript,

    #[strum(serialize = "image/png", serialize = "png")]
    ImagePng,

    #[strum(serialize = "image/jpeg", serialize = "jpeg")]
    ImageJpeg,

    #[strum(serialize = "image/gif", serialize = "gif")]
    ImageGif,

    #[strum(serialize = "multipart/form-data")]
    MultipartFormData,

    #[strum(serialize = "application/x-www-form-urlencoded")]
    FormUrlEncoded,

    #[strum(serialize = "*/*")]
    Any,
}

#[derive(Debug, PartialEq, EnumString, Display)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub enum Connection {
    KeepAlive,
    Close,
}

#[derive(Debug, PartialEq, EnumString, Display)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum TransferEncoding {
    Chunked,
}

fn parse_set_cookie(cookie_str: &str) -> SmallVec<[(String, String); 1]> {
    cookie_str
        .split("; ")
        .filter_map(|pair| {
            let parts: Vec<&str> = pair.split('=').collect();
            if parts.len() >= 2 {
                let key = parts[0].to_string();
                let value = parts[1..].join("=");
                Some((key, value))
            } else {
                None
            }
        })
        .collect()
}

pub fn parse_range_header(range_str: &str) -> Option<(u16, u16)> {
    if !range_str.starts_with("bytes=") {
        return None;
    }

    let bytes_range = &range_str["bytes=".len()..];

    let parts: Vec<&str> = bytes_range.split('-').collect();
    if parts.len() != 2 {
        return None;
    }

    let start = parts[0].parse::<u16>().ok()?;
    let end = parts[1].parse::<u16>().ok()?;

    if start > end {
        return None;
    }

    Some((start, end))
}