micro_http_async/
routes.rs

1use crate::Request;
2use chunked_transfer::Encoder;
3use futures::future::BoxFuture;
4use std::collections::HashMap;
5use std::future::Future;
6use std::io::Write;
7use tokio::io::AsyncReadExt;
8
9/// # RouteDef
10///
11/// This trait creates a helpful function that can convert an asynchrynous function without much user input.
12///
13/// This cleans up the API quite a bit, only requiring the user to Box the function they want to use.
14///
15/// Hopefully I figure out macros soon so I can simplify the whole process further to a single macro.
16pub trait RouteDef {
17    fn call(&self, request: Request) -> BoxFuture<'static, Result<String, String>>;
18}
19impl<T, F> RouteDef for T
20where
21    T: Fn(Request) -> F,
22    F: Future<Output = Result<String, String>> + Send + 'static,
23{
24    /// # Call
25    /// Run the function (defined as being a future of type T), taking in the `request` we want to use
26    fn call(&self, request: Request) -> BoxFuture<'static, Result<String, String>> {
27        Box::pin(self(request))
28    }
29}
30
31/// # Route
32///
33/// This struct defines a `Route`. A route is an address defined on a webserver by a `/`. For example, `localhost/search` - `/search` is the route.
34///
35/// This struct will store a reference to a function or closure, and will run it automatically when a user visits the corresponding route defined to the function.
36pub struct Route {
37    /// The async callback function, boxed so that it can live on the heap
38    function: Box<dyn RouteDef>,
39}
40
41impl Route {
42    /// # New
43    ///
44    /// Create a new route, taking in a Boxed function as its input.
45    pub fn new(function: Box<dyn RouteDef>) -> Self {
46        Self { function }
47    }
48
49    /// # Run
50    ///
51    /// Run the function, taking in the request as its input. It will return a corresponding DataType based on the output of the function.
52    pub async fn run(&self, request: Request) -> DataType {
53        // Check that our function returned an Ok result, and unwrap it after it executes
54        if let Ok(v) = self.function.call(request).await {
55            DataType::Text(v)
56        } else {
57            DataType::Text(String::new()) // Err returned, just return nothing
58        }
59    }
60}
61
62/// # DataType
63///
64/// This returns the data type of the response, wrapping the response as well
65///
66/// Used mostly for returning static images as bytes
67///
68/// for example, if you're requesting for a static image from say `/static/img.png`,
69/// you would want `Bytes(content)` instead of `Text(content)`. The API already handles
70/// this for you, but it is worth keeping in mind how it works behind the scenes
71pub enum DataType {
72    /// Defines a Text data type - this is for returning text, such as HTML
73    Text(String),
74    /// Defines a Bytes data type - this is for returning binary data, such as images
75    Bytes(Vec<u8>),
76}
77
78/// # Routes
79///
80/// This struct defines the routes. It uses a hashmap to do this.
81///
82/// `HashMap<Route, Content>` where content is the return content (ie, html or json).
83pub struct Routes {
84    /// The hashmap of routes. This stores the route (ie, `/`) and the content (a valid Route, which holds the callback function)
85    routes: HashMap<String, Route>,
86}
87
88impl Routes {
89    /// # New
90    ///
91    /// Create a new `Route` struct
92    pub async fn new() -> Self {
93        Self {
94            routes: HashMap::<String, Route>::new(),
95        }
96    }
97
98    /// # Add Route
99    ///
100    /// Adds a new route to the routes hashmap. If the route already exists,
101    /// its value is updated
102    pub async fn add_route(&mut self, route: String, content: Route) {
103        self.routes.insert(route, content);
104    }
105
106    /// # Get Route
107    ///
108    /// This function takes in the response string from the `TcpStream` and searches the hashmap
109    /// for the callback function associated with the route. It then checks that the route is valid,
110    /// and runs it asynchrynously (using the request so that the callback can make use of the request data)
111    ///
112    /// This function only runs the callback - handling POST and GET requests is up to the callback.
113    ///
114    /// If this function detects a request for static content - which it can only detect if the data is stored in
115    /// `/static/`, then it will return early with the static content, and not run any functions.
116    ///
117    /// If an error handler is not set, and a route is not found, a panic will occur.
118    pub async fn get_route(
119        &self,
120        request: String,
121        user_addr: std::net::SocketAddr,
122        is_secure: bool,
123    ) -> Result<DataType, &str> {
124        let request = Request::new(request, user_addr, is_secure).await.unwrap();
125
126        // Handle static files - check if theyre binary or text, and handle appropriately.
127        // Probably not the best method but it *works*
128        if request.uri.contains("static") {
129            let file_path = format!(".{}", request.uri);
130            return match tokio::fs::File::open(file_path).await {
131                Ok(mut file_handle) => {
132                    let mut contents = vec![];
133                    file_handle.read_to_end(&mut contents).await.unwrap();
134
135                    let result = String::from("HTTP/1.1 {}\r\nContent-type: image/jpeg;\r\nTransfer-Encoding: chunked\r\n\r\n");
136                    let mut result = result.into_bytes();
137
138                    // We split the data into chunks so we don't allocate a ton of data to the stack
139                    let chunks = contents.chunks(5);
140                    let mut iter_chunks = Vec::<std::io::IoSlice>::new();
141                    for chunk in chunks {
142                        iter_chunks.push(std::io::IoSlice::new(chunk));
143                    }
144
145                    // TODO: we need to figure out how to write chunked to the buffer using non-nightly features
146                    // Also, it might be worth moving to HTTP 2 for this as chunked is http 1 only
147                    let mut encoded = Vec::new();
148                    {
149                        let mut encoder = Encoder::with_chunks_size(&mut encoded, 8);
150                        encoder.write_all_vectored(&mut iter_chunks).unwrap();
151                    }
152                    result.extend(&encoded);
153
154                    match String::from_utf8(result.clone()) {
155                        Ok(_) => {
156                            let result = String::from("HTTP/1.1 {} {}\r\nContent-type: text/css;\r\nTransfer-Encoding: chunked\r\n\r\n");
157                            let mut result = result.into_bytes();
158                            result.extend(&encoded);
159                            let v = String::from_utf8(result).expect("This should work");
160                            return Ok(DataType::Text(v));
161                        }
162                        Err(_) => return Ok(DataType::Bytes(result)),
163                    }
164                }
165                Err(e) => {
166                    println!("Error loading static content: {}", e);
167                    Ok(DataType::Text(String::from(
168                        "ERROR - CONTENT NOT AVAILABLE",
169                    )))
170                }
171            };
172        }
173
174        // If not static, handle the request
175        let func = match self.routes.get(&request.uri) {
176            Some(v) => v,
177            None => {
178                println!(
179                    "Error - user requested '{}', which does not exist on this server.",
180                    request.uri
181                );
182                self.routes.get(&"err".to_string()).unwrap() // we assume we've got an error handler
183            }
184        };
185
186        let result = func.run(request).await;
187
188        Ok(result)
189    }
190}