yan_net 0.1.0

A simple library for sending HTTP requests and creating HTTP servers
Documentation
use std::{
    fmt::Display,
    fs,
    io::{BufRead, BufReader, Read, Write},
    net::TcpStream,
};
use yan_json::prelude::*;

#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum HttpMethod {
    Get,
    Post,
}

impl TryFrom<&str> for HttpMethod {
    type Error = ();

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "GET" => Ok(Self::Get),
            "POST" => Ok(Self::Post),
            _ => Err(()),
        }
    }
}

impl Display for HttpMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Get => write!(f, "GET"),
            Self::Post => write!(f, "POST"),
        }
    }
}

#[derive(Debug)]
pub enum HttpBody {
    Json(JsonNode),
    File(String),
    None,
}

impl HttpBody {
    pub fn get_type(&self) -> String {
        match self {
            Self::None => "-",
            Self::Json(_) => "application/json",
            Self::File(name) => match name.rfind('.').map(|i| &name[i..]) {
                Some(".html") => "text/html",
                Some(".js") => "text/js",
                Some(".png") => "image/png",
                Some(".svg") => "image/svg+xml",
                _ => panic!("extension of '{name}' could not be mapped"),
            },
        }
        .into()
    }
    pub fn as_json(&self) -> Option<&JsonNode> {
        match self {
            Self::Json(node) => Some(node),
            _ => None,
        }
    }
    pub fn get_byte_count(&self) -> usize {
        match self {
            Self::None => 0,
            Self::Json(node) => node.to_string().len(),
            Self::File(path) => fs::read(path).unwrap().len(),
        }
    }
}

impl TryFrom<HttpBody> for JsonNode {
    type Error = ();
    fn try_from(body: HttpBody) -> Result<Self, Self::Error> {
        match body {
            HttpBody::Json(root) => Ok(root),
            _ => Err(()),
        }
    }
}

#[derive(Debug)]
pub struct Request {
    pub method: HttpMethod,
    pub path: String,
    pub headers: HashMap<String, String>,
    pub body: HttpBody,
}

impl Request {
    pub fn send(&self) -> Option<Response> {
        let Some(destination) = self.headers.get("Host") else {
            println!("Host header not found");
            return None;
        };
        let mut stream = match TcpStream::connect(destination) {
            Ok(stream) => stream,
            Err(e) => panic!("{e}"),
        };
        // let Ok(mut stream) = TcpStream::connect(destination) else {
        //     println!("Could not connect to stream");
        //     return None;
        // };

        let first_line = format!("{} {} HTTP/1.1", self.method, self.path);
        stream.write_all(first_line.as_bytes()).ok()?;
        stream.write_all(b"\r\n").ok()?;

        for (key, val) in &self.headers {
            let header_line = format!("{key}: {val}\r\n");
            stream.write_all(header_line.as_bytes()).ok()?;
        }
        stream.write_all(b"\r\n").ok()?;

        match &self.body {
            HttpBody::Json(node) => {
                stream.write_all(node.to_string().as_bytes()).ok()?;
            }
            HttpBody::File(path) => {
                let data = fs::read(path).ok()?;
                stream.write_all(&data).ok()?;
            }
            HttpBody::None => {}
        }
        Response::parse(&mut stream)
    }

    pub fn parse(stream: &mut TcpStream) -> Option<Self> {
        let mut br = BufReader::new(stream);
        let mut lines = br.by_ref().lines();

        // GET /images/logo.png HTTP/1.1
        let req_status_line: String = lines.next()?.ok()?;

        let mut it = req_status_line.split(' ');
        let method: HttpMethod = it.next()?.try_into().ok()?;
        let path: String = it.next().unwrap().into();

        // <header-name>: <header-value>
        // Content-Type: application/json
        let headers = lines
            .map(|l| l.ok())
            .take_while(|l| matches!(l, Some(l) if !l.trim().is_empty()))
            .map(|l| {
                let l = l?;
                let mut it = l.split(": ");
                Some((it.next()?.to_lowercase(), it.next()?.to_lowercase()))
            })
            .collect::<Option<HashMap<String, String>>>()?;

        // body
        let body_type: &str = headers
            .get("content-type")
            .map(|s| s.as_str())
            .unwrap_or("None");
        let body_byte_count: usize = headers
            .get("content-length")
            .and_then(|s| s.parse().ok())
            .unwrap_or(0);

        let mut body_raw = vec![0_u8; body_byte_count];

        br.read_exact(&mut body_raw).unwrap();

        let body = match body_type {
            "application/json" => {
                let json_str = String::from_utf8(body_raw).ok()?;
                HttpBody::Json(JsonNode::parse(&json_str)?)
            }
            "None" => HttpBody::None,
            _ => panic!(),
        };

        Some(Self {
            method,
            headers,
            path,
            body,
        })
    }
}

