protorbit/http/
request.rs

1use anyhow::{anyhow, Result};
2use std::collections::HashMap;
3
4use crate::http::Method;
5
6use super::Version;
7
8#[derive(Debug, PartialEq, Clone)]
9pub struct Request {
10    pub method: super::method::Method,
11    pub path: String,
12    pub version: super::version::Version,
13    pub headers: HashMap<String, String>,
14    pub body: String,
15}
16
17impl Request {
18    pub fn new(
19        method: super::method::Method,
20        path: String,
21        version: super::version::Version,
22        headers: HashMap<String, String>,
23        body: String,
24    ) -> Self {
25        Self {
26            method,
27            path,
28            version,
29            headers,
30            body,
31        }
32    }
33
34    fn parse(data: impl Into<String>) -> Result<Self> {
35        let data = Into::<String>::into(data);
36        let mut lines = data.lines();
37        let req_line = match lines.next() {
38            Some(line) => line,
39            None => return Err(anyhow!("No request line")),
40        };
41
42        let (method, rest) = match req_line.split_once(" ") {
43            Some(kv) => kv,
44            None => return Err(anyhow!("Invalid request line")),
45        };
46        let method = Method::try_from(Into::<String>::into(method))?;
47        let (uri, version) = match rest.split_once(" ") {
48            Some(kv) => kv,
49            None => return Err(anyhow!("Invalid request line")),
50        };
51        let version = Version::try_from(Into::<String>::into(version))?;
52        let mut headers: HashMap<String, String> = HashMap::new();
53
54        while let Some(header) = lines.next() {
55            // \r\n\r\n
56            if header == "" {
57                break;
58            }
59            if let Some((k, v)) = header.split_once(':') {
60                let _ = headers.insert(k.into(), v.into());
61            }
62        }
63
64        let mut body = String::new();
65        while let Some(line) = lines.next() {
66            body.push_str(line);
67            body.push('\n'); // newline is important in some cases
68        }
69
70        Ok(Self {
71            method,
72            path: uri.into(),
73            version,
74            headers,
75            body,
76        })
77    }
78}
79
80impl TryFrom<String> for Request {
81    type Error = anyhow::Error;
82
83    fn try_from(value: String) -> Result<Self, Self::Error> {
84        Request::parse(Into::<String>::into(value))
85    }
86}
87
88impl Into<String> for Request {
89    fn into(self) -> String {
90        let mut buffer = String::new();
91        let s = format!(
92            "{} {} {}\r\n",
93            Into::<String>::into(self.method),
94            self.path,
95            Into::<String>::into(self.version),
96        );
97        buffer.push_str(s.as_str());
98        for (h, v) in self.headers.iter() {
99            buffer.push_str(&format!("{}: {}\r\n", h, v));
100        }
101        buffer.push_str("\r\n");
102        buffer.push_str(&self.body);
103        buffer
104    }
105}
106
107#[cfg(test)]
108mod test {
109    #[test]
110    fn test() {
111        let raw_req = "GET / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{}";
112        let req = super::Request::try_from(Into::<String>::into(raw_req)).unwrap();
113        let expected_req = super::Request::new(
114            crate::http::method::Method::GET,
115            "/".to_string(),
116            crate::http::version::Version::HTTP1_1,
117            std::collections::HashMap::from([(
118                "Content-Type".to_string(),
119                "application/json".to_string(),
120            )]),
121            "{}".to_string(),
122        );
123        assert_eq!(req, expected_req);
124        let request_string: String = req.into();
125        assert_eq!(raw_req, request_string);
126    }
127}