yan_net 0.2.1

A simple library for sending HTTP requests and creating HTTP servers
Documentation
use std::{
    collections::HashMap,
    net::TcpListener,
    path::PathBuf,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    thread,
    time::Duration,
};

use crate::prelude::*;

/// Server for static files like html as well as custom handlers
///
/// # Examples
///
/// ```
/// // file system:
/// // static_files/
/// // -> index.html
/// // -> more/
/// //    -> index.html
/// //    -> other.html
///
/// use yan_net::prelude::*;
///
/// let mut server = Server::new();
/// server.mount_static("/files", "static_files/");
///
/// let stop_flag = Arc::new(AtomicBool::new(false));
/// let handle = server.thread_start_at_port(8000, stop_flag.clone());
///
/// // build the request
/// let req = RequestBuilder::new()
///     .method(HttpMethod::Get)
///     .path("/files")
///     .header("Host", "127.0.0.1:8000")
///     .body_and_auto_content_headers(HttpBody::None)
///     .build();
///
/// // send the request you just build and save the response
/// let res = req.send().unwrap();
///
/// // make sure the response matches index.html in static_files/
/// assert_eq!(res.body, HttpBody::Html(
/// r#"<!DOCTYPE html>
/// <html lang="en">
/// <head>
///   <meta charset="UTF-8">
///   <meta name="viewport" content="width=device-width, initial-scale=1.0">
///   <title>Document</title>
/// </head>
/// <body>
///   <p>This is static_files/index.html</p>
/// </body>
/// </html>
/// "#.to_string()));
///
/// stop_flag.store(true, Ordering::SeqCst);
/// handle.join().unwrap();
/// ```
#[derive(Default, Clone)]
pub struct Server {
    mappings: HashMap<(String, HttpMethod), RequestHandler>,
    static_mounts: HashMap<String, String>,
}

impl Server {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn map(&mut self, route: &str, method: HttpMethod, handler: RequestHandler) {
        self.mappings.insert((route.to_string(), method), handler);
    }

    pub fn mount_static(&mut self, http_path: &str, fs_path: &str) {
        self.static_mounts
            .insert(http_path.to_string(), fs_path.to_string());
    }
    pub fn thread_start_at_port(
        self,
        port: usize,
        stop_flag: Arc<AtomicBool>,
    ) -> thread::JoinHandle<()> {
        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
        listener.set_nonblocking(true).unwrap();

        let handle = std::thread::spawn(move || {
            while !stop_flag.load(Ordering::SeqCst) {
                match listener.accept() {
                    Ok((mut stream, _)) => {
                        let req = Request::parse(&mut stream);
                        let res = match req {
                            Some(req) => self.handle_req(req).unwrap_or_else(Self::handler_404),
                            None => Self::handler_400(),
                        };
                        res.write_to_stream(&mut stream);
                    }
                    Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                        thread::sleep(Duration::from_millis(10))
                    }
                    Err(e) => {
                        eprintln!("{e}");
                        break;
                    }
                }
            }
        });
        // make sure the server is started
        thread::sleep(Duration::from_millis(100));
        handle
    }
    fn handle_req(&self, req: Request) -> Option<Response> {
        println!();
        println!("{:?} Request recieved at '{}'", req.method, req.path);
        match req.method {
            HttpMethod::Get => {
                if self.static_mounts.keys().any(|hp| req.path.starts_with(hp)) {
                    return self.static_handler(req);
                }
                self.mappings.get(&(req.path.clone(), HttpMethod::Get))?(req)
            }
            HttpMethod::Post => self.mappings.get(&(req.path.clone(), HttpMethod::Post))?(req),
        }
    }
    fn static_handler(&self, req: Request) -> Option<Response> {
        let http_path_root = self
            .static_mounts
            .keys()
            .find(|http_path_root| req.path.starts_with(*http_path_root))
            .unwrap();
        let fs_path_root = self.static_mounts.get(http_path_root).unwrap();
        let path = req
            .path
            .to_string()
            .replacen(http_path_root, fs_path_root, 1);

        println!(
            "static map from http-path to fs-path: {} => {}",
            req.path, path
        );

        let mut path_buf = PathBuf::from(path);
        if !path_buf.exists() {
            return None;
        }
        match path_buf.metadata() {
            Ok(metadata) => {
                if metadata.is_dir() {
                    path_buf.push("index.html");
                }

                eprintln!("File path: {}", path_buf.to_str().unwrap());

                Some(
                    ResponseBuilder::new()
                        .status_ok()
                        .body_and_auto_content_headers(HttpBody::create_html(
                            path_buf.to_str().unwrap(),
                        ))
                        .build(),
                )
            }
            Err(_) => None,
        }
    }
    fn handler_404() -> Response {
        println!("returning generic 404...");
        ResponseBuilder::new()
            .status(404, "Not Found")
            .body_and_auto_content_headers(HttpBody::create_html("static/generic/404.html"))
            .build()
    }
    fn handler_400() -> Response {
        println!("returning generic 400 ...");
        ResponseBuilder::new()
            .status(400, "Bad Request")
            .body_and_auto_content_headers(HttpBody::create_html("static/generic/400.html"))
            .build()
    }
}

trait DebugNone {
    fn debug_none(self, text: &str) -> Self;
}

impl<T> DebugNone for Option<T> {
    fn debug_none(self, text: &str) -> Self {
        if self.is_none() {
            println!("{text}");
        }
        self
    }
}