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}