Skip to main content

oxihttp_server/
extractor.rs

1//! Request parts extraction framework for OxiHTTP server handlers.
2//!
3//! Provides the [`FromRequestParts`] trait and [`TypedHeader`] extractor for
4//! typed access to HTTP request components without consuming the request body.
5
6use std::collections::HashMap;
7
8use http::{HeaderMap, Method, Uri};
9use oxihttp_core::{Header, OxiHttpError};
10
11/// A view into the non-body parts of a server [`Request`][crate::Request].
12///
13/// Passed to [`FromRequestParts::from_request_parts`] implementations.
14pub struct RequestParts<'a> {
15    /// The HTTP method of the request.
16    pub method: &'a Method,
17    /// The request URI.
18    pub uri: &'a Uri,
19    /// The request headers.
20    pub headers: &'a HeaderMap,
21    /// Extracted path parameters (e.g. `:id` in `/users/:id`).
22    pub path_params: &'a HashMap<String, String>,
23}
24
25/// Trait for types that can be extracted from [`RequestParts`].
26///
27/// Implement this trait to create custom extractors that can be used with
28/// [`Request::extract`][crate::Request::extract].
29pub trait FromRequestParts: Sized {
30    /// The error type returned when extraction fails.
31    type Rejection: Into<OxiHttpError>;
32
33    /// Attempt to extract `Self` from the request parts.
34    fn from_request_parts(parts: &RequestParts<'_>) -> Result<Self, Self::Rejection>;
35}
36
37/// Extractor for a single typed HTTP header.
38///
39/// Extracts and decodes the header `H` from the request, returning an error
40/// if the header is missing or its value is invalid.
41///
42/// # Example
43///
44/// ```rust,no_run
45/// use oxihttp_server::extractor::TypedHeader;
46/// use oxihttp_core::ContentType;
47///
48/// // In a handler:
49/// // let TypedHeader(ct) = req.extract::<TypedHeader<ContentType>>()?;
50/// ```
51pub struct TypedHeader<H: Header>(pub H);
52
53impl<H: Header> FromRequestParts for TypedHeader<H> {
54    type Rejection = OxiHttpError;
55
56    fn from_request_parts(parts: &RequestParts<'_>) -> Result<Self, Self::Rejection> {
57        H::decode(parts.headers).map(TypedHeader)
58    }
59}
60
61// ---------------------------------------------------------------------------
62// Unit tests
63// ---------------------------------------------------------------------------
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use http::HeaderValue;
69    use oxihttp_core::{Authorization, ContentType, Host};
70
71    fn make_parts<'a>(
72        method: &'a Method,
73        uri: &'a Uri,
74        headers: &'a HeaderMap,
75        path_params: &'a HashMap<String, String>,
76    ) -> RequestParts<'a> {
77        RequestParts {
78            method,
79            uri,
80            headers,
81            path_params,
82        }
83    }
84
85    #[test]
86    fn test_typed_header_extraction_via_request_parts() {
87        let method = Method::POST;
88        let uri: Uri = "/upload".parse().expect("parse uri");
89        let mut headers = HeaderMap::new();
90        headers.insert(
91            http::header::CONTENT_TYPE,
92            HeaderValue::from_static("application/json"),
93        );
94        let path_params = HashMap::new();
95        let parts = make_parts(&method, &uri, &headers, &path_params);
96
97        let TypedHeader(ct) =
98            TypedHeader::<ContentType>::from_request_parts(&parts).expect("should extract");
99        assert_eq!(ct, ContentType::Json);
100    }
101
102    #[test]
103    fn test_typed_header_missing_returns_error() {
104        let method = Method::POST;
105        let uri: Uri = "/upload".parse().expect("parse uri");
106        let headers = HeaderMap::new();
107        let path_params = HashMap::new();
108        let parts = make_parts(&method, &uri, &headers, &path_params);
109
110        let result = TypedHeader::<ContentType>::from_request_parts(&parts);
111        assert!(result.is_err(), "should fail when Content-Type is absent");
112    }
113
114    #[test]
115    fn test_typed_header_host_ok() {
116        let method = Method::GET;
117        let uri: Uri = "/".parse().expect("parse uri");
118        let mut headers = HeaderMap::new();
119        headers.insert(http::header::HOST, HeaderValue::from_static("example.com"));
120        let path_params = HashMap::new();
121        let parts = make_parts(&method, &uri, &headers, &path_params);
122
123        let TypedHeader(host) =
124            TypedHeader::<Host>::from_request_parts(&parts).expect("should extract");
125        assert_eq!(host, Host("example.com".to_string()));
126    }
127
128    #[test]
129    fn test_typed_header_authorization_ok() {
130        let method = Method::GET;
131        let uri: Uri = "/secure".parse().expect("parse uri");
132        let mut headers = HeaderMap::new();
133        headers.insert(
134            http::header::AUTHORIZATION,
135            HeaderValue::from_static("Bearer secret-token"),
136        );
137        let path_params = HashMap::new();
138        let parts = make_parts(&method, &uri, &headers, &path_params);
139
140        let TypedHeader(auth) =
141            TypedHeader::<Authorization>::from_request_parts(&parts).expect("should extract");
142        assert_eq!(auth, Authorization("Bearer secret-token".to_string()));
143    }
144}