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}