1use std::collections::HashMap;
2use std::io::Error as IoError;
3
4use tokio::io::AsyncBufRead;
5
6use crate::http::{HttpMethod, HttpVersion};
7
8#[derive(Debug)]
10pub struct HttpRequest {
11 pub method: HttpMethod,
13 pub path: String,
15 pub version: HttpVersion,
17 pub headers: HashMap<String, String>,
19}
20
21#[derive(Debug)]
23pub enum RequestError {
24 Io(IoError),
25 MalformedRequest,
26 UnsupportedMethod,
27 UnsupportedVersion,
28}
29
30impl From<IoError> for RequestError {
31 fn from(e: IoError) -> Self {
32 RequestError::Io(e)
33 }
34}
35
36impl HttpRequest {
37 pub fn parse_line(line: &str) -> Result<(HttpMethod, String, HttpVersion), RequestError> {
54 let parts: Vec<&str> = line.split_whitespace().collect();
55 if parts.len() != 3 {
56 return Err(RequestError::MalformedRequest);
57 }
58
59 let method = parts[0]
60 .parse::<HttpMethod>()
61 .map_err(|_| RequestError::UnsupportedMethod)?;
62 let path = parts[1].to_string();
63 let version = parts[2]
64 .parse::<HttpVersion>()
65 .map_err(|_| RequestError::UnsupportedVersion)?;
66
67 Ok((method, path, version))
68 }
69
70 pub async fn from_async_reader(reader: &mut (impl AsyncBufRead + Unpin)) -> Result<Self, RequestError> {
72 use tokio::io::AsyncBufReadExt;
73
74 let mut line = String::new();
75 reader.read_line(&mut line).await?; let (method, path, version) = Self::parse_line(&line)?;
77
78 let mut headers = HashMap::new();
79 loop {
80 line.clear();
81 let bytes = reader.read_line(&mut line).await?;
82 if bytes == 0 || line == "\r\n" {
83 break;
84 }
85
86 if let Some((key, value)) = line.split_once(':') {
87 headers.insert(key.trim().to_lowercase(), value.trim().to_string());
88 }
89 }
90
91 Ok(HttpRequest {
92 method,
93 path,
94 version,
95 headers,
96 })
97 }
98
99 pub fn header(&self, key: &str) -> Option<&str> {
101 self.headers.get(&key.to_lowercase()).map(String::as_str)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use tokio::io::BufReader;
109 use tokio::runtime::Runtime;
110
111 #[test]
112 fn test_parse_line_valid() {
113 let (method, path, version) = HttpRequest::parse_line("GET /foo HTTP/1.0").unwrap();
114 assert_eq!(method, HttpMethod::GET);
115 assert_eq!(path, "/foo");
116 assert_eq!(version, HttpVersion::Http1_0);
117 }
118
119 #[test]
120 fn test_parse_line_invalid() {
121 assert!(matches!(HttpRequest::parse_line("BAD LINE"), Err(RequestError::MalformedRequest)));
122 }
123
124 #[test]
125 fn test_parse_line_unknown_method() {
126 assert!(matches!(HttpRequest::parse_line("POST / HTTP/1.1"), Err(RequestError::UnsupportedMethod)));
127 }
128
129 #[test]
130 fn test_parse_line_unknown_version() {
131 assert!(matches!(HttpRequest::parse_line("GET / HTTP/2.0"), Err(RequestError::UnsupportedVersion)));
132 }
133
134 #[test]
135 fn test_header_lookup() {
136 let mut req = HttpRequest { method: HttpMethod::GET, path: String::new(), version: HttpVersion::Http1_1, headers: HashMap::new() };
137 req.headers.insert("content-type".into(), "text/html".into());
138 assert_eq!(req.header("Content-Type"), Some("text/html"));
139 }
140
141 #[test]
142 fn test_from_async_reader() {
143 let rt = Runtime::new().unwrap();
144 rt.block_on(async {
145 let data = b"HEAD /test HTTP/1.1\r\nHost: example.com\r\n\r\n";
146 let mut reader = BufReader::new(&data[..]);
147 let req = HttpRequest::from_async_reader(&mut reader).await.unwrap();
148 assert_eq!(req.method, HttpMethod::HEAD);
149 assert_eq!(req.path, "/test");
150 assert_eq!(req.version, HttpVersion::Http1_1);
151 assert_eq!(req.header("host"), Some("example.com"));
152 });
153 }
154
155 #[test]
156 fn test_from_async_reader_malformed() {
157 let rt = Runtime::new().unwrap();
158 rt.block_on(async {
159 let data = b"INVALID\r\n";
160 let mut reader = BufReader::new(&data[..]);
161 assert!(matches!(HttpRequest::from_async_reader(&mut reader).await, Err(RequestError::MalformedRequest)));
162 });
163 }
164}