spikard_http/server/
request_extraction.rs

1//! Request parsing and data extraction utilities
2
3use crate::handler_trait::RequestData;
4use crate::query_parser::parse_query_string_to_json;
5use axum::body::Body;
6use http_body_util::BodyExt;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Extract and parse query parameters from request URI
12pub fn extract_query_params(uri: &axum::http::Uri) -> Value {
13    let query_string = uri.query().unwrap_or("");
14    if query_string.is_empty() {
15        Value::Object(serde_json::Map::new())
16    } else {
17        parse_query_string_to_json(query_string.as_bytes(), true)
18    }
19}
20
21/// Extract raw query parameters as strings (no type conversion)
22/// Used for validation error messages to show the actual input values
23pub fn extract_raw_query_params(uri: &axum::http::Uri) -> HashMap<String, Vec<String>> {
24    let query_string = uri.query().unwrap_or("");
25    if query_string.is_empty() {
26        HashMap::new()
27    } else {
28        crate::query_parser::parse_query_string(query_string.as_bytes(), '&')
29            .into_iter()
30            .fold(HashMap::new(), |mut acc, (k, v)| {
31                acc.entry(k).or_insert_with(Vec::new).push(v);
32                acc
33            })
34    }
35}
36
37/// Extract headers from request
38pub fn extract_headers(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
39    let mut map = HashMap::new();
40    for (name, value) in headers.iter() {
41        if let Ok(val_str) = value.to_str() {
42            map.insert(name.as_str().to_lowercase(), val_str.to_string());
43        }
44    }
45    map
46}
47
48/// Extract cookies from request headers
49pub fn extract_cookies(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
50    let mut cookies = HashMap::new();
51
52    if let Some(cookie_str) = headers.get(axum::http::header::COOKIE).and_then(|h| h.to_str().ok()) {
53        for cookie in cookie::Cookie::split_parse(cookie_str).flatten() {
54            cookies.insert(cookie.name().to_string(), cookie.value().to_string());
55        }
56    }
57
58    cookies
59}
60
61/// Create RequestData from request parts (for requests without body)
62///
63/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
64pub fn create_request_data_without_body(
65    uri: &axum::http::Uri,
66    method: &axum::http::Method,
67    headers: &axum::http::HeaderMap,
68    path_params: HashMap<String, String>,
69) -> RequestData {
70    RequestData {
71        path_params: Arc::new(path_params),
72        query_params: extract_query_params(uri),
73        raw_query_params: Arc::new(extract_raw_query_params(uri)),
74        headers: Arc::new(extract_headers(headers)),
75        cookies: Arc::new(extract_cookies(headers)),
76        body: Value::Null,
77        raw_body: None,
78        method: method.as_str().to_string(),
79        path: uri.path().to_string(),
80        #[cfg(feature = "di")]
81        dependencies: None,
82    }
83}
84
85/// Create RequestData from request parts (for requests with body)
86///
87/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
88/// Performance optimization: stores raw body bytes without parsing JSON.
89/// JSON parsing is deferred until actually needed (e.g., for validation).
90pub async fn create_request_data_with_body(
91    parts: &axum::http::request::Parts,
92    path_params: HashMap<String, String>,
93    body: Body,
94) -> Result<RequestData, (axum::http::StatusCode, String)> {
95    let body_bytes = body
96        .collect()
97        .await
98        .map_err(|e| {
99            (
100                axum::http::StatusCode::BAD_REQUEST,
101                format!("Failed to read body: {}", e),
102            )
103        })?
104        .to_bytes();
105
106    Ok(RequestData {
107        path_params: Arc::new(path_params),
108        query_params: extract_query_params(&parts.uri),
109        raw_query_params: Arc::new(extract_raw_query_params(&parts.uri)),
110        headers: Arc::new(extract_headers(&parts.headers)),
111        cookies: Arc::new(extract_cookies(&parts.headers)),
112        body: Value::Null,
113        raw_body: if body_bytes.is_empty() { None } else { Some(body_bytes) },
114        method: parts.method.as_str().to_string(),
115        path: parts.uri.path().to_string(),
116        #[cfg(feature = "di")]
117        dependencies: None,
118    })
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use axum::http::{HeaderMap, HeaderValue, Method, Uri};
125    use serde_json::json;
126
127    #[test]
128    fn test_extract_query_params_empty() {
129        let uri: Uri = "/path".parse().unwrap();
130        let result = extract_query_params(&uri);
131        assert_eq!(result, json!({}));
132    }
133
134    #[test]
135    fn test_extract_query_params_single_param() {
136        let uri: Uri = "/path?name=value".parse().unwrap();
137        let result = extract_query_params(&uri);
138        assert_eq!(result, json!({"name": "value"}));
139    }
140
141    #[test]
142    fn test_extract_query_params_multiple_params() {
143        let uri: Uri = "/path?foo=1&bar=2".parse().unwrap();
144        let result = extract_query_params(&uri);
145        assert_eq!(result, json!({"foo": 1, "bar": 2}));
146    }
147
148    #[test]
149    fn test_extract_query_params_array_params() {
150        let uri: Uri = "/path?tags=rust&tags=http".parse().unwrap();
151        let result = extract_query_params(&uri);
152        assert_eq!(result, json!({"tags": ["rust", "http"]}));
153    }
154
155    #[test]
156    fn test_extract_query_params_mixed_array_and_single() {
157        let uri: Uri = "/path?tags=rust&tags=web&id=123".parse().unwrap();
158        let result = extract_query_params(&uri);
159        assert_eq!(result, json!({"tags": ["rust", "web"], "id": 123}));
160    }
161
162    #[test]
163    fn test_extract_query_params_url_encoded() {
164        let uri: Uri = "/path?email=test%40example.com&name=john+doe".parse().unwrap();
165        let result = extract_query_params(&uri);
166        assert_eq!(result, json!({"email": "test@example.com", "name": "john doe"}));
167    }
168
169    #[test]
170    fn test_extract_query_params_boolean_values() {
171        let uri: Uri = "/path?active=true&enabled=false".parse().unwrap();
172        let result = extract_query_params(&uri);
173        assert_eq!(result, json!({"active": true, "enabled": false}));
174    }
175
176    #[test]
177    fn test_extract_query_params_null_value() {
178        let uri: Uri = "/path?value=null".parse().unwrap();
179        let result = extract_query_params(&uri);
180        assert_eq!(result, json!({"value": null}));
181    }
182
183    #[test]
184    fn test_extract_query_params_empty_string_value() {
185        let uri: Uri = "/path?key=".parse().unwrap();
186        let result = extract_query_params(&uri);
187        assert_eq!(result, json!({"key": ""}));
188    }
189
190    #[test]
191    fn test_extract_raw_query_params_empty() {
192        let uri: Uri = "/path".parse().unwrap();
193        let result = extract_raw_query_params(&uri);
194        assert!(result.is_empty());
195    }
196
197    #[test]
198    fn test_extract_raw_query_params_single() {
199        let uri: Uri = "/path?name=value".parse().unwrap();
200        let result = extract_raw_query_params(&uri);
201        assert_eq!(result.get("name"), Some(&vec!["value".to_string()]));
202    }
203
204    #[test]
205    fn test_extract_raw_query_params_multiple_values() {
206        let uri: Uri = "/path?tag=rust&tag=http".parse().unwrap();
207        let result = extract_raw_query_params(&uri);
208        assert_eq!(result.get("tag"), Some(&vec!["rust".to_string(), "http".to_string()]));
209    }
210
211    #[test]
212    fn test_extract_raw_query_params_mixed() {
213        let uri: Uri = "/path?id=123&tags=rust&tags=web&active=true".parse().unwrap();
214        let result = extract_raw_query_params(&uri);
215        assert_eq!(result.get("id"), Some(&vec!["123".to_string()]));
216        assert_eq!(result.get("tags"), Some(&vec!["rust".to_string(), "web".to_string()]));
217        assert_eq!(result.get("active"), Some(&vec!["true".to_string()]));
218    }
219
220    #[test]
221    fn test_extract_raw_query_params_url_encoded() {
222        let uri: Uri = "/path?email=test%40example.com".parse().unwrap();
223        let result = extract_raw_query_params(&uri);
224        assert_eq!(result.get("email"), Some(&vec!["test@example.com".to_string()]));
225    }
226
227    #[test]
228    fn test_extract_headers_empty() {
229        let headers = HeaderMap::new();
230        let result = extract_headers(&headers);
231        assert!(result.is_empty());
232    }
233
234    #[test]
235    fn test_extract_headers_single() {
236        let mut headers = HeaderMap::new();
237        headers.insert("content-type", HeaderValue::from_static("application/json"));
238        let result = extract_headers(&headers);
239        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
240    }
241
242    #[test]
243    fn test_extract_headers_multiple() {
244        let mut headers = HeaderMap::new();
245        headers.insert("content-type", HeaderValue::from_static("application/json"));
246        headers.insert("user-agent", HeaderValue::from_static("test-agent"));
247        headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
248
249        let result = extract_headers(&headers);
250        assert_eq!(result.len(), 3);
251        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
252        assert_eq!(result.get("user-agent"), Some(&"test-agent".to_string()));
253        assert_eq!(result.get("authorization"), Some(&"Bearer token123".to_string()));
254    }
255
256    #[test]
257    fn test_extract_headers_case_insensitive() {
258        let mut headers = HeaderMap::new();
259        headers.insert("Content-Type", HeaderValue::from_static("text/html"));
260        headers.insert("USER-Agent", HeaderValue::from_static("chrome"));
261
262        let result = extract_headers(&headers);
263        assert_eq!(result.get("content-type"), Some(&"text/html".to_string()));
264        assert_eq!(result.get("user-agent"), Some(&"chrome".to_string()));
265    }
266
267    #[test]
268    fn test_extract_headers_with_dashes() {
269        let mut headers = HeaderMap::new();
270        headers.insert("x-custom-header", HeaderValue::from_static("custom-value"));
271        headers.insert("x-request-id", HeaderValue::from_static("req-12345"));
272
273        let result = extract_headers(&headers);
274        assert_eq!(result.get("x-custom-header"), Some(&"custom-value".to_string()));
275        assert_eq!(result.get("x-request-id"), Some(&"req-12345".to_string()));
276    }
277
278    #[test]
279    fn test_extract_cookies_no_cookie_header() {
280        let headers = HeaderMap::new();
281        let result = extract_cookies(&headers);
282        assert!(result.is_empty());
283    }
284
285    #[test]
286    fn test_extract_cookies_single() {
287        let mut headers = HeaderMap::new();
288        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc123"));
289
290        let result = extract_cookies(&headers);
291        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
292    }
293
294    #[test]
295    fn test_extract_cookies_multiple() {
296        let mut headers = HeaderMap::new();
297        headers.insert(
298            axum::http::header::COOKIE,
299            HeaderValue::from_static("session=abc123; user_id=42; theme=dark"),
300        );
301
302        let result = extract_cookies(&headers);
303        assert_eq!(result.len(), 3);
304        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
305        assert_eq!(result.get("user_id"), Some(&"42".to_string()));
306        assert_eq!(result.get("theme"), Some(&"dark".to_string()));
307    }
308
309    #[test]
310    fn test_extract_cookies_with_spaces() {
311        let mut headers = HeaderMap::new();
312        headers.insert(
313            axum::http::header::COOKIE,
314            HeaderValue::from_static("session = abc123 ; theme = light"),
315        );
316
317        let result = extract_cookies(&headers);
318        assert!(result.len() >= 1);
319    }
320
321    #[test]
322    fn test_extract_cookies_empty_value() {
323        let mut headers = HeaderMap::new();
324        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("empty="));
325
326        let result = extract_cookies(&headers);
327        assert_eq!(result.get("empty"), Some(&String::new()));
328    }
329
330    #[test]
331    fn test_create_request_data_without_body_minimal() {
332        let uri: Uri = "/test".parse().unwrap();
333        let method = Method::GET;
334        let headers = HeaderMap::new();
335        let path_params = HashMap::new();
336
337        let result = create_request_data_without_body(&uri, &method, &headers, path_params);
338
339        assert_eq!(result.method, "GET");
340        assert_eq!(result.path, "/test");
341        assert!(result.path_params.is_empty());
342        assert_eq!(result.query_params, json!({}));
343        assert!(result.raw_query_params.is_empty());
344        assert!(result.headers.is_empty());
345        assert!(result.cookies.is_empty());
346        assert_eq!(result.body, Value::Null);
347        assert!(result.raw_body.is_none());
348    }
349
350    #[test]
351    fn test_create_request_data_without_body_with_path_params() {
352        let uri: Uri = "/users/42".parse().unwrap();
353        let method = Method::GET;
354        let headers = HeaderMap::new();
355        let mut path_params = HashMap::new();
356        path_params.insert("user_id".to_string(), "42".to_string());
357
358        let result = create_request_data_without_body(&uri, &method, &headers, path_params);
359
360        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
361    }
362
363    #[test]
364    fn test_create_request_data_without_body_with_query_params() {
365        let uri: Uri = "/search?q=rust&limit=10".parse().unwrap();
366        let method = Method::GET;
367        let headers = HeaderMap::new();
368        let path_params = HashMap::new();
369
370        let result = create_request_data_without_body(&uri, &method, &headers, path_params);
371
372        assert_eq!(result.query_params, json!({"q": "rust", "limit": 10}));
373        assert_eq!(result.raw_query_params.get("q"), Some(&vec!["rust".to_string()]));
374        assert_eq!(result.raw_query_params.get("limit"), Some(&vec!["10".to_string()]));
375    }
376
377    #[test]
378    fn test_create_request_data_without_body_with_headers() {
379        let uri: Uri = "/test".parse().unwrap();
380        let method = Method::POST;
381        let mut headers = HeaderMap::new();
382        headers.insert("content-type", HeaderValue::from_static("application/json"));
383        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
384        let path_params = HashMap::new();
385
386        let result = create_request_data_without_body(&uri, &method, &headers, path_params);
387
388        assert_eq!(
389            result.headers.get("content-type"),
390            Some(&"application/json".to_string())
391        );
392        assert_eq!(result.headers.get("authorization"), Some(&"Bearer token".to_string()));
393    }
394
395    #[test]
396    fn test_create_request_data_without_body_with_cookies() {
397        let uri: Uri = "/test".parse().unwrap();
398        let method = Method::GET;
399        let mut headers = HeaderMap::new();
400        headers.insert(
401            axum::http::header::COOKIE,
402            HeaderValue::from_static("session=xyz; theme=dark"),
403        );
404        let path_params = HashMap::new();
405
406        let result = create_request_data_without_body(&uri, &method, &headers, path_params);
407
408        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
409        assert_eq!(result.cookies.get("theme"), Some(&"dark".to_string()));
410    }
411
412    #[test]
413    fn test_create_request_data_without_body_different_methods() {
414        let uri: Uri = "/resource".parse().unwrap();
415        let headers = HeaderMap::new();
416        let path_params = HashMap::new();
417
418        for method in &[Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::PATCH] {
419            let result = create_request_data_without_body(&uri, method, &headers, path_params.clone());
420            assert_eq!(result.method, method.as_str());
421        }
422    }
423
424    #[tokio::test]
425    async fn test_create_request_data_with_body_empty() {
426        let parts = axum::http::request::Request::builder()
427            .method(Method::POST)
428            .uri("/test")
429            .body(Body::empty())
430            .unwrap()
431            .into_parts();
432
433        let body = Body::empty();
434        let path_params = HashMap::new();
435
436        let result = create_request_data_with_body(&parts.0, path_params, body)
437            .await
438            .unwrap();
439
440        assert_eq!(result.method, "POST");
441        assert_eq!(result.path, "/test");
442        assert_eq!(result.body, Value::Null);
443        assert!(result.raw_body.is_none());
444    }
445
446    #[tokio::test]
447    async fn test_create_request_data_with_body_json() {
448        let request_body = Body::from(r#"{"key":"value"}"#);
449        let request = axum::http::request::Request::builder()
450            .method(Method::POST)
451            .uri("/test")
452            .body(Body::empty())
453            .unwrap();
454
455        let (parts, _) = request.into_parts();
456        let path_params = HashMap::new();
457
458        let result = create_request_data_with_body(&parts, path_params, request_body)
459            .await
460            .unwrap();
461
462        assert_eq!(result.method, "POST");
463        assert_eq!(result.body, Value::Null);
464        assert!(result.raw_body.is_some());
465        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), br#"{"key":"value"}"#);
466    }
467
468    #[tokio::test]
469    async fn test_create_request_data_with_body_with_query_params() {
470        let request_body = Body::from("test");
471        let request = axum::http::request::Request::builder()
472            .method(Method::POST)
473            .uri("/test?foo=bar&baz=qux")
474            .body(Body::empty())
475            .unwrap();
476
477        let (parts, _) = request.into_parts();
478        let path_params = HashMap::new();
479
480        let result = create_request_data_with_body(&parts, path_params, request_body)
481            .await
482            .unwrap();
483
484        assert_eq!(result.query_params, json!({"foo": "bar", "baz": "qux"}));
485    }
486
487    #[tokio::test]
488    async fn test_create_request_data_with_body_with_headers() {
489        let request_body = Body::from("test");
490        let request = axum::http::request::Request::builder()
491            .method(Method::POST)
492            .uri("/test")
493            .header("content-type", "application/json")
494            .header("x-request-id", "req123")
495            .body(Body::empty())
496            .unwrap();
497
498        let (parts, _) = request.into_parts();
499        let path_params = HashMap::new();
500
501        let result = create_request_data_with_body(&parts, path_params, request_body)
502            .await
503            .unwrap();
504
505        assert_eq!(
506            result.headers.get("content-type"),
507            Some(&"application/json".to_string())
508        );
509        assert_eq!(result.headers.get("x-request-id"), Some(&"req123".to_string()));
510    }
511
512    #[tokio::test]
513    async fn test_create_request_data_with_body_with_cookies() {
514        let request_body = Body::from("test");
515        let request = axum::http::request::Request::builder()
516            .method(Method::POST)
517            .uri("/test")
518            .header("cookie", "session=xyz; user=123")
519            .body(Body::empty())
520            .unwrap();
521
522        let (parts, _) = request.into_parts();
523        let path_params = HashMap::new();
524
525        let result = create_request_data_with_body(&parts, path_params, request_body)
526            .await
527            .unwrap();
528
529        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
530        assert_eq!(result.cookies.get("user"), Some(&"123".to_string()));
531    }
532
533    #[tokio::test]
534    async fn test_create_request_data_with_body_large_payload() {
535        let large_json = json!({
536            "data": (0..100).map(|i| json!({"id": i, "value": format!("item-{}", i)})).collect::<Vec<_>>()
537        });
538        let json_str = serde_json::to_string(&large_json).unwrap();
539        let request_body = Body::from(json_str.clone());
540
541        let request = axum::http::request::Request::builder()
542            .method(Method::POST)
543            .uri("/test")
544            .body(Body::empty())
545            .unwrap();
546
547        let (parts, _) = request.into_parts();
548        let path_params = HashMap::new();
549
550        let result = create_request_data_with_body(&parts, path_params, request_body)
551            .await
552            .unwrap();
553
554        assert!(result.raw_body.is_some());
555        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), json_str.as_bytes());
556    }
557
558    #[tokio::test]
559    async fn test_create_request_data_with_body_preserves_all_fields() {
560        let request_body = Body::from("request data");
561        let request = axum::http::request::Request::builder()
562            .method(Method::PUT)
563            .uri("/users/42?action=update")
564            .header("authorization", "Bearer token")
565            .header("cookie", "session=abc")
566            .body(Body::empty())
567            .unwrap();
568
569        let (parts, _) = request.into_parts();
570        let mut path_params = HashMap::new();
571        path_params.insert("user_id".to_string(), "42".to_string());
572
573        let result = create_request_data_with_body(&parts, path_params, request_body)
574            .await
575            .unwrap();
576
577        assert_eq!(result.method, "PUT");
578        assert_eq!(result.path, "/users/42");
579        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
580        assert_eq!(result.query_params, json!({"action": "update"}));
581        assert!(result.headers.contains_key("authorization"));
582        assert!(result.cookies.contains_key("session"));
583        assert!(result.raw_body.is_some());
584    }
585
586    #[test]
587    fn test_arc_wrapping_for_cheap_cloning() {
588        let uri: Uri = "/test".parse().unwrap();
589        let method = Method::GET;
590        let mut headers = HeaderMap::new();
591        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc"));
592        let mut path_params = HashMap::new();
593        path_params.insert("id".to_string(), "1".to_string());
594
595        let request_data = create_request_data_without_body(&uri, &method, &headers, path_params.clone());
596
597        let cloned = request_data.clone();
598
599        assert!(Arc::ptr_eq(&request_data.path_params, &cloned.path_params));
600        assert!(Arc::ptr_eq(&request_data.headers, &cloned.headers));
601        assert!(Arc::ptr_eq(&request_data.cookies, &cloned.cookies));
602        assert!(Arc::ptr_eq(&request_data.raw_query_params, &cloned.raw_query_params));
603    }
604}