spikard_http/server/
request_extraction.rs

1//! Request parsing and data extraction utilities
2//!
3//! Performance optimizations in this module:
4//! - Static singletons for empty collections avoid repeated allocations
5//! - HashMap::with_capacity pre-allocates based on expected sizes
6//! - Arc wrapping enables cheap cloning of RequestData
7
8use crate::handler_trait::RequestData;
9use crate::query_parser::{parse_query_pairs_to_json, parse_query_string};
10use axum::body::Body;
11use http_body_util::BodyExt;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::sync::Arc;
15use std::sync::OnceLock;
16
17// Performance: Static singletons for empty collections.
18// These avoid allocating new empty HashMaps/Values for every request that doesn't
19// need them (e.g., requests without query params, headers disabled, etc.).
20
21/// Static empty JSON object value, shared across all requests without query params.
22fn empty_json_object() -> Arc<Value> {
23    static EMPTY: OnceLock<Arc<Value>> = OnceLock::new();
24    Arc::clone(EMPTY.get_or_init(|| Arc::new(Value::Object(serde_json::Map::new()))))
25}
26
27/// Static null JSON value for requests without bodies.
28fn null_json_value() -> Arc<Value> {
29    static NULL: OnceLock<Arc<Value>> = OnceLock::new();
30    Arc::clone(NULL.get_or_init(|| Arc::new(Value::Null)))
31}
32
33/// Static empty path params map.
34fn empty_path_params() -> Arc<HashMap<String, String>> {
35    static EMPTY: OnceLock<Arc<HashMap<String, String>>> = OnceLock::new();
36    Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
37}
38
39#[derive(Debug, Clone, Copy)]
40pub struct WithoutBodyExtractionOptions {
41    pub include_raw_query_params: bool,
42    pub include_query_params_json: bool,
43    pub include_headers: bool,
44    pub include_cookies: bool,
45}
46
47fn extract_query_params_and_raw(
48    uri: &axum::http::Uri,
49    include_raw_query_params: bool,
50    include_query_params_json: bool,
51) -> (Value, HashMap<String, Vec<String>>) {
52    let query_string = uri.query().unwrap_or("");
53    if query_string.is_empty() {
54        // Performance: Return empty object for empty query string.
55        return (Value::Object(serde_json::Map::new()), HashMap::new());
56    }
57
58    match (include_raw_query_params, include_query_params_json) {
59        (false, false) => (Value::Null, HashMap::new()),
60        (false, true) => (
61            crate::query_parser::parse_query_string_to_json(query_string.as_bytes(), true),
62            HashMap::new(),
63        ),
64        (true, false) => {
65            let pairs = parse_query_string(query_string.as_bytes(), '&');
66            // Performance: Pre-allocate HashMap with estimated unique key count.
67            // In practice, most keys are unique, so pairs.len() is a good estimate.
68            let mut raw = HashMap::with_capacity(pairs.len());
69            for (k, v) in pairs {
70                raw.entry(k).or_insert_with(Vec::new).push(v);
71            }
72            (Value::Null, raw)
73        }
74        (true, true) => {
75            let pairs = parse_query_string(query_string.as_bytes(), '&');
76            let json = parse_query_pairs_to_json(&pairs, true);
77            // Performance: Pre-allocate HashMap with estimated unique key count.
78            let mut raw = HashMap::with_capacity(pairs.len());
79            for (k, v) in pairs {
80                raw.entry(k).or_insert_with(Vec::new).push(v);
81            }
82            (json, raw)
83        }
84    }
85}
86
87/// Extract and parse query parameters from request URI
88pub fn extract_query_params(uri: &axum::http::Uri) -> Value {
89    let query_string = uri.query().unwrap_or("");
90    if query_string.is_empty() {
91        Value::Object(serde_json::Map::new())
92    } else {
93        crate::query_parser::parse_query_string_to_json(query_string.as_bytes(), true)
94    }
95}
96
97/// Extract raw query parameters as strings (no type conversion)
98/// Used for validation error messages to show the actual input values
99///
100/// Performance: Pre-allocates HashMap based on parsed pair count.
101pub fn extract_raw_query_params(uri: &axum::http::Uri) -> HashMap<String, Vec<String>> {
102    let query_string = uri.query().unwrap_or("");
103    if query_string.is_empty() {
104        return HashMap::new();
105    }
106
107    let pairs = parse_query_string(query_string.as_bytes(), '&');
108    // Performance: Pre-allocate with estimated unique key count.
109    let mut map = HashMap::with_capacity(pairs.len());
110    for (k, v) in pairs {
111        map.entry(k).or_insert_with(Vec::new).push(v);
112    }
113    map
114}
115
116/// Extract headers from request
117pub fn extract_headers(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
118    let mut map = HashMap::with_capacity(headers.len());
119    for (name, value) in headers.iter() {
120        if let Ok(val_str) = value.to_str() {
121            // `HeaderName::as_str()` is already normalized to lowercase.
122            map.insert(name.as_str().to_string(), val_str.to_string());
123        }
124    }
125    map
126}
127
128fn extract_content_type_header(headers: &axum::http::HeaderMap) -> Arc<HashMap<String, String>> {
129    let Some(value) = headers
130        .get(axum::http::header::CONTENT_TYPE)
131        .and_then(|h| h.to_str().ok())
132    else {
133        return empty_string_map();
134    };
135
136    let mut map = HashMap::with_capacity(1);
137    map.insert("content-type".to_string(), value.to_string());
138    Arc::new(map)
139}
140
141/// Extract cookies from request headers
142///
143/// Performance: Pre-allocates HashMap capacity based on estimated cookie count
144/// by counting semicolons in the cookie string (each cookie separated by "; ").
145pub fn extract_cookies(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
146    let Some(cookie_str) = headers.get(axum::http::header::COOKIE).and_then(|h| h.to_str().ok()) else {
147        return HashMap::new();
148    };
149
150    // Performance: Estimate cookie count by counting semicolons + 1.
151    // Typical cookie string: "session=abc; user=123; theme=dark"
152    let estimated_count = cookie_str.bytes().filter(|&b| b == b';').count() + 1;
153    let mut cookies = HashMap::with_capacity(estimated_count);
154
155    for cookie in cookie::Cookie::split_parse(cookie_str).flatten() {
156        cookies.insert(cookie.name().to_string(), cookie.value().to_string());
157    }
158
159    cookies
160}
161
162fn empty_string_map() -> Arc<HashMap<String, String>> {
163    static EMPTY: OnceLock<Arc<HashMap<String, String>>> = OnceLock::new();
164    Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
165}
166
167fn empty_raw_query_map() -> Arc<HashMap<String, Vec<String>>> {
168    static EMPTY: OnceLock<Arc<HashMap<String, Vec<String>>>> = OnceLock::new();
169    Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
170}
171
172/// Create RequestData from request parts (for requests without body)
173///
174/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
175///
176/// Performance optimizations:
177/// - Uses static singletons for empty path params, body, headers, cookies
178/// - Only allocates when data is present
179pub fn create_request_data_without_body(
180    uri: &axum::http::Uri,
181    method: &axum::http::Method,
182    headers: &axum::http::HeaderMap,
183    path_params: HashMap<String, String>,
184    options: WithoutBodyExtractionOptions,
185) -> RequestData {
186    let (query_params, raw_query_params) =
187        extract_query_params_and_raw(uri, options.include_raw_query_params, options.include_query_params_json);
188
189    // Performance: Use static singleton for empty path params (common for routes without params).
190    let path_params_arc = if path_params.is_empty() {
191        empty_path_params()
192    } else {
193        Arc::new(path_params)
194    };
195
196    // Performance: Use static singleton for empty query params.
197    let query_params_arc = if matches!(query_params, Value::Object(ref m) if m.is_empty()) {
198        empty_json_object()
199    } else if matches!(query_params, Value::Null) {
200        null_json_value()
201    } else {
202        Arc::new(query_params)
203    };
204
205    RequestData {
206        path_params: path_params_arc,
207        query_params: query_params_arc,
208        raw_query_params: if raw_query_params.is_empty() {
209            empty_raw_query_map()
210        } else {
211            Arc::new(raw_query_params)
212        },
213        validated_params: None,
214        headers: if options.include_headers {
215            Arc::new(extract_headers(headers))
216        } else {
217            empty_string_map()
218        },
219        cookies: if options.include_cookies {
220            Arc::new(extract_cookies(headers))
221        } else {
222            empty_string_map()
223        },
224        // Performance: Use static null value singleton.
225        body: null_json_value(),
226        raw_body: None,
227        method: method.as_str().to_string(),
228        path: uri.path().to_string(),
229        #[cfg(feature = "di")]
230        dependencies: None,
231    }
232}
233
234/// Create RequestData from request parts (for requests with body)
235///
236/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
237///
238/// Performance optimizations:
239/// - Stores raw body bytes without parsing JSON (deferred parsing)
240/// - Uses static singletons for empty collections
241/// - Pre-read body bytes are reused if available in extensions
242pub async fn create_request_data_with_body(
243    parts: &axum::http::request::Parts,
244    path_params: HashMap<String, String>,
245    body: Body,
246    include_raw_query_params: bool,
247    include_query_params_json: bool,
248    include_headers: bool,
249    include_cookies: bool,
250) -> Result<RequestData, (axum::http::StatusCode, String)> {
251    let body_bytes = if let Some(pre_read) = parts.extensions.get::<crate::middleware::PreReadBody>() {
252        pre_read.0.clone()
253    } else {
254        body.collect()
255            .await
256            .map_err(|e| {
257                (
258                    axum::http::StatusCode::BAD_REQUEST,
259                    format!("Failed to read body: {}", e),
260                )
261            })?
262            .to_bytes()
263    };
264
265    let (query_params, raw_query_params) =
266        extract_query_params_and_raw(&parts.uri, include_raw_query_params, include_query_params_json);
267
268    // Performance: Use static singleton for empty path params.
269    let path_params_arc = if path_params.is_empty() {
270        empty_path_params()
271    } else {
272        Arc::new(path_params)
273    };
274
275    // Performance: Use static singleton for empty/null query params.
276    let query_params_arc = if matches!(query_params, Value::Object(ref m) if m.is_empty()) {
277        empty_json_object()
278    } else if matches!(query_params, Value::Null) {
279        null_json_value()
280    } else {
281        Arc::new(query_params)
282    };
283
284    // Performance: Reuse pre-parsed JSON if available, otherwise use static null.
285    let body_arc = if let Some(parsed) = parts.extensions.get::<crate::middleware::PreParsedJson>() {
286        Arc::new(parsed.0.clone())
287    } else {
288        null_json_value()
289    };
290
291    Ok(RequestData {
292        path_params: path_params_arc,
293        query_params: query_params_arc,
294        raw_query_params: if raw_query_params.is_empty() {
295            empty_raw_query_map()
296        } else {
297            Arc::new(raw_query_params)
298        },
299        validated_params: None,
300        headers: if include_headers {
301            Arc::new(extract_headers(&parts.headers))
302        } else {
303            extract_content_type_header(&parts.headers)
304        },
305        cookies: if include_cookies {
306            Arc::new(extract_cookies(&parts.headers))
307        } else {
308            empty_string_map()
309        },
310        body: body_arc,
311        raw_body: if body_bytes.is_empty() { None } else { Some(body_bytes) },
312        method: parts.method.as_str().to_string(),
313        path: parts.uri.path().to_string(),
314        #[cfg(feature = "di")]
315        dependencies: None,
316    })
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use axum::http::{HeaderMap, HeaderValue, Method, Uri};
323    use futures_util::stream;
324    use serde_json::json;
325
326    const OPTIONS_ALL: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
327        include_raw_query_params: true,
328        include_query_params_json: true,
329        include_headers: true,
330        include_cookies: true,
331    };
332
333    const OPTIONS_SKIP_HEADERS: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
334        include_raw_query_params: true,
335        include_query_params_json: true,
336        include_headers: false,
337        include_cookies: true,
338    };
339
340    const OPTIONS_SKIP_COOKIES: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
341        include_raw_query_params: true,
342        include_query_params_json: true,
343        include_headers: true,
344        include_cookies: false,
345    };
346
347    #[test]
348    fn test_extract_query_params_empty() {
349        let uri: Uri = "/path".parse().unwrap();
350        let result = extract_query_params(&uri);
351        assert_eq!(result, json!({}));
352    }
353
354    #[test]
355    fn test_extract_query_params_single_param() {
356        let uri: Uri = "/path?name=value".parse().unwrap();
357        let result = extract_query_params(&uri);
358        assert_eq!(result, json!({"name": "value"}));
359    }
360
361    #[test]
362    fn test_extract_query_params_multiple_params() {
363        let uri: Uri = "/path?foo=1&bar=2".parse().unwrap();
364        let result = extract_query_params(&uri);
365        assert_eq!(result, json!({"foo": 1, "bar": 2}));
366    }
367
368    #[test]
369    fn test_extract_query_params_array_params() {
370        let uri: Uri = "/path?tags=rust&tags=http".parse().unwrap();
371        let result = extract_query_params(&uri);
372        assert_eq!(result, json!({"tags": ["rust", "http"]}));
373    }
374
375    #[test]
376    fn test_extract_query_params_mixed_array_and_single() {
377        let uri: Uri = "/path?tags=rust&tags=web&id=123".parse().unwrap();
378        let result = extract_query_params(&uri);
379        assert_eq!(result, json!({"tags": ["rust", "web"], "id": 123}));
380    }
381
382    #[test]
383    fn test_extract_query_params_url_encoded() {
384        let uri: Uri = "/path?email=test%40example.com&name=john+doe".parse().unwrap();
385        let result = extract_query_params(&uri);
386        assert_eq!(result, json!({"email": "test@example.com", "name": "john doe"}));
387    }
388
389    #[test]
390    fn test_extract_query_params_boolean_values() {
391        let uri: Uri = "/path?active=true&enabled=false".parse().unwrap();
392        let result = extract_query_params(&uri);
393        assert_eq!(result, json!({"active": true, "enabled": false}));
394    }
395
396    #[test]
397    fn test_extract_query_params_null_value() {
398        let uri: Uri = "/path?value=null".parse().unwrap();
399        let result = extract_query_params(&uri);
400        assert_eq!(result, json!({"value": null}));
401    }
402
403    #[test]
404    fn test_extract_query_params_empty_string_value() {
405        let uri: Uri = "/path?key=".parse().unwrap();
406        let result = extract_query_params(&uri);
407        assert_eq!(result, json!({"key": ""}));
408    }
409
410    #[test]
411    fn test_extract_raw_query_params_empty() {
412        let uri: Uri = "/path".parse().unwrap();
413        let result = extract_raw_query_params(&uri);
414        assert!(result.is_empty());
415    }
416
417    #[test]
418    fn test_extract_raw_query_params_single() {
419        let uri: Uri = "/path?name=value".parse().unwrap();
420        let result = extract_raw_query_params(&uri);
421        assert_eq!(result.get("name"), Some(&vec!["value".to_string()]));
422    }
423
424    #[test]
425    fn test_extract_raw_query_params_multiple_values() {
426        let uri: Uri = "/path?tag=rust&tag=http".parse().unwrap();
427        let result = extract_raw_query_params(&uri);
428        assert_eq!(result.get("tag"), Some(&vec!["rust".to_string(), "http".to_string()]));
429    }
430
431    #[test]
432    fn test_extract_raw_query_params_mixed() {
433        let uri: Uri = "/path?id=123&tags=rust&tags=web&active=true".parse().unwrap();
434        let result = extract_raw_query_params(&uri);
435        assert_eq!(result.get("id"), Some(&vec!["123".to_string()]));
436        assert_eq!(result.get("tags"), Some(&vec!["rust".to_string(), "web".to_string()]));
437        assert_eq!(result.get("active"), Some(&vec!["true".to_string()]));
438    }
439
440    #[test]
441    fn test_extract_raw_query_params_url_encoded() {
442        let uri: Uri = "/path?email=test%40example.com".parse().unwrap();
443        let result = extract_raw_query_params(&uri);
444        assert_eq!(result.get("email"), Some(&vec!["test@example.com".to_string()]));
445    }
446
447    #[test]
448    fn test_extract_headers_empty() {
449        let headers = HeaderMap::new();
450        let result = extract_headers(&headers);
451        assert!(result.is_empty());
452    }
453
454    #[test]
455    fn test_extract_headers_single() {
456        let mut headers = HeaderMap::new();
457        headers.insert("content-type", HeaderValue::from_static("application/json"));
458        let result = extract_headers(&headers);
459        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
460    }
461
462    #[test]
463    fn test_extract_headers_multiple() {
464        let mut headers = HeaderMap::new();
465        headers.insert("content-type", HeaderValue::from_static("application/json"));
466        headers.insert("user-agent", HeaderValue::from_static("test-agent"));
467        headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
468
469        let result = extract_headers(&headers);
470        assert_eq!(result.len(), 3);
471        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
472        assert_eq!(result.get("user-agent"), Some(&"test-agent".to_string()));
473        assert_eq!(result.get("authorization"), Some(&"Bearer token123".to_string()));
474    }
475
476    #[test]
477    fn test_extract_headers_case_insensitive() {
478        let mut headers = HeaderMap::new();
479        headers.insert("Content-Type", HeaderValue::from_static("text/html"));
480        headers.insert("USER-Agent", HeaderValue::from_static("chrome"));
481
482        let result = extract_headers(&headers);
483        assert_eq!(result.get("content-type"), Some(&"text/html".to_string()));
484        assert_eq!(result.get("user-agent"), Some(&"chrome".to_string()));
485    }
486
487    #[test]
488    fn test_extract_headers_with_dashes() {
489        let mut headers = HeaderMap::new();
490        headers.insert("x-custom-header", HeaderValue::from_static("custom-value"));
491        headers.insert("x-request-id", HeaderValue::from_static("req-12345"));
492
493        let result = extract_headers(&headers);
494        assert_eq!(result.get("x-custom-header"), Some(&"custom-value".to_string()));
495        assert_eq!(result.get("x-request-id"), Some(&"req-12345".to_string()));
496    }
497
498    #[test]
499    fn test_extract_cookies_no_cookie_header() {
500        let headers = HeaderMap::new();
501        let result = extract_cookies(&headers);
502        assert!(result.is_empty());
503    }
504
505    #[test]
506    fn test_extract_cookies_single() {
507        let mut headers = HeaderMap::new();
508        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc123"));
509
510        let result = extract_cookies(&headers);
511        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
512    }
513
514    #[test]
515    fn test_extract_cookies_multiple() {
516        let mut headers = HeaderMap::new();
517        headers.insert(
518            axum::http::header::COOKIE,
519            HeaderValue::from_static("session=abc123; user_id=42; theme=dark"),
520        );
521
522        let result = extract_cookies(&headers);
523        assert_eq!(result.len(), 3);
524        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
525        assert_eq!(result.get("user_id"), Some(&"42".to_string()));
526        assert_eq!(result.get("theme"), Some(&"dark".to_string()));
527    }
528
529    #[test]
530    fn test_extract_cookies_with_spaces() {
531        let mut headers = HeaderMap::new();
532        headers.insert(
533            axum::http::header::COOKIE,
534            HeaderValue::from_static("session = abc123 ; theme = light"),
535        );
536
537        let result = extract_cookies(&headers);
538        assert!(result.len() >= 1);
539    }
540
541    #[test]
542    fn test_extract_cookies_empty_value() {
543        let mut headers = HeaderMap::new();
544        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("empty="));
545
546        let result = extract_cookies(&headers);
547        assert_eq!(result.get("empty"), Some(&String::new()));
548    }
549
550    #[test]
551    fn test_create_request_data_without_body_minimal() {
552        let uri: Uri = "/test".parse().unwrap();
553        let method = Method::GET;
554        let headers = HeaderMap::new();
555        let path_params = HashMap::new();
556
557        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
558
559        assert_eq!(result.method, "GET");
560        assert_eq!(result.path, "/test");
561        assert!(result.path_params.is_empty());
562        assert_eq!(*result.query_params, json!({}));
563        assert!(result.raw_query_params.is_empty());
564        assert!(result.headers.is_empty());
565        assert!(result.cookies.is_empty());
566        assert_eq!(*result.body, Value::Null);
567        assert!(result.raw_body.is_none());
568    }
569
570    #[test]
571    fn test_create_request_data_without_body_with_path_params() {
572        let uri: Uri = "/users/42".parse().unwrap();
573        let method = Method::GET;
574        let headers = HeaderMap::new();
575        let mut path_params = HashMap::new();
576        path_params.insert("user_id".to_string(), "42".to_string());
577
578        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
579
580        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
581    }
582
583    #[test]
584    fn test_create_request_data_without_body_with_query_params() {
585        let uri: Uri = "/search?q=rust&limit=10".parse().unwrap();
586        let method = Method::GET;
587        let headers = HeaderMap::new();
588        let path_params = HashMap::new();
589
590        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
591
592        assert_eq!(*result.query_params, json!({"q": "rust", "limit": 10}));
593        assert_eq!(result.raw_query_params.get("q"), Some(&vec!["rust".to_string()]));
594        assert_eq!(result.raw_query_params.get("limit"), Some(&vec!["10".to_string()]));
595    }
596
597    #[test]
598    fn test_create_request_data_without_body_with_headers() {
599        let uri: Uri = "/test".parse().unwrap();
600        let method = Method::POST;
601        let mut headers = HeaderMap::new();
602        headers.insert("content-type", HeaderValue::from_static("application/json"));
603        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
604        let path_params = HashMap::new();
605
606        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
607
608        assert_eq!(
609            result.headers.get("content-type"),
610            Some(&"application/json".to_string())
611        );
612        assert_eq!(result.headers.get("authorization"), Some(&"Bearer token".to_string()));
613    }
614
615    #[test]
616    fn test_create_request_data_without_body_with_cookies() {
617        let uri: Uri = "/test".parse().unwrap();
618        let method = Method::GET;
619        let mut headers = HeaderMap::new();
620        headers.insert(
621            axum::http::header::COOKIE,
622            HeaderValue::from_static("session=xyz; theme=dark"),
623        );
624        let path_params = HashMap::new();
625
626        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
627
628        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
629        assert_eq!(result.cookies.get("theme"), Some(&"dark".to_string()));
630    }
631
632    #[test]
633    fn test_create_request_data_without_body_skip_headers() {
634        let uri: Uri = "/test".parse().unwrap();
635        let method = Method::GET;
636        let mut headers = HeaderMap::new();
637        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
638        let path_params = HashMap::new();
639
640        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_SKIP_HEADERS);
641
642        assert!(result.headers.is_empty());
643    }
644
645    #[test]
646    fn test_create_request_data_without_body_skip_cookies() {
647        let uri: Uri = "/test".parse().unwrap();
648        let method = Method::GET;
649        let mut headers = HeaderMap::new();
650        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=xyz"));
651        let path_params = HashMap::new();
652
653        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_SKIP_COOKIES);
654
655        assert!(result.cookies.is_empty());
656    }
657
658    #[test]
659    fn test_create_request_data_without_body_different_methods() {
660        let uri: Uri = "/resource".parse().unwrap();
661        let headers = HeaderMap::new();
662        let path_params = HashMap::new();
663
664        for method in &[Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::PATCH] {
665            let result = create_request_data_without_body(&uri, method, &headers, path_params.clone(), OPTIONS_ALL);
666            assert_eq!(result.method, method.as_str());
667        }
668    }
669
670    #[tokio::test]
671    async fn test_create_request_data_with_body_empty() {
672        let parts = axum::http::request::Request::builder()
673            .method(Method::POST)
674            .uri("/test")
675            .body(Body::empty())
676            .unwrap()
677            .into_parts();
678
679        let body = Body::empty();
680        let path_params = HashMap::new();
681
682        let result = create_request_data_with_body(&parts.0, path_params, body, true, true, true, true)
683            .await
684            .unwrap();
685
686        assert_eq!(result.method, "POST");
687        assert_eq!(result.path, "/test");
688        assert_eq!(*result.body, Value::Null);
689        assert!(result.raw_body.is_none());
690    }
691
692    #[tokio::test]
693    async fn test_create_request_data_with_body_json() {
694        let request_body = Body::from(r#"{"key":"value"}"#);
695        let request = axum::http::request::Request::builder()
696            .method(Method::POST)
697            .uri("/test")
698            .body(Body::empty())
699            .unwrap();
700
701        let (parts, _) = request.into_parts();
702        let path_params = HashMap::new();
703
704        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
705            .await
706            .unwrap();
707
708        assert_eq!(result.method, "POST");
709        assert_eq!(*result.body, Value::Null);
710        assert!(result.raw_body.is_some());
711        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), br#"{"key":"value"}"#);
712    }
713
714    #[tokio::test]
715    async fn test_create_request_data_with_body_with_query_params() {
716        let request_body = Body::from("test");
717        let request = axum::http::request::Request::builder()
718            .method(Method::POST)
719            .uri("/test?foo=bar&baz=qux")
720            .body(Body::empty())
721            .unwrap();
722
723        let (parts, _) = request.into_parts();
724        let path_params = HashMap::new();
725
726        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
727            .await
728            .unwrap();
729
730        assert_eq!(*result.query_params, json!({"foo": "bar", "baz": "qux"}));
731    }
732
733    #[tokio::test]
734    async fn test_create_request_data_with_body_with_headers() {
735        let request_body = Body::from("test");
736        let request = axum::http::request::Request::builder()
737            .method(Method::POST)
738            .uri("/test")
739            .header("content-type", "application/json")
740            .header("x-request-id", "req123")
741            .body(Body::empty())
742            .unwrap();
743
744        let (parts, _) = request.into_parts();
745        let path_params = HashMap::new();
746
747        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
748            .await
749            .unwrap();
750
751        assert_eq!(
752            result.headers.get("content-type"),
753            Some(&"application/json".to_string())
754        );
755        assert_eq!(result.headers.get("x-request-id"), Some(&"req123".to_string()));
756    }
757
758    #[tokio::test]
759    async fn test_create_request_data_with_body_with_cookies() {
760        let request_body = Body::from("test");
761        let request = axum::http::request::Request::builder()
762            .method(Method::POST)
763            .uri("/test")
764            .header("cookie", "session=xyz; user=123")
765            .body(Body::empty())
766            .unwrap();
767
768        let (parts, _) = request.into_parts();
769        let path_params = HashMap::new();
770
771        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
772            .await
773            .unwrap();
774
775        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
776        assert_eq!(result.cookies.get("user"), Some(&"123".to_string()));
777    }
778
779    #[tokio::test]
780    async fn test_create_request_data_with_body_large_payload() {
781        let large_json = json!({
782            "data": (0..100).map(|i| json!({"id": i, "value": format!("item-{}", i)})).collect::<Vec<_>>()
783        });
784        let json_str = serde_json::to_string(&large_json).unwrap();
785        let request_body = Body::from(json_str.clone());
786
787        let request = axum::http::request::Request::builder()
788            .method(Method::POST)
789            .uri("/test")
790            .body(Body::empty())
791            .unwrap();
792
793        let (parts, _) = request.into_parts();
794        let path_params = HashMap::new();
795
796        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
797            .await
798            .unwrap();
799
800        assert!(result.raw_body.is_some());
801        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), json_str.as_bytes());
802    }
803
804    #[tokio::test]
805    async fn test_create_request_data_with_body_preserves_all_fields() {
806        let request_body = Body::from("request data");
807        let request = axum::http::request::Request::builder()
808            .method(Method::PUT)
809            .uri("/users/42?action=update")
810            .header("authorization", "Bearer token")
811            .header("cookie", "session=abc")
812            .body(Body::empty())
813            .unwrap();
814
815        let (parts, _) = request.into_parts();
816        let mut path_params = HashMap::new();
817        path_params.insert("user_id".to_string(), "42".to_string());
818
819        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
820            .await
821            .unwrap();
822
823        assert_eq!(result.method, "PUT");
824        assert_eq!(result.path, "/users/42");
825        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
826        assert_eq!(*result.query_params, json!({"action": "update"}));
827        assert!(result.headers.contains_key("authorization"));
828        assert!(result.cookies.contains_key("session"));
829        assert!(result.raw_body.is_some());
830    }
831
832    #[test]
833    fn test_arc_wrapping_for_cheap_cloning() {
834        let uri: Uri = "/test".parse().unwrap();
835        let method = Method::GET;
836        let mut headers = HeaderMap::new();
837        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc"));
838        let mut path_params = HashMap::new();
839        path_params.insert("id".to_string(), "1".to_string());
840
841        let request_data = create_request_data_without_body(&uri, &method, &headers, path_params.clone(), OPTIONS_ALL);
842
843        let cloned = request_data.clone();
844
845        assert!(Arc::ptr_eq(&request_data.path_params, &cloned.path_params));
846        assert!(Arc::ptr_eq(&request_data.headers, &cloned.headers));
847        assert!(Arc::ptr_eq(&request_data.cookies, &cloned.cookies));
848        assert!(Arc::ptr_eq(&request_data.raw_query_params, &cloned.raw_query_params));
849    }
850
851    #[tokio::test]
852    async fn create_request_data_with_body_returns_bad_request_when_body_stream_errors() {
853        let request = axum::http::Request::builder()
854            .method(Method::POST)
855            .uri("/path")
856            .body(Body::empty())
857            .unwrap();
858        let (parts, _) = request.into_parts();
859
860        let stream = stream::once(async move {
861            Err::<bytes::Bytes, std::io::Error>(std::io::Error::new(std::io::ErrorKind::Other, "boom"))
862        });
863        let body = Body::from_stream(stream);
864
865        let err = create_request_data_with_body(&parts, HashMap::new(), body, true, true, true, true)
866            .await
867            .unwrap_err();
868        assert_eq!(err.0, axum::http::StatusCode::BAD_REQUEST);
869        assert!(err.1.contains("Failed to read body:"));
870    }
871}