fastserve 0.2.3

A simple and quick way of creating a backend to serve static files / api endpoints
Documentation
#![allow(deprecated, dead_code)]

use std::collections::HashMap;
use std::net::TcpStream;
use std::io::Write;
use std::path::Path;
use lazy_static::lazy_static;
use std::hash::{ Hash, Hasher };
use std::collections::hash_map::DefaultHasher;
use termcolor::{ Color, ColorChoice, ColorSpec, StandardStream, WriteColor };

/*- Static mutable variables -*/
lazy_static! {
    static ref STATUS_CODES:&'static [(&'static u16, &'static str); 58] = &[
    (&400, "Bad Request"),                      (&500, "Internal Server Error"),
    (&401, "Unauthorized"),                     (&501, "Not Implemented"),
    (&402, "Payment Required"),                 (&502, "Bad Gateway"),
    (&403, "Forbidden"),                        (&503, "Service Unavailable"),          /*=-----------=*/
    (&404, "Not Found"),                        (&504, "Gateway Timeout"),              //             \\
    (&405, "Method Not Allowed"),               (&505, "HTTP Version Not Supported"),   //     500     \\
    (&406, "Not Acceptable"),                   (&506, "Variant Also Negotiates"),      //             \\
    (&407, "Proxy Authentication Required"),    (&507, "Insufficient Storage"),         /*=-----------=*/
    (&408, "Request Timeout"),                  (&508, "Loop Detected"),
    (&409, "Conflict"),                         (&510, "Not Extended"),
    (&410, "Gone"),                             (&511, "Network Authentication Required"),
    (&411, "Length Required"),                              (&200, "OK"),
    (&412, "Precondition Failed"),                          (&201, "Created"),
    (&413, "Payload Too Large"),           /* 200 OK -> */  (&202, "Accepted"),
    (&414, "URI Too Long"),                /* 200 OK -> */  (&204, "No Content"),
    (&415, "Unsupported Media Type"),      /* 200 OK -> */  (&205, "Reset Content"),
    (&416, "Range Not Satisfiable"),       /* 200 OK -> */  (&206, "Partial Content"),
    (&417, "Expectation Failed"),          /* 200 OK -> */  (&207, "Multi-status"),
    (&418, "I'm a teapot"),                                 (&208, "Already reported"), 
    (&421, "Misdirected Request"),                          (&226, "IM Used"),
    (&422, "Unprocessable Entity"),             (&300, "Multiple Choices"),
    (&423, "Locked"),                           (&301, "Moved Permanently"),
    (&424, "Failed Dependency"),                (&302, "Found"),                    /*=-----------=*/
    (&425, "Too Early"),                        (&303, "See Other"),                //             \\
    (&426, "Upgrade Required"),                 (&304, "Not Modified"),             //     300     \\
    (&428, "Precondition Required"),            (&305, "Use Proxy"),                //             \\
    (&429, "Too Many Requests"),                (&306, "Switch Proxy"),             /*=-----------=*/
    (&431, "Request Header Fields Too Large"),  (&307, "Temporary Redirect"),
    (&451, "Unavailable For Legal Reasons"),    (&308, "Permanent Redirect"),


    ];
}

/// The get_header function can either take multiple header requests as input, or just one
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum HeaderReturn {
    Single(String),
    Multiple(Vec<String>),
    Values(HashMap<String, String>),
    All,
    None,
}

