kit_rs/http/
request.rs

1use super::body::{collect_body, parse_form, parse_json};
2use super::ParamError;
3use crate::error::FrameworkError;
4use bytes::Bytes;
5use serde::de::DeserializeOwned;
6use std::collections::HashMap;
7
8/// HTTP Request wrapper providing Laravel-like access to request data
9pub struct Request {
10    inner: hyper::Request<hyper::body::Incoming>,
11    params: HashMap<String, String>,
12}
13
14impl Request {
15    pub fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
16        Self {
17            inner,
18            params: HashMap::new(),
19        }
20    }
21
22    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
23        self.params = params;
24        self
25    }
26
27    /// Get the request method
28    pub fn method(&self) -> &hyper::Method {
29        self.inner.method()
30    }
31
32    /// Get the request path
33    pub fn path(&self) -> &str {
34        self.inner.uri().path()
35    }
36
37    /// Get a route parameter by name (e.g., /users/{id})
38    /// Returns Err(ParamError) if the parameter is missing, enabling use of `?` operator
39    pub fn param(&self, name: &str) -> Result<&str, ParamError> {
40        self.params
41            .get(name)
42            .map(|s| s.as_str())
43            .ok_or_else(|| ParamError {
44                param_name: name.to_string(),
45            })
46    }
47
48    /// Get all route parameters
49    pub fn params(&self) -> &HashMap<String, String> {
50        &self.params
51    }
52
53    /// Get the inner hyper request
54    pub fn inner(&self) -> &hyper::Request<hyper::body::Incoming> {
55        &self.inner
56    }
57
58    /// Get a header value by name
59    pub fn header(&self, name: &str) -> Option<&str> {
60        self.inner.headers().get(name).and_then(|v| v.to_str().ok())
61    }
62
63    /// Get the Content-Type header
64    pub fn content_type(&self) -> Option<&str> {
65        self.header("content-type")
66    }
67
68    /// Check if this is an Inertia XHR request
69    pub fn is_inertia(&self) -> bool {
70        self.header("X-Inertia")
71            .map(|v| v == "true")
72            .unwrap_or(false)
73    }
74
75    /// Get the Inertia version from request headers
76    pub fn inertia_version(&self) -> Option<&str> {
77        self.header("X-Inertia-Version")
78    }
79
80    /// Get partial component name for partial reloads
81    pub fn inertia_partial_component(&self) -> Option<&str> {
82        self.header("X-Inertia-Partial-Component")
83    }
84
85    /// Get partial data keys for partial reloads
86    pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
87        self.header("X-Inertia-Partial-Data")
88            .map(|v| v.split(',').collect())
89    }
90
91    /// Consume the request and collect the body as bytes
92    pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
93        let content_type = self
94            .inner
95            .headers()
96            .get("content-type")
97            .and_then(|v| v.to_str().ok())
98            .map(|s| s.to_string());
99
100        let params = self.params;
101        let bytes = collect_body(self.inner.into_body()).await?;
102
103        Ok((
104            RequestParts {
105                params,
106                content_type,
107            },
108            bytes,
109        ))
110    }
111
112    /// Parse the request body as JSON
113    ///
114    /// Consumes the request since the body can only be read once.
115    ///
116    /// # Example
117    ///
118    /// ```rust,ignore
119    /// #[derive(Deserialize)]
120    /// struct CreateUser { name: String, email: String }
121    ///
122    /// pub async fn store(req: Request) -> Response {
123    ///     let data: CreateUser = req.json().await?;
124    ///     // ...
125    /// }
126    /// ```
127    pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
128        let (_, bytes) = self.body_bytes().await?;
129        parse_json(&bytes)
130    }
131
132    /// Parse the request body as form-urlencoded
133    ///
134    /// Consumes the request since the body can only be read once.
135    ///
136    /// # Example
137    ///
138    /// ```rust,ignore
139    /// #[derive(Deserialize)]
140    /// struct LoginForm { username: String, password: String }
141    ///
142    /// pub async fn login(req: Request) -> Response {
143    ///     let form: LoginForm = req.form().await?;
144    ///     // ...
145    /// }
146    /// ```
147    pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
148        let (_, bytes) = self.body_bytes().await?;
149        parse_form(&bytes)
150    }
151
152    /// Parse the request body based on Content-Type header
153    ///
154    /// - `application/json` -> JSON parsing
155    /// - `application/x-www-form-urlencoded` -> Form parsing
156    /// - Otherwise -> JSON parsing (default)
157    ///
158    /// Consumes the request since the body can only be read once.
159    pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
160        let (parts, bytes) = self.body_bytes().await?;
161
162        match parts.content_type.as_deref() {
163            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
164            _ => parse_json(&bytes),
165        }
166    }
167
168    /// Consume the request and return its parts along with the inner hyper request body
169    ///
170    /// This is used internally by the handler macro for FormRequest extraction.
171    pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
172        let content_type = self
173            .inner
174            .headers()
175            .get("content-type")
176            .and_then(|v| v.to_str().ok())
177            .map(|s| s.to_string());
178
179        let params = self.params;
180        let body = self.inner.into_body();
181
182        (
183            RequestParts {
184                params,
185                content_type,
186            },
187            body,
188        )
189    }
190}
191
192/// Request parts after body has been separated
193///
194/// Contains metadata needed for body parsing without the body itself.
195#[derive(Clone)]
196pub struct RequestParts {
197    pub params: HashMap<String, String>,
198    pub content_type: Option<String>,
199}