Skip to main content

rust_web_server/extract/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::collections::HashMap;
5
6use crate::header::Header;
7use crate::request::Request;
8use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
9use crate::url::URL;
10
11/// Types that can be extracted from a [`Request`].
12///
13/// Implement this trait to build reusable request-parsing logic that maps
14/// cleanly to an HTTP error response on failure.
15///
16/// # Example
17///
18/// ```rust,no_run
19/// use rust_web_server::extract::{Body, BodyText, Query, FromRequest};
20/// use rust_web_server::request::Request;
21///
22/// // inside a Controller::process implementation
23/// fn handle(request: &Request) {
24///     let body = Body::from_request(request).unwrap();
25///     let text = BodyText::from_request(request).unwrap();
26///     let params = Query::from_request(request).unwrap();
27///     let id = params.get("id").map(String::as_str).unwrap_or("");
28/// }
29/// ```
30pub trait FromRequest: Sized {
31    /// Extract `Self` from `request`, or return a ready-to-send error [`Response`].
32    fn from_request(request: &Request) -> Result<Self, Response>;
33}
34
35/// Raw request body bytes.
36///
37/// Never fails — an empty body produces an empty `Vec`.
38#[derive(Debug)]
39pub struct Body(pub Vec<u8>);
40
41impl FromRequest for Body {
42    fn from_request(request: &Request) -> Result<Self, Response> {
43        Ok(Body(request.body.to_vec()))
44    }
45}
46
47impl Body {
48    pub fn into_bytes(self) -> Vec<u8> {
49        self.0
50    }
51}
52
53/// Request body decoded as UTF-8 text.
54///
55/// Returns `400 Bad Request` if the body is not valid UTF-8.
56#[derive(Debug)]
57pub struct BodyText(pub String);
58
59impl FromRequest for BodyText {
60    fn from_request(request: &Request) -> Result<Self, Response> {
61        String::from_utf8(request.body.to_vec())
62            .map(BodyText)
63            .map_err(|_| bad_request(request, "request body is not valid UTF-8"))
64    }
65}
66
67impl BodyText {
68    pub fn as_str(&self) -> &str {
69        &self.0
70    }
71}
72
73/// Parsed query parameters from the request URI.
74///
75/// Never fails — a URI with no query string produces an empty map.
76#[derive(Debug)]
77///
78/// # Example
79///
80/// ```rust,no_run
81/// use rust_web_server::extract::{Query, FromRequest};
82/// use rust_web_server::request::Request;
83///
84/// fn handle(request: &Request) {
85///     let q = Query::from_request(request).unwrap();
86///     let page = q.get("page").map(String::as_str).unwrap_or("1");
87/// }
88/// ```
89pub struct Query(pub HashMap<String, String>);
90
91impl FromRequest for Query {
92    fn from_request(request: &Request) -> Result<Self, Response> {
93        let query_str = request.request_uri
94            .splitn(2, '?')
95            .nth(1)
96            .unwrap_or("");
97        Ok(Query(URL::parse_query(query_str)))
98    }
99}
100
101impl Query {
102    pub fn get(&self, key: &str) -> Option<&String> {
103        self.0.get(key)
104    }
105}
106
107/// Clone of all request headers.
108#[derive(Debug)]
109pub struct RequestHeaders(pub Vec<Header>);
110
111impl FromRequest for RequestHeaders {
112    fn from_request(request: &Request) -> Result<Self, Response> {
113        Ok(RequestHeaders(request.headers.clone()))
114    }
115}
116
117impl RequestHeaders {
118    /// Return the value of the first header matching `name` (case-insensitive).
119    pub fn get(&self, name: &str) -> Option<&str> {
120        let lower = name.to_lowercase();
121        self.0.iter()
122            .find(|h| h.name.to_lowercase() == lower)
123            .map(|h| h.value.as_ref())
124    }
125}
126
127/// The request's correlation ID (`X-Request-Id` by default — see
128/// [`crate::request_id::DEFAULT_HEADER`]), for handlers that want it without
129/// reaching for [`RequestHeaders`] directly.
130///
131/// Never fails: if [`crate::request_id::RequestIdLayer`] isn't wrapping this
132/// app (or the caller didn't send the header), `.0` is an empty string.
133#[derive(Debug, Clone)]
134pub struct RequestId(pub String);
135
136impl FromRequest for RequestId {
137    fn from_request(request: &Request) -> Result<Self, Response> {
138        Ok(RequestId(
139            (request
140                .get_header(crate::request_id::DEFAULT_HEADER.to_string())
141                .map(|h| h.value.clone())
142                .unwrap_or_default()).into_owned(),
143        ))
144    }
145}
146
147impl RequestId {
148    /// Returns the ID as a string slice, or `""` if none was set.
149    pub fn as_str(&self) -> &str {
150        &self.0
151    }
152}
153
154fn bad_request(request: &Request, msg: &str) -> Response {
155    let header_list = Header::get_header_list(request);
156    crate::error::problem::with_headers(STATUS_CODE_REASON_PHRASE.n400_bad_request, msg, msg, Some(header_list))
157}