oak_http_server/
structs.rs

1use std::{collections::HashMap, fmt};
2
3/// The HTTP version of a request or a response
4#[derive(PartialEq, Clone)]
5pub struct Version {
6    /// The major revision number of the HTTP version
7    pub major: usize,
8    /// The minor revision number of the HTTP version
9    pub minor: usize,
10}
11
12impl Version {
13    /// Initialize a [`Version`] by passing a [`&str`] or [`String`] to it in the format `HTTP/{major}.{minor}`. The corresponding numbers `major` and `minor` represent a HTTP version and are stored in the struct's fields.
14    ///
15    /// If for whatever reason this function fails to parse the [`&str`] provided into a [`Version`], it will return [`None`].
16    ///
17    /// If the [`&str`] provided is parsed successfully, then the function will return a [`Some`] value containing a [`Version`] struct
18    ///
19    /// # Example
20    ///
21    /// ```
22    /// # use oak_http_server::Version;
23    ///
24    /// fn main() {
25    /// 	let version = Version::new("HTTP/1.1").unwrap(); // Unwrap the `Some` value the `new` function returns
26    /// 	println!("{}", version); // Prints "HTTP/1.1" in the console
27    /// }
28    /// ```
29    pub fn new<S>(version: S) -> Option<Self>
30    where
31        S: Into<String>,
32    {
33        let version = version.into();
34
35        if version.len() >= 5 {
36            if &version[0..4] == "HTTP" && &version[4..5] == "/" {
37                let version_split = &mut version[5..].split(".");
38                if version_split.clone().count() == 2 {
39                    let parse_int = |option_input: Option<&str>| -> Option<usize> {
40                        let Some(string_num) = option_input else {
41							return None;
42						};
43
44                        let Ok(number) = string_num.parse::<usize>() else {
45							return None;
46						};
47
48                        Some(number)
49                    };
50
51                    let Some(major) = parse_int(version_split.next()) else {return None};
52                    let Some(minor) = parse_int(version_split.next()) else {return None};
53
54                    return Some(Self { major, minor });
55                }
56            }
57        }
58
59        None
60    }
61}
62
63impl fmt::Display for Version {
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        write!(f, "HTTP/{}.{}", self.major, self.minor)
66    }
67}
68
69/// Represents a HTTP URL (named [`Target`] for formality reasons)
70#[derive(Clone)]
71pub struct Target {
72    /// Contains the path of the current handler (Empty by default. Modified by the server before being passed to a handler). Primarily used by directory handlers.
73    ///
74    /// For example, if a directory handler is assigned at path `\www\etc` and the client attempts to access `\www\etc\main.txt`,
75    /// this field's String's contents  will be `\www\etc` and the [relative path](Self::relative_path) will be equal to `\main.txt`
76    pub target_path: String,
77    /// Check the [target path](Self::target_path) documentation
78    pub relative_path: String,
79    /// A HashMap with a String key representing the query value and a String value representing the query value (query is defined in RFC 3986 as well)
80    pub queries: HashMap<String, String>,
81}
82
83impl Target {
84    /// Parses a [`&str`] or [`String`] into a [`Target`]
85    pub fn new<S>(target: S) -> Self
86    where
87        S: Into<String>,
88    {
89        let target_string: String = Self::decode_url(target.into());
90
91        let (absolute_path, queries_str) = target_string
92            .split_once('?')
93            .unwrap_or((&target_string, ""));
94
95        let mut queries = HashMap::new();
96
97        if !queries_str.is_empty() {
98            let queries_split = queries_str.split("&");
99
100            for query_str in queries_split {
101                if let Some((name, value)) = query_str.split_once("=") {
102                    queries.insert(name.to_string(), value.to_string());
103                }
104            }
105        }
106
107        Self {
108            target_path: String::new(),
109            relative_path: absolute_path.to_string(),
110            queries,
111        }
112    }
113
114    /// Returns the URL path, according to RFC 3986
115    pub fn full_url(&self) -> String {
116        format!("{}{}", &self.target_path, &self.relative_path)
117    }
118
119    fn decode_url(encoded_url: String) -> String {
120        let mut url_iterator = encoded_url.split("%");
121
122        [
123            url_iterator.next().unwrap().to_string(),
124            url_iterator
125                .map(|str_to_decode| {
126                    if str_to_decode.len() >= 2 {
127                        if str_to_decode[..2]
128                            .chars()
129                            .all(|char_to_check| char_to_check.is_digit(16))
130                        {
131                            let mut concatenated_string = String::new();
132                            concatenated_string.push(
133                                char::from_u32(
134                                    u32::from_str_radix(&str_to_decode[..2], 16).unwrap(),
135                                )
136                                .unwrap(),
137                            );
138                            concatenated_string.push_str(&str_to_decode[2..]);
139                            return concatenated_string;
140                        }
141                    }
142
143                    str_to_decode.to_string()
144                })
145                .collect::<Vec<String>>()
146                .join(""),
147        ]
148        .join("")
149    }
150}
151
152impl fmt::Display for Target {
153    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
154        write!(f, "{}{}", self.full_url(), {
155            let mut queries_string = self
156                .queries
157                .iter()
158                .map(|(name, value)| format!("{}: {}&", name, value))
159                .collect::<String>();
160
161            if !queries_string.is_empty() {
162                queries_string.insert_str(0, "?");
163                queries_string.pop();
164            }
165
166            queries_string
167        })
168    }
169}