rustapi_core/
extract.rs

1//! Extractors for RustAPI
2//!
3//! Extractors automatically parse and validate data from incoming HTTP requests.
4//! They implement the [`FromRequest`] or [`FromRequestParts`] traits and can be
5//! used as handler function parameters.
6//!
7//! # Available Extractors
8//!
9//! | Extractor | Description | Consumes Body |
10//! |-----------|-------------|---------------|
11//! | [`Json<T>`] | Parse JSON request body | Yes |
12//! | [`ValidatedJson<T>`] | Parse and validate JSON body | Yes |
13//! | [`Query<T>`] | Parse query string parameters | No |
14//! | [`Path<T>`] | Extract path parameters | No |
15//! | [`State<T>`] | Access shared application state | No |
16//! | [`Body`] | Raw request body bytes | Yes |
17//! | [`Headers`] | Access all request headers | No |
18//! | [`HeaderValue`] | Extract a specific header | No |
19//! | [`Extension<T>`] | Access middleware-injected data | No |
20//! | [`ClientIp`] | Extract client IP address | No |
21//! | [`Cookies`] | Parse request cookies (requires `cookies` feature) | No |
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use rustapi_core::{Json, Query, Path, State};
27//! use serde::{Deserialize, Serialize};
28//!
29//! #[derive(Deserialize)]
30//! struct CreateUser {
31//!     name: String,
32//!     email: String,
33//! }
34//!
35//! #[derive(Deserialize)]
36//! struct Pagination {
37//!     page: Option<u32>,
38//!     limit: Option<u32>,
39//! }
40//!
41//! // Multiple extractors can be combined
42//! async fn create_user(
43//!     State(db): State<DbPool>,
44//!     Query(pagination): Query<Pagination>,
45//!     Json(body): Json<CreateUser>,
46//! ) -> impl IntoResponse {
47//!     // Use db, pagination, and body...
48//! }
49//! ```
50//!
51//! # Extractor Order
52//!
53//! When using multiple extractors, body-consuming extractors (like `Json` or `Body`)
54//! must come last since they consume the request body. Non-body extractors can be
55//! in any order.
56
57use crate::error::{ApiError, Result};
58use crate::json;
59use crate::request::Request;
60use crate::response::IntoResponse;
61use crate::stream::{StreamingBody, StreamingConfig};
62use bytes::Bytes;
63use http::{header, StatusCode};
64
65use serde::de::DeserializeOwned;
66use serde::Serialize;
67use std::future::Future;
68use std::ops::{Deref, DerefMut};
69use std::str::FromStr;
70
71/// Trait for extracting data from request parts (headers, path, query)
72///
73/// This is used for extractors that don't need the request body.
74///
75/// # Example: Implementing a custom extractor that requires a specific header
76///
77/// ```rust
78/// use rustapi_core::FromRequestParts;
79/// use rustapi_core::{Request, ApiError, Result};
80/// use http::StatusCode;
81///
82/// struct ApiKey(String);
83///
84/// impl FromRequestParts for ApiKey {
85///     fn from_request_parts(req: &Request) -> Result<Self> {
86///         if let Some(key) = req.headers().get("x-api-key") {
87///             if let Ok(key_str) = key.to_str() {
88///                 return Ok(ApiKey(key_str.to_string()));
89///             }
90///         }
91///         Err(ApiError::unauthorized("Missing or invalid API key"))
92///     }
93/// }
94/// ```
95pub trait FromRequestParts: Sized {
96    /// Extract from request parts
97    fn from_request_parts(req: &Request) -> Result<Self>;
98}
99
100/// Trait for extracting data from the full request (including body)
101///
102/// This is used for extractors that consume the request body.
103///
104/// # Example: Implementing a custom extractor that consumes the body
105///
106/// ```rust
107/// use rustapi_core::FromRequest;
108/// use rustapi_core::{Request, ApiError, Result};
109/// use std::future::Future;
110///
111/// struct PlainText(String);
112///
113/// impl FromRequest for PlainText {
114///     async fn from_request(req: &mut Request) -> Result<Self> {
115///         // Ensure body is loaded
116///         req.load_body().await?;
117///         
118///         // Consume the body
119///         if let Some(bytes) = req.take_body() {
120///             if let Ok(text) = String::from_utf8(bytes.to_vec()) {
121///                 return Ok(PlainText(text));
122///             }
123///         }
124///         
125///         Err(ApiError::bad_request("Invalid plain text body"))
126///     }
127/// }
128/// ```
129pub trait FromRequest: Sized {
130    /// Extract from the full request
131    fn from_request(req: &mut Request) -> impl Future<Output = Result<Self>> + Send;
132}
133
134// Blanket impl: FromRequestParts -> FromRequest
135impl<T: FromRequestParts> FromRequest for T {
136    async fn from_request(req: &mut Request) -> Result<Self> {
137        T::from_request_parts(req)
138    }
139}
140
141/// JSON body extractor
142///
143/// Parses the request body as JSON and deserializes into type `T`.
144/// Also works as a response type when T: Serialize.
145///
146/// # Example
147///
148/// ```rust,ignore
149/// #[derive(Deserialize)]
150/// struct CreateUser {
151///     name: String,
152///     email: String,
153/// }
154///
155/// async fn create_user(Json(body): Json<CreateUser>) -> impl IntoResponse {
156///     // body is already deserialized
157/// }
158/// ```
159#[derive(Debug, Clone, Copy, Default)]
160pub struct Json<T>(pub T);
161
162impl<T: DeserializeOwned + Send> FromRequest for Json<T> {
163    async fn from_request(req: &mut Request) -> Result<Self> {
164        req.load_body().await?;
165        let body = req
166            .take_body()
167            .ok_or_else(|| ApiError::internal("Body already consumed"))?;
168
169        // Use simd-json accelerated parsing when available (2-4x faster)
170        let value: T = json::from_slice(&body)?;
171        Ok(Json(value))
172    }
173}
174
175impl<T> Deref for Json<T> {
176    type Target = T;
177
178    fn deref(&self) -> &Self::Target {
179        &self.0
180    }
181}
182
183impl<T> DerefMut for Json<T> {
184    fn deref_mut(&mut self) -> &mut Self::Target {
185        &mut self.0
186    }
187}
188
189impl<T> From<T> for Json<T> {
190    fn from(value: T) -> Self {
191        Json(value)
192    }
193}
194
195/// Default pre-allocation size for JSON response buffers (256 bytes)
196/// This covers most small to medium JSON responses without reallocation.
197const JSON_RESPONSE_INITIAL_CAPACITY: usize = 256;
198
199// IntoResponse for Json - allows using Json<T> as a return type
200impl<T: Serialize> IntoResponse for Json<T> {
201    fn into_response(self) -> crate::response::Response {
202        // Use pre-allocated buffer to reduce allocations
203        match json::to_vec_with_capacity(&self.0, JSON_RESPONSE_INITIAL_CAPACITY) {
204            Ok(body) => http::Response::builder()
205                .status(StatusCode::OK)
206                .header(header::CONTENT_TYPE, "application/json")
207                .body(crate::response::Body::from(body))
208                .unwrap(),
209            Err(err) => {
210                ApiError::internal(format!("Failed to serialize response: {}", err)).into_response()
211            }
212        }
213    }
214}
215
216/// Validated JSON body extractor
217///
218/// Parses the request body as JSON, deserializes into type `T`, and validates
219/// using the `Validate` trait. Returns a 422 Unprocessable Entity error with
220/// detailed field-level validation errors if validation fails.
221///
222/// # Example
223///
224/// ```rust,ignore
225/// use rustapi_rs::prelude::*;
226/// use validator::Validate;
227///
228/// #[derive(Deserialize, Validate)]
229/// struct CreateUser {
230///     #[validate(email)]
231///     email: String,
232///     #[validate(length(min = 8))]
233///     password: String,
234/// }
235///
236/// async fn register(ValidatedJson(body): ValidatedJson<CreateUser>) -> impl IntoResponse {
237///     // body is already validated!
238///     // If email is invalid or password too short, a 422 error is returned automatically
239/// }
240/// ```
241#[derive(Debug, Clone, Copy, Default)]
242pub struct ValidatedJson<T>(pub T);
243
244impl<T> ValidatedJson<T> {
245    /// Create a new ValidatedJson wrapper
246    pub fn new(value: T) -> Self {
247        Self(value)
248    }
249
250    /// Get the inner value
251    pub fn into_inner(self) -> T {
252        self.0
253    }
254}
255
256impl<T: DeserializeOwned + rustapi_validate::Validate + Send> FromRequest for ValidatedJson<T> {
257    async fn from_request(req: &mut Request) -> Result<Self> {
258        req.load_body().await?;
259        // First, deserialize the JSON body using simd-json when available
260        let body = req
261            .take_body()
262            .ok_or_else(|| ApiError::internal("Body already consumed"))?;
263
264        let value: T = json::from_slice(&body)?;
265
266        // Then, validate it
267        if let Err(validation_error) = rustapi_validate::Validate::validate(&value) {
268            // Convert validation error to API error with 422 status
269            return Err(validation_error.into());
270        }
271
272        Ok(ValidatedJson(value))
273    }
274}
275
276impl<T> Deref for ValidatedJson<T> {
277    type Target = T;
278
279    fn deref(&self) -> &Self::Target {
280        &self.0
281    }
282}
283
284impl<T> DerefMut for ValidatedJson<T> {
285    fn deref_mut(&mut self) -> &mut Self::Target {
286        &mut self.0
287    }
288}
289
290impl<T> From<T> for ValidatedJson<T> {
291    fn from(value: T) -> Self {
292        ValidatedJson(value)
293    }
294}
295
296impl<T: Serialize> IntoResponse for ValidatedJson<T> {
297    fn into_response(self) -> crate::response::Response {
298        Json(self.0).into_response()
299    }
300}
301
302/// Query string extractor
303///
304/// Parses the query string into type `T`.
305///
306/// # Example
307///
308/// ```rust,ignore
309/// #[derive(Deserialize)]
310/// struct Pagination {
311///     page: Option<u32>,
312///     limit: Option<u32>,
313/// }
314///
315/// async fn list_users(Query(params): Query<Pagination>) -> impl IntoResponse {
316///     // params.page, params.limit
317/// }
318/// ```
319#[derive(Debug, Clone)]
320pub struct Query<T>(pub T);
321
322impl<T: DeserializeOwned> FromRequestParts for Query<T> {
323    fn from_request_parts(req: &Request) -> Result<Self> {
324        let query = req.query_string().unwrap_or("");
325        let value: T = serde_urlencoded::from_str(query)
326            .map_err(|e| ApiError::bad_request(format!("Invalid query string: {}", e)))?;
327        Ok(Query(value))
328    }
329}
330
331impl<T> Deref for Query<T> {
332    type Target = T;
333
334    fn deref(&self) -> &Self::Target {
335        &self.0
336    }
337}
338
339/// Path parameter extractor
340///
341/// Extracts path parameters defined in the route pattern.
342///
343/// # Example
344///
345/// For route `/users/{id}`:
346///
347/// ```rust,ignore
348/// async fn get_user(Path(id): Path<i64>) -> impl IntoResponse {
349///     // id is extracted from path
350/// }
351/// ```
352///
353/// For multiple params `/users/{user_id}/posts/{post_id}`:
354///
355/// ```rust,ignore
356/// async fn get_post(Path((user_id, post_id)): Path<(i64, i64)>) -> impl IntoResponse {
357///     // Both params extracted
358/// }
359/// ```
360#[derive(Debug, Clone)]
361pub struct Path<T>(pub T);
362
363impl<T: FromStr> FromRequestParts for Path<T>
364where
365    T::Err: std::fmt::Display,
366{
367    fn from_request_parts(req: &Request) -> Result<Self> {
368        let params = req.path_params();
369
370        // For single param, get the first one
371        if let Some((_, value)) = params.iter().next() {
372            let parsed = value
373                .parse::<T>()
374                .map_err(|e| ApiError::bad_request(format!("Invalid path parameter: {}", e)))?;
375            return Ok(Path(parsed));
376        }
377
378        Err(ApiError::internal("Missing path parameter"))
379    }
380}
381
382impl<T> Deref for Path<T> {
383    type Target = T;
384
385    fn deref(&self) -> &Self::Target {
386        &self.0
387    }
388}
389
390/// Typed path extractor
391///
392/// Extracts path parameters and deserializes them into a struct implementing `Deserialize`.
393/// This is similar to `Path<T>`, but supports complex structs that can be deserialized
394/// from a map of parameter names to values (e.g. via `serde_json`).
395///
396/// # Example
397///
398/// ```rust,ignore
399/// #[derive(Deserialize)]
400/// struct UserParams {
401///     id: u64,
402///     category: String,
403/// }
404///
405/// async fn get_user(Typed(params): Typed<UserParams>) -> impl IntoResponse {
406///     // params.id, params.category
407/// }
408/// ```
409#[derive(Debug, Clone)]
410pub struct Typed<T>(pub T);
411
412impl<T: DeserializeOwned + Send> FromRequestParts for Typed<T> {
413    fn from_request_parts(req: &Request) -> Result<Self> {
414        let params = req.path_params();
415        let mut map = serde_json::Map::new();
416        for (k, v) in params.iter() {
417            map.insert(k.to_string(), serde_json::Value::String(v.to_string()));
418        }
419        let value = serde_json::Value::Object(map);
420        let parsed: T = serde_json::from_value(value)
421            .map_err(|e| ApiError::bad_request(format!("Invalid path parameters: {}", e)))?;
422        Ok(Typed(parsed))
423    }
424}
425
426impl<T> Deref for Typed<T> {
427    type Target = T;
428
429    fn deref(&self) -> &Self::Target {
430        &self.0
431    }
432}
433
434/// State extractor
435///
436/// Extracts shared application state.
437///
438/// # Example
439///
440/// ```rust,ignore
441/// #[derive(Clone)]
442/// struct AppState {
443///     db: DbPool,
444/// }
445///
446/// async fn handler(State(state): State<AppState>) -> impl IntoResponse {
447///     // Use state.db
448/// }
449/// ```
450#[derive(Debug, Clone)]
451pub struct State<T>(pub T);
452
453impl<T: Clone + Send + Sync + 'static> FromRequestParts for State<T> {
454    fn from_request_parts(req: &Request) -> Result<Self> {
455        req.state().get::<T>().cloned().map(State).ok_or_else(|| {
456            ApiError::internal(format!(
457                "State of type `{}` not found. Did you forget to call .state()?",
458                std::any::type_name::<T>()
459            ))
460        })
461    }
462}
463
464impl<T> Deref for State<T> {
465    type Target = T;
466
467    fn deref(&self) -> &Self::Target {
468        &self.0
469    }
470}
471
472/// Raw body bytes extractor
473#[derive(Debug, Clone)]
474pub struct Body(pub Bytes);
475
476impl FromRequest for Body {
477    async fn from_request(req: &mut Request) -> Result<Self> {
478        req.load_body().await?;
479        let body = req
480            .take_body()
481            .ok_or_else(|| ApiError::internal("Body already consumed"))?;
482        Ok(Body(body))
483    }
484}
485
486impl Deref for Body {
487    type Target = Bytes;
488
489    fn deref(&self) -> &Self::Target {
490        &self.0
491    }
492}
493
494/// Streaming body extractor
495pub struct BodyStream(pub StreamingBody);
496
497impl FromRequest for BodyStream {
498    async fn from_request(req: &mut Request) -> Result<Self> {
499        let config = StreamingConfig::default();
500
501        if let Some(stream) = req.take_stream() {
502            Ok(BodyStream(StreamingBody::new(stream, config.max_body_size)))
503        } else if let Some(bytes) = req.take_body() {
504            // Handle buffered body as stream
505            let stream = futures_util::stream::once(async move { Ok(bytes) });
506            Ok(BodyStream(StreamingBody::from_stream(
507                stream,
508                config.max_body_size,
509            )))
510        } else {
511            Err(ApiError::internal("Body already consumed"))
512        }
513    }
514}
515
516impl Deref for BodyStream {
517    type Target = StreamingBody;
518
519    fn deref(&self) -> &Self::Target {
520        &self.0
521    }
522}
523
524impl DerefMut for BodyStream {
525    fn deref_mut(&mut self) -> &mut Self::Target {
526        &mut self.0
527    }
528}
529
530// Forward stream implementation
531impl futures_util::Stream for BodyStream {
532    type Item = Result<Bytes, ApiError>;
533
534    fn poll_next(
535        mut self: std::pin::Pin<&mut Self>,
536        cx: &mut std::task::Context<'_>,
537    ) -> std::task::Poll<Option<Self::Item>> {
538        std::pin::Pin::new(&mut self.0).poll_next(cx)
539    }
540}
541
542/// Optional extractor wrapper
543///
544/// Makes any extractor optional - returns None instead of error on failure.
545impl<T: FromRequestParts> FromRequestParts for Option<T> {
546    fn from_request_parts(req: &Request) -> Result<Self> {
547        Ok(T::from_request_parts(req).ok())
548    }
549}
550
551/// Headers extractor
552///
553/// Provides access to all request headers as a typed map.
554///
555/// # Example
556///
557/// ```rust,ignore
558/// use rustapi_core::extract::Headers;
559///
560/// async fn handler(headers: Headers) -> impl IntoResponse {
561///     if let Some(content_type) = headers.get("content-type") {
562///         format!("Content-Type: {:?}", content_type)
563///     } else {
564///         "No Content-Type header".to_string()
565///     }
566/// }
567/// ```
568#[derive(Debug, Clone)]
569pub struct Headers(pub http::HeaderMap);
570
571impl Headers {
572    /// Get a header value by name
573    pub fn get(&self, name: &str) -> Option<&http::HeaderValue> {
574        self.0.get(name)
575    }
576
577    /// Check if a header exists
578    pub fn contains(&self, name: &str) -> bool {
579        self.0.contains_key(name)
580    }
581
582    /// Get the number of headers
583    pub fn len(&self) -> usize {
584        self.0.len()
585    }
586
587    /// Check if headers are empty
588    pub fn is_empty(&self) -> bool {
589        self.0.is_empty()
590    }
591
592    /// Iterate over all headers
593    pub fn iter(&self) -> http::header::Iter<'_, http::HeaderValue> {
594        self.0.iter()
595    }
596}
597
598impl FromRequestParts for Headers {
599    fn from_request_parts(req: &Request) -> Result<Self> {
600        Ok(Headers(req.headers().clone()))
601    }
602}
603
604impl Deref for Headers {
605    type Target = http::HeaderMap;
606
607    fn deref(&self) -> &Self::Target {
608        &self.0
609    }
610}
611
612/// Single header value extractor
613///
614/// Extracts a specific header value by name. Returns an error if the header is missing.
615///
616/// # Example
617///
618/// ```rust,ignore
619/// use rustapi_core::extract::HeaderValue;
620///
621/// async fn handler(
622///     auth: HeaderValue<{ "authorization" }>,
623/// ) -> impl IntoResponse {
624///     format!("Auth header: {}", auth.0)
625/// }
626/// ```
627///
628/// Note: Due to Rust's const generics limitations, you may need to use the
629/// `HeaderValueOf` type alias or extract headers manually using the `Headers` extractor.
630#[derive(Debug, Clone)]
631pub struct HeaderValue(pub String, pub &'static str);
632
633impl HeaderValue {
634    /// Create a new HeaderValue extractor for a specific header name
635    pub fn new(name: &'static str, value: String) -> Self {
636        Self(value, name)
637    }
638
639    /// Get the header value
640    pub fn value(&self) -> &str {
641        &self.0
642    }
643
644    /// Get the header name
645    pub fn name(&self) -> &'static str {
646        self.1
647    }
648
649    /// Extract a specific header from a request
650    pub fn extract(req: &Request, name: &'static str) -> Result<Self> {
651        req.headers()
652            .get(name)
653            .and_then(|v| v.to_str().ok())
654            .map(|s| HeaderValue(s.to_string(), name))
655            .ok_or_else(|| ApiError::bad_request(format!("Missing required header: {}", name)))
656    }
657}
658
659impl Deref for HeaderValue {
660    type Target = String;
661
662    fn deref(&self) -> &Self::Target {
663        &self.0
664    }
665}
666
667/// Extension extractor
668///
669/// Retrieves typed data from request extensions that was inserted by middleware.
670///
671/// # Example
672///
673/// ```rust,ignore
674/// use rustapi_core::extract::Extension;
675///
676/// // Middleware inserts user data
677/// #[derive(Clone)]
678/// struct CurrentUser { id: i64 }
679///
680/// async fn handler(Extension(user): Extension<CurrentUser>) -> impl IntoResponse {
681///     format!("User ID: {}", user.id)
682/// }
683/// ```
684#[derive(Debug, Clone)]
685pub struct Extension<T>(pub T);
686
687impl<T: Clone + Send + Sync + 'static> FromRequestParts for Extension<T> {
688    fn from_request_parts(req: &Request) -> Result<Self> {
689        req.extensions()
690            .get::<T>()
691            .cloned()
692            .map(Extension)
693            .ok_or_else(|| {
694                ApiError::internal(format!(
695                    "Extension of type `{}` not found. Did middleware insert it?",
696                    std::any::type_name::<T>()
697                ))
698            })
699    }
700}
701
702impl<T> Deref for Extension<T> {
703    type Target = T;
704
705    fn deref(&self) -> &Self::Target {
706        &self.0
707    }
708}
709
710impl<T> DerefMut for Extension<T> {
711    fn deref_mut(&mut self) -> &mut Self::Target {
712        &mut self.0
713    }
714}
715
716/// Client IP address extractor
717///
718/// Extracts the client IP address from the request. When `trust_proxy` is enabled,
719/// it will use the `X-Forwarded-For` header if present.
720///
721/// # Example
722///
723/// ```rust,ignore
724/// use rustapi_core::extract::ClientIp;
725///
726/// async fn handler(ClientIp(ip): ClientIp) -> impl IntoResponse {
727///     format!("Your IP: {}", ip)
728/// }
729/// ```
730#[derive(Debug, Clone)]
731pub struct ClientIp(pub std::net::IpAddr);
732
733impl ClientIp {
734    /// Extract client IP, optionally trusting X-Forwarded-For header
735    pub fn extract_with_config(req: &Request, trust_proxy: bool) -> Result<Self> {
736        if trust_proxy {
737            // Try X-Forwarded-For header first
738            if let Some(forwarded) = req.headers().get("x-forwarded-for") {
739                if let Ok(forwarded_str) = forwarded.to_str() {
740                    // X-Forwarded-For can contain multiple IPs, take the first one
741                    if let Some(first_ip) = forwarded_str.split(',').next() {
742                        if let Ok(ip) = first_ip.trim().parse() {
743                            return Ok(ClientIp(ip));
744                        }
745                    }
746                }
747            }
748        }
749
750        // Fall back to socket address from extensions (if set by server)
751        if let Some(addr) = req.extensions().get::<std::net::SocketAddr>() {
752            return Ok(ClientIp(addr.ip()));
753        }
754
755        // Default to localhost if no IP information available
756        Ok(ClientIp(std::net::IpAddr::V4(std::net::Ipv4Addr::new(
757            127, 0, 0, 1,
758        ))))
759    }
760}
761
762impl FromRequestParts for ClientIp {
763    fn from_request_parts(req: &Request) -> Result<Self> {
764        // By default, trust proxy headers
765        Self::extract_with_config(req, true)
766    }
767}
768
769/// Cookies extractor
770///
771/// Parses and provides access to request cookies from the Cookie header.
772///
773/// # Example
774///
775/// ```rust,ignore
776/// use rustapi_core::extract::Cookies;
777///
778/// async fn handler(cookies: Cookies) -> impl IntoResponse {
779///     if let Some(session) = cookies.get("session_id") {
780///         format!("Session: {}", session.value())
781///     } else {
782///         "No session cookie".to_string()
783///     }
784/// }
785/// ```
786#[cfg(feature = "cookies")]
787#[derive(Debug, Clone)]
788pub struct Cookies(pub cookie::CookieJar);
789
790#[cfg(feature = "cookies")]
791impl Cookies {
792    /// Get a cookie by name
793    pub fn get(&self, name: &str) -> Option<&cookie::Cookie<'static>> {
794        self.0.get(name)
795    }
796
797    /// Iterate over all cookies
798    pub fn iter(&self) -> impl Iterator<Item = &cookie::Cookie<'static>> {
799        self.0.iter()
800    }
801
802    /// Check if a cookie exists
803    pub fn contains(&self, name: &str) -> bool {
804        self.0.get(name).is_some()
805    }
806}
807
808#[cfg(feature = "cookies")]
809impl FromRequestParts for Cookies {
810    fn from_request_parts(req: &Request) -> Result<Self> {
811        let mut jar = cookie::CookieJar::new();
812
813        if let Some(cookie_header) = req.headers().get(header::COOKIE) {
814            if let Ok(cookie_str) = cookie_header.to_str() {
815                // Parse each cookie from the header
816                for cookie_part in cookie_str.split(';') {
817                    let trimmed = cookie_part.trim();
818                    if !trimmed.is_empty() {
819                        if let Ok(cookie) = cookie::Cookie::parse(trimmed.to_string()) {
820                            jar.add_original(cookie.into_owned());
821                        }
822                    }
823                }
824            }
825        }
826
827        Ok(Cookies(jar))
828    }
829}
830
831#[cfg(feature = "cookies")]
832impl Deref for Cookies {
833    type Target = cookie::CookieJar;
834
835    fn deref(&self) -> &Self::Target {
836        &self.0
837    }
838}
839
840// Implement FromRequestParts for common primitive types (path params)
841macro_rules! impl_from_request_parts_for_primitives {
842    ($($ty:ty),*) => {
843        $(
844            impl FromRequestParts for $ty {
845                fn from_request_parts(req: &Request) -> Result<Self> {
846                    let Path(value) = Path::<$ty>::from_request_parts(req)?;
847                    Ok(value)
848                }
849            }
850        )*
851    };
852}
853
854impl_from_request_parts_for_primitives!(
855    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
856);
857
858// OperationModifier implementations for extractors
859
860use rustapi_openapi::utoipa_types::openapi;
861use rustapi_openapi::{
862    IntoParams, MediaType, Operation, OperationModifier, Parameter, RequestBody, ResponseModifier,
863    ResponseSpec, Schema, SchemaRef,
864};
865use std::collections::HashMap;
866
867// ValidatedJson - Adds request body
868impl<T: for<'a> Schema<'a>> OperationModifier for ValidatedJson<T> {
869    fn update_operation(op: &mut Operation) {
870        let (name, _) = T::schema();
871
872        let schema_ref = SchemaRef::Ref {
873            reference: format!("#/components/schemas/{}", name),
874        };
875
876        let mut content = HashMap::new();
877        content.insert(
878            "application/json".to_string(),
879            MediaType { schema: schema_ref },
880        );
881
882        op.request_body = Some(RequestBody {
883            required: true,
884            content,
885        });
886
887        // Add 422 Validation Error response
888        op.responses.insert(
889            "422".to_string(),
890            ResponseSpec {
891                description: "Validation Error".to_string(),
892                content: {
893                    let mut map = HashMap::new();
894                    map.insert(
895                        "application/json".to_string(),
896                        MediaType {
897                            schema: SchemaRef::Ref {
898                                reference: "#/components/schemas/ValidationErrorSchema".to_string(),
899                            },
900                        },
901                    );
902                    Some(map)
903                },
904            },
905        );
906    }
907}
908
909// Json - Adds request body (Same as ValidatedJson)
910impl<T: for<'a> Schema<'a>> OperationModifier for Json<T> {
911    fn update_operation(op: &mut Operation) {
912        let (name, _) = T::schema();
913
914        let schema_ref = SchemaRef::Ref {
915            reference: format!("#/components/schemas/{}", name),
916        };
917
918        let mut content = HashMap::new();
919        content.insert(
920            "application/json".to_string(),
921            MediaType { schema: schema_ref },
922        );
923
924        op.request_body = Some(RequestBody {
925            required: true,
926            content,
927        });
928    }
929}
930
931// Path - Path parameters are automatically extracted from route patterns
932// The add_path_params_to_operation function in app.rs handles OpenAPI documentation
933// based on the {param} syntax in route paths (e.g., "/users/{id}")
934impl<T> OperationModifier for Path<T> {
935    fn update_operation(_op: &mut Operation) {
936        // Path parameters are automatically documented by add_path_params_to_operation
937        // in app.rs based on the route pattern. No additional implementation needed here.
938        //
939        // For typed path params, the schema type defaults to "string" but will be
940        // inferred from the actual type T when more sophisticated type introspection
941        // is implemented.
942    }
943}
944
945// Typed - Same as Path, parameters are documented by route pattern
946impl<T> OperationModifier for Typed<T> {
947    fn update_operation(_op: &mut Operation) {
948        // No-op, managed by route registration
949    }
950}
951
952// Query - Extracts query params using IntoParams
953impl<T: IntoParams> OperationModifier for Query<T> {
954    fn update_operation(op: &mut Operation) {
955        let params = T::into_params(|| Some(openapi::path::ParameterIn::Query));
956
957        let new_params: Vec<Parameter> = params
958            .into_iter()
959            .map(|p| {
960                let schema = match p.schema {
961                    Some(schema) => match schema {
962                        openapi::RefOr::Ref(r) => SchemaRef::Ref {
963                            reference: r.ref_location,
964                        },
965                        openapi::RefOr::T(s) => {
966                            let value = serde_json::to_value(s).unwrap_or(serde_json::Value::Null);
967                            SchemaRef::Inline(value)
968                        }
969                    },
970                    None => SchemaRef::Inline(serde_json::Value::Null),
971                };
972
973                let required = match p.required {
974                    openapi::Required::True => true,
975                    openapi::Required::False => false,
976                };
977
978                Parameter {
979                    name: p.name,
980                    location: "query".to_string(), // explicitly query
981                    required,
982                    description: p.description,
983                    schema,
984                }
985            })
986            .collect();
987
988        if let Some(existing) = &mut op.parameters {
989            existing.extend(new_params);
990        } else {
991            op.parameters = Some(new_params);
992        }
993    }
994}
995
996// State - No op
997impl<T> OperationModifier for State<T> {
998    fn update_operation(_op: &mut Operation) {}
999}
1000
1001// Body - Generic binary body
1002impl OperationModifier for Body {
1003    fn update_operation(op: &mut Operation) {
1004        let mut content = HashMap::new();
1005        content.insert(
1006            "application/octet-stream".to_string(),
1007            MediaType {
1008                schema: SchemaRef::Inline(
1009                    serde_json::json!({ "type": "string", "format": "binary" }),
1010                ),
1011            },
1012        );
1013
1014        op.request_body = Some(RequestBody {
1015            required: true,
1016            content,
1017        });
1018    }
1019}
1020
1021// BodyStream - Generic binary stream (Same as Body)
1022impl OperationModifier for BodyStream {
1023    fn update_operation(op: &mut Operation) {
1024        let mut content = HashMap::new();
1025        content.insert(
1026            "application/octet-stream".to_string(),
1027            MediaType {
1028                schema: SchemaRef::Inline(
1029                    serde_json::json!({ "type": "string", "format": "binary" }),
1030                ),
1031            },
1032        );
1033
1034        op.request_body = Some(RequestBody {
1035            required: true,
1036            content,
1037        });
1038    }
1039}
1040
1041// ResponseModifier implementations for extractors
1042
1043// Json<T> - 200 OK with schema T
1044impl<T: for<'a> Schema<'a>> ResponseModifier for Json<T> {
1045    fn update_response(op: &mut Operation) {
1046        let (name, _) = T::schema();
1047
1048        let schema_ref = SchemaRef::Ref {
1049            reference: format!("#/components/schemas/{}", name),
1050        };
1051
1052        op.responses.insert(
1053            "200".to_string(),
1054            ResponseSpec {
1055                description: "Successful response".to_string(),
1056                content: {
1057                    let mut map = HashMap::new();
1058                    map.insert(
1059                        "application/json".to_string(),
1060                        MediaType { schema: schema_ref },
1061                    );
1062                    Some(map)
1063                },
1064            },
1065        );
1066    }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071    use super::*;
1072    use crate::path_params::PathParams;
1073    use bytes::Bytes;
1074    use http::{Extensions, Method};
1075    use proptest::prelude::*;
1076    use proptest::test_runner::TestCaseError;
1077    use std::sync::Arc;
1078
1079    /// Create a test request with the given method, path, and headers
1080    fn create_test_request_with_headers(
1081        method: Method,
1082        path: &str,
1083        headers: Vec<(&str, &str)>,
1084    ) -> Request {
1085        let uri: http::Uri = path.parse().unwrap();
1086        let mut builder = http::Request::builder().method(method).uri(uri);
1087
1088        for (name, value) in headers {
1089            builder = builder.header(name, value);
1090        }
1091
1092        let req = builder.body(()).unwrap();
1093        let (parts, _) = req.into_parts();
1094
1095        Request::new(
1096            parts,
1097            crate::request::BodyVariant::Buffered(Bytes::new()),
1098            Arc::new(Extensions::new()),
1099            PathParams::new(),
1100        )
1101    }
1102
1103    /// Create a test request with extensions
1104    fn create_test_request_with_extensions<T: Clone + Send + Sync + 'static>(
1105        method: Method,
1106        path: &str,
1107        extension: T,
1108    ) -> Request {
1109        let uri: http::Uri = path.parse().unwrap();
1110        let builder = http::Request::builder().method(method).uri(uri);
1111
1112        let req = builder.body(()).unwrap();
1113        let (mut parts, _) = req.into_parts();
1114        parts.extensions.insert(extension);
1115
1116        Request::new(
1117            parts,
1118            crate::request::BodyVariant::Buffered(Bytes::new()),
1119            Arc::new(Extensions::new()),
1120            PathParams::new(),
1121        )
1122    }
1123
1124    // **Feature: phase3-batteries-included, Property 14: Headers extractor completeness**
1125    //
1126    // For any request with headers H, the `Headers` extractor SHALL return a map
1127    // containing all key-value pairs in H.
1128    //
1129    // **Validates: Requirements 5.1**
1130    proptest! {
1131        #![proptest_config(ProptestConfig::with_cases(100))]
1132
1133        #[test]
1134        fn prop_headers_extractor_completeness(
1135            // Generate random header names and values
1136            // Using alphanumeric strings to ensure valid header names/values
1137            headers in prop::collection::vec(
1138                (
1139                    "[a-z][a-z0-9-]{0,20}",  // Valid header name pattern
1140                    "[a-zA-Z0-9 ]{1,50}"     // Valid header value pattern
1141                ),
1142                0..10
1143            )
1144        ) {
1145            let result: Result<(), TestCaseError> = (|| {
1146                // Convert to header tuples
1147                let header_tuples: Vec<(&str, &str)> = headers
1148                    .iter()
1149                    .map(|(k, v)| (k.as_str(), v.as_str()))
1150                    .collect();
1151
1152                // Create request with headers
1153                let request = create_test_request_with_headers(
1154                    Method::GET,
1155                    "/test",
1156                    header_tuples.clone(),
1157                );
1158
1159                // Extract headers
1160                let extracted = Headers::from_request_parts(&request)
1161                    .map_err(|e| TestCaseError::fail(format!("Failed to extract headers: {}", e)))?;
1162
1163                // Verify all original headers are present
1164                // HTTP allows duplicate headers - get_all() returns all values for a header name
1165                for (name, value) in &headers {
1166                    // Check that the header name exists
1167                    let all_values: Vec<_> = extracted.get_all(name.as_str()).iter().collect();
1168                    prop_assert!(
1169                        !all_values.is_empty(),
1170                        "Header '{}' not found",
1171                        name
1172                    );
1173
1174                    // Check that the value is among the extracted values
1175                    let value_found = all_values.iter().any(|v| {
1176                        v.to_str().map(|s| s == value.as_str()).unwrap_or(false)
1177                    });
1178
1179                    prop_assert!(
1180                        value_found,
1181                        "Header '{}' value '{}' not found in extracted values",
1182                        name,
1183                        value
1184                    );
1185                }
1186
1187                Ok(())
1188            })();
1189            result?;
1190        }
1191    }
1192
1193    // **Feature: phase3-batteries-included, Property 15: HeaderValue extractor correctness**
1194    //
1195    // For any request with header "X" having value V, `HeaderValue::extract(req, "X")` SHALL return V;
1196    // for requests without header "X", it SHALL return an error.
1197    //
1198    // **Validates: Requirements 5.2**
1199    proptest! {
1200        #![proptest_config(ProptestConfig::with_cases(100))]
1201
1202        #[test]
1203        fn prop_header_value_extractor_correctness(
1204            header_name in "[a-z][a-z0-9-]{0,20}",
1205            header_value in "[a-zA-Z0-9 ]{1,50}",
1206            has_header in prop::bool::ANY,
1207        ) {
1208            let result: Result<(), TestCaseError> = (|| {
1209                let headers = if has_header {
1210                    vec![(header_name.as_str(), header_value.as_str())]
1211                } else {
1212                    vec![]
1213                };
1214
1215                let _request = create_test_request_with_headers(Method::GET, "/test", headers);
1216
1217                // We need to use a static string for the header name in the extractor
1218                // So we'll test with a known header name
1219                let test_header = "x-test-header";
1220                let request_with_known_header = if has_header {
1221                    create_test_request_with_headers(
1222                        Method::GET,
1223                        "/test",
1224                        vec![(test_header, header_value.as_str())],
1225                    )
1226                } else {
1227                    create_test_request_with_headers(Method::GET, "/test", vec![])
1228                };
1229
1230                let result = HeaderValue::extract(&request_with_known_header, test_header);
1231
1232                if has_header {
1233                    let extracted = result
1234                        .map_err(|e| TestCaseError::fail(format!("Expected header to be found: {}", e)))?;
1235                    prop_assert_eq!(
1236                        extracted.value(),
1237                        header_value.as_str(),
1238                        "Header value mismatch"
1239                    );
1240                } else {
1241                    prop_assert!(
1242                        result.is_err(),
1243                        "Expected error when header is missing"
1244                    );
1245                }
1246
1247                Ok(())
1248            })();
1249            result?;
1250        }
1251    }
1252
1253    // **Feature: phase3-batteries-included, Property 17: ClientIp extractor with forwarding**
1254    //
1255    // For any request with socket IP S and X-Forwarded-For header F, when forwarding is enabled,
1256    // `ClientIp` SHALL return the first IP in F; when disabled, it SHALL return S.
1257    //
1258    // **Validates: Requirements 5.4**
1259    proptest! {
1260        #![proptest_config(ProptestConfig::with_cases(100))]
1261
1262        #[test]
1263        fn prop_client_ip_extractor_with_forwarding(
1264            // Generate valid IPv4 addresses
1265            forwarded_ip in (0u8..=255, 0u8..=255, 0u8..=255, 0u8..=255)
1266                .prop_map(|(a, b, c, d)| format!("{}.{}.{}.{}", a, b, c, d)),
1267            socket_ip in (0u8..=255, 0u8..=255, 0u8..=255, 0u8..=255)
1268                .prop_map(|(a, b, c, d)| std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d))),
1269            has_forwarded_header in prop::bool::ANY,
1270            trust_proxy in prop::bool::ANY,
1271        ) {
1272            let result: Result<(), TestCaseError> = (|| {
1273                let headers = if has_forwarded_header {
1274                    vec![("x-forwarded-for", forwarded_ip.as_str())]
1275                } else {
1276                    vec![]
1277                };
1278
1279                // Create request with headers
1280                let uri: http::Uri = "/test".parse().unwrap();
1281                let mut builder = http::Request::builder().method(Method::GET).uri(uri);
1282                for (name, value) in &headers {
1283                    builder = builder.header(*name, *value);
1284                }
1285                let req = builder.body(()).unwrap();
1286                let (mut parts, _) = req.into_parts();
1287
1288                // Add socket address to extensions
1289                let socket_addr = std::net::SocketAddr::new(socket_ip, 8080);
1290                parts.extensions.insert(socket_addr);
1291
1292                let request = Request::new(
1293                    parts,
1294                    crate::request::BodyVariant::Buffered(Bytes::new()),
1295                    Arc::new(Extensions::new()),
1296                    PathParams::new(),
1297                );
1298
1299                let extracted = ClientIp::extract_with_config(&request, trust_proxy)
1300                    .map_err(|e| TestCaseError::fail(format!("Failed to extract ClientIp: {}", e)))?;
1301
1302                if trust_proxy && has_forwarded_header {
1303                    // Should use X-Forwarded-For
1304                    let expected_ip: std::net::IpAddr = forwarded_ip.parse()
1305                        .map_err(|e| TestCaseError::fail(format!("Invalid IP: {}", e)))?;
1306                    prop_assert_eq!(
1307                        extracted.0,
1308                        expected_ip,
1309                        "Should use X-Forwarded-For IP when trust_proxy is enabled"
1310                    );
1311                } else {
1312                    // Should use socket IP
1313                    prop_assert_eq!(
1314                        extracted.0,
1315                        socket_ip,
1316                        "Should use socket IP when trust_proxy is disabled or no X-Forwarded-For"
1317                    );
1318                }
1319
1320                Ok(())
1321            })();
1322            result?;
1323        }
1324    }
1325
1326    // **Feature: phase3-batteries-included, Property 18: Extension extractor retrieval**
1327    //
1328    // For any type T and value V inserted into request extensions by middleware,
1329    // `Extension<T>` SHALL return V.
1330    //
1331    // **Validates: Requirements 5.5**
1332    proptest! {
1333        #![proptest_config(ProptestConfig::with_cases(100))]
1334
1335        #[test]
1336        fn prop_extension_extractor_retrieval(
1337            value in any::<i64>(),
1338            has_extension in prop::bool::ANY,
1339        ) {
1340            let result: Result<(), TestCaseError> = (|| {
1341                // Create a simple wrapper type for testing
1342                #[derive(Clone, Debug, PartialEq)]
1343                struct TestExtension(i64);
1344
1345                let uri: http::Uri = "/test".parse().unwrap();
1346                let builder = http::Request::builder().method(Method::GET).uri(uri);
1347                let req = builder.body(()).unwrap();
1348                let (mut parts, _) = req.into_parts();
1349
1350                if has_extension {
1351                    parts.extensions.insert(TestExtension(value));
1352                }
1353
1354                let request = Request::new(
1355                    parts,
1356                    crate::request::BodyVariant::Buffered(Bytes::new()),
1357                    Arc::new(Extensions::new()),
1358                    PathParams::new(),
1359                );
1360
1361                let result = Extension::<TestExtension>::from_request_parts(&request);
1362
1363                if has_extension {
1364                    let extracted = result
1365                        .map_err(|e| TestCaseError::fail(format!("Expected extension to be found: {}", e)))?;
1366                    prop_assert_eq!(
1367                        extracted.0,
1368                        TestExtension(value),
1369                        "Extension value mismatch"
1370                    );
1371                } else {
1372                    prop_assert!(
1373                        result.is_err(),
1374                        "Expected error when extension is missing"
1375                    );
1376                }
1377
1378                Ok(())
1379            })();
1380            result?;
1381        }
1382    }
1383
1384    // Unit tests for basic functionality
1385
1386    #[test]
1387    fn test_headers_extractor_basic() {
1388        let request = create_test_request_with_headers(
1389            Method::GET,
1390            "/test",
1391            vec![
1392                ("content-type", "application/json"),
1393                ("accept", "text/html"),
1394            ],
1395        );
1396
1397        let headers = Headers::from_request_parts(&request).unwrap();
1398
1399        assert!(headers.contains("content-type"));
1400        assert!(headers.contains("accept"));
1401        assert!(!headers.contains("x-custom"));
1402        assert_eq!(headers.len(), 2);
1403    }
1404
1405    #[test]
1406    fn test_header_value_extractor_present() {
1407        let request = create_test_request_with_headers(
1408            Method::GET,
1409            "/test",
1410            vec![("authorization", "Bearer token123")],
1411        );
1412
1413        let result = HeaderValue::extract(&request, "authorization");
1414        assert!(result.is_ok());
1415        assert_eq!(result.unwrap().value(), "Bearer token123");
1416    }
1417
1418    #[test]
1419    fn test_header_value_extractor_missing() {
1420        let request = create_test_request_with_headers(Method::GET, "/test", vec![]);
1421
1422        let result = HeaderValue::extract(&request, "authorization");
1423        assert!(result.is_err());
1424    }
1425
1426    #[test]
1427    fn test_client_ip_from_forwarded_header() {
1428        let request = create_test_request_with_headers(
1429            Method::GET,
1430            "/test",
1431            vec![("x-forwarded-for", "192.168.1.100, 10.0.0.1")],
1432        );
1433
1434        let ip = ClientIp::extract_with_config(&request, true).unwrap();
1435        assert_eq!(ip.0, "192.168.1.100".parse::<std::net::IpAddr>().unwrap());
1436    }
1437
1438    #[test]
1439    fn test_client_ip_ignores_forwarded_when_not_trusted() {
1440        let uri: http::Uri = "/test".parse().unwrap();
1441        let builder = http::Request::builder()
1442            .method(Method::GET)
1443            .uri(uri)
1444            .header("x-forwarded-for", "192.168.1.100");
1445        let req = builder.body(()).unwrap();
1446        let (mut parts, _) = req.into_parts();
1447
1448        let socket_addr = std::net::SocketAddr::new(
1449            std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 0, 1)),
1450            8080,
1451        );
1452        parts.extensions.insert(socket_addr);
1453
1454        let request = Request::new(
1455            parts,
1456            crate::request::BodyVariant::Buffered(Bytes::new()),
1457            Arc::new(Extensions::new()),
1458            PathParams::new(),
1459        );
1460
1461        let ip = ClientIp::extract_with_config(&request, false).unwrap();
1462        assert_eq!(ip.0, "10.0.0.1".parse::<std::net::IpAddr>().unwrap());
1463    }
1464
1465    #[test]
1466    fn test_extension_extractor_present() {
1467        #[derive(Clone, Debug, PartialEq)]
1468        struct MyData(String);
1469
1470        let request =
1471            create_test_request_with_extensions(Method::GET, "/test", MyData("hello".to_string()));
1472
1473        let result = Extension::<MyData>::from_request_parts(&request);
1474        assert!(result.is_ok());
1475        assert_eq!(result.unwrap().0, MyData("hello".to_string()));
1476    }
1477
1478    #[test]
1479    fn test_extension_extractor_missing() {
1480        #[derive(Clone, Debug)]
1481        #[allow(dead_code)]
1482        struct MyData(String);
1483
1484        let request = create_test_request_with_headers(Method::GET, "/test", vec![]);
1485
1486        let result = Extension::<MyData>::from_request_parts(&request);
1487        assert!(result.is_err());
1488    }
1489
1490    // Cookies tests (feature-gated)
1491    #[cfg(feature = "cookies")]
1492    mod cookies_tests {
1493        use super::*;
1494
1495        // **Feature: phase3-batteries-included, Property 16: Cookies extractor parsing**
1496        //
1497        // For any request with Cookie header containing cookies C, the `Cookies` extractor
1498        // SHALL return a CookieJar containing exactly the cookies in C.
1499        // Note: Duplicate cookie names result in only the last value being kept.
1500        //
1501        // **Validates: Requirements 5.3**
1502        proptest! {
1503            #![proptest_config(ProptestConfig::with_cases(100))]
1504
1505            #[test]
1506            fn prop_cookies_extractor_parsing(
1507                // Generate random cookie names and values
1508                // Using alphanumeric strings to ensure valid cookie names/values
1509                cookies in prop::collection::vec(
1510                    (
1511                        "[a-zA-Z][a-zA-Z0-9_]{0,15}",  // Valid cookie name pattern
1512                        "[a-zA-Z0-9]{1,30}"            // Valid cookie value pattern (no special chars)
1513                    ),
1514                    0..5
1515                )
1516            ) {
1517                let result: Result<(), TestCaseError> = (|| {
1518                    // Build cookie header string
1519                    let cookie_header = cookies
1520                        .iter()
1521                        .map(|(name, value)| format!("{}={}", name, value))
1522                        .collect::<Vec<_>>()
1523                        .join("; ");
1524
1525                    let headers = if !cookies.is_empty() {
1526                        vec![("cookie", cookie_header.as_str())]
1527                    } else {
1528                        vec![]
1529                    };
1530
1531                    let request = create_test_request_with_headers(Method::GET, "/test", headers);
1532
1533                    // Extract cookies
1534                    let extracted = Cookies::from_request_parts(&request)
1535                        .map_err(|e| TestCaseError::fail(format!("Failed to extract cookies: {}", e)))?;
1536
1537                    // Build expected cookies map - last value wins for duplicate names
1538                    let mut expected_cookies: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
1539                    for (name, value) in &cookies {
1540                        expected_cookies.insert(name.as_str(), value.as_str());
1541                    }
1542
1543                    // Verify all expected cookies are present with correct values
1544                    for (name, expected_value) in &expected_cookies {
1545                        let cookie = extracted.get(name)
1546                            .ok_or_else(|| TestCaseError::fail(format!("Cookie '{}' not found", name)))?;
1547
1548                        prop_assert_eq!(
1549                            cookie.value(),
1550                            *expected_value,
1551                            "Cookie '{}' value mismatch",
1552                            name
1553                        );
1554                    }
1555
1556                    // Count cookies in jar should match unique cookie names
1557                    let extracted_count = extracted.iter().count();
1558                    prop_assert_eq!(
1559                        extracted_count,
1560                        expected_cookies.len(),
1561                        "Expected {} unique cookies, got {}",
1562                        expected_cookies.len(),
1563                        extracted_count
1564                    );
1565
1566                    Ok(())
1567                })();
1568                result?;
1569            }
1570        }
1571
1572        #[test]
1573        fn test_cookies_extractor_basic() {
1574            let request = create_test_request_with_headers(
1575                Method::GET,
1576                "/test",
1577                vec![("cookie", "session=abc123; user=john")],
1578            );
1579
1580            let cookies = Cookies::from_request_parts(&request).unwrap();
1581
1582            assert!(cookies.contains("session"));
1583            assert!(cookies.contains("user"));
1584            assert!(!cookies.contains("other"));
1585
1586            assert_eq!(cookies.get("session").unwrap().value(), "abc123");
1587            assert_eq!(cookies.get("user").unwrap().value(), "john");
1588        }
1589
1590        #[test]
1591        fn test_cookies_extractor_empty() {
1592            let request = create_test_request_with_headers(Method::GET, "/test", vec![]);
1593
1594            let cookies = Cookies::from_request_parts(&request).unwrap();
1595            assert_eq!(cookies.iter().count(), 0);
1596        }
1597
1598        #[test]
1599        fn test_cookies_extractor_single() {
1600            let request = create_test_request_with_headers(
1601                Method::GET,
1602                "/test",
1603                vec![("cookie", "token=xyz789")],
1604            );
1605
1606            let cookies = Cookies::from_request_parts(&request).unwrap();
1607            assert_eq!(cookies.iter().count(), 1);
1608            assert_eq!(cookies.get("token").unwrap().value(), "xyz789");
1609        }
1610    }
1611}