yan_net 0.2.0

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::{self, JoinHandle},
    time::Duration,
};

use crate::prelude::*;

#[derive(Default, Clone)]
pub struct RouteMapper {
    mappings: HashMap<(String, HttpMethod), RequestHandler>,
    static_mounts: HashMap<String, String>,
}

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

    pub fn mount_static(&mut self, http_path: String, fs_path: String) {
        self.static_mounts.insert(http_path, fs_path);
    }
    pub fn thread_start_at_port(self, port: usize, stop_flag: Arc<AtomicBool>) -> 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");
                }

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

                Some(
                    ResponseBuilder::new()
                        .status(200, "OK".into())
                        .body_and_auto_content_headers(HttpBody::File(
                            path_buf.to_str().unwrap().into(),
                        ))
                        .build(),
                )
            }
            Err(_) => None,
        }
    }
    fn handler_404() -> Response {
        println!("returning generic 404...");
        ResponseBuilder::new()
            .status(404, "Not Found".into())
            .body_and_auto_content_headers(HttpBody::File("static/generic/404.html".into()))
            .build()
    }
    fn handler_400() -> Response {
        println!("returning generic 400 ...");
        ResponseBuilder::new()
            .status(400, "Bad Request".into())
            .body_and_auto_content_headers(HttpBody::File("static/generic/400.html".into()))
            .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
    }
}