/// Parse headers by using a request-string as input, and specify what headers you want returned
/// By using ```HeaderReturn``` you can specify if you either want all headers back or just some
pub fn parse_headers(request:String, header:HeaderReturn) -> HeaderReturn {
    /*- Get the headers from the request -*/
    let header_strings:Vec<String> = request.split("\n").map(|e| e.to_string()).collect::<Vec<String>>();

    /*- A hasmap of all heders -*/
    let mut headers:HashMap<String, String> = HashMap::new();

    /*- We want to split by the first colon, if we
            don't do that and users start inputting colons
            in their header string, it'll lead to an error -*/
    for header in header_strings.iter() {

        /*- Loop through every char -*/
        'charLoop: for (i, c) in header.chars().enumerate() {
            /*- If we find a colon, split the string -*/
            if c == ':' {
                /*- Get the key and value -*/
                let key = &header[0..i].to_ascii_lowercase();
                let value = &header[i+1..];

                /*- Add the key and value to the headers -*/
                headers.insert(key.to_string(), value.to_string());

                /*- Break out of the loop -*/
                break 'charLoop;
            };
        };
    };

    /*- See what type of header the user wants -*/
    match header {
        HeaderReturn::Single(v) => {
            return HeaderReturn::Single(
                headers.get(&v).unwrap_or(&"".to_string()).trim().to_string()
            );
        },
        HeaderReturn::Multiple(v) => {

            /*- Header "queue" -*/
            let headers_to_get:Vec<String> = v.into_iter().collect::<Vec<String>>();

            /*- Return the headers -*/
            return HeaderReturn::Values(
                headers_to_get.into_iter()
                                .map(|v| (
                                    v.to_string(),
                                    headers.get(&v)
                                        .unwrap_or(&"".to_string())
                                        .trim().to_string()
                                ))
                                .collect::<HashMap<String, String>>()
            );
        },
        HeaderReturn::All => {
            return HeaderReturn::Values(
                /*- Trim all values -*/
                headers.into_iter().map(|(k, v)| {
                    (k, v.trim().to_string())
                }).collect::<HashMap<String, String>>()
            );
        },
        HeaderReturn::Values(_) => panic!("Can't get values - Values is read only"),
        HeaderReturn::None =>      panic!("Can't get None - None is read only"),
    };
}

#[derive(Debug)]
pub enum ResponseType {
    Text,
    Json,
    Html,
    Image(ResponseTypeImage)
}
#[derive(Debug)]
pub enum ResponseTypeImage {
    Jpeg,
    Png,
    Gif,
    Webp,
    Svg,
}
///
/// Return a http response containing the status, and optionally some content
/// # Examples
/// ```
/// respond(&mut stream, 200u16, Some(ResponseType::Text), "Hello World");
/// ```
/// 
pub fn respond(
    stream:&mut TcpStream,
    status:u16,
    respond:Option<(ResponseType, &str)>,
    additional_headers:Option<Vec<&str>>
) -> () {

    /*- Get the status string -*/
    let status_msg = STATUS_CODES.iter().find(|&x| x.0 == &status).unwrap_or(&(&0u16, "Internal error - Missing status code")).1;

    /*- Get the response type -*/
    let response_type = match &respond {
        Some(response_type) => match &response_type.0 {
            ResponseType::Text => "text/plain",
            ResponseType::Json => "application/json",
            ResponseType::Html => "text/html",
            ResponseType::Image(c) => {
                match c {
                    ResponseTypeImage::Jpeg => "image/jpeg",
                    ResponseTypeImage::Png => "image/png",
                    ResponseTypeImage::Gif => "image/gif",
                    ResponseTypeImage::Webp => "image/webp",
                    ResponseTypeImage::Svg => "image/svg+xml",
                }
            }
        },
        None => "text/plain",
    };

    /*- Get the additional headers -*/
    let additional_headers:String = match additional_headers {
        Some(v) => {
            format!("\r\n{}", v.join("\r\n"))
        },
        None => "".to_string(),
    };
    
    /*- Get the content exists -*/
    if let Some(c) = respond {
        /*- Write the status to the stream -*/
        stream.write(
            format!("HTTP/1.1 {}\r\nContent-Length: {}\r\nContent-Type: {}{additional_headers}\r\n\r\n{}", status, &c.1.len(), response_type, &c.1).as_bytes()
        ).unwrap();
    }else {
        /*- Write the status to the stream -*/
        stream.write(
            format!("HTTP/1.1 {}{additional_headers}\r\n\r\n{} {}", status, status, status_msg).as_bytes()
        ).unwrap();
    };

    /*- Flush the stream -*/
    stream.flush().unwrap();
}

