torch_web/
request.rs

1//! # HTTP Request Handling
2//!
3//! This module provides the [`Request`] struct, which wraps HTTP requests and provides
4//! convenient methods for accessing request data like headers, body, path parameters,
5//! and query parameters.
6
7use std::collections::HashMap;
8use std::any::{Any, TypeId};
9use std::sync::Arc;
10use http::{HeaderMap, Method, Uri, Version};
11use http_body_util::BodyExt;
12use hyper::body::Incoming;
13use crate::extractors::state::StateMap;
14
15/// HTTP request wrapper that provides convenient access to request data.
16///
17/// The `Request` struct encapsulates all the information about an incoming HTTP request,
18/// including the method, URI, headers, body, and extracted path parameters. It provides
19/// a high-level API for accessing this data in handlers.
20///
21/// # Examples
22///
23/// ## Basic Usage in Handlers
24///
25/// ```rust
26/// use torch_web::{App, Request, Response};
27///
28/// let app = App::new()
29///     .get("/", |req: Request| async move {
30///         println!("Method: {}", req.method());
31///         println!("Path: {}", req.path());
32///         Response::ok().body("Hello!")
33///     });
34/// ```
35///
36/// ## Accessing Headers
37///
38/// ```rust
39/// use torch_web::{App, Request, Response};
40///
41/// let app = App::new()
42///     .post("/api/data", |req: Request| async move {
43///         if let Some(content_type) = req.header("content-type") {
44///             println!("Content-Type: {}", content_type);
45///         }
46///
47///         if let Some(auth) = req.header("authorization") {
48///             println!("Authorization: {}", auth);
49///         }
50///
51///         Response::ok().body("Received")
52///     });
53/// ```
54///
55/// ## Working with Request Body
56///
57/// ```rust
58/// use torch_web::{App, Request, Response};
59///
60/// let app = App::new()
61///     .post("/upload", |req: Request| async move {
62///         let body_bytes = req.body();
63///         println!("Received {} bytes", body_bytes.len());
64///
65///         if let Ok(body_text) = req.body_string() {
66///             println!("Body: {}", body_text);
67///         }
68///
69///         Response::ok().body("Upload complete")
70///     });
71/// ```
72///
73/// ## Path Parameters
74///
75/// ```rust
76/// use torch_web::{App, Request, Response};
77///
78/// let app = App::new()
79///     .get("/users/:id/posts/:post_id", |req: Request| async move {
80///         let user_id = req.param("id").unwrap_or("unknown");
81///         let post_id = req.param("post_id").unwrap_or("unknown");
82///
83///         Response::ok().body(format!("User {} Post {}", user_id, post_id))
84///     });
85/// ```
86#[derive(Debug)]
87pub struct Request {
88    method: Method,
89    uri: Uri,
90    version: Version,
91    headers: HeaderMap,
92    body: Vec<u8>,
93    params: HashMap<String, String>,
94    query: HashMap<String, String>,
95    extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
96}
97
98impl Request {
99    /// Creates a new empty request with default values.
100    ///
101    /// This is primarily used for testing and internal purposes. In normal operation,
102    /// requests are created from incoming HTTP requests via `from_hyper`.
103    ///
104    /// # Examples
105    ///
106    /// ```rust
107    /// use torch_web::Request;
108    ///
109    /// let req = Request::new();
110    /// assert_eq!(req.path(), "/");
111    /// assert_eq!(req.method().as_str(), "GET");
112    /// ```
113    pub fn new() -> Self {
114        Self {
115            method: Method::GET,
116            uri: "/".parse().unwrap(),
117            version: Version::HTTP_11,
118            headers: HeaderMap::new(),
119            body: Vec::new(),
120            params: HashMap::new(),
121            query: HashMap::new(),
122            extensions: HashMap::new(),
123        }
124    }
125
126    /// Creates a new Request from Hyper's request parts and body.
127    ///
128    /// This is an internal method used by the framework to convert incoming
129    /// Hyper requests into Torch Request objects. It reads the entire request
130    /// body into memory and parses query parameters.
131    ///
132    /// # Parameters
133    ///
134    /// * `parts` - The HTTP request parts (method, URI, headers, etc.)
135    /// * `body` - The request body stream
136    ///
137    /// # Returns
138    ///
139    /// Returns a `Result` containing the constructed `Request` or an error if
140    /// the body cannot be read or the request is malformed.
141    ///
142    /// # Examples
143    ///
144    /// ```rust,no_run
145    /// use torch_web::Request;
146    /// use hyper::body::Incoming;
147    /// use http::request::Parts;
148    ///
149    /// async fn handle_hyper_request(parts: Parts, body: Incoming) {
150    ///     match Request::from_hyper(parts, body).await {
151    ///         Ok(req) => {
152    ///             println!("Received request to {}", req.path());
153    ///         }
154    ///         Err(e) => {
155    ///             eprintln!("Failed to parse request: {}", e);
156    ///         }
157    ///     }
158    /// }
159    /// ```
160    pub async fn from_hyper(
161        parts: http::request::Parts,
162        body: Incoming,
163    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
164        let body_bytes = body.collect().await?.to_bytes().to_vec();
165
166        let query = Self::parse_query_string(parts.uri.query().unwrap_or(""));
167
168        Ok(Request {
169            method: parts.method,
170            uri: parts.uri,
171            version: parts.version,
172            headers: parts.headers,
173            body: body_bytes,
174            params: HashMap::new(),
175            query,
176            extensions: HashMap::new(),
177        })
178    }
179
180    /// Returns the HTTP method of the request.
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// use torch_web::{App, Request, Response, Method};
186    ///
187    /// let app = App::new()
188    ///     .route(Method::POST, "/api/data", |req: Request| async move {
189    ///         match req.method() {
190    ///             &Method::POST => Response::ok().body("POST request"),
191    ///             &Method::GET => Response::ok().body("GET request"),
192    ///             _ => Response::method_not_allowed().body("Method not allowed"),
193    ///         }
194    ///     });
195    /// ```
196    pub fn method(&self) -> &Method {
197        &self.method
198    }
199
200    /// Returns the complete URI of the request.
201    ///
202    /// This includes the path, query string, and fragment (if present).
203    ///
204    /// # Examples
205    ///
206    /// ```rust
207    /// use torch_web::{App, Request, Response};
208    ///
209    /// let app = App::new()
210    ///     .get("/debug", |req: Request| async move {
211    ///         let uri = req.uri();
212    ///         Response::ok().body(format!("Full URI: {}", uri))
213    ///     });
214    /// ```
215    pub fn uri(&self) -> &Uri {
216        &self.uri
217    }
218
219    /// Returns the path portion of the request URI.
220    ///
221    /// This excludes the query string and fragment, returning only the path component.
222    ///
223    /// # Examples
224    ///
225    /// ```rust
226    /// use torch_web::{App, Request, Response};
227    ///
228    /// let app = App::new()
229    ///     .get("/users/:id", |req: Request| async move {
230    ///         println!("Request path: {}", req.path()); // "/users/123"
231    ///         Response::ok().body("User page")
232    ///     });
233    /// ```
234    pub fn path(&self) -> &str {
235        self.uri.path()
236    }
237
238    /// Returns the HTTP version of the request.
239    ///
240    /// # Examples
241    ///
242    /// ```rust
243    /// use torch_web::{App, Request, Response};
244    /// use http::Version;
245    ///
246    /// let app = App::new()
247    ///     .get("/version", |req: Request| async move {
248    ///         let version = match req.version() {
249    ///             Version::HTTP_09 => "HTTP/0.9",
250    ///             Version::HTTP_10 => "HTTP/1.0",
251    ///             Version::HTTP_11 => "HTTP/1.1",
252    ///             Version::HTTP_2 => "HTTP/2.0",
253    ///             Version::HTTP_3 => "HTTP/3.0",
254    ///             _ => "Unknown",
255    ///         };
256    ///         Response::ok().body(format!("HTTP Version: {}", version))
257    ///     });
258    /// ```
259    pub fn version(&self) -> Version {
260        self.version
261    }
262
263    /// Returns a reference to the request headers.
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// use torch_web::{App, Request, Response};
269    ///
270    /// let app = App::new()
271    ///     .post("/api/data", |req: Request| async move {
272    ///         let headers = req.headers();
273    ///
274    ///         for (name, value) in headers.iter() {
275    ///             println!("{}: {:?}", name, value);
276    ///         }
277    ///
278    ///         Response::ok().body("Headers logged")
279    ///     });
280    /// ```
281    pub fn headers(&self) -> &HeaderMap {
282        &self.headers
283    }
284
285    /// Returns the value of a specific header.
286    ///
287    /// This is a convenience method that looks up a header by name and converts
288    /// it to a string. Returns `None` if the header doesn't exist or contains
289    /// invalid UTF-8.
290    ///
291    /// # Parameters
292    ///
293    /// * `name` - The header name to look up (case-insensitive)
294    ///
295    /// # Examples
296    ///
297    /// ```rust
298    /// use torch_web::{App, Request, Response};
299    ///
300    /// let app = App::new()
301    ///     .post("/api/upload", |req: Request| async move {
302    ///         if let Some(content_type) = req.header("content-type") {
303    ///             println!("Content-Type: {}", content_type);
304    ///
305    ///             if content_type.starts_with("application/json") {
306    ///                 return Response::ok().body("JSON data received");
307    ///             }
308    ///         }
309    ///
310    ///         Response::bad_request().body("Content-Type required")
311    ///     });
312    /// ```
313    pub fn header(&self, name: &str) -> Option<&str> {
314        self.headers.get(name)?.to_str().ok()
315    }
316
317    /// Returns the request body as a byte slice.
318    ///
319    /// The entire request body is read into memory when the request is created,
320    /// so this method provides immediate access to the raw bytes.
321    ///
322    /// # Examples
323    ///
324    /// ```rust
325    /// use torch_web::{App, Request, Response};
326    ///
327    /// let app = App::new()
328    ///     .post("/upload", |req: Request| async move {
329    ///         let body_bytes = req.body();
330    ///         println!("Received {} bytes", body_bytes.len());
331    ///
332    ///         // Process binary data
333    ///         if body_bytes.starts_with(b"PNG") {
334    ///             Response::ok().body("PNG image received")
335    ///         } else {
336    ///             Response::ok().body("Data received")
337    ///         }
338    ///     });
339    /// ```
340    pub fn body(&self) -> &[u8] {
341        &self.body
342    }
343
344    /// Returns the request body as a UTF-8 string.
345    ///
346    /// This method attempts to convert the request body bytes into a valid UTF-8 string.
347    /// Returns an error if the body contains invalid UTF-8 sequences.
348    ///
349    /// # Returns
350    ///
351    /// Returns `Ok(String)` if the body is valid UTF-8, or `Err(FromUtf8Error)` otherwise.
352    ///
353    /// # Examples
354    ///
355    /// ```rust
356    /// use torch_web::{App, Request, Response};
357    ///
358    /// let app = App::new()
359    ///     .post("/text", |req: Request| async move {
360    ///         match req.body_string() {
361    ///             Ok(text) => {
362    ///                 println!("Received text: {}", text);
363    ///                 Response::ok().body(format!("Echo: {}", text))
364    ///             }
365    ///             Err(_) => {
366    ///                 Response::bad_request().body("Invalid UTF-8 in request body")
367    ///             }
368    ///         }
369    ///     });
370    /// ```
371    pub fn body_string(&self) -> Result<String, std::string::FromUtf8Error> {
372        String::from_utf8(self.body.clone())
373    }
374
375    /// Parse the request body as JSON (requires "json" feature)
376    #[cfg(feature = "json")]
377    pub async fn json<T>(&self) -> Result<T, serde_json::Error>
378    where
379        T: serde::de::DeserializeOwned,
380    {
381        serde_json::from_slice(&self.body)
382    }
383
384    /// Get a path parameter by name
385    pub fn param(&self, name: &str) -> Option<&str> {
386        self.params.get(name).map(|s| s.as_str())
387    }
388
389    /// Get all path parameters
390    pub fn params(&self) -> &HashMap<String, String> {
391        &self.params
392    }
393
394    /// Get all path parameters (for extractors)
395    pub fn path_params(&self) -> &HashMap<String, String> {
396        &self.params
397    }
398
399    /// Set a path parameter (used internally by the router)
400    pub(crate) fn set_param(&mut self, name: String, value: String) {
401        self.params.insert(name, value);
402    }
403
404    /// Get a reference to the request extensions
405    pub fn extensions(&self) -> &HashMap<TypeId, Box<dyn Any + Send + Sync>> {
406        &self.extensions
407    }
408
409    /// Get a mutable reference to the request extensions
410    pub fn extensions_mut(&mut self) -> &mut HashMap<TypeId, Box<dyn Any + Send + Sync>> {
411        &mut self.extensions
412    }
413
414    /// Insert a value into the request extensions
415    pub fn insert_extension<T: Send + Sync + 'static>(&mut self, value: T) {
416        self.extensions.insert(TypeId::of::<T>(), Box::new(value));
417    }
418
419    /// Get a value from the request extensions
420    pub fn get_extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
421        self.extensions
422            .get(&TypeId::of::<T>())
423            .and_then(|boxed| boxed.downcast_ref())
424    }
425
426    /// Get a query parameter by name
427    pub fn query(&self, name: &str) -> Option<&str> {
428        self.query.get(name).map(|s| s.as_str())
429    }
430
431    /// Get all query parameters
432    pub fn query_params(&self) -> &HashMap<String, String> {
433        &self.query
434    }
435
436    /// Get the raw query string
437    pub fn query_string(&self) -> Option<&str> {
438        self.uri.query()
439    }
440
441    /// Get the request body as bytes (for extractors)
442    pub fn body_bytes(&self) -> &[u8] {
443        &self.body
444    }
445
446    /// Set the request body (for testing)
447    #[cfg(test)]
448    pub fn set_body(&mut self, body: Vec<u8>) {
449        self.body = body;
450    }
451
452    /// Get mutable access to headers (for extractors)
453    pub fn headers_mut(&mut self) -> &mut HeaderMap {
454        &mut self.headers
455    }
456
457    /// Parse query string into a HashMap
458    fn parse_query_string(query: &str) -> HashMap<String, String> {
459        let mut params = HashMap::new();
460        
461        for pair in query.split('&') {
462            if let Some((key, value)) = pair.split_once('=') {
463                let key = urlencoding::decode(key).unwrap_or_else(|_| key.into()).into_owned();
464                let value = urlencoding::decode(value).unwrap_or_else(|_| value.into()).into_owned();
465                params.insert(key, value);
466            } else if !pair.is_empty() {
467                let key = urlencoding::decode(pair).unwrap_or_else(|_| pair.into()).into_owned();
468                params.insert(key, String::new());
469            }
470        }
471        
472        params
473    }
474}
475
476/// Implementation of RequestStateExt for Request
477impl crate::extractors::state::RequestStateExt for Request {
478    fn get_state(&self, type_id: TypeId) -> Option<&Arc<dyn Any + Send + Sync>> {
479        // Check if we have a StateMap stored in extensions
480        if let Some(state_map_any) = self.extensions.get(&TypeId::of::<StateMap>()) {
481            if let Some(state_map) = state_map_any.downcast_ref::<StateMap>() {
482                return state_map.get_by_type_id(type_id);
483            }
484        }
485        None
486    }
487
488    fn set_state_map(&mut self, state_map: StateMap) {
489        self.extensions.insert(TypeId::of::<StateMap>(), Box::new(state_map));
490    }
491
492    fn state_map(&self) -> Option<&StateMap> {
493        self.extensions
494            .get(&TypeId::of::<StateMap>())
495            .and_then(|state_map_any| state_map_any.downcast_ref::<StateMap>())
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    // Imports for potential future test use
503
504    #[test]
505    fn test_parse_query_string() {
506        let query = "name=John&age=30&city=New%20York";
507        let params = Request::parse_query_string(query);
508        
509        assert_eq!(params.get("name"), Some(&"John".to_string()));
510        assert_eq!(params.get("age"), Some(&"30".to_string()));
511        assert_eq!(params.get("city"), Some(&"New York".to_string()));
512    }
513
514    #[test]
515    fn test_parse_empty_query_string() {
516        let params = Request::parse_query_string("");
517        assert!(params.is_empty());
518    }
519}