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 /// Rewrite the request path (server-side only — the browser URL is unchanged).
79 ///
80 /// Replaces the URI path component while preserving the scheme, authority, and
81 /// query string. Used by pre-route middleware (e.g. `HostMiddleware`) to map
82 /// custom-domain requests onto internal slug-based routes before routing occurs.
83 ///
84 /// `new_path` must begin with `/`. Panics in debug mode if it does not.
85 pub fn set_path(&mut self, new_path: &str) {
86 debug_assert!(
87 new_path.starts_with('/'),
88 "set_path: path must begin with '/', got {new_path:?}"
89 );
90 let old_uri = self.inner.uri();
91 // Preserve scheme, authority, and query string; replace path only.
92 let mut parts = old_uri.clone().into_parts();
93 let path_and_query = match old_uri.query() {
94 Some(q) => format!("{new_path}?{q}"),
95 None => new_path.to_string(),
96 };
97 parts.path_and_query = Some(
98 path_and_query
99 .parse()
100 .unwrap_or_else(|_| new_path.parse().expect("invalid path")),
101 );
102 if let Ok(new_uri) = http::Uri::from_parts(parts) {
103 *self.inner.uri_mut() = new_uri;
104 }
105 }
106
107 /// Get a route parameter by name (e.g., /users/{id})
108 /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
109 pub fn param(&self, name: &str) -> Result<&str, ParamError> {
110 self.params
111 .get(name)
112 .map(|s| s.as_str())
113 .ok_or_else(|| ParamError {
114 param_name: name.to_string(),
115 })
116 }
117
118 /// Get a route parameter parsed as a specific type
119 ///
120 /// Combines `param()` with parsing, returning a typed value.
121 ///
122 /// # Example
123 ///
124 /// ```rust,ignore
125 /// pub async fn show(req: Request) -> Response {
126 /// let id: i32 = req.param_as("id")?;
127 /// // ...
128 /// }
129 /// ```
130 pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Result<T, ParamError>
131 where
132 T::Err: std::fmt::Display,
133 {
134 let value = self.param(name)?;
135 value.parse::<T>().map_err(|e| ParamError {
136 param_name: format!("{name} (parse error: {e})"),
137 })
138 }
139
140 /// Get all route parameters
141 pub fn params(&self) -> &HashMap<String, String> {
142 &self.params
143 }
144
145 /// Get a query string parameter by name
146 ///
147 /// # Example
148 ///
149 /// ```rust,ignore
150 /// // URL: /users?page=2&limit=10
151 /// let page = req.query("page"); // Some("2")
152 /// let sort = req.query("sort"); // None
153 /// ```
154 pub fn query(&self, name: &str) -> Option<String> {
155 self.inner.uri().query().and_then(|q| {
156 form_urlencoded::parse(q.as_bytes())
157 .find(|(key, _)| key == name)
158 .map(|(_, value)| value.into_owned())
159 })
160 }
161
162 /// Get a query string parameter or a default value
163 ///
164 /// # Example
165 ///
166 /// ```rust,ignore
167 /// // URL: /users?page=2
168 /// let page = req.query_or("page", "1"); // "2"
169 /// let limit = req.query_or("limit", "10"); // "10"
170 /// ```
171 pub fn query_or(&self, name: &str, default: &str) -> String {
172 self.query(name).unwrap_or_else(|| default.to_string())
173 }
174
175 /// Get a query string parameter parsed as a specific type
176 ///
177 /// # Example
178 ///
179 /// ```rust,ignore
180 /// // URL: /users?page=2&limit=10
181 /// let page: Option<i32> = req.query_as("page"); // Some(2)
182 /// ```
183 pub fn query_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
184 self.query(name).and_then(|v| v.parse().ok())
185 }
186
187 /// Get a query string parameter parsed as a specific type, or a default
188 ///
189 /// # Example
190 ///
191 /// ```rust,ignore
192 /// // URL: /users?page=2
193 /// let page: i32 = req.query_as_or("page", 1); // 2
194 /// let limit: i32 = req.query_as_or("limit", 10); // 10
195 /// ```
196 pub fn query_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
197 self.query_as(name).unwrap_or(default)
198 }
199
200 // ── Phase 137: validation flash round-trip helpers ────────────────────────
201
202 /// Read a previously-submitted form value from session flash.
203 ///
204 /// After a POST handler calls `ValidationError::with_old_input(&data).redirect_back(...)`,
205 /// the GET handler retrieves the value with `req.old("field_name")` and passes it as
206 /// `InputProps.default_value` to repopulate the form.
207 ///
208 /// Reads from `_flash.old._old_input.<field>` without clearing (read-only semantics).
209 /// Flash aging (move new→old→deleted) is handled by the session middleware at request
210 /// boundaries, so multiple reads in the same GET handler are safe.
211 ///
212 /// Returns `None` when no flash value exists, no session is active, or the key is absent.
213 pub fn old(&self, field: &str) -> Option<String> {
214 let key = format!("_flash.old._old_input.{field}");
215 crate::session::session().and_then(|s| s.get::<String>(&key))
216 }
217
218 /// Read the first validation error message for a field from session flash.
219 ///
220 /// After a POST handler calls `errors.redirect_back(...)`, the GET handler calls
221 /// `req.validation_error("field_name")` and passes the result as `InputProps.error`.
222 ///
223 /// Reads from `_flash.old._validation_errors` without clearing (read-only semantics).
224 ///
225 /// Returns `None` when no flash errors exist, no session is active, or the field has no error.
226 pub fn validation_error(&self, field: &str) -> Option<String> {
227 let errors: Option<std::collections::HashMap<String, Vec<String>>> =
228 crate::session::session().and_then(|s| {
229 s.get::<std::collections::HashMap<String, Vec<String>>>(
230 "_flash.old._validation_errors",
231 )
232 });
233 errors.and_then(|map| map.get(field).and_then(|v| v.first()).cloned())
234 }
235
236 /// Returns `true` when any validation errors were flashed from a prior request.
237 ///
238 /// Useful for rendering a form-wide error summary banner.
239 pub fn has_validation_errors(&self) -> bool {
240 crate::session::session()
241 .and_then(|s| {
242 s.get::<std::collections::HashMap<String, Vec<String>>>(
243 "_flash.old._validation_errors",
244 )
245 })
246 .map(|m| !m.is_empty())
247 .unwrap_or(false)
248 }
249
250 /// Get the inner hyper request
251 pub fn inner(&self) -> &hyper::Request<hyper::body::Incoming> {
252 &self.inner
253 }
254
255 /// Get a header value by name
256 pub fn header(&self, name: &str) -> Option<&str> {
257 self.inner.headers().get(name).and_then(|v| v.to_str().ok())
258 }
259
260 /// Get the Content-Type header
261 pub fn content_type(&self) -> Option<&str> {
262 self.header("content-type")
263 }
264
265 /// Check if this is an Inertia XHR request
266 pub fn is_inertia(&self) -> bool {
267 self.header("X-Inertia")
268 .map(|v| v == "true")
269 .unwrap_or(false)
270 }
271
272 /// Get all cookies from the request
273 ///
274 /// Parses the Cookie header and returns a HashMap of cookie names to values.
275 ///
276 /// # Example
277 ///
278 /// ```rust,ignore
279 /// let cookies = req.cookies();
280 /// if let Some(session) = cookies.get("session") {
281 /// println!("Session: {}", session);
282 /// }
283 /// ```
284 pub fn cookies(&self) -> HashMap<String, String> {
285 self.header("Cookie").map(parse_cookies).unwrap_or_default()
286 }
287
288 /// Get a specific cookie value by name
289 ///
290 /// # Example
291 ///
292 /// ```rust,ignore
293 /// if let Some(session_id) = req.cookie("session") {
294 /// // Use session_id
295 /// }
296 /// ```
297 pub fn cookie(&self, name: &str) -> Option<String> {
298 self.cookies().get(name).cloned()
299 }
300
301 /// Get the Inertia version from request headers
302 pub fn inertia_version(&self) -> Option<&str> {
303 self.header("X-Inertia-Version")
304 }
305
306 /// Get partial component name for partial reloads
307 pub fn inertia_partial_component(&self) -> Option<&str> {
308 self.header("X-Inertia-Partial-Component")
309 }
310
311 /// Get partial data keys for partial reloads
312 pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
313 self.header("X-Inertia-Partial-Data")
314 .map(|v| v.split(',').collect())
315 }
316
317 /// Consume the request and collect the body as bytes
318 pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
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 bytes = collect_body(self.inner.into_body()).await?;
328
329 Ok((
330 RequestParts {
331 params,
332 content_type,
333 },
334 bytes,
335 ))
336 }
337
338 /// Parse the request body as JSON
339 ///
340 /// Consumes the request since the body can only be read once.
341 ///
342 /// # Example
343 ///
344 /// ```rust,ignore
345 /// #[derive(Deserialize)]
346 /// struct CreateUser { name: String, email: String }
347 ///
348 /// pub async fn store(req: Request) -> Response {
349 /// let data: CreateUser = req.json().await?;
350 /// // ...
351 /// }
352 /// ```
353 pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
354 let (_, bytes) = self.body_bytes().await?;
355 parse_json(&bytes)
356 }
357
358 /// Parse the request body as form-urlencoded
359 ///
360 /// Consumes the request since the body can only be read once.
361 ///
362 /// # Example
363 ///
364 /// ```rust,ignore
365 /// #[derive(Deserialize)]
366 /// struct LoginForm { username: String, password: String }
367 ///
368 /// pub async fn login(req: Request) -> Response {
369 /// let form: LoginForm = req.form().await?;
370 /// // ...
371 /// }
372 /// ```
373 pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
374 let (_, bytes) = self.body_bytes().await?;
375 parse_form(&bytes)
376 }
377
378 /// Parse the request body based on Content-Type header
379 ///
380 /// - `application/json` -> JSON parsing
381 /// - `application/x-www-form-urlencoded` -> Form parsing
382 /// - Otherwise -> JSON parsing (default)
383 ///
384 /// Consumes the request since the body can only be read once.
385 pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
386 let (parts, bytes) = self.body_bytes().await?;
387
388 match parts.content_type.as_deref() {
389 Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
390 _ => parse_json(&bytes),
391 }
392 }
393
394 /// Consume the request and return its parts along with the inner hyper request body
395 ///
396 /// This is used internally by the handler macro for FormRequest extraction.
397 pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
398 let content_type = self
399 .inner
400 .headers()
401 .get("content-type")
402 .and_then(|v| v.to_str().ok())
403 .map(|s| s.to_string());
404
405 let params = self.params;
406 let body = self.inner.into_body();
407
408 (
409 RequestParts {
410 params,
411 content_type,
412 },
413 body,
414 )
415 }
416}
417
418/// Request parts after body has been separated
419///
420/// Contains metadata needed for body parsing without the body itself.
421#[derive(Clone)]
422pub struct RequestParts {
423 /// Route parameters extracted from the URL path.
424 pub params: HashMap<String, String>,
425 /// Content-Type header value, if present.
426 pub content_type: Option<String>,
427}
428
429#[cfg(test)]
430mod tests {
431 // Phase 137: unit tests for old() / validation_error() / has_validation_errors().
432 //
433 // The Request struct wraps hyper::body::Incoming which cannot be constructed
434 // in unit tests. We therefore test the underlying session-reading logic
435 // directly (the same code path the methods delegate to) using
436 // SESSION_CONTEXT.scope() to inject a session.
437 //
438 // Full end-to-end round-trips (POST → flash → GET → InputProps) live in the
439 // gestiscilo integration test scaffold (validation_roundtrip_tests.rs).
440
441 use crate::session::middleware::SESSION_CONTEXT;
442 use crate::session::store::SessionData;
443 use std::collections::HashMap;
444 use std::sync::Arc;
445 use tokio::sync::RwLock;
446
447 // ── No-session guard tests ────────────────────────────────────────────────
448
449 #[tokio::test]
450 async fn test_session_absent_old_returns_none() {
451 // Outside any SESSION_CONTEXT scope, session() returns None.
452 // old() delegates to session().and_then(...) so it must also return None.
453 let val =
454 crate::session::session().and_then(|s| s.get::<String>("_flash.old._old_input.email"));
455 assert_eq!(val, None);
456 }
457
458 #[tokio::test]
459 async fn test_session_absent_validation_error_returns_none() {
460 let val = crate::session::session().and_then(|s| {
461 s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
462 .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
463 });
464 assert_eq!(val, None);
465 }
466
467 #[tokio::test]
468 async fn test_session_absent_has_validation_errors_false() {
469 let val = crate::session::session()
470 .and_then(|s| s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors"))
471 .map(|m| !m.is_empty())
472 .unwrap_or(false);
473 assert!(!val);
474 }
475
476 // ── Session-present tests (direct logic, mirrors Request method bodies) ───
477
478 #[tokio::test]
479 async fn test_old_reads_from_flash_old_key() {
480 let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
481 // Simulate age_flash_data() having moved the flash to _flash.old.*
482 session.put(
483 "_flash.old._old_input.email",
484 "user@example.com".to_string(),
485 );
486
487 let ctx = Arc::new(RwLock::new(Some(session)));
488 let val = SESSION_CONTEXT
489 .scope(ctx, async {
490 crate::session::session()
491 .and_then(|s| s.get::<String>("_flash.old._old_input.email"))
492 })
493 .await;
494
495 assert_eq!(val, Some("user@example.com".to_string()));
496 }
497
498 #[tokio::test]
499 async fn test_validation_error_reads_first_message_for_field() {
500 let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
501 let mut errors: HashMap<String, Vec<String>> = HashMap::new();
502 errors.insert(
503 "email".to_string(),
504 vec!["Inserisci un indirizzo email valido".to_string()],
505 );
506 session.put("_flash.old._validation_errors", &errors);
507
508 let ctx = Arc::new(RwLock::new(Some(session)));
509 let (email_err, other_err) = SESSION_CONTEXT
510 .scope(ctx, async {
511 let email_err = crate::session::session().and_then(|s| {
512 s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
513 .and_then(|map| map.get("email").and_then(|v| v.first()).cloned())
514 });
515 // Reading the same session twice must not clear the data.
516 let other_err = crate::session::session().and_then(|s| {
517 s.get::<HashMap<String, Vec<String>>>("_flash.old._validation_errors")
518 .and_then(|map| map.get("name").and_then(|v| v.first()).cloned())
519 });
520 (email_err, other_err)
521 })
522 .await;
523
524 assert_eq!(
525 email_err,
526 Some("Inserisci un indirizzo email valido".to_string())
527 );
528 assert_eq!(other_err, None);
529 }
530
531 #[tokio::test]
532 async fn test_multiple_reads_do_not_clear_flash() {
533 // Validates read-only semantics: calling session().get() twice returns
534 // the same value (unlike get_flash which clears on read).
535 let mut session = SessionData::new("test-id".to_string(), "csrf".to_string());
536 session.put("_flash.old._old_input.name", "Mario".to_string());
537
538 let ctx = Arc::new(RwLock::new(Some(session)));
539 let (first, second) = SESSION_CONTEXT
540 .scope(ctx, async {
541 let a = crate::session::session()
542 .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
543 let b = crate::session::session()
544 .and_then(|s| s.get::<String>("_flash.old._old_input.name"));
545 (a, b)
546 })
547 .await;
548
549 assert_eq!(first, Some("Mario".to_string()));
550 assert_eq!(second, Some("Mario".to_string()));
551 }
552}