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