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    /// 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}