muffy 0.5.1

The static website validator
Documentation
use crate::http_client::BareResponse;
use core::{
    str::{self, Utf8Error},
    time::Duration,
};
use http::{
    StatusCode,
    header::{CONTENT_TYPE, HeaderMap},
};
use serde::{Deserialize, Serialize};
use url::Url;

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Response {
    url: Url,
    #[serde(with = "http_serde::status_code")]
    status: StatusCode,
    #[serde(with = "http_serde::header_map")]
    headers: HeaderMap,
    body: Vec<u8>,
    duration: Duration,
}

impl Response {
    pub const fn new(
        url: Url,
        status: StatusCode,
        headers: HeaderMap,
        body: Vec<u8>,
        duration: Duration,
    ) -> Self {
        Self {
            url,
            status,
            headers,
            body,
            duration,
        }
    }

    pub fn from_bare(response: BareResponse, duration: Duration) -> Self {
        Self::new(
            response.url,
            response.status,
            response.headers,
            response.body,
            duration,
        )
    }

    pub const fn url(&self) -> &Url {
        &self.url
    }

    pub const fn status(&self) -> StatusCode {
        self.status
    }

    pub const fn headers(&self) -> &HeaderMap {
        &self.headers
    }

    pub fn media_type(&self) -> Result<Option<&str>, Utf8Error> {
        self.headers
            .get(CONTENT_TYPE)
            .map(|value| {
                Ok(str::from_utf8(
                    value
                        .as_bytes()
                        .split(|byte| *byte == b';')
                        .next()
                        .unwrap_or_default(),
                )?
                .trim())
            })
            .transpose()
    }

    #[allow(clippy::missing_const_for_fn)]
    pub fn body(&self) -> &[u8] {
        &self.body
    }

    pub const fn duration(&self) -> Duration {
        self.duration
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::HeaderValue;
    use pretty_assertions::assert_eq;

    fn create_response(headers: HeaderMap) -> Response {
        Response::new(
            Url::parse("https://foo.com").unwrap(),
            StatusCode::OK,
            headers,
            Default::default(),
            Default::default(),
        )
    }

    #[test]
    fn parse_no_media_type() {
        assert_eq!(create_response(Default::default()).media_type(), Ok(None));
    }

    #[test]
    fn parse_media_type() {
        assert_eq!(
            create_response(HeaderMap::from_iter([(
                CONTENT_TYPE,
                HeaderValue::from_static("text/html")
            )]))
            .media_type(),
            Ok(Some("text/html"))
        );
    }

    #[test]
    fn trim_media_type_with_parameter() {
        assert_eq!(
            create_response(HeaderMap::from_iter([(
                CONTENT_TYPE,
                HeaderValue::from_static(" text/html ; charset=utf-8")
            )]))
            .media_type(),
            Ok(Some("text/html"))
        );
    }

    #[test]
    fn keep_media_type_case() {
        assert_eq!(
            create_response(HeaderMap::from_iter([(
                CONTENT_TYPE,
                HeaderValue::from_static("Image/SVG+XML")
            )]))
            .media_type(),
            Ok(Some("Image/SVG+XML"))
        );
    }

    #[test]
    fn parse_media_type_with_non_ascii_parameter() {
        assert_eq!(
            create_response(HeaderMap::from_iter([(
                CONTENT_TYPE,
                HeaderValue::from_bytes(b"image/svg+xml; note=caf\xE9").unwrap()
            )]))
            .media_type(),
            Ok(Some("image/svg+xml"))
        );
    }

    #[test]
    fn fail_on_invalid_media_type_encoding() {
        assert!(
            create_response(HeaderMap::from_iter([(
                CONTENT_TYPE,
                HeaderValue::from_bytes(b"image/\xFFsvg").unwrap()
            )]))
            .media_type()
            .is_err()
        );
    }
}