ferro_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::any::{Any, TypeId};
8use std::collections::HashMap;
9
10/// HTTP Request wrapper providing Laravel-like access to request data
11pub struct Request {
12 inner: hyper::Request<hyper::body::Incoming>,
13 params: HashMap<String, String>,
14 extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
15 /// Route pattern for metrics (e.g., "/users/{id}" instead of "/users/123")
16 route_pattern: Option<String>,
17}
18
19impl Request {
20 /// Create a new request from a raw hyper request.
21 pub fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
22 Self {
23 inner,
24 params: HashMap::new(),
25 extensions: HashMap::new(),
26 route_pattern: None,
27 }
28 }
29
30 /// Attach route parameters extracted from the URL path.
31 pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
32 self.params = params;
33 self
34 }
35
36 /// Set the route pattern (e.g., "/users/{id}")
37 pub fn with_route_pattern(mut self, pattern: String) -> Self {
38 self.route_pattern = Some(pattern);
39 self
40 }
41
42 /// Get the route pattern for metrics grouping
43 pub fn route_pattern(&self) -> Option<String> {
44 self.route_pattern.clone()
45 }
46
47 /// Insert a value into the request extensions (type-map pattern)
48 ///
49 /// This is async-safe unlike thread-local storage.
50 pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
51 self.extensions.insert(TypeId::of::<T>(), Box::new(value));
52 }
53
54 /// Get a reference to a value from the request extensions
55 pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
56 self.extensions
57 .get(&TypeId::of::<T>())
58 .and_then(|boxed| boxed.downcast_ref::<T>())
59 }
60
61 /// Get a mutable reference to a value from the request extensions
62 pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
63 self.extensions
64 .get_mut(&TypeId::of::<T>())
65 .and_then(|boxed| boxed.downcast_mut::<T>())
66 }
67
68 /// Get the request method
69 pub fn method(&self) -> &hyper::Method {
70 self.inner.method()
71 }
72
73 /// Get the request path
74 pub fn path(&self) -> &str {
75 self.inner.uri().path()
76 }
77
78 /// Get a route parameter by name (e.g., /users/{id})
79 /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
80 pub fn param(&self, name: &str) -> Result<&str, ParamError> {
81 self.params
82 .get(name)
83 .map(|s| s.as_str())
84 .ok_or_else(|| ParamError {
85 param_name: name.to_string(),
86 })
87 }
88
89 /// Get a route parameter parsed as a specific type
90 ///
91 /// Combines `param()` with parsing, returning a typed value.
92 ///
93 /// # Example
94 ///
95 /// ```rust,ignore
96 /// pub async fn show(req: Request) -> Response {
97 /// let id: i32 = req.param_as("id")?;
98 /// // ...
99 /// }
100 /// ```
101 pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Result<T, ParamError>
102 where
103 T::Err: std::fmt::Display,
104 {
105 let value = self.param(name)?;
106 value.parse::<T>().map_err(|e| ParamError {
107 param_name: format!("{name} (parse error: {e})"),
108 })
109 }
110
111 /// Get all route parameters
112 pub fn params(&self) -> &HashMap<String, String> {
113 &self.params
114 }
115
116 /// Get a query string parameter by name
117 ///
118 /// # Example
119 ///
120 /// ```rust,ignore
121 /// // URL: /users?page=2&limit=10
122 /// let page = req.query("page"); // Some("2")
123 /// let sort = req.query("sort"); // None
124 /// ```
125 pub fn query(&self, name: &str) -> Option<String> {
126 self.inner.uri().query().and_then(|q| {
127 form_urlencoded::parse(q.as_bytes())
128 .find(|(key, _)| key == name)
129 .map(|(_, value)| value.into_owned())
130 })
131 }
132
133 /// Get a query string parameter or a default value
134 ///
135 /// # Example
136 ///
137 /// ```rust,ignore
138 /// // URL: /users?page=2
139 /// let page = req.query_or("page", "1"); // "2"
140 /// let limit = req.query_or("limit", "10"); // "10"
141 /// ```
142 pub fn query_or(&self, name: &str, default: &str) -> String {
143 self.query(name).unwrap_or_else(|| default.to_string())
144 }
145
146 /// Get a query string parameter parsed as a specific type
147 ///
148 /// # Example
149 ///
150 /// ```rust,ignore
151 /// // URL: /users?page=2&limit=10
152 /// let page: Option<i32> = req.query_as("page"); // Some(2)
153 /// ```
154 pub fn query_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
155 self.query(name).and_then(|v| v.parse().ok())
156 }
157
158 /// Get a query string parameter parsed as a specific type, or a default
159 ///
160 /// # Example
161 ///
162 /// ```rust,ignore
163 /// // URL: /users?page=2
164 /// let page: i32 = req.query_as_or("page", 1); // 2
165 /// let limit: i32 = req.query_as_or("limit", 10); // 10
166 /// ```
167 pub fn query_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
168 self.query_as(name).unwrap_or(default)
169 }
170
171 /// Get the inner hyper request
172 pub fn inner(&self) -> &hyper::Request<hyper::body::Incoming> {
173 &self.inner
174 }
175
176 /// Get a header value by name
177 pub fn header(&self, name: &str) -> Option<&str> {
178 self.inner.headers().get(name).and_then(|v| v.to_str().ok())
179 }
180
181 /// Get the Content-Type header
182 pub fn content_type(&self) -> Option<&str> {
183 self.header("content-type")
184 }
185
186 /// Check if this is an Inertia XHR request
187 pub fn is_inertia(&self) -> bool {
188 self.header("X-Inertia")
189 .map(|v| v == "true")
190 .unwrap_or(false)
191 }
192
193 /// Get all cookies from the request
194 ///
195 /// Parses the Cookie header and returns a HashMap of cookie names to values.
196 ///
197 /// # Example
198 ///
199 /// ```rust,ignore
200 /// let cookies = req.cookies();
201 /// if let Some(session) = cookies.get("session") {
202 /// println!("Session: {}", session);
203 /// }
204 /// ```
205 pub fn cookies(&self) -> HashMap<String, String> {
206 self.header("Cookie").map(parse_cookies).unwrap_or_default()
207 }
208
209 /// Get a specific cookie value by name
210 ///
211 /// # Example
212 ///
213 /// ```rust,ignore
214 /// if let Some(session_id) = req.cookie("session") {
215 /// // Use session_id
216 /// }
217 /// ```
218 pub fn cookie(&self, name: &str) -> Option<String> {
219 self.cookies().get(name).cloned()
220 }
221
222 /// Get the Inertia version from request headers
223 pub fn inertia_version(&self) -> Option<&str> {
224 self.header("X-Inertia-Version")
225 }
226
227 /// Get partial component name for partial reloads
228 pub fn inertia_partial_component(&self) -> Option<&str> {
229 self.header("X-Inertia-Partial-Component")
230 }
231
232 /// Get partial data keys for partial reloads
233 pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
234 self.header("X-Inertia-Partial-Data")
235 .map(|v| v.split(',').collect())
236 }
237
238 /// Consume the request and collect the body as bytes
239 pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
240 let content_type = self
241 .inner
242 .headers()
243 .get("content-type")
244 .and_then(|v| v.to_str().ok())
245 .map(|s| s.to_string());
246
247 let params = self.params;
248 let bytes = collect_body(self.inner.into_body()).await?;
249
250 Ok((
251 RequestParts {
252 params,
253 content_type,
254 },
255 bytes,
256 ))
257 }
258
259 /// Parse the request body as JSON
260 ///
261 /// Consumes the request since the body can only be read once.
262 ///
263 /// # Example
264 ///
265 /// ```rust,ignore
266 /// #[derive(Deserialize)]
267 /// struct CreateUser { name: String, email: String }
268 ///
269 /// pub async fn store(req: Request) -> Response {
270 /// let data: CreateUser = req.json().await?;
271 /// // ...
272 /// }
273 /// ```
274 pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
275 let (_, bytes) = self.body_bytes().await?;
276 parse_json(&bytes)
277 }
278
279 /// Parse the request body as form-urlencoded
280 ///
281 /// Consumes the request since the body can only be read once.
282 ///
283 /// # Example
284 ///
285 /// ```rust,ignore
286 /// #[derive(Deserialize)]
287 /// struct LoginForm { username: String, password: String }
288 ///
289 /// pub async fn login(req: Request) -> Response {
290 /// let form: LoginForm = req.form().await?;
291 /// // ...
292 /// }
293 /// ```
294 pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
295 let (_, bytes) = self.body_bytes().await?;
296 parse_form(&bytes)
297 }
298
299 /// Parse the request body based on Content-Type header
300 ///
301 /// - `application/json` -> JSON parsing
302 /// - `application/x-www-form-urlencoded` -> Form parsing
303 /// - Otherwise -> JSON parsing (default)
304 ///
305 /// Consumes the request since the body can only be read once.
306 pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
307 let (parts, bytes) = self.body_bytes().await?;
308
309 match parts.content_type.as_deref() {
310 Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
311 _ => parse_json(&bytes),
312 }
313 }
314
315 /// Consume the request and return its parts along with the inner hyper request body
316 ///
317 /// This is used internally by the handler macro for FormRequest extraction.
318 pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
319 let content_type = self
320 .inner
321 .headers()
322 .get("content-type")
323 .and_then(|v| v.to_str().ok())
324 .map(|s| s.to_string());
325
326 let params = self.params;
327 let body = self.inner.into_body();
328
329 (
330 RequestParts {
331 params,
332 content_type,
333 },
334 body,
335 )
336 }
337}
338
339/// Request parts after body has been separated
340///
341/// Contains metadata needed for body parsing without the body itself.
342#[derive(Clone)]
343pub struct RequestParts {
344 /// Route parameters extracted from the URL path.
345 pub params: HashMap<String, String>,
346 /// Content-Type header value, if present.
347 pub content_type: Option<String>,
348}