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