picky/http/
http_request.rs

1use std::borrow::Cow;
2use thiserror::Error;
3
4#[derive(Debug, Error, Clone)]
5#[non_exhaustive]
6pub enum HttpRequestError {
7    /// couldn't convert a http header value to string
8    #[error("couldn't convert http header value to string for header key {key}")]
9    HeaderValueToStr { key: String },
10
11    /// unexpected error occurred
12    #[error("unexpected error: {reason}")]
13    Unexpected { reason: String },
14}
15
16pub trait HttpRequest {
17    fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError>;
18    fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError>;
19    fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError>;
20}
21
22#[cfg(feature = "http_trait_impl")]
23mod http_trait_impl {
24    use super::*;
25
26    impl HttpRequest for http::request::Parts {
27        fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
28            let mut values = Vec::new();
29            let all_values = self.headers.get_all(header_name);
30            for value in all_values {
31                let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
32                    key: header_name.to_owned(),
33                })?;
34                values.push(value_str.trim());
35            }
36            Ok(Cow::Owned(values.join(", ")))
37        }
38
39        fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
40            Ok(Cow::Owned(self.method.as_str().to_lowercase()))
41        }
42
43        fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
44            Ok(Cow::Borrowed(self.uri.path()))
45        }
46    }
47    impl<T> HttpRequest for http::request::Request<T> {
48        fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result<Cow<'a, str>, HttpRequestError> {
49            let mut values = Vec::new();
50            let all_values = self.headers().get_all(header_name);
51            for value in all_values {
52                let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr {
53                    key: header_name.to_owned(),
54                })?;
55                values.push(value_str.trim());
56            }
57            Ok(Cow::Owned(values.join(", ")))
58        }
59
60        fn get_lowercased_method(&self) -> Result<Cow<'_, str>, HttpRequestError> {
61            Ok(Cow::Owned(self.method().as_str().to_lowercase()))
62        }
63
64        fn get_target(&self) -> Result<Cow<'_, str>, HttpRequestError> {
65            Ok(Cow::Borrowed(self.uri().path()))
66        }
67    }
68
69    #[cfg(test)]
70    mod tests {
71        use super::*;
72        use http::method::Method;
73        use http::{header, request};
74
75        #[test]
76        fn http_request_parts() {
77            let req = request::Builder::new()
78                .method(Method::GET)
79                .uri("/foo")
80                .header("Host", "example.org")
81                .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
82                .header("X-Example", " Example header       with some whitespace.   ")
83                .header("X-EmptyHeader", "")
84                .header(header::CACHE_CONTROL, "max-age=60")
85                .header(header::CACHE_CONTROL, "must-revalidate")
86                .body(())
87                .expect("couldn't build request");
88
89            let (parts, _) = req.into_parts();
90
91            assert_eq!(parts.get_target().expect("target"), "/foo");
92            assert_eq!(parts.get_lowercased_method().expect("method"), "get");
93            assert_eq!(
94                parts.get_header_concatenated_values("host").expect("host"),
95                "example.org"
96            );
97            assert_eq!(
98                parts.get_header_concatenated_values("date").expect("date"),
99                "Tue, 07 Jun 2014 20:51:35 GMT"
100            );
101            assert_eq!(
102                parts.get_header_concatenated_values("x-example").expect("example"),
103                "Example header       with some whitespace."
104            );
105            assert_eq!(
106                parts.get_header_concatenated_values("X-EmptyHeader").expect("empty"),
107                ""
108            );
109            assert_eq!(
110                parts
111                    .get_header_concatenated_values(header::CACHE_CONTROL.as_str())
112                    .expect("cache control"),
113                "max-age=60, must-revalidate"
114            );
115        }
116
117        #[test]
118        fn http_request_request() {
119            let req = request::Builder::new()
120                .method(Method::GET)
121                .uri("/foo")
122                .header("Host", "example.org")
123                .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT")
124                .header("X-Example", " Example header       with some whitespace.   ")
125                .header("X-EmptyHeader", "")
126                .header(header::CACHE_CONTROL, "max-age=60")
127                .header(header::CACHE_CONTROL, "must-revalidate")
128                .body(())
129                .expect("couldn't build request");
130
131            assert_eq!(req.get_target().expect("target"), "/foo");
132            assert_eq!(req.get_lowercased_method().expect("method"), "get");
133            assert_eq!(req.get_header_concatenated_values("host").expect("host"), "example.org");
134            assert_eq!(
135                req.get_header_concatenated_values("date").expect("date"),
136                "Tue, 07 Jun 2014 20:51:35 GMT"
137            );
138            assert_eq!(
139                req.get_header_concatenated_values("x-example").expect("example"),
140                "Example header       with some whitespace."
141            );
142            assert_eq!(req.get_header_concatenated_values("X-EmptyHeader").expect("empty"), "");
143            assert_eq!(
144                req.get_header_concatenated_values(header::CACHE_CONTROL.as_str())
145                    .expect("cache control"),
146                "max-age=60, must-revalidate"
147            );
148        }
149    }
150}