kit_rs/http/
request.rs

1use super::body::{collect_body, parse_form, parse_json};
2use super::cookie::parse_cookies;
3use super::ParamError;
4use crate::error::FrameworkError;
5use bytes::Bytes;
6use serde::de::DeserializeOwned;
7use std::collections::HashMap;
8
9/// HTTP Request wrapper providing Laravel-like access to request data
10pub struct Request {
11    inner: hyper::Request<hyper::body::Incoming>,
12    params: HashMap<String, String>,
13}
14
15impl Request {
16    pub fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
17        Self {
18            inner,
19            params: HashMap::new(),
20        }
21    }
22
23    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
24        self.params = params;
25        self
26    }
27
28    /// Get the request method
29    pub fn method(&self) -> &hyper::Method {
30        self.inner.method()
31    }
32
33    /// Get the request path
34    pub fn path(&self) -> &str {
35        self.inner.uri().path()
36    }
37
38    /// Get a route parameter by name (e.g., /users/{id})
39    /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
40    pub fn param(&self, name: &str) -> Result<&str, ParamError> {
41        self.params
42            .get(name)
43            .map(|s| s.as_str())
44            .ok_or_else(|| ParamError {
45                param_name: name.to_string(),
46            })
47    }
48
49    /// Get all route parameters
50    pub fn params(&self) -> &HashMap<String, String> {
51        &self.params
52    }
53
54    /// Get the inner hyper request
55    pub fn inner(&self) -> &hyper::Request<hyper::body::Incoming> {
56        &self.inner
57    }
58
59    /// Get a header value by name
60    pub fn header(&self, name: &str) -> Option<&str> {
61        self.inner.headers().get(name).and_then(|v| v.to_str().ok())
62    }
63
64    /// Get the Content-Type header
65    pub fn content_type(&self) -> Option<&str> {
66        self.header("content-type")
67    }
68
69    /// Check if this is an Inertia XHR request
70    pub fn is_inertia(&self) -> bool {
71        self.header("X-Inertia")
72            .map(|v| v == "true")
73            .unwrap_or(false)
74    }
75
76    /// Get all cookies from the request
77    ///
78    /// Parses the Cookie header and returns a HashMap of cookie names to values.
79    ///
80    /// # Example
81    ///
82    /// ```rust,ignore
83    /// let cookies = req.cookies();
84    /// if let Some(session) = cookies.get("session") {
85    ///     println!("Session: {}", session);
86    /// }
87    /// ```
88    pub fn cookies(&self) -> HashMap<String, String> {
89        self.header("Cookie")
90            .map(parse_cookies)
91            .unwrap_or_default()
92    }
93
94    /// Get a specific cookie value by name
95    ///
96    /// # Example
97    ///
98    /// ```rust,ignore
99    /// if let Some(session_id) = req.cookie("session") {
100    ///     // Use session_id
101    /// }
102    /// ```
103    pub fn cookie(&self, name: &str) -> Option<String> {
104        self.cookies().get(name).cloned()
105    }
106
107    /// Get the Inertia version from request headers
108    pub fn inertia_version(&self) -> Option<&str> {
109        self.header("X-Inertia-Version")
110    }
111
112    /// Get partial component name for partial reloads
113    pub fn inertia_partial_component(&self) -> Option<&str> {
114        self.header("X-Inertia-Partial-Component")
115    }
116
117    /// Get partial data keys for partial reloads
118    pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
119        self.header("X-Inertia-Partial-Data")
120            .map(|v| v.split(',').collect())
121    }
122
123    /// Consume the request and collect the body as bytes
124    pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
125        let content_type = self
126            .inner
127            .headers()
128            .get("content-type")
129            .and_then(|v| v.to_str().ok())
130            .map(|s| s.to_string());
131
132        let params = self.params;
133        let bytes = collect_body(self.inner.into_body()).await?;
134
135        Ok((
136            RequestParts {
137                params,
138                content_type,
139            },
140            bytes,
141        ))
142    }
143
144    /// Parse the request body as JSON
145    ///
146    /// Consumes the request since the body can only be read once.
147    ///
148    /// # Example
149    ///
150    /// ```rust,ignore
151    /// #[derive(Deserialize)]
152    /// struct CreateUser { name: String, email: String }
153    ///
154    /// pub async fn store(req: Request) -> Response {
155    ///     let data: CreateUser = req.json().await?;
156    ///     // ...
157    /// }
158    /// ```
159    pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
160        let (_, bytes) = self.body_bytes().await?;
161        parse_json(&bytes)
162    }
163
164    /// Parse the request body as form-urlencoded
165    ///
166    /// Consumes the request since the body can only be read once.
167    ///
168    /// # Example
169    ///
170    /// ```rust,ignore
171    /// #[derive(Deserialize)]
172    /// struct LoginForm { username: String, password: String }
173    ///
174    /// pub async fn login(req: Request) -> Response {
175    ///     let form: LoginForm = req.form().await?;
176    ///     // ...
177    /// }
178    /// ```
179    pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
180        let (_, bytes) = self.body_bytes().await?;
181        parse_form(&bytes)
182    }
183
184    /// Parse the request body based on Content-Type header
185    ///
186    /// - `application/json` -> JSON parsing
187    /// - `application/x-www-form-urlencoded` -> Form parsing
188    /// - Otherwise -> JSON parsing (default)
189    ///
190    /// Consumes the request since the body can only be read once.
191    pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
192        let (parts, bytes) = self.body_bytes().await?;
193
194        match parts.content_type.as_deref() {
195            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
196            _ => parse_json(&bytes),
197        }
198    }
199
200    /// Consume the request and return its parts along with the inner hyper request body
201    ///
202    /// This is used internally by the handler macro for FormRequest extraction.
203    pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
204        let content_type = self
205            .inner
206            .headers()
207            .get("content-type")
208            .and_then(|v| v.to_str().ok())
209            .map(|s| s.to_string());
210
211        let params = self.params;
212        let body = self.inner.into_body();
213
214        (
215            RequestParts {
216                params,
217                content_type,
218            },
219            body,
220        )
221    }
222}
223
224/// Request parts after body has been separated
225///
226/// Contains metadata needed for body parsing without the body itself.
227#[derive(Clone)]
228pub struct RequestParts {
229    pub params: HashMap<String, String>,
230    pub content_type: Option<String>,
231}