mashrl 0.0.4

Minimal and simple HTTP(s) request library
Documentation
pub struct Request<'a, T> {
    pub method: Method<'a>,
    pub root: &'a str,
    pub path: &'a str,
    pub headers: &'a Headers<'a>,
    pub content: T,
}

pub type RequestNoBody<'a> = Request<'a, std::io::Empty>;

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Method<'a>(pub &'a str);

impl Method<'static> {
    pub const GET: Self = Method("GET");
    pub const HEAD: Self = Method("HEAD");
    pub const POST: Self = Method("POST");
    pub const PUT: Self = Method("PUT");
    pub const PATCH: Self = Method("PATCH");
    pub const CONNECT: Self = Method("CONNECT");
    pub const TRACE: Self = Method("TRACE");
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ResponseCode(pub u16);

impl ResponseCode {
    pub const CONTINUE: ResponseCode = ResponseCode(100);
    pub const SWITCHING_PROTOCOLS: ResponseCode = ResponseCode(101);
    pub const PROCESSING: ResponseCode = ResponseCode(102);
    pub const EARLY_HINTS: ResponseCode = ResponseCode(103);

    pub const OK: ResponseCode = ResponseCode(200);
    pub const CREATED: ResponseCode = ResponseCode(201);
    pub const ACCEPTED: ResponseCode = ResponseCode(202);
    pub const NON_AUTHORITATIVE_INFORMATION: ResponseCode = ResponseCode(203);
    pub const NO_CONTENT: ResponseCode = ResponseCode(204);
    pub const RESET_CONTENT: ResponseCode = ResponseCode(205);
    pub const PARTIAL_CONTENT: ResponseCode = ResponseCode(206);
    pub const MULTI_STATUS: ResponseCode = ResponseCode(207);
    pub const ALREADY_REPORTED: ResponseCode = ResponseCode(208);
    pub const IM_USED: ResponseCode = ResponseCode(226);

    pub const MULTIPLE_CHOICES: ResponseCode = ResponseCode(300);
    pub const MOVED_PERMANENTLY: ResponseCode = ResponseCode(301);
    pub const FOUND: ResponseCode = ResponseCode(302);
    pub const SEE_OTHER: ResponseCode = ResponseCode(303);
    pub const NOT_MODIFIED: ResponseCode = ResponseCode(304);
    pub const TEMPORARY_REDIRECT: ResponseCode = ResponseCode(307);
    pub const PERMANENT_REDIRECT: ResponseCode = ResponseCode(308);

    pub const BAD_REQUEST: ResponseCode = ResponseCode(400);
    pub const UNAUTHORIZED: ResponseCode = ResponseCode(401);
    pub const PAYMENT_REQUIRED: ResponseCode = ResponseCode(402);
    pub const FORBIDDEN: ResponseCode = ResponseCode(403);
    pub const NOT_FOUND: ResponseCode = ResponseCode(404);
    pub const METHOD_NOT_ALLOWED: ResponseCode = ResponseCode(405);
    pub const NOT_ACCEPTABLE: ResponseCode = ResponseCode(406);
    pub const PROXY_AUTHENTICATION_REQUIRED: ResponseCode = ResponseCode(407);
    pub const REQUEST_TIMEOUT: ResponseCode = ResponseCode(408);
    pub const CONFLICT: ResponseCode = ResponseCode(409);
    pub const GONE: ResponseCode = ResponseCode(410);
    pub const LENGTH_REQUIRED: ResponseCode = ResponseCode(411);
    pub const PRECONDITION_FAILED: ResponseCode = ResponseCode(412);
    pub const CONTENT_TOO_LARGE: ResponseCode = ResponseCode(413);
    pub const URI_TOO_LONG: ResponseCode = ResponseCode(414);
    pub const UNSUPPORTED_MEDIA_TYPE: ResponseCode = ResponseCode(415);
    pub const RANGE_NOT_SATISFIABLE: ResponseCode = ResponseCode(416);
    pub const EXPECTATION_FAILED: ResponseCode = ResponseCode(417);
    pub const IM_A_TEAPOT: ResponseCode = ResponseCode(418);
    pub const MISDIRECTED_REQUEST: ResponseCode = ResponseCode(421);
    pub const UNPROCESSABLE_CONTENT: ResponseCode = ResponseCode(422);
    pub const LOCKED: ResponseCode = ResponseCode(423);
    pub const FAILED_DEPENDENCY: ResponseCode = ResponseCode(424);
    pub const TOO_EARLY: ResponseCode = ResponseCode(425);
    pub const UPGRADE_REQUIRED: ResponseCode = ResponseCode(426);
    pub const PRECONDITION_REQUIRED: ResponseCode = ResponseCode(428);
    pub const TOO_MANY_REQUESTS: ResponseCode = ResponseCode(429);
    pub const REQUEST_HEADER_FIELDS_TOO_LARGE: ResponseCode = ResponseCode(431);
    pub const UNAVAILABLE_FOR_LEGAL_REASONS: ResponseCode = ResponseCode(451);

