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
61            .headers()
62            .get(name)
63            .and_then(|v| v.to_str().ok())
64    }
65
66    /// Get the Content-Type header
67    pub fn content_type(&self) -> Option<&str> {
68        self.header("content-type")
69    }
70
71    /// Check if this is an Inertia XHR request
72    pub fn is_inertia(&self) -> bool {
73        self.header("X-Inertia")
74            .map(|v| v == "true")
75            .unwrap_or(false)
76    }
77
78    /// Get the Inertia version from request headers
79    pub fn inertia_version(&self) -> Option<&str> {
80        self.header("X-Inertia-Version")
81    }
82
83    /// Get partial component name for partial reloads
84    pub fn inertia_partial_component(&self) -> Option<&str> {
85        self.header("X-Inertia-Partial-Component")
86    }
87
88    /// Get partial data keys for partial reloads
89    pub fn inertia_partial_data(&self) -> Option<Vec<&str>> {
90        self.header("X-Inertia-Partial-Data")
91            .map(|v| v.split(',').collect())
92    }
93
94    /// Consume the request and collect the body as bytes
95    pub async fn body_bytes(self) -> Result<(RequestParts, Bytes), FrameworkError> {
96        let content_type = self
97            .inner
98            .headers()
99            .get("content-type")
100            .and_then(|v| v.to_str().ok())
101            .map(|s| s.to_string());
102
103        let params = self.params;
104        let bytes = collect_body(self.inner.into_body()).await?;
105
106        Ok((RequestParts { params, content_type }, bytes))
107    }
108
109    /// Parse the request body as JSON
110    ///
111    /// Consumes the request since the body can only be read once.
112    ///
113    /// # Example
114    ///
115    /// ```rust,ignore
116    /// #[derive(Deserialize)]
117    /// struct CreateUser { name: String, email: String }
118    ///
119    /// pub async fn store(req: Request) -> Response {
120    ///     let data: CreateUser = req.json().await?;
121    ///     // ...
122    /// }
123    /// ```
124    pub async fn json<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
125        let (_, bytes) = self.body_bytes().await?;
126        parse_json(&bytes)
127    }
128
129    /// Parse the request body as form-urlencoded
130    ///
131    /// Consumes the request since the body can only be read once.
132    ///
133    /// # Example
134    ///
135    /// ```rust,ignore
136    /// #[derive(Deserialize)]
137    /// struct LoginForm { username: String, password: String }
138    ///
139    /// pub async fn login(req: Request) -> Response {
140    ///     let form: LoginForm = req.form().await?;
141    ///     // ...
142    /// }
143    /// ```
144    pub async fn form<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
145        let (_, bytes) = self.body_bytes().await?;
146        parse_form(&bytes)
147    }
148
149    /// Parse the request body based on Content-Type header
150    ///
151    /// - `application/json` -> JSON parsing
152    /// - `application/x-www-form-urlencoded` -> Form parsing
153    /// - Otherwise -> JSON parsing (default)
154    ///
155    /// Consumes the request since the body can only be read once.
156    pub async fn input<T: DeserializeOwned>(self) -> Result<T, FrameworkError> {
157        let (parts, bytes) = self.body_bytes().await?;
158
159        match parts.content_type.as_deref() {
160            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes),
161            _ => parse_json(&bytes),
162        }
163    }
164
165    /// Consume the request and return its parts along with the inner hyper request body
166    ///
167    /// This is used internally by the handler macro for FormRequest extraction.
168    pub fn into_parts(self) -> (RequestParts, hyper::body::Incoming) {
169        let content_type = self
170            .inner
171            .headers()
172            .get("content-type")
173            .and_then(|v| v.to_str().ok())
174            .map(|s| s.to_string());
175
176        let params = self.params;
177        let body = self.inner.into_body();
178
179        (RequestParts { params, content_type }, body)
180    }
181}
182
183/// Request parts after body has been separated
184///
185/// Contains metadata needed for body parsing without the body itself.
186#[derive(Clone)]
187pub struct RequestParts {
188    pub params: HashMap<String, String>,
189    pub content_type: Option<String>,
190}