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