    pub const INTERNAL_SERVER_ERROR: ResponseCode = ResponseCode(500);
    pub const NOT_IMPLEMENTED: ResponseCode = ResponseCode(501);
    pub const BAD_GATEWAY: ResponseCode = ResponseCode(502);
    pub const SERVICE_UNAVAILABLE: ResponseCode = ResponseCode(503);
    pub const GATEWAY_TIMEOUT: ResponseCode = ResponseCode(504);
    pub const HTTP_VERSION_NOT_SUPPORTED: ResponseCode = ResponseCode(505);
    pub const VARIANT_ALSO_NEGOTIATES: ResponseCode = ResponseCode(506);
    pub const INSUFFICIENT_STORAGE: ResponseCode = ResponseCode(507);
    pub const LOOP_DETECTED: ResponseCode = ResponseCode(508);
    pub const NOT_EXTENDED: ResponseCode = ResponseCode(510);
    pub const NETWORK_AUTHENTICATION_REQUIRED: ResponseCode = ResponseCode(511);

    /// # Errors
    ///
    /// returns an `Err` if the input is not a known HTTP response code
    pub fn from_line(item: &str) -> Result<Self, &str> {
        Ok(match item {
            "100 Continue" => Self::CONTINUE,
            "101 Switching Protocols" => Self::SWITCHING_PROTOCOLS,
            "102 Processing" => Self::PROCESSING,
            "103 Early Hints" => Self::EARLY_HINTS,
            "200 OK" => Self::OK,
            "201 Created" => Self::CREATED,
            "202 Accepted" => Self::ACCEPTED,
            "203 Non-Authoritative Information" => Self::NON_AUTHORITATIVE_INFORMATION,
            "204 No Content" => Self::NO_CONTENT,
            "205 Reset Content" => Self::RESET_CONTENT,
            "206 Partial Content" => Self::PARTIAL_CONTENT,
            "207 Multi-Status" => Self::MULTI_STATUS,
            "208 Already Reported" => Self::ALREADY_REPORTED,
            "226 IM Used" => Self::IM_USED,
            "300 Multiple Choices" => Self::MULTIPLE_CHOICES,
            "301 Moved Permanently" => Self::MOVED_PERMANENTLY,
            "302 Found" => Self::FOUND,
            "303 See Other" => Self::SEE_OTHER,
            "304 Not Modified" => Self::NOT_MODIFIED,
            "307 Temporary Redirect" => Self::TEMPORARY_REDIRECT,
            "308 Permanent Redirect" => Self::PERMANENT_REDIRECT,
            "400 Bad Request" => Self::BAD_REQUEST,
            "401 Unauthorized" => Self::UNAUTHORIZED,
            "402 Payment Required" => Self::PAYMENT_REQUIRED,
            "403 Forbidden" => Self::FORBIDDEN,
            "404 Not Found" => Self::NOT_FOUND,
            "405 Method Not Allowed" => Self::METHOD_NOT_ALLOWED,
            "406 Not Acceptable" => Self::NOT_ACCEPTABLE,
            "407 Proxy Authentication Required" => Self::PROXY_AUTHENTICATION_REQUIRED,
            "408 Request Timeout" => Self::REQUEST_TIMEOUT,
            "409 Conflict" => Self::CONFLICT,
            "410 Gone" => Self::GONE,
            "411 Length Required" => Self::LENGTH_REQUIRED,
            "412 Precondition Failed" => Self::PRECONDITION_FAILED,
            "413 Content Too Large" => Self::CONTENT_TOO_LARGE,
            "414 URI Too Long" => Self::URI_TOO_LONG,
            "415 Unsupported Media Type" => Self::UNSUPPORTED_MEDIA_TYPE,
            "416 Range Not Satisfiable" => Self::RANGE_NOT_SATISFIABLE,
            "417 Expectation Failed" => Self::EXPECTATION_FAILED,
            "418 I'm a teapot" => Self::IM_A_TEAPOT,
            "421 Misdirected Request" => Self::MISDIRECTED_REQUEST,
            "422 Unprocessable Content" => Self::UNPROCESSABLE_CONTENT,
            "423 Locked" => Self::LOCKED,
            "424 Failed Dependency" => Self::FAILED_DEPENDENCY,
            "425 Too Early" => Self::TOO_EARLY,
            "426 Upgrade Required" => Self::UPGRADE_REQUIRED,
            "428 Precondition Required" => Self::PRECONDITION_REQUIRED,
            "429 Too Many Requests" => Self::TOO_MANY_REQUESTS,
            "431 Request Header Fields Too Large" => Self::REQUEST_HEADER_FIELDS_TOO_LARGE,
            "451 Unavailable For Legal Reasons" => Self::UNAVAILABLE_FOR_LEGAL_REASONS,
            "500 Internal Server Error" => Self::INTERNAL_SERVER_ERROR,
            "501 Not Implemented" => Self::NOT_IMPLEMENTED,
            "502 Bad Gateway" => Self::BAD_GATEWAY,
            "503 Service Unavailable" => Self::SERVICE_UNAVAILABLE,
            "504 Gateway Timeout" => Self::GATEWAY_TIMEOUT,
            "505 HTTP Version Not Supported" => Self::HTTP_VERSION_NOT_SUPPORTED,
            "506 Variant Also Negotiates" => Self::VARIANT_ALSO_NEGOTIATES,
            "507 Insufficient Storage" => Self::INSUFFICIENT_STORAGE,
            "508 Loop Detected" => Self::LOOP_DETECTED,
            "510 Not Extended" => Self::NOT_EXTENDED,
            "511 Network Authentication Required" => Self::NETWORK_AUTHENTICATION_REQUIRED,
            item => {
                // Custom status code
                if let Some(value) = item
                    .split_once(' ')
                    .and_then(|(lhs, _)| lhs.parse::<u16>().ok())
                {
                    Self(value)
                } else {
                    return Err(item);
                }
            }
        })
    }
}

