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}