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}