1use crate::cookie;
2use crate::headers::Headers;
3use crate::method::Method;
4use crate::url;
5use rustlavel_core::{Config, Context, Json};
6use std::any::{Any, TypeId};
7use std::collections::{BTreeMap, HashMap};
8use std::net::SocketAddr;
9
10pub struct Request {
12 pub(crate) method: Method,
13 pub(crate) target: String,
14 pub(crate) path: String,
15 pub(crate) query: Vec<(String, String)>,
16 pub(crate) headers: Headers,
17 pub(crate) body: Vec<u8>,
18 pub(crate) params: BTreeMap<String, String>,
19 pub(crate) context: Context,
20 pub(crate) peer: Option<SocketAddr>,
21 pub(crate) route: Option<String>,
22 extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
24 parsed_body: Option<ParsedBody>,
26}
27
28enum ParsedBody {
29 Json(Json),
30 Form(Vec<(String, String)>),
31 None,
32}
33
34impl Request {
35 pub fn new(method: Method, target: impl Into<String>) -> Self {
38 let target = target.into();
39 let (path, query) = url::split_target(&target);
40 Request {
41 method,
42 path: path.to_string(),
43 query: url::parse_query(query),
44 target,
45 headers: Headers::new(),
46 body: Vec::new(),
47 params: BTreeMap::new(),
48 context: Context::default(),
49 peer: None,
50 route: None,
51 extensions: HashMap::new(),
52 parsed_body: None,
53 }
54 }
55
56 pub fn method(&self) -> Method {
57 self.method
58 }
59
60 pub fn path(&self) -> &str {
62 &self.path
63 }
64
65 pub fn target(&self) -> &str {
67 &self.target
68 }
69
70 pub fn route(&self) -> Option<&str> {
73 self.route.as_deref()
74 }
75
76 pub fn headers(&self) -> &Headers {
77 &self.headers
78 }
79
80 pub fn headers_mut(&mut self) -> &mut Headers {
81 &mut self.headers
82 }
83
84 pub fn header(&self, name: &str) -> Option<&str> {
85 self.headers.get(name)
86 }
87
88 pub fn body(&self) -> &[u8] {
89 &self.body
90 }
91
92 pub fn body_string(&self) -> String {
93 String::from_utf8_lossy(&self.body).into_owned()
94 }
95
96 pub fn context(&self) -> &Context {
97 &self.context
98 }
99
100 pub fn config(&self) -> &Config {
101 self.context.config()
102 }
103
104 pub fn state<T: Send + Sync + 'static>(&self) -> Option<&T> {
106 self.context.state::<T>()
107 }
108
109 pub fn peer_addr(&self) -> Option<SocketAddr> {
110 self.peer
111 }
112
113 pub fn ip(&self) -> Option<String> {
115 if let Some(forwarded) = self.headers.get("x-forwarded-for")
116 && let Some(first) = forwarded.split(',').next() {
117 return Some(first.trim().to_string());
118 }
119 self.peer.map(|addr| addr.ip().to_string())
120 }
121
122 pub fn param(&self, name: &str) -> Option<&str> {
125 self.params.get(name).map(String::as_str)
126 }
127
128 pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
131 self.param(name)?.parse().ok()
132 }
133
134 pub fn params(&self) -> &BTreeMap<String, String> {
135 &self.params
136 }
137
138 pub fn query(&self, name: &str) -> Option<&str> {
139 self.query.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str())
140 }
141
142 pub fn query_all(&self, name: &str) -> Vec<&str> {
144 self.query
145 .iter()
146 .filter(|(key, _)| key == name)
147 .map(|(_, value)| value.as_str())
148 .collect()
149 }
150
151 pub fn query_pairs(&self) -> &[(String, String)] {
152 &self.query
153 }
154
155 pub fn content_type(&self) -> Option<&str> {
156 self.headers.content_type()
157 }
158
159 pub fn is_json(&self) -> bool {
160 self.content_type().is_some_and(|ct| ct.ends_with("json"))
161 }
162
163 pub fn wants_json(&self) -> bool {
165 self.is_json()
166 || self.headers.get("accept").is_some_and(|a| a.contains("application/json"))
167 || self.headers.get("x-requested-with").is_some_and(|x| x == "XMLHttpRequest")
168 }
169
170 pub fn json(&mut self) -> Option<&Json> {
172 self.parse_body();
173 match self.parsed_body.as_ref()? {
174 ParsedBody::Json(value) => Some(value),
175 _ => None,
176 }
177 }
178
179 pub fn input(&mut self, name: &str) -> Option<String> {
182 self.parse_body();
183 match self.parsed_body.as_ref() {
184 Some(ParsedBody::Json(value)) => {
185 if let Some(found) = value.get(name) {
186 return Some(match found {
187 Json::String(s) => s.clone(),
188 Json::Null => String::new(),
189 other => other.to_string(),
190 });
191 }
192 }
193 Some(ParsedBody::Form(pairs)) => {
194 if let Some((_, value)) = pairs.iter().find(|(key, _)| key == name) {
195 return Some(value.clone());
196 }
197 }
198 _ => {}
199 }
200 self.query(name).map(str::to_string)
201 }
202
203 pub fn form(&mut self) -> &[(String, String)] {
205 self.parse_body();
206 match self.parsed_body.as_ref() {
207 Some(ParsedBody::Form(pairs)) => pairs,
208 _ => &[],
209 }
210 }
211
212 fn parse_body(&mut self) {
213 if self.parsed_body.is_some() {
214 return;
215 }
216 let parsed = match self.headers.content_type() {
217 _ if self.body.is_empty() => ParsedBody::None,
218 Some(ct) if ct.ends_with("json") => match std::str::from_utf8(&self.body) {
219 Ok(text) => Json::parse(text).map_or(ParsedBody::None, ParsedBody::Json),
220 Err(_) => ParsedBody::None,
221 },
222 Some("application/x-www-form-urlencoded") => {
223 ParsedBody::Form(url::parse_query(&String::from_utf8_lossy(&self.body)))
224 }
225 _ => ParsedBody::None,
226 };
227 self.parsed_body = Some(parsed);
228 }
229
230 pub fn cookies(&self) -> BTreeMap<String, String> {
231 self.headers.get("cookie").map(cookie::parse_header).unwrap_or_default()
232 }
233
234 pub fn cookie(&self, name: &str) -> Option<String> {
235 self.cookies().remove(name)
236 }
237
238 pub fn extend<T: Send + Sync + 'static>(&mut self, value: T) {
240 self.extensions.insert(TypeId::of::<T>(), Box::new(value));
241 }
242
243 pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
245 self.extensions.get(&TypeId::of::<T>()).and_then(|value| value.downcast_ref::<T>())
246 }
247
248 pub fn with_header(mut self, name: &str, value: impl Into<String>) -> Self {
251 self.headers.set(name, value);
252 self
253 }
254
255 pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
256 self.body = body.into();
257 self.parsed_body = None;
258 self
259 }
260
261 pub fn with_json(self, value: Json) -> Self {
262 self.with_header("content-type", "application/json").with_body(value.to_string())
263 }
264
265 pub fn with_form(self, fields: &[(&str, &str)]) -> Self {
266 let encoded = fields
267 .iter()
268 .map(|(key, value)| format!("{}={}", url::encode(key), url::encode(value)))
269 .collect::<Vec<_>>()
270 .join("&");
271 self.with_header("content-type", "application/x-www-form-urlencoded").with_body(encoded)
272 }
273
274 pub fn with_context(mut self, context: Context) -> Self {
275 self.context = context;
276 self
277 }
278
279 pub(crate) fn set_params(&mut self, params: BTreeMap<String, String>) {
280 self.params = params;
281 }
282}
283
284impl std::fmt::Debug for Request {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.debug_struct("Request")
287 .field("method", &self.method)
288 .field("target", &self.target)
289 .field("headers", &self.headers)
290 .field("body_len", &self.body.len())
291 .finish()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn splits_path_and_query() {
301 let request = Request::new(Method::Get, "/users?page=2&tag=a&tag=b");
302
303 assert_eq!(request.path(), "/users");
304 assert_eq!(request.query("page"), Some("2"));
305 assert_eq!(request.query_all("tag"), ["a", "b"]);
306 assert_eq!(request.query("missing"), None);
307 }
308
309 #[test]
310 fn input_prefers_the_body_over_the_query() {
311 let mut request = Request::new(Method::Post, "/users?name=from-query")
312 .with_json(Json::object([("name", "from-body".into())]));
313
314 assert_eq!(request.input("name").as_deref(), Some("from-body"));
315 assert_eq!(request.input("missing"), None);
317 }
318
319 #[test]
320 fn reads_urlencoded_form_bodies() {
321 let mut request =
322 Request::new(Method::Post, "/login").with_form(&[("email", "a@b.com"), ("password", "s e c")]);
323
324 assert_eq!(request.input("email").as_deref(), Some("a@b.com"));
325 assert_eq!(request.input("password").as_deref(), Some("s e c"));
326 assert_eq!(request.form().len(), 2);
327 }
328
329 #[test]
330 fn parses_cookies_from_the_header() {
331 let request = Request::new(Method::Get, "/").with_header("cookie", "session=abc; theme=dark");
332
333 assert_eq!(request.cookie("session").as_deref(), Some("abc"));
334 assert_eq!(request.cookies().len(), 2);
335 }
336
337 #[test]
338 fn extensions_round_trip_through_middleware() {
339 struct User(&'static str);
340 let mut request = Request::new(Method::Get, "/");
341 request.extend(User("ada"));
342
343 assert_eq!(request.extension::<User>().unwrap().0, "ada");
344 }
345
346 #[test]
347 fn forwarded_header_wins_over_socket_address() {
348 let request = Request::new(Method::Get, "/").with_header("x-forwarded-for", "203.0.113.9, 10.0.0.1");
349 assert_eq!(request.ip().as_deref(), Some("203.0.113.9"));
350 }
351
352 #[test]
353 fn detects_clients_that_want_json() {
354 let api = Request::new(Method::Get, "/").with_header("accept", "application/json");
355 let browser = Request::new(Method::Get, "/").with_header("accept", "text/html");
356
357 assert!(api.wants_json());
358 assert!(!browser.wants_json());
359 }
360}