nucleus_http/
http.rs

1use core::fmt;
2use enum_map::Enum;
3use memchr::memmem;
4use std::path::PathBuf;
5
6#[derive(PartialEq, Debug, Clone, Copy, Enum)]
7pub enum Method {
8    GET,
9    POST,
10}
11
12#[derive(PartialEq, Debug, Clone, Copy)]
13pub enum Version {
14    V0_9,
15    V1_0,
16    V1_1,
17    V2_0,
18}
19
20pub type StatusCode = http::StatusCode;
21
22#[derive(Debug, PartialEq, Eq, Clone, Copy)]
23pub enum MimeType {
24    HTML,
25    PlainText,
26    JavaScript,
27    Json,
28    CSS,
29    SVG,
30    Icon,
31    Binary,
32    JPEG,
33    PNG,
34    Custom(&'static str),
35}
36
37impl TryFrom<&[u8]> for Method {
38    type Error = String;
39    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
40        match value {
41            b"GET" => Ok(Method::GET),
42            b"POST" => Ok(Method::POST),
43            _ => Err("Invalid Method".to_owned()),
44        }
45    }
46}
47
48impl TryFrom<&[u8]> for Version {
49    type Error = String;
50    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
51        match value {
52            b"HTTP/1.0" => Ok(Version::V1_0),
53            b"HTTP/1.1" => Ok(Version::V1_1),
54            b"HTTP/2.2" => Ok(Version::V2_0),
55            _ => Err("invalid version".to_owned()),
56        }
57    }
58}
59
60/// HTTP headers are simple key value pairs both strings
61#[derive(Debug, PartialEq, Clone)]
62pub struct Header {
63    pub key: String,
64    pub value: String,
65}
66
67pub trait IntoHeader {
68    fn into_header(self) -> Header;
69}
70
71impl IntoHeader for Header {
72    fn into_header(self) -> Header {
73        self
74    }
75}
76
77impl IntoHeader for &Header {
78    fn into_header(self) -> Header {
79        self.clone()
80    }
81}
82
83impl IntoHeader for (&str, &str) {
84    fn into_header(self) -> Header {
85        let (key, value) = self;
86        Header::new(key, value)
87    }
88}
89
90impl IntoHeader for (&str, &String) {
91    fn into_header(self) -> Header {
92        let (key, value) = self;
93        Header::new(key, value)
94    }
95}
96
97impl fmt::Display for Method {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::GET => write!(f, "GET"),
101            Self::POST => write!(f, "POST"),
102        }
103    }
104}
105impl From<&MimeType> for String {
106    fn from(mime: &MimeType) -> String {
107        let media_type = mime.media_type();
108        let charset = mime.charset();
109        let boundary = mime.boundary();
110        if let (Some(boundary), Some(charset)) = (boundary, charset) {
111            format!("{}; charset={}; boundary={}", media_type, charset, boundary)
112        } else if let (None, Some(charset)) = (boundary, charset) {
113            format!("{}; charset={}", media_type, charset)
114        } else {
115            format!("{};", media_type)
116        }
117    }
118}
119
120impl From<MimeType> for String {
121    fn from(mime: MimeType) -> String {
122        String::from(&mime)
123    }
124}
125
126impl From<PathBuf> for MimeType {
127    fn from(value: PathBuf) -> Self {
128        if let Some(ext) = value.extension() {
129            return MimeType::from_extension(&ext.to_string_lossy());
130        } else {
131            MimeType::PlainText
132        }
133    }
134}
135
136impl MimeType {
137    pub fn media_type(&self) -> &str {
138        match self {
139            Self::PlainText => "text/plain",
140            Self::HTML => "text/html",
141            Self::JavaScript => "text/javascript",
142            Self::Json => "application/json",
143            Self::CSS => "text/css",
144            Self::SVG => "image/svg+xml",
145            Self::Icon => "image/vnd.microsoft.icon",
146            Self::Binary => "application/octet-stream",
147            Self::JPEG => "image/jpeg",
148            Self::PNG => "image/png",
149            Self::Custom(str) => str,
150        }
151    }
152
153    pub fn charset(&self) -> Option<&str> {
154        match self {
155            Self::SVG | Self::Icon | Self::Binary | Self::JPEG => None,
156            _ => Some("utf-8"),
157        }
158    }
159
160    pub fn boundary(&self) -> Option<&str> {
161        None
162    }
163
164    pub fn from_extension(extension: &str) -> Self {
165        match extension {
166            "json" => Self::Json,
167            "js" => Self::JavaScript,
168            "css" => Self::CSS,
169            "svg" => Self::SVG,
170            "ico" => Self::Icon,
171            "bin" => Self::Binary,
172            "html" => Self::HTML,
173            "jpeg" | "jpg" => Self::JPEG,
174            "png" => Self::PNG,
175            _ => Self::PlainText,
176        }
177    }
178}
179
180impl TryFrom<String> for Header {
181    type Error = &'static str;
182    fn try_from(string: String) -> Result<Self, Self::Error> {
183        let split: Vec<&str> = string.split(": ").collect();
184        match split.len().cmp(&2) {
185            std::cmp::Ordering::Equal => {
186                let key = split[0].to_lowercase();
187                let value = split[1].to_string();
188                Ok(Self { key, value })
189            }
190            std::cmp::Ordering::Greater => Err("Too many ': '"),
191            std::cmp::Ordering::Less => Err("Invalid Key Value Pair"),
192        }
193    }
194}
195
196impl TryFrom<&String> for Header {
197    type Error = &'static str;
198    fn try_from(string: &String) -> Result<Self, Self::Error> {
199        let split: Vec<&str> = string.split(": ").collect();
200        match split.len().cmp(&2) {
201            std::cmp::Ordering::Equal => {
202                let key = split[0].to_lowercase();
203                let value = split[1].to_string();
204                Ok(Self { key, value })
205            }
206            std::cmp::Ordering::Greater => Err("Too many ': '"),
207            std::cmp::Ordering::Less => Err("Invalid Key Value Pair"),
208        }
209    }
210}
211
212impl TryFrom<&str> for Header {
213    type Error = &'static str;
214    fn try_from(string: &str) -> Result<Self, Self::Error> {
215        let split: Vec<&str> = string.split(": ").collect();
216        match split.len().cmp(&2) {
217            std::cmp::Ordering::Equal => {
218                let key = split[0].to_lowercase();
219                let value = split[1].to_string();
220                Ok(Self { key, value })
221            }
222            std::cmp::Ordering::Greater => Err("Too many ': '"),
223            std::cmp::Ordering::Less => Err("Invalid Key Value Pair"),
224        }
225    }
226}
227
228impl TryFrom<&[u8]> for Header {
229    type Error = &'static str;
230    fn try_from(h_str: &[u8]) -> Result<Self, Self::Error> {
231        let sep = b": ";
232        let key_end = memmem::find(h_str, sep).ok_or("missing ': '")?;
233        let value_start = key_end + sep.len();
234        let key = &h_str[0..key_end];
235        let value = &h_str[value_start..h_str.len()];
236        Ok(Self {
237            key: String::from_utf8_lossy(key).to_string().to_lowercase(),
238            value: String::from_utf8_lossy(value).to_string(),
239        })
240    }
241}
242
243impl From<&Header> for String {
244    fn from(header: &Header) -> String {
245        format!("{}: {}", header.key, header.value)
246    }
247}
248
249impl From<Header> for String {
250    fn from(header: Header) -> String {
251        format!("{}: {}", header.key, header.value)
252    }
253}
254
255impl Header {
256    pub fn new(key: &str, value: &str) -> Header {
257        Header {
258            key: key.to_lowercase(),
259            value: value.to_string(),
260        }
261    }
262    /// Create new vector of headers for server
263    pub fn new_server() -> Vec<Header> {
264        const VERSION: &str = env!("CARGO_PKG_VERSION");
265        const NAME: &str = env!("CARGO_PKG_NAME");
266        vec![Header {
267            key: "Server".to_string(),
268            value: format!("{NAME} {VERSION}"),
269        }]
270    }
271}
272
273impl fmt::Display for Version {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        let s = match self {
276            Version::V0_9 => "",
277            Version::V1_0 => "HTTP/1.0",
278            Version::V1_1 => "HTTP/1.1",
279            Version::V2_0 => "HTTP/2",
280        };
281        write!(f, "{}", s)
282    }
283}
284
285impl From<Version> for &str {
286    fn from(version: Version) -> &'static str {
287        match version {
288            Version::V0_9 => "",
289            Version::V1_0 => "HTTP/1.0",
290            Version::V1_1 => "HTTP/1.1",
291            Version::V2_0 => "HTTP/2",
292        }
293    }
294}
295
296impl From<Version> for String {
297    fn from(version: Version) -> String {
298        version.to_string()
299    }
300}