Skip to main content

armature_core/
extractors.rs

1//! Request parameter extractors
2//!
3//! This module provides types for extracting data from HTTP requests
4//! in a type-safe manner, similar to NestJS decorators.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use armature::prelude::*;
10//! use armature_core::extractors::{Body, Query, Path, Header};
11//!
12//! #[derive(Deserialize)]
13//! struct CreateUser {
14//!     name: String,
15//!     email: String,
16//! }
17//!
18//! #[derive(Deserialize)]
19//! struct UserFilters {
20//!     page: Option<u32>,
21//!     limit: Option<u32>,
22//! }
23//!
24//! // Extract body as JSON
25//! let body: Body<CreateUser> = Body::from_request(&request)?;
26//!
27//! // Extract query parameters
28//! let query: Query<UserFilters> = Query::from_request(&request)?;
29//!
30//! // Extract path parameter
31//! let id: Path<u32> = Path::from_request(&request, "id")?;
32//!
33//! // Extract header
34//! let auth: Header = Header::from_request(&request, "Authorization")?;
35//! ```
36
37use crate::{Error, HttpRequest};
38use bytes::Bytes;
39use serde::de::DeserializeOwned;
40use std::ops::Deref;
41use std::sync::Arc;
42
43/// Trait for extracting data from an HTTP request
44pub trait FromRequest: Sized {
45    /// Extract data from the request
46    fn from_request(request: &HttpRequest) -> Result<Self, Error>;
47}
48
49// ========== State Extractor ==========
50
51/// Zero-cost application state extractor.
52///
53/// `State<T>` provides type-safe access to application state without
54/// runtime type checking overhead. State is stored in request extensions
55/// and retrieved via `TypeId` lookup followed by a direct pointer cast.
56///
57/// # Performance
58///
59/// Unlike DI container lookups which use `Any::downcast`, `State<T>` uses
60/// a pre-verified `TypeId` for O(1) retrieval with no runtime type checking.
61///
62/// # Example
63///
64/// ```rust,ignore
65/// use armature_core::extractors::State;
66/// use std::sync::Arc;
67///
68/// // Define your application state
69/// #[derive(Clone)]
70/// struct AppState {
71///     db_pool: Pool,
72///     config: AppConfig,
73/// }
74///
75/// // Insert state into the application (done once at startup)
76/// let state = Arc::new(AppState { db_pool, config });
77/// app.with_state(state);
78///
79/// // Extract in handler - zero-cost after setup
80/// #[get("/users")]
81/// async fn list_users(state: State<AppState>) -> Result<HttpResponse, Error> {
82///     let users = state.db_pool.query_param("SELECT * FROM users").await?;
83///     HttpResponse::json(&users)
84/// }
85/// ```
86///
87/// # Notes
88///
89/// - State must be `Send + Sync + 'static`
90/// - State should be wrapped in `Arc` for efficient cloning
91/// - Multiple state types can be registered
92#[derive(Debug)]
93pub struct State<T: Send + Sync + 'static>(pub Arc<T>);
94
95impl<T: Send + Sync + 'static> State<T> {
96    /// Create a new State wrapper.
97    #[inline]
98    pub fn new(value: Arc<T>) -> Self {
99        Self(value)
100    }
101
102    /// Get the inner Arc.
103    #[inline]
104    pub fn into_inner(self) -> Arc<T> {
105        self.0
106    }
107}
108
109impl<T: Send + Sync + 'static> Clone for State<T> {
110    #[inline]
111    fn clone(&self) -> Self {
112        Self(Arc::clone(&self.0))
113    }
114}
115
116impl<T: Send + Sync + 'static> Deref for State<T> {
117    type Target = T;
118
119    #[inline]
120    fn deref(&self) -> &Self::Target {
121        &self.0
122    }
123}
124
125impl<T: Send + Sync + 'static> AsRef<T> for State<T> {
126    #[inline]
127    fn as_ref(&self) -> &T {
128        &self.0
129    }
130}
131
132impl<T: Send + Sync + 'static> FromRequest for State<T> {
133    /// Extract state from request extensions.
134    ///
135    /// # Errors
136    ///
137    /// Returns `Error::ProviderNotFound` if state of type `T` was not
138    /// registered in the application.
139    #[inline]
140    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
141        request.extensions.get_arc::<T>().map(State).ok_or_else(|| {
142            Error::ProviderNotFound(format!(
143                "State<{}> not found in request extensions. \
144                     Did you forget to register it with `app.with_state()`?",
145                std::any::type_name::<T>()
146            ))
147        })
148    }
149}
150
151/// Trait for extracting named parameters from a request
152pub trait FromRequestNamed: Sized {
153    /// Extract a named parameter from the request
154    fn from_request(request: &HttpRequest, name: &str) -> Result<Self, Error>;
155}
156
157// ========== Body Extractor ==========
158
159/// Extracts and deserializes the request body as JSON
160///
161/// # Example
162///
163/// ```rust,ignore
164/// #[derive(Deserialize)]
165/// struct CreateUser {
166///     name: String,
167///     email: String,
168/// }
169///
170/// let body: Body<CreateUser> = Body::from_request(&request)?;
171/// println!("Creating user: {}", body.name);
172/// ```
173#[derive(Debug, Clone)]
174pub struct Body<T>(pub T);
175
176impl<T> Body<T> {
177    /// Create a new Body wrapper
178    pub fn new(value: T) -> Self {
179        Self(value)
180    }
181
182    /// Get the inner value
183    pub fn into_inner(self) -> T {
184        self.0
185    }
186}
187
188impl<T> Deref for Body<T> {
189    type Target = T;
190
191    fn deref(&self) -> &Self::Target {
192        &self.0
193    }
194}
195
196impl<T: DeserializeOwned> FromRequest for Body<T> {
197    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
198        let value: T = request.json()?;
199        Ok(Body(value))
200    }
201}
202
203// ========== Query Extractor ==========
204
205/// Extracts and deserializes query parameters
206///
207/// # Example
208///
209/// ```rust,ignore
210/// #[derive(Deserialize)]
211/// struct Pagination {
212///     page: Option<u32>,
213///     limit: Option<u32>,
214///     sort: Option<String>,
215/// }
216///
217/// let query: Query<Pagination> = Query::from_request(&request)?;
218/// let page = query.page.unwrap_or(1);
219/// ```
220#[derive(Debug, Clone)]
221pub struct Query<T>(pub T);
222
223impl<T> Query<T> {
224    /// Create a new Query wrapper
225    pub fn new(value: T) -> Self {
226        Self(value)
227    }
228
229    /// Get the inner value
230    pub fn into_inner(self) -> T {
231        self.0
232    }
233}
234
235impl<T> Deref for Query<T> {
236    type Target = T;
237
238    fn deref(&self) -> &Self::Target {
239        &self.0
240    }
241}
242
243impl<T: DeserializeOwned> FromRequest for Query<T> {
244    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
245        // Deserialize the raw query string. Going through the decoded pairs and
246        // re-joining them on `&`/`=` corrupted any value that itself contained
247        // one of those characters.
248        let value: T = serde_urlencoded::from_str(request.query_string().unwrap_or(""))
249            .map_err(|e| Error::Validation(format!("Invalid query parameters: {}", e)))?;
250
251        Ok(Query(value))
252    }
253}
254
255// ========== Path Extractor ==========
256
257/// Extracts a path parameter by name
258///
259/// # Example
260///
261/// ```rust,ignore
262/// // For route /users/:id
263/// let id: Path<u32> = Path::from_request(&request, "id")?;
264/// println!("User ID: {}", *id);
265/// ```
266#[derive(Debug, Clone)]
267pub struct Path<T>(pub T);
268
269impl<T> Path<T> {
270    /// Create a new Path wrapper
271    pub fn new(value: T) -> Self {
272        Self(value)
273    }
274
275    /// Get the inner value
276    pub fn into_inner(self) -> T {
277        self.0
278    }
279}
280
281impl<T> Deref for Path<T> {
282    type Target = T;
283
284    fn deref(&self) -> &Self::Target {
285        &self.0
286    }
287}
288
289impl<T: std::str::FromStr> FromRequestNamed for Path<T>
290where
291    T::Err: std::fmt::Display,
292{
293    fn from_request(request: &HttpRequest, name: &str) -> Result<Self, Error> {
294        let value_str = request
295            .param(name)
296            .ok_or_else(|| Error::Validation(format!("Missing path parameter: {}", name)))?;
297
298        let value: T = value_str.parse().map_err(|e: T::Err| {
299            Error::Validation(format!("Invalid path parameter '{}': {}", name, e))
300        })?;
301
302        Ok(Path(value))
303    }
304}
305
306// ========== PathParams Extractor ==========
307
308/// Extracts all path parameters into a struct
309///
310/// # Example
311///
312/// ```rust,ignore
313/// #[derive(Deserialize)]
314/// struct UserParams {
315///     user_id: u32,
316///     post_id: u32,
317/// }
318///
319/// // For route /users/:user_id/posts/:post_id
320/// let params: PathParams<UserParams> = PathParams::from_request(&request)?;
321/// ```
322#[derive(Debug, Clone)]
323pub struct PathParams<T>(pub T);
324
325impl<T> PathParams<T> {
326    /// Create a new PathParams wrapper
327    pub fn new(value: T) -> Self {
328        Self(value)
329    }
330
331    /// Get the inner value
332    pub fn into_inner(self) -> T {
333        self.0
334    }
335}
336
337impl<T> Deref for PathParams<T> {
338    type Target = T;
339
340    fn deref(&self) -> &Self::Target {
341        &self.0
342    }
343}
344
345impl<T: DeserializeOwned> FromRequest for PathParams<T> {
346    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
347        // Percent-encoded rather than joined by hand, so a captured segment
348        // containing `&`, `=` or `%` survives the round-trip.
349        let pairs: Vec<(&str, &str)> = request
350            .path_params
351            .iter()
352            .filter_map(|(k, v)| std::str::from_utf8(v).ok().map(|v| (*k, v)))
353            .collect();
354        let params_string = serde_urlencoded::to_string(&pairs)
355            .map_err(|e| Error::Validation(format!("Invalid path parameters: {}", e)))?;
356
357        let value: T = serde_urlencoded::from_str(&params_string)
358            .map_err(|e| Error::Validation(format!("Invalid path parameters: {}", e)))?;
359
360        Ok(PathParams(value))
361    }
362}
363
364// ========== Header Extractor ==========
365
366/// Extracts a header value by name
367///
368/// # Example
369///
370/// ```rust,ignore
371/// let auth: Header = Header::from_request(&request, "Authorization")?;
372/// println!("Auth: {}", auth.value());
373///
374/// // Or as optional
375/// let custom: Option<Header> = Header::optional(&request, "X-Custom-Header");
376/// ```
377#[derive(Debug, Clone)]
378pub struct Header {
379    name: String,
380    value: String,
381}
382
383impl Header {
384    /// Create a new Header
385    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
386        Self {
387            name: name.into(),
388            value: value.into(),
389        }
390    }
391
392    /// Get the header name
393    pub fn name(&self) -> &str {
394        &self.name
395    }
396
397    /// Get the header value
398    pub fn value(&self) -> &str {
399        &self.value
400    }
401
402    /// Get the header value, consuming self
403    pub fn into_value(self) -> String {
404        self.value
405    }
406
407    /// Extract a header, returning None if not present
408    pub fn optional(request: &HttpRequest, name: &str) -> Option<Self> {
409        // One lookup: header names intern case-insensitively, so the
410        // lowercased retry was always redundant.
411        request.headers.get(name).map(|v| Header::new(name, v))
412    }
413}
414
415impl FromRequestNamed for Header {
416    fn from_request(request: &HttpRequest, name: &str) -> Result<Self, Error> {
417        let value = request
418            .headers
419            .get(name)
420            .ok_or_else(|| Error::Validation(format!("Missing header: {}", name)))?;
421
422        Ok(Header::new(name, value))
423    }
424}
425
426impl Deref for Header {
427    type Target = str;
428
429    fn deref(&self) -> &Self::Target {
430        &self.value
431    }
432}
433
434// ========== Headers Extractor ==========
435
436/// Extracts all headers as a map
437#[derive(Debug, Clone)]
438pub struct Headers(pub std::collections::HashMap<String, String>);
439
440impl Headers {
441    /// Get a header value by name, case-insensitively.
442    pub fn get(&self, name: &str) -> Option<&String> {
443        // Hash lookup first: `HeaderMap` emits lowercase names, so a lowercase
444        // needle — including every name this map was built from — lands here.
445        if let Some(value) = self.0.get(name) {
446            return Some(value);
447        }
448        // Otherwise walk. A hash lookup for a differently-cased name would need
449        // a lowercased copy of the needle just to compute the hash, so the
450        // "fall back" branch used to allocate on essentially every
451        // conventionally-cased call (`Content-Type`, `X-Request-ID`). A map of
452        // request headers is a handful of entries; scanning it is cheaper than
453        // the allocation was.
454        self.0
455            .iter()
456            .find(|(k, _)| k.eq_ignore_ascii_case(name))
457            .map(|(_, v)| v)
458    }
459
460    /// Check if a header exists
461    pub fn contains(&self, name: &str) -> bool {
462        self.get(name).is_some()
463    }
464
465    /// Iterate over all headers
466    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
467        self.0.iter()
468    }
469}
470
471impl FromRequest for Headers {
472    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
473        // Public API stays a HashMap; convert from the internal HeaderMap.
474        Ok(Headers(request.headers.clone().into()))
475    }
476}
477
478impl Deref for Headers {
479    type Target = std::collections::HashMap<String, String>;
480
481    fn deref(&self) -> &Self::Target {
482        &self.0
483    }
484}
485
486// ========== RawBody Extractor ==========
487
488/// Extracts the raw request body as bytes
489///
490/// # Example
491///
492/// ```rust,ignore
493/// let raw: RawBody = RawBody::from_request(&request)?;
494/// println!("Body length: {} bytes", raw.len());
495/// ```
496#[derive(Debug, Clone)]
497pub struct RawBody(pub Bytes);
498
499impl RawBody {
500    /// Create a new RawBody
501    pub fn new(data: impl Into<Bytes>) -> Self {
502        Self(data.into())
503    }
504
505    /// Get the body length
506    pub fn len(&self) -> usize {
507        self.0.len()
508    }
509
510    /// Check if the body is empty
511    pub fn is_empty(&self) -> bool {
512        self.0.is_empty()
513    }
514
515    /// Convert to a UTF-8 string
516    pub fn to_string_lossy(&self) -> String {
517        String::from_utf8_lossy(&self.0).to_string()
518    }
519
520    /// Try to convert to a UTF-8 string
521    pub fn to_string(&self) -> Result<String, std::string::FromUtf8Error> {
522        String::from_utf8(self.0.to_vec())
523    }
524
525    /// Get the inner bytes
526    pub fn into_inner(self) -> Bytes {
527        self.0
528    }
529}
530
531impl FromRequest for RawBody {
532    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
533        Ok(RawBody(request.body.clone()))
534    }
535}
536
537impl Deref for RawBody {
538    type Target = [u8];
539
540    fn deref(&self) -> &Self::Target {
541        &self.0
542    }
543}
544
545// ========== Form Extractor ==========
546
547/// Extracts and deserializes form data (application/x-www-form-urlencoded)
548///
549/// # Example
550///
551/// ```rust,ignore
552/// #[derive(Deserialize)]
553/// struct LoginForm {
554///     username: String,
555///     password: String,
556/// }
557///
558/// let form: Form<LoginForm> = Form::from_request(&request)?;
559/// ```
560#[derive(Debug, Clone)]
561pub struct Form<T>(pub T);
562
563impl<T> Form<T> {
564    /// Create a new Form wrapper
565    pub fn new(value: T) -> Self {
566        Self(value)
567    }
568
569    /// Get the inner value
570    pub fn into_inner(self) -> T {
571        self.0
572    }
573}
574
575impl<T> Deref for Form<T> {
576    type Target = T;
577
578    fn deref(&self) -> &Self::Target {
579        &self.0
580    }
581}
582
583impl<T: DeserializeOwned> FromRequest for Form<T> {
584    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
585        let value: T = request.form()?;
586        Ok(Form(value))
587    }
588}
589
590// ========== ContentType Extractor ==========
591
592/// Extracts the Content-Type header
593#[derive(Debug, Clone)]
594pub struct ContentType(pub String);
595
596impl ContentType {
597    /// Check if the content type is JSON
598    pub fn is_json(&self) -> bool {
599        self.0.contains("application/json")
600    }
601
602    /// Check if the content type is form data
603    pub fn is_form(&self) -> bool {
604        self.0.contains("application/x-www-form-urlencoded")
605    }
606
607    /// Check if the content type is multipart
608    pub fn is_multipart(&self) -> bool {
609        self.0.contains("multipart/form-data")
610    }
611
612    /// Get the inner value
613    pub fn into_inner(self) -> String {
614        self.0
615    }
616}
617
618impl FromRequest for ContentType {
619    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
620        let value = request
621            .headers
622            .get("content-type")
623            .map(str::to_owned)
624            .unwrap_or_default();
625
626        Ok(ContentType(value))
627    }
628}
629
630impl Deref for ContentType {
631    type Target = str;
632
633    fn deref(&self) -> &Self::Target {
634        &self.0
635    }
636}
637
638// ========== Method Extractor ==========
639
640/// Extracts the HTTP method.
641///
642/// Renamed from `Method` in 0.6: `armature_core::Method` is now the wire method
643/// type re-exported from `armature-h1`, and one crate root cannot hold both. The
644/// extractor is the less-used of the two names, so it is the one that moved.
645#[derive(Debug, Clone)]
646pub struct MethodExtractor(pub crate::Method);
647
648impl MethodExtractor {
649    /// Check if the method is GET
650    pub fn is_get(&self) -> bool {
651        self.0 == "GET"
652    }
653
654    /// Check if the method is POST
655    pub fn is_post(&self) -> bool {
656        self.0 == "POST"
657    }
658
659    /// Check if the method is PUT
660    pub fn is_put(&self) -> bool {
661        self.0 == "PUT"
662    }
663
664    /// Check if the method is DELETE
665    pub fn is_delete(&self) -> bool {
666        self.0 == "DELETE"
667    }
668
669    /// Check if the method is PATCH
670    pub fn is_patch(&self) -> bool {
671        self.0 == "PATCH"
672    }
673}
674
675impl FromRequest for MethodExtractor {
676    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
677        Ok(MethodExtractor(request.method.clone()))
678    }
679}
680
681impl Deref for MethodExtractor {
682    type Target = crate::Method;
683
684    fn deref(&self) -> &Self::Target {
685        &self.0
686    }
687}
688
689// ========== Extension: FromRequest for primitives ==========
690
691impl FromRequest for HttpRequest {
692    fn from_request(request: &HttpRequest) -> Result<Self, Error> {
693        Ok(request.clone())
694    }
695}
696
697// ========== Helper Macros ==========
698
699/// Extract body from request as the specified type
700///
701/// # Example
702///
703/// ```rust,ignore
704/// let user: CreateUser = body!(request, CreateUser)?;
705/// // or with type inference if annotated
706/// let user = body!(request, CreateUser)?;
707/// ```
708#[macro_export]
709macro_rules! body {
710    ($request:expr, $type:ty) => {
711        <$crate::extractors::Body<$type> as $crate::extractors::FromRequest>::from_request(
712            &$request,
713        )
714        .map(|b| b.into_inner())
715    };
716}
717
718/// Extract query parameters from request as the specified type
719///
720/// # Example
721///
722/// ```rust,ignore
723/// let filters = query!(request, UserFilters)?;
724/// ```
725#[macro_export]
726macro_rules! query {
727    ($request:expr, $type:ty) => {
728        <$crate::extractors::Query<$type> as $crate::extractors::FromRequest>::from_request(
729            &$request,
730        )
731        .map(|q| q.into_inner())
732    };
733}
734
735/// Extract path parameter from request
736///
737/// # Example
738///
739/// ```rust,ignore
740/// let id: u32 = path!(request, "id", u32)?;
741/// ```
742#[macro_export]
743macro_rules! path {
744    ($request:expr, $name:expr, $type:ty) => {
745        <$crate::extractors::Path<$type> as $crate::extractors::FromRequestNamed>::from_request(
746            &$request, $name,
747        )
748        .map(|p| p.into_inner())
749    };
750}
751
752/// Extract header from request
753///
754/// # Example
755///
756/// ```rust,ignore
757/// let auth: String = header!(request, "Authorization")?;
758/// ```
759#[macro_export]
760macro_rules! header {
761    ($request:expr, $name:expr) => {
762        <$crate::extractors::Header as $crate::extractors::FromRequestNamed>::from_request(
763            &$request, $name,
764        )
765        .map(|h| h.into_value())
766    };
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use serde::Deserialize;
773
774    fn create_request() -> HttpRequest {
775        let mut req = HttpRequest::new("GET", "/users/123?page=1&limit=10");
776        req.push_param("id", "123");
777        req.headers
778            .insert("Authorization", "Bearer token123".to_string());
779        req.headers
780            .insert("Content-Type", "application/json".to_string());
781        req
782    }
783
784    #[test]
785    fn test_path_extraction() {
786        let request = create_request();
787        let id: Path<u32> = Path::from_request(&request, "id").unwrap();
788        assert_eq!(*id, 123);
789    }
790
791    #[test]
792    fn test_path_missing() {
793        let request = create_request();
794        let result: Result<Path<u32>, _> = Path::from_request(&request, "missing");
795        assert!(result.is_err());
796    }
797
798    #[test]
799    fn test_header_extraction() {
800        let request = create_request();
801        let auth: Header = Header::from_request(&request, "Authorization").unwrap();
802        assert_eq!(auth.value(), "Bearer token123");
803    }
804
805    #[test]
806    fn test_header_optional() {
807        let request = create_request();
808
809        let auth = Header::optional(&request, "Authorization");
810        assert!(auth.is_some());
811
812        let missing = Header::optional(&request, "X-Missing");
813        assert!(missing.is_none());
814    }
815
816    #[test]
817    fn test_headers_extraction() {
818        let request = create_request();
819        let headers: Headers = Headers::from_request(&request).unwrap();
820
821        assert!(headers.contains("Authorization"));
822        assert!(headers.contains("Content-Type"));
823        assert!(!headers.contains("X-Missing"));
824    }
825
826    #[test]
827    fn test_query_extraction() {
828        let request = create_request();
829
830        #[derive(Debug, Deserialize, PartialEq)]
831        struct Pagination {
832            page: u32,
833            limit: u32,
834        }
835
836        let query: Query<Pagination> = Query::from_request(&request).unwrap();
837        assert_eq!(query.page, 1);
838        assert_eq!(query.limit, 10);
839    }
840
841    #[test]
842    fn test_query_extraction_with_ampersand_and_equals_in_value() {
843        // Regression test: a value containing a literal `&` or `=` arrives
844        // percent-encoded on the wire and must survive deserialization intact.
845        let request = HttpRequest::new("GET", "/items?note=1%26b%3D2&name=a%3Db%26c");
846
847        #[derive(Debug, Deserialize, PartialEq)]
848        struct Filters {
849            note: String,
850            name: String,
851        }
852
853        let query: Query<Filters> = Query::from_request(&request).unwrap();
854        assert_eq!(query.note, "1&b=2");
855        assert_eq!(query.name, "a=b&c");
856    }
857
858    #[test]
859    fn test_path_params_extraction_with_ampersand_and_equals_in_value() {
860        // Regression test: same corruption risk as query params, but for
861        // path params extracted via `PathParams<T>`.
862        let mut request = HttpRequest::new("GET", "/items/x");
863        request.push_param("slug", Bytes::from_static(b"a&b=c"));
864
865        #[derive(Debug, Deserialize, PartialEq)]
866        struct Params {
867            slug: String,
868        }
869
870        let params: PathParams<Params> = PathParams::from_request(&request).unwrap();
871        assert_eq!(params.slug, "a&b=c");
872    }
873
874    #[test]
875    fn test_body_extraction() {
876        let mut request = create_request();
877        request.body = Bytes::from(
878            serde_json::to_vec(&serde_json::json!({
879                "name": "Test",
880                "email": "test@example.com"
881            }))
882            .unwrap(),
883        );
884
885        #[derive(Debug, Deserialize)]
886        struct CreateUser {
887            name: String,
888            email: String,
889        }
890
891        let body: Body<CreateUser> = Body::from_request(&request).unwrap();
892        assert_eq!(body.name, "Test");
893        assert_eq!(body.email, "test@example.com");
894    }
895
896    #[test]
897    fn test_raw_body() {
898        let mut request = create_request();
899        request.body = Bytes::from_static(b"raw content");
900
901        let raw: RawBody = RawBody::from_request(&request).unwrap();
902        assert_eq!(raw.len(), 11);
903        assert_eq!(raw.to_string_lossy(), "raw content");
904    }
905
906    #[test]
907    fn test_content_type() {
908        let request = create_request();
909        let ct: ContentType = ContentType::from_request(&request).unwrap();
910
911        assert!(ct.is_json());
912        assert!(!ct.is_form());
913        assert!(!ct.is_multipart());
914    }
915
916    #[test]
917    fn test_method() {
918        let request = create_request();
919        let method: MethodExtractor = MethodExtractor::from_request(&request).unwrap();
920
921        assert!(method.is_get());
922        assert!(!method.is_post());
923    }
924
925    #[test]
926    fn test_request_extraction() {
927        let request = create_request();
928        let extracted: HttpRequest = HttpRequest::from_request(&request).unwrap();
929
930        assert_eq!(extracted.method, request.method);
931        assert_eq!(extracted.path, request.path);
932    }
933}