request_http_parser/
parser.rs

1//! Parse string request from incoming http to Request struct model which include method, path,
2//! headers, params (optional) and body (optional)
3use anyhow::{Context, Result, anyhow};
4use std::collections::HashMap;
5
6#[derive(Debug, PartialEq, Eq)]
7pub enum Method {
8    GET,
9    POST,
10    OPTIONS,
11}
12
13impl TryFrom<&str> for Method {
14    type Error = anyhow::Error;
15    fn try_from(value: &str) -> Result<Self, anyhow::Error> {
16        match value {
17            "GET" => Ok(Method::GET),
18            "POST" => Ok(Method::POST),
19            "OPTIONS" => Ok(Method::OPTIONS),
20            _ => Err(anyhow!("Method not supported")),
21        }
22    }
23}
24
25pub struct Request {
26    pub method: Method,
27    pub path: String,
28    pub params: Option<std::collections::HashMap<String, String>>,
29    pub headers: std::collections::HashMap<String, String>,
30    pub body: Option<String>,
31}
32
33impl Request {
34    /// # Examples
35    ///
36    /// ```
37    /// use request_http_parser::parser::{Method,Request};
38    ///     let req_str = format!(
39    ///         "POST /login HTTP/1.1\r\n\
40    ///         Content-Type: application/json\r\n\
41    ///         User-Agent: Test\r\n\
42    ///         Content-Length: {}\r\n\\r\n\
43    ///         {{\"username\": \"{}\",\"password\": \"{}\"}}",
44    ///         44, "crisandolin", "rumahorbo");
45    ///     let req = Request::new(&req_str).unwrap();
46    ///
47    ///     assert_eq!(Method::POST, req.method);
48    ///     assert_eq!("/login", req.path);  
49    /// ```
50    ///
51    pub fn new(request: &str) -> Result<Self> {
52        let mut parts = request.split("\r\n\r\n");
53        let head = parts.next().context("Headline Error")?;
54        // Body
55        let body = parts.next().map(|b| b.to_string());
56
57        // Method and path
58        let mut head_line = head.lines();
59        let first: &str = head_line.next().context("Empty Request")?;
60        let mut request_parts: std::str::SplitWhitespace<'_> = first.split_whitespace();
61        let method: Method = request_parts
62            .next()
63            .ok_or(anyhow!("missing method"))
64            .and_then(TryInto::try_into)
65            .context("Missing Method")?;
66        let url = request_parts.next().context("No Path")?;
67        let (path, params) = Self::extract_query_param(url);
68
69        // Headers
70        let mut headers = HashMap::new();
71        for line in head_line {
72            if let Some((k, v)) = line.split_once(":") {
73                headers.insert(k.trim().to_lowercase(), v.trim().to_string());
74            }
75        }
76        Ok(Request {
77            method,
78            path,
79            headers,
80            body,
81            params,
82        })
83    }
84
85    /// extract query param from url
86    fn extract_query_param(url: &str) -> (String, Option<HashMap<String, String>>) {
87        // Find the query string
88        if let Some(pos) = url.find('?') {
89            let path = &url[0..pos];
90            let query_string = &url[pos + 1..]; // Get substring after '?'
91
92            // Parse query params into a HashMap
93            let params: HashMap<_, _> = query_string
94                .split('&')
95                .filter_map(|pair| {
96                    let mut kv = pair.split('=');
97                    Some((kv.next()?.to_string(), kv.next()?.to_string()))
98                })
99                .collect();
100
101            // Return the token if it exists
102            (path.to_string(), Some(params))
103            // params.get("token").map(|s| s.to_string())
104        } else {
105            (url.to_string(), None)
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use crate::parser::Method;
113
114    use super::Request;
115
116    #[test]
117    fn parser() {
118        let req_str = format!(
119            "POST /login HTTP/1.1\r\n\
120                Content-Type: application/json\r\n\
121                User-Agent: Test\r\n\
122                Content-Length: {}\r\n\
123                \r\n\
124                {{\"username\": \"{}\",\"password\": \"{}\"}}",
125            44, "crisandolin", "rumahorbo"
126        );
127
128        let req = Request::new(&req_str).unwrap();
129
130        assert_eq!(Method::POST, req.method);
131        assert_eq!("/login", req.path);
132    }
133}