Skip to main content

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