x_http/
request.rs

1use crate::error::Result;
2use crate::response::Response;
3use reqwest::blocking::Client;
4use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
5use serde::Serialize;
6use std::collections::HashMap;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Method {
11    Get,
12    Post,
13    Put,
14    Delete,
15    Patch,
16    Head,
17    Options,
18}
19
20impl Method {
21    fn as_reqwest_method(&self) -> reqwest::Method {
22        match self {
23            Method::Get => reqwest::Method::GET,
24            Method::Post => reqwest::Method::POST,
25            Method::Put => reqwest::Method::PUT,
26            Method::Delete => reqwest::Method::DELETE,
27            Method::Patch => reqwest::Method::PATCH,
28            Method::Head => reqwest::Method::HEAD,
29            Method::Options => reqwest::Method::OPTIONS,
30        }
31    }
32}
33
34#[derive(Debug)]
35pub struct Request {
36    method: Method,
37    url: String,
38    headers: HeaderMap,
39    body: Option<Vec<u8>>,
40    query_params: HashMap<String, String>,
41    timeout: Option<Duration>,
42    follow_redirects: bool,
43}
44
45impl Request {
46    pub fn new(method: Method, url: impl Into<String>) -> Self {
47        Self {
48            method,
49            url: url.into(),
50            headers: HeaderMap::new(),
51            body: None,
52            query_params: HashMap::new(),
53            timeout: Some(Duration::from_secs(30)),
54            follow_redirects: true,
55        }
56    }
57
58    pub fn get(url: impl Into<String>) -> Self {
59        Self::new(Method::Get, url)
60    }
61
62    pub fn post(url: impl Into<String>) -> Self {
63        Self::new(Method::Post, url)
64    }
65
66    pub fn put(url: impl Into<String>) -> Self {
67        Self::new(Method::Put, url)
68    }
69
70    pub fn delete(url: impl Into<String>) -> Self {
71        Self::new(Method::Delete, url)
72    }
73
74    pub fn patch(url: impl Into<String>) -> Self {
75        Self::new(Method::Patch, url)
76    }
77
78    pub fn head(url: impl Into<String>) -> Self {
79        Self::new(Method::Head, url)
80    }
81
82    pub fn options(url: impl Into<String>) -> Self {
83        Self::new(Method::Options, url)
84    }
85
86    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
87        if let (Ok(name), Ok(val)) = (
88            HeaderName::try_from(key.as_ref()),
89            HeaderValue::try_from(value.as_ref()),
90        ) {
91            self.headers.insert(name, val);
92        }
93        self
94    }
95
96    pub fn headers(mut self, headers: Vec<(impl AsRef<str>, impl AsRef<str>)>) -> Self {
97        for (key, value) in headers {
98            self = self.header(key, value);
99        }
100        self
101    }
102
103    pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self> {
104        let json_string = serde_json::to_string(body)?;
105        self.body = Some(json_string.into_bytes());
106        self = self.header("Content-Type", "application/json");
107        Ok(self)
108    }
109
110    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
111        self.body = Some(body.into());
112        self
113    }
114
115    pub fn text(self, text: impl Into<String>) -> Self {
116        self.body(text.into().into_bytes())
117            .header("Content-Type", "text/plain")
118    }
119
120    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
121        self.query_params.insert(key.into(), value.into());
122        self
123    }
124
125    pub fn timeout(mut self, duration: Duration) -> Self {
126        self.timeout = Some(duration);
127        self
128    }
129
130    pub fn no_timeout(mut self) -> Self {
131        self.timeout = None;
132        self
133    }
134
135    pub fn follow_redirects(mut self, follow: bool) -> Self {
136        self.follow_redirects = follow;
137        self
138    }
139
140    pub fn send(self) -> Result<Response> {
141        let client = Client::builder()
142            .redirect(if self.follow_redirects {
143                reqwest::redirect::Policy::default()
144            } else {
145                reqwest::redirect::Policy::none()
146            })
147            .build()?;
148
149        let mut url = url::Url::parse(&self.url)?;
150
151        for (key, value) in self.query_params {
152            url.query_pairs_mut().append_pair(&key, &value);
153        }
154
155        let mut request_builder = client
156            .request(self.method.as_reqwest_method(), url)
157            .headers(self.headers);
158
159        if let Some(timeout) = self.timeout {
160            request_builder = request_builder.timeout(timeout);
161        }
162
163        if let Some(body) = self.body {
164            request_builder = request_builder.body(body);
165        }
166
167        let start = std::time::Instant::now();
168        let response = request_builder.send()?;
169        let duration = start.elapsed();
170
171        Response::from_reqwest(response, duration)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_request_builder() {
181        let req = Request::get("https://example.com")
182            .header("Authorization", "Bearer token")
183            .query("page", "1")
184            .timeout(Duration::from_secs(10));
185
186        assert_eq!(req.method, Method::Get);
187        assert_eq!(req.url, "https://example.com");
188        assert_eq!(req.timeout, Some(Duration::from_secs(10)));
189    }
190
191    #[test]
192    fn test_json_body() {
193        use serde_json::json;
194
195        let body = json!({
196            "name": "test",
197            "value": 42
198        });
199
200        let req = Request::post("https://example.com").json(&body).unwrap();
201
202        assert!(req.body.is_some());
203        assert!(req.headers.contains_key("content-type"));
204    }
205}