1use std::{collections::HashMap, str::FromStr};
3
4use crate::method::HttpMethod;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct HttpRequest {
9 pub method: HttpMethod,
11 pub path: String,
13 pub headers: HashMap<String, String>,
14 pub body: Option<String>,
15 pub query: Option<HashMap<String, String>>,
17}
18
19impl FromStr for HttpRequest {
20 type Err = std::io::Error;
21
22 fn from_str(input: &str) -> Result<Self, Self::Err> {
23 let mut first_line = input
24 .lines()
25 .next()
26 .ok_or({
27 std::io::Error::new(
28 std::io::ErrorKind::InvalidInput,
29 "failed getting first http line",
30 )
31 })?
32 .split_whitespace();
33
34 let (Some(method_str), Some(path), Some(_version)) =
35 (first_line.next(), first_line.next(), first_line.next())
36 else {
37 return Err(std::io::Error::new(
38 std::io::ErrorKind::InvalidInput,
39 "Invalid http top header",
40 ));
41 };
42
43 let method = HttpMethod::from_str_val(method_str);
44
45 let (path, query): (&str, Option<HashMap<String, String>>) = match path.split_once('?') {
46 Some((path, query)) => {
47 let query_map: HashMap<String, String> = query
48 .split('&')
49 .filter_map(|q| q.split_once('='))
50 .map(|(k, v)| (k.to_owned(), v.to_owned()))
51 .collect();
52 (path, Some(query_map))
53 }
54 None => (path, None),
55 };
56
57 let headers: HashMap<String, String> = input
58 .lines()
59 .take_while(|line| !line.is_empty())
60 .skip(1)
61 .filter_map(|line| {
62 line.split_once(':')
63 .map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
64 })
65 .collect();
66
67 let body = if method == HttpMethod::Get {
68 None
69 } else {
70 input
71 .split_once("\r\n\r\n")
72 .map(|head_body_split| head_body_split.1.to_owned())
73 .filter(|body| !body.is_empty())
74 };
75
76 let path = path.to_owned();
77
78 let req: HttpRequest = HttpRequest {
79 method,
80 path,
81 headers,
82 body,
83 query,
84 };
85 Ok(req)
86 }
87}