1use std::collections::HashMap;
7use std::fmt;
8
9use crate::clients::errors::InvalidHttpRequestError;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum HttpMethod {
16 Get,
18 Post,
20 Put,
22 Delete,
24}
25
26impl fmt::Display for HttpMethod {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Get => write!(f, "get"),
30 Self::Post => write!(f, "post"),
31 Self::Put => write!(f, "put"),
32 Self::Delete => write!(f, "delete"),
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum DataType {
43 Json,
45 GraphQL,
47}
48
49impl DataType {
50 #[must_use]
52 pub const fn as_content_type(&self) -> &'static str {
53 match self {
54 Self::Json => "application/json",
55 Self::GraphQL => "application/graphql",
56 }
57 }
58}
59
60#[derive(Clone, Debug)]
83pub struct HttpRequest {
84 pub http_method: HttpMethod,
86 pub path: String,
88 pub body: Option<serde_json::Value>,
90 pub body_type: Option<DataType>,
92 pub query: Option<HashMap<String, String>>,
94 pub extra_headers: Option<HashMap<String, String>>,
96 pub tries: u32,
98}
99
100impl HttpRequest {
101 #[must_use]
119 pub fn builder(method: HttpMethod, path: impl Into<String>) -> HttpRequestBuilder {
120 HttpRequestBuilder::new(method, path)
121 }
122
123 pub fn verify(&self) -> Result<(), InvalidHttpRequestError> {
131 if self.body.is_some() && self.body_type.is_none() {
133 return Err(InvalidHttpRequestError::MissingBodyType);
134 }
135
136 if matches!(self.http_method, HttpMethod::Post | HttpMethod::Put) && self.body.is_none() {
138 return Err(InvalidHttpRequestError::MissingBody {
139 method: self.http_method.to_string(),
140 });
141 }
142
143 Ok(())
144 }
145}
146
147#[derive(Debug)]
151pub struct HttpRequestBuilder {
152 http_method: HttpMethod,
153 path: String,
154 body: Option<serde_json::Value>,
155 body_type: Option<DataType>,
156 query: Option<HashMap<String, String>>,
157 extra_headers: Option<HashMap<String, String>>,
158 tries: u32,
159}
160
161impl HttpRequestBuilder {
162 fn new(method: HttpMethod, path: impl Into<String>) -> Self {
164 Self {
165 http_method: method,
166 path: path.into(),
167 body: None,
168 body_type: None,
169 query: None,
170 extra_headers: None,
171 tries: 1,
172 }
173 }
174
175 #[must_use]
179 pub fn body(mut self, body: impl Into<serde_json::Value>) -> Self {
180 self.body = Some(body.into());
181 self
182 }
183
184 #[must_use]
186 pub const fn body_type(mut self, body_type: DataType) -> Self {
187 self.body_type = Some(body_type);
188 self
189 }
190
191 #[must_use]
193 pub fn query(mut self, query: HashMap<String, String>) -> Self {
194 self.query = Some(query);
195 self
196 }
197
198 #[must_use]
200 pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
201 self.query
202 .get_or_insert_with(HashMap::new)
203 .insert(key.into(), value.into());
204 self
205 }
206
207 #[must_use]
209 pub fn extra_headers(mut self, headers: HashMap<String, String>) -> Self {
210 self.extra_headers = Some(headers);
211 self
212 }
213
214 #[must_use]
216 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
217 self.extra_headers
218 .get_or_insert_with(HashMap::new)
219 .insert(key.into(), value.into());
220 self
221 }
222
223 #[must_use]
228 pub const fn tries(mut self, tries: u32) -> Self {
229 self.tries = tries;
230 self
231 }
232
233 pub fn build(self) -> Result<HttpRequest, InvalidHttpRequestError> {
239 let request = HttpRequest {
240 http_method: self.http_method,
241 path: self.path,
242 body: self.body,
243 body_type: self.body_type,
244 query: self.query,
245 extra_headers: self.extra_headers,
246 tries: self.tries,
247 };
248 request.verify()?;
249 Ok(request)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use serde_json::json;
257
258 #[test]
259 fn test_http_method_display() {
260 assert_eq!(HttpMethod::Get.to_string(), "get");
261 assert_eq!(HttpMethod::Post.to_string(), "post");
262 assert_eq!(HttpMethod::Put.to_string(), "put");
263 assert_eq!(HttpMethod::Delete.to_string(), "delete");
264 }
265
266 #[test]
267 fn test_data_type_content_type() {
268 assert_eq!(DataType::Json.as_content_type(), "application/json");
269 assert_eq!(DataType::GraphQL.as_content_type(), "application/graphql");
270 }
271
272 #[test]
273 fn test_builder_creates_valid_get_request() {
274 let request = HttpRequest::builder(HttpMethod::Get, "products.json")
275 .build()
276 .unwrap();
277
278 assert_eq!(request.http_method, HttpMethod::Get);
279 assert_eq!(request.path, "products.json");
280 assert!(request.body.is_none());
281 assert!(request.body_type.is_none());
282 assert_eq!(request.tries, 1);
283 }
284
285 #[test]
286 fn test_builder_creates_valid_post_request() {
287 let request = HttpRequest::builder(HttpMethod::Post, "products.json")
288 .body(json!({"product": {"title": "Test"}}))
289 .body_type(DataType::Json)
290 .build()
291 .unwrap();
292
293 assert_eq!(request.http_method, HttpMethod::Post);
294 assert!(request.body.is_some());
295 assert_eq!(request.body_type, Some(DataType::Json));
296 }
297
298 #[test]
299 fn test_verify_requires_body_for_post() {
300 let result = HttpRequest::builder(HttpMethod::Post, "products.json").build();
301
302 assert!(matches!(
303 result,
304 Err(InvalidHttpRequestError::MissingBody { method }) if method == "post"
305 ));
306 }
307
308 #[test]
309 fn test_verify_requires_body_for_put() {
310 let result = HttpRequest::builder(HttpMethod::Put, "products/123.json").build();
311
312 assert!(matches!(
313 result,
314 Err(InvalidHttpRequestError::MissingBody { method }) if method == "put"
315 ));
316 }
317
318 #[test]
319 fn test_verify_requires_body_type_when_body_present() {
320 let request = HttpRequest {
321 http_method: HttpMethod::Get,
322 path: "test".to_string(),
323 body: Some(json!({"key": "value"})),
324 body_type: None,
325 query: None,
326 extra_headers: None,
327 tries: 1,
328 };
329
330 assert!(matches!(
331 request.verify(),
332 Err(InvalidHttpRequestError::MissingBodyType)
333 ));
334 }
335
336 #[test]
337 fn test_builder_with_query_params() {
338 let request = HttpRequest::builder(HttpMethod::Get, "products.json")
339 .query_param("limit", "50")
340 .query_param("page_info", "abc123")
341 .build()
342 .unwrap();
343
344 let query = request.query.unwrap();
345 assert_eq!(query.get("limit"), Some(&"50".to_string()));
346 assert_eq!(query.get("page_info"), Some(&"abc123".to_string()));
347 }
348
349 #[test]
350 fn test_builder_with_extra_headers() {
351 let request = HttpRequest::builder(HttpMethod::Get, "products.json")
352 .header("X-Custom-Header", "custom-value")
353 .build()
354 .unwrap();
355
356 let headers = request.extra_headers.unwrap();
357 assert_eq!(
358 headers.get("X-Custom-Header"),
359 Some(&"custom-value".to_string())
360 );
361 }
362
363 #[test]
364 fn test_default_tries_is_one() {
365 let request = HttpRequest::builder(HttpMethod::Get, "test")
366 .build()
367 .unwrap();
368 assert_eq!(request.tries, 1);
369 }
370}