pub type ResponseBody = Box<dyn std::io::Read + Send>;

pub struct Response<'a> {
    pub code: ResponseCode,
    pub headers: Headers<'a>,
    pub body: ResponseBody,
}

#[derive(Clone, Debug)]
pub struct Headers<'a>(pub std::borrow::Cow<'a, str>);

impl Headers<'static> {
    #[must_use]
    pub fn empty() -> Headers<'static> {
        Headers(std::borrow::Cow::Borrowed(""))
    }

    /// # Errors
    ///
    /// returns an `Err` if the input is not `utf8`
    pub fn from_raw(on: Vec<u8>) -> Result<Headers<'static>, std::string::FromUtf8Error> {
        String::from_utf8(on).map(Headers::from_string)
    }

    #[must_use]
    pub fn from_string(on: String) -> Headers<'static> {
        Headers(std::borrow::Cow::Owned(on))
    }

    #[must_use]
    pub fn iter(&self) -> HeaderIter<'_> {
        HeaderIter(self.0.lines())
    }

    pub fn append(&mut self, key: &str, value: &str) {
        let buffer = self.0.to_mut();
        buffer.push_str(key);
        buffer.push_str(": ");
        buffer.push_str(value);
    }
}

impl<T> FromIterator<(T, T)> for Headers<'static>
where
    T: AsRef<str>,
{
    fn from_iter<I: IntoIterator<Item = (T, T)>>(iter: I) -> Self {
        let mut buf = String::new();
        for (key, value) in iter {
            if !buf.is_empty() {
                buf.push_str("\r\n");
            }
            buf.push_str(key.as_ref());
            buf.push_str(": ");
            buf.push_str(value.as_ref());
        }
        Self::from_string(buf)
    }
}

impl<'a> IntoIterator for &'a Headers<'_> {
    type Item = (&'a str, &'a str);
    type IntoIter = HeaderIter<'a>;

    fn into_iter(self) -> Self::IntoIter {
        HeaderIter(self.0.lines())
    }
}

pub struct HeaderIter<'a>(pub(super) std::str::Lines<'a>);

impl<'a> Iterator for HeaderIter<'a> {
    type Item = (&'a str, &'a str);

    fn next(&mut self) -> Option<Self::Item> {
        let row = self.0.next()?;
        // TODO what if `None` here?
        let (key, value) = row.split_once(':')?;
        Some((key, value.trim()))
    }
}

pub struct ChunkedReader<T> {
    reader: T,
    to_read: usize,
}

impl<T> ChunkedReader<T>
where
    T: std::io::Read,
{
    pub fn new(reader: T) -> Self {
        Self { reader, to_read: 0 }
    }
}

impl<T> std::io::Read for ChunkedReader<T>
where
    T: std::io::BufRead,
{
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        // use std::io::BufRead;

        if self.to_read == 0 {
            let mut chunk_size_buf = String::new();
            self.reader.read_line(&mut chunk_size_buf)?;

            let chunk_size_str = chunk_size_buf.trim_end();
            let hex = u64::from_str_radix(chunk_size_str, 16);
            let Ok(chunk_size) = hex else {
                let message = format!("invalid chunk length {chunk_size_str:?}");
                let error = std::io::Error::new(std::io::ErrorKind::InvalidData, message);
                return Err(error);
            };

            // The terminating chunk is a zero-length chunk
            if chunk_size == 0 {
                return Ok(0);
            }

            let chunk_size = usize::try_from(chunk_size).unwrap_or(usize::MAX);

            self.to_read = chunk_size;
        }

        let mut reader_over_chunk = self.reader.by_ref().take(self.to_read as u64);
        let bytes_tranferred = reader_over_chunk.read(buf)?;
        self.to_read -= bytes_tranferred;

        if self.to_read == 0 {
            let mut end: [u8; 2] = [0, 0];
            self.reader.read_exact(&mut end)?;

            #[cfg(debug_assertions)]
            if &end != b"\r\n" {
                let message = "expected '\r\n' at end of chunked frame";
                let error = std::io::Error::new(std::io::ErrorKind::InvalidData, message);
                return Err(error);
            }
        }

        Ok(bytes_tranferred)
    }
}