#[derive(Default)]
pub struct RequestBuilder {
    path: Option<String>,
    method: Option<HttpMethod>,
    headers: HashMap<String, String>,
    body: Option<HttpBody>,
}

impl RequestBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn build(self) -> Request {
        Request {
            method: self.method.unwrap(),
            path: self.path.unwrap(),
            headers: self.headers,
            body: self.body.unwrap(),
        }
    }
    pub fn method(mut self, method: HttpMethod) -> Self {
        self.method = Some(method);
        self
    }
    pub fn path(mut self, path: String) -> Self {
        self.path = Some(path);
        self
    }
    pub fn header(mut self, key: String, value: String) -> Self {
        *self.headers.entry(key).or_default() = value;
        self
    }
    pub fn body_and_auto_content_headers(self, body: HttpBody) -> Self {
        self.header("Content-Type".into(), body.get_type())
            .header("Content-Length".into(), body.get_byte_count().to_string())
            .body(body)
    }
    fn body(mut self, body: HttpBody) -> Self {
        self.body = Some(body);
        self
    }
}

#[derive(Debug)]
pub struct Response {
    pub status: (usize, String),
    pub headers: HashMap<String, String>,
    pub body: HttpBody,
}

impl Response {
    pub fn write_to_stream(&self, stream: &mut TcpStream) {
        let status_line: String = format!("HTTP/1.1 {} {}", self.status.0, self.status.1);
        stream.write_all(status_line.as_bytes()).unwrap();
        stream.write_all(b"\r\n").unwrap();

        for (key, val) in &self.headers {
            stream.write_all(key.as_bytes()).unwrap();
            stream.write_all(b": ").unwrap();
            stream.write_all(val.as_bytes()).unwrap();
            stream.write_all(b"\r\n").unwrap();
        }

        stream.write_all(b"\r\n").unwrap();

        match &self.body {
            HttpBody::Json(node) => {
                stream.write_all(node.to_string().as_bytes()).unwrap();
            }
            HttpBody::File(path) => {
                let data = fs::read(path).unwrap();
                stream.write_all(&data).unwrap();
            }
            HttpBody::None => {}
        }
    }

    pub fn parse(stream: &mut TcpStream) -> Option<Self> {
        let mut br = BufReader::new(stream);
        let mut lines = br.by_ref().lines();

        // HTTP/1.1 200 OK
        let req_status_line = lines.next()?;
        let req_status_line = req_status_line.ok()?;
        let mut it = req_status_line.split(' ');
        let _http_version = it.next()?;
        let status_code = it.next()?.parse::<usize>().ok()?;
        let status_text = it.next()?.to_string();

        // <header-name>: <header-value>
        // Content-Type: application/json
        let headers = lines
            .map(|l| l.ok())
            .take_while(|l| matches!(l, Some(l) if !l.trim().is_empty()))
            .map(|l| {
                let l = l?;
                let mut it = l.split(": ");
                Some((it.next()?.to_lowercase(), it.next()?.to_lowercase()))
            })
            .collect::<Option<HashMap<String, String>>>()?;

        // body
        let body_type: &str = headers
            .get("content-type")
            .map(|s| s.as_str())
            .unwrap_or("None");
        let body_byte_count: usize = headers
            .get("content-length")
            .and_then(|s| s.parse().ok())
            .unwrap_or(0);

        let mut body_raw = vec![0_u8; body_byte_count];

        br.read_exact(&mut body_raw).unwrap();

        let body = match body_type {
            "application/json" => {
                let json_str = String::from_utf8(body_raw).ok()?;
                HttpBody::Json(JsonNode::parse(&json_str)?)
            }
            "None" => HttpBody::None,
            other_type => panic!("{other_type}"),
        };

        Some(Self {
            status: (status_code, status_text),
            headers,
            body,
        })
    }
}

#[derive(Default)]
pub struct ResponseBuilder {
    status: Option<(usize, String)>,
    headers: HashMap<String, String>,
    body: Option<HttpBody>,
}

impl ResponseBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn build(self) -> Response {
        Response {
            status: self.status.unwrap(),
            headers: self.headers,
            body: self.body.unwrap(),
        }
    }
    pub fn status(mut self, code: usize, text: String) -> Self {
        self.status = Some((code, text));
        self
    }
    pub fn header(mut self, key: String, value: String) -> Self {
        *self.headers.entry(key).or_default() = value;
        self
    }
    pub fn body_and_auto_content_headers(self, body: HttpBody) -> Self {
        self.header("Content-Type".into(), body.get_type())
            .header("Content-Length".into(), body.get_byte_count().to_string())
            .body(body)
    }
    fn body(mut self, body: HttpBody) -> Self {
        self.body = Some(body);
        self
    }
}