/// Quick function to respond with a message saying that some headers might be missing
/// # Examples
/// ```
/// if !expect_headers(&mut stream, &headers, vec!["Content-Type", "Content-Length"]) {
///     return;
/// };
/// ```
pub fn expect_headers(
    stream:&mut TcpStream,
    headers:&HeaderReturn,
    required:Vec<String>,
) -> bool {

    /*- Quick way of sending a message of which headers that are missing -*/
    fn get_missing_response(missing:&str) -> String { format!("Missing headers: {:?}", missing) }

    /*- Check what type of headers we got as input -*/
    match headers {
        HeaderReturn::Values(v) => {
            for request in &required {
                if !v.contains_key(&request.to_string()) || v.get(&request.to_string()).unwrap().trim().len() == 0 {
                    respond(stream, 400, Some((ResponseType::Text, &get_missing_response(request))), None);
                    return false;
                };
            };
        },
        _ => panic!("Expected HeaderReturn::Values"),
    };

    /*- Return that all headers are specified -*/
    true
}

/*- Because when we change the terminal color, 
        it will keep the same color for future lines -*/
fn reset_terminal_color(stdout: &mut StandardStream) {
    stdout.set_color(
        ColorSpec::new()
            .set_fg(Some(Color::Rgb(171, 178, 191))))
            .unwrap();
}

/// Print a response with colors
/// # Examples
/// ```
/// log(Color::Green, "Hello World");
/// ```
pub fn log(clr:Color, msg:&str) {
    /*- Set new standard output -*/
    let mut stdout = StandardStream::stdout(ColorChoice::Always);

    /*- Set the color to the inputted one -*/
    stdout.set_color(
        ColorSpec::new()
            .set_fg(Some(clr)))
            .unwrap();

    /*- Print it -*/
    writeln!(&mut stdout, "{}", msg).unwrap();

    /*- Reset the color -*/
    reset_terminal_color(&mut stdout);
}

/*- Hash input -*/
pub(crate) fn hash<H: Hash>(hashval:&H,u:bool) -> String {
    /*- Initialize the hasher -*/
    let mut hasher = DefaultHasher::new();

    /*- Hash the string and end it -*/
    hashval.hash(&mut hasher);

    /*- Make it hex -*/
    if u { return format!("{:X}", hasher.finish()); }
    else { return format!("{:x}", hasher.finish()); }
}

pub fn guess_response_type(path:&str) -> ResponseType {
    let path:&Path = Path::new(path);

    /*- Check extensions -*/
    match path.extension() {
        Some(ext) => {
            match ext.to_str() {
                /*- Html -*/
                Some("html") => return ResponseType::Html,
                Some("htm")  => return ResponseType::Html,

                /*- Json -*/
                Some("json") => return ResponseType::Json,
                Some("yml")  => return ResponseType::Json,
                Some("yaml") => return ResponseType::Json,

                /*- Images -*/
                Some("gif")  => return ResponseType::Image(ResponseTypeImage::Gif),
                Some("png")  => return ResponseType::Image(ResponseTypeImage::Png),
                Some("jpg")  => return ResponseType::Image(ResponseTypeImage::Jpeg),
                Some("jpeg") => return ResponseType::Image(ResponseTypeImage::Jpeg),
                Some("webp") => return ResponseType::Image(ResponseTypeImage::Webp),
                Some("svg")  => return ResponseType::Image(ResponseTypeImage::Svg),
 
                /*- Text -*/
                Some(_)   => return ResponseType::Text,
                None      => return ResponseType::Text,
            };
        },
        None => return ResponseType::Text,
    };
}