Skip to main content

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}