1use reqwest::{Client as HttpClient, Response};
2use serde::de::DeserializeOwned;
3use std::time::Instant;
4use tokio::time::sleep;
5
6use lago_types::{error::LagoError, error::Result};
7
8use crate::{Config, RetryMode};
9
10#[derive(Clone)]
15pub struct LagoClient {
16 pub(crate) config: Config,
17 http_client: HttpClient,
18}
19
20impl LagoClient {
21 pub fn new(config: Config) -> Self {
29 let http_client = HttpClient::builder()
30 .timeout(config.timeout())
31 .user_agent(config.user_agent())
32 .build()
33 .expect("Failed to create HTTP client");
34
35 Self {
36 config,
37 http_client,
38 }
39 }
40
41 pub fn from_env() -> Result<Self> {
49 let config = Config::default();
50 Ok(Self::new(config))
51 }
52
53 pub(crate) async fn make_request<T, B>(
66 &self,
67 method: &str,
68 url: &str,
69 body: Option<&B>,
70 ) -> Result<T>
71 where
72 T: DeserializeOwned,
73 B: serde::Serialize,
74 {
75 let credentials = self.config.credentials()?;
76 let mut attempt = 0;
77
78 loop {
79 let _start_time = Instant::now();
80
81 let mut request_builder = match method {
82 "GET" => self.http_client.get(url),
83 "POST" => self.http_client.post(url),
84 "PUT" => self.http_client.put(url),
85 "DELETE" => self.http_client.delete(url),
86 _ => {
87 return Err(LagoError::Configuration(format!(
88 "Unsupported method: {method}"
89 )));
90 }
91 };
92
93 request_builder = request_builder.bearer_auth(credentials.api_key());
94
95 if let Some(body) = body {
96 request_builder = request_builder.json(body);
97 }
98
99 let response = match request_builder.send().await {
100 Ok(response) => response,
101 Err(e) => {
102 if attempt >= self.config.retry_config().max_attempts {
103 return Err(LagoError::Http(e));
104 }
105
106 attempt += 1;
107 let delay = self.config.retry_config().delay_for_attempt(attempt);
108 sleep(delay).await;
109 continue;
110 }
111 };
112
113 match self.handle_response(response).await {
114 Ok(result) => return Ok(result),
115 Err(e) => {
116 if !self.should_retry(&e, attempt) {
117 return Err(e);
118 }
119
120 attempt += 1;
121 let delay = self.config.retry_config().delay_for_attempt(attempt);
122 sleep(delay).await;
123 continue;
124 }
125 }
126 }
127 }
128
129 async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
140 let status = response.status();
141
142 if status.is_success() {
143 let text = response.text().await.map_err(LagoError::Http)?;
144 if text.is_empty() {
146 return serde_json::from_str("{}").map_err(LagoError::Serialization);
147 }
148 serde_json::from_str(&text).map_err(LagoError::Serialization)
149 } else {
150 let error_text = response
151 .text()
152 .await
153 .unwrap_or_else(|_| "Unknown error".to_string());
154
155 match status.as_u16() {
156 401 => Err(LagoError::Unauthorized),
157 404 => Err(LagoError::Api {
158 status: status.as_u16(),
159 message: error_text,
160 }),
161 429 => Err(LagoError::RateLimit),
162 _ => Err(LagoError::Api {
163 status: status.as_u16(),
164 message: error_text,
165 }),
166 }
167 }
168 }
169
170 fn should_retry(&self, error: &LagoError, attempt: u32) -> bool {
183 if attempt >= self.config.retry_config().max_attempts {
184 return false;
185 }
186
187 if self.config.retry_config().mode == RetryMode::Off {
188 return false;
189 }
190
191 match error {
192 LagoError::Http(_) => true,
193 LagoError::RateLimit => true,
194 LagoError::Api { status, .. } => *status >= 500,
195 _ => false,
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::{Config, Credentials, Region, RetryConfig, RetryMode};
204 use lago_types::error::LagoError;
205 use mockito::Server;
206 use serde::{Deserialize, Serialize};
207 use serde_json::json;
208 use std::time::Duration;
209
210 #[derive(Debug, Deserialize, Serialize)]
211 struct TestResponse {
212 id: String,
213 name: String,
214 }
215
216 #[derive(Serialize)]
217 struct TestRequest {
218 name: String,
219 }
220
221 fn create_test_client(base_url: &str) -> LagoClient {
222 let config = Config::builder()
223 .credentials(Credentials::new("test-api-key".to_string()))
224 .region(Region::Custom(base_url.to_string()))
225 .timeout(Duration::from_secs(10))
226 .build();
227
228 LagoClient::new(config)
229 }
230
231 fn create_retry_client(base_url: &str, max_attempts: u32) -> LagoClient {
232 let retry_config = RetryConfig::builder()
233 .max_attempts(max_attempts)
234 .mode(RetryMode::Adaptive)
235 .build();
236
237 let config = Config::builder()
238 .credentials(Credentials::new("test-api-key".to_string()))
239 .region(Region::Custom(base_url.to_string()))
240 .retry_config(retry_config)
241 .timeout(Duration::from_secs(5))
242 .build();
243
244 LagoClient::new(config)
245 }
246
247 #[test]
248 fn test_new_client_creation() {
249 let config = Config::default();
250 let client = LagoClient::new(config.clone());
251
252 assert_eq!(client.config.timeout(), config.timeout());
253 assert_eq!(client.config.user_agent(), config.user_agent());
254 }
255
256 #[test]
257 fn test_from_env_client_creation() {
258 let result = LagoClient::from_env();
259 assert!(result.is_ok());
260 }
261
262 #[tokio::test]
263 async fn test_successful_get_request() {
264 let mut server = Server::new_async().await;
265 let mock = server
266 .mock("GET", "/test")
267 .with_status(200)
268 .with_header("content-type", "application/json")
269 .with_body(
270 json!({
271 "id": "123",
272 "name": "Test"
273 })
274 .to_string(),
275 )
276 .create_async()
277 .await;
278
279 let client = create_test_client(&server.url());
280 let url = format!("{}/test", server.url());
281
282 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
283
284 assert!(result.is_ok());
285
286 let response = result.unwrap();
287 assert_eq!(response.id, "123");
288 assert_eq!(response.name, "Test");
289
290 mock.assert_async().await;
291 }
292
293 #[tokio::test]
294 async fn test_successful_post_request() {
295 let mut server = Server::new_async().await;
296 let mock = server
297 .mock("POST", "/test")
298 .with_status(201)
299 .with_header("content-type", "application/json")
300 .match_body(mockito::Matcher::Json(json!({
301 "name": "New Item"
302 })))
303 .with_body(
304 json!({
305 "id": "456",
306 "name": "New Item"
307
308 })
309 .to_string(),
310 )
311 .create_async()
312 .await;
313
314 let client = create_test_client(&server.url());
315 let url = format!("{}/test", server.url());
316 let request = TestRequest {
317 name: "New Item".to_string(),
318 };
319
320 let result: Result<TestResponse> = client.make_request("POST", &url, Some(&request)).await;
321
322 assert!(result.is_ok());
323
324 let response = result.unwrap();
325 assert_eq!(response.id, "456");
326 assert_eq!(response.name, "New Item");
327
328 mock.assert_async().await;
329 }
330
331 #[tokio::test]
332 async fn test_authentication_header() {
333 let mut server = Server::new_async().await;
334 let mock = server
335 .mock("GET", "/test")
336 .match_header("Authorization", "Bearer test-api-key")
337 .with_status(200)
338 .with_body(json!({"id": "123", "name": "Test"}).to_string())
339 .create_async()
340 .await;
341
342 let client = create_test_client(&server.url());
343 let url = format!("{}/test", server.url());
344
345 let _result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
346
347 mock.assert_async().await;
348 }
349
350 #[tokio::test]
351 async fn test_unsupported_method() {
352 let server = Server::new_async().await;
353 let client = create_test_client(&server.url());
354 let url = format!("{}/test", server.url());
355
356 let result: Result<TestResponse> = client.make_request("PATCH", &url, None::<&()>).await;
357
358 assert!(result.is_err());
359
360 match result.unwrap_err() {
361 LagoError::Configuration(msg) => {
362 assert!(msg.contains("Unsupported method: PATCH"));
363 }
364 _ => panic!("Expected Configuration error"),
365 }
366 }
367
368 #[tokio::test]
369 async fn test_unauthorized_error() {
370 let mut server = Server::new_async().await;
371 let mock = server
372 .mock("GET", "/test")
373 .with_status(401)
374 .with_body("Unauthorized")
375 .create_async()
376 .await;
377
378 let client = create_test_client(&server.url());
379 let url = format!("{}/test", server.url());
380
381 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
382
383 assert!(result.is_err());
384
385 match result.unwrap_err() {
386 LagoError::Unauthorized => {}
387 _ => panic!("Expected Unauthorized error"),
388 }
389
390 mock.assert_async().await;
391 }
392
393 #[tokio::test]
394 async fn test_not_found_error() {
395 let mut server = Server::new_async().await;
396 let mock = server
397 .mock("GET", "/test")
398 .with_status(404)
399 .with_body("Not Found")
400 .create_async()
401 .await;
402
403 let client = create_test_client(&server.url());
404 let url = format!("{}/test", server.url());
405
406 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
407
408 assert!(result.is_err());
409
410 match result.unwrap_err() {
411 LagoError::Api { status, message } => {
412 assert_eq!(status, 404);
413 assert_eq!(message, "Not Found");
414 }
415 _ => panic!("Expected Api Error"),
416 }
417
418 mock.assert_async().await;
419 }
420
421 #[tokio::test]
422 async fn test_rate_limit_error() {
423 let mut server = Server::new_async().await;
424 let mock = server
425 .mock("GET", "/test")
426 .with_status(429)
427 .with_body("Rate Limited")
428 .create_async()
429 .await;
430
431 let client = create_test_client(&server.url());
432 let url = format!("{}/test", server.url());
433
434 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
435
436 assert!(result.is_err());
437
438 match result.unwrap_err() {
439 LagoError::RateLimit => {}
440 _ => panic!("Expected RateLimit error"),
441 }
442
443 mock.assert_async().await;
444 }
445
446 #[tokio::test]
447 async fn test_server_error() {
448 let mut server = Server::new_async().await;
449 let mock = server
450 .mock("GET", "/test")
451 .with_status(500)
452 .with_body("Internal Server Error")
453 .create_async()
454 .await;
455
456 let client = create_test_client(&server.url());
457 let url = format!("{}/test", server.url());
458
459 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
460
461 assert!(result.is_err());
462
463 match result.unwrap_err() {
464 LagoError::Api { status, message } => {
465 assert_eq!(status, 500);
466 assert_eq!(message, "Internal Server Error");
467 }
468 _ => panic!("Expected Api Error"),
469 }
470
471 mock.assert_async().await;
472 }
473
474 #[tokio::test]
475 async fn test_json_deserialization_error() {
476 let mut server = Server::new_async().await;
477 let mock = server
478 .mock("GET", "/test")
479 .with_status(200)
480 .with_body("invalid json")
481 .create_async()
482 .await;
483
484 let client = create_test_client(&server.url());
485 let url = format!("{}/test", server.url());
486
487 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
488
489 assert!(result.is_err());
490
491 match result.unwrap_err() {
492 LagoError::Serialization(_) => {}
493 _ => panic!("Expected Serialization error"),
494 }
495
496 mock.assert_async().await;
497 }
498
499 #[tokio::test]
500 async fn test_retry_on_server_error() {
501 let mut server = Server::new_async().await;
502 let mock = server
503 .mock("GET", "/test")
504 .with_status(500)
505 .with_body("Server Error")
506 .expect(4)
507 .create_async()
508 .await;
509
510 let client = create_retry_client(&server.url(), 3);
511 let url = format!("{}/test", server.url());
512
513 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
514
515 assert!(result.is_err());
516 mock.assert_async().await;
517 }
518
519 #[tokio::test]
520 async fn test_retry_then_success() {
521 let mut server = Server::new_async().await;
522
523 let mock_fail = server
524 .mock("GET", "/test")
525 .with_status(500)
526 .with_body("Server Error")
527 .expect(2)
528 .create_async()
529 .await;
530
531 let mock_success = server
532 .mock("GET", "/test")
533 .with_status(200)
534 .with_body(json!({"id": "123", "name": "Success"}).to_string())
535 .expect(1)
536 .create_async()
537 .await;
538
539 let client = create_retry_client(&server.url(), 5);
540 let url = format!("{}/test", server.url());
541
542 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
543
544 assert!(result.is_ok());
545
546 let response = result.unwrap();
547 assert_eq!(response.id, "123");
548 assert_eq!(response.name, "Success");
549
550 mock_fail.assert_async().await;
551 mock_success.assert_async().await;
552 }
553
554 #[tokio::test]
555 async fn test_no_retry_on_client_error() {
556 let mut server = Server::new_async().await;
557 let mock = server
558 .mock("GET", "/test")
559 .with_status(400)
560 .with_body("Bad Request")
561 .expect(1)
562 .create_async()
563 .await;
564
565 let client = create_retry_client(&server.url(), 3);
566 let url = format!("{}/test", server.url());
567
568 let result: Result<TestResponse> = client.make_request("GET", &url, None::<&()>).await;
569
570 assert!(result.is_err());
571 mock.assert_async().await;
572 }
573
574 #[tokio::test]
575 async fn test_should_retry_logic() {
576 let client = create_retry_client("http://localhost:8080", 3);
577
578 let rate_limit_error = LagoError::RateLimit;
579 assert!(client.should_retry(&rate_limit_error, 1));
580
581 let server_error = LagoError::Api {
582 status: 500,
583 message: "Server Error".to_string(),
584 };
585 assert!(client.should_retry(&server_error, 1));
586
587 let client_error = LagoError::Api {
588 status: 400,
589 message: "Bad Request".to_string(),
590 };
591 assert!(!client.should_retry(&client_error, 1));
592
593 assert!(!client.should_retry(&server_error, 3));
594
595 let auth_error = LagoError::Unauthorized;
596 assert!(!client.should_retry(&auth_error, 1));
597
598 let client_no_retry = create_test_client("http://localhost:8080");
599 assert!(!client_no_retry.should_retry(&server_error, 1));
600 }
601
602 #[tokio::test]
603 async fn test_timeout_handling() {
604 let config = Config::builder()
606 .credentials(Credentials::new("test-api-key".to_string()))
607 .region(Region::Custom("http://10.255.255.1:80".to_string()))
608 .timeout(Duration::from_millis(100))
609 .build();
610
611 let client = LagoClient::new(config);
612 let url = "http://10.255.255.1:80/test";
613
614 let result: Result<TestResponse> = client.make_request("GET", url, None::<&()>).await;
615
616 assert!(result.is_err());
617
618 match result.unwrap_err() {
619 LagoError::Http(_) => {}
620 other => panic!("Expected HTTP error, got: {other:?}"),
621 }
622 }
623
624 #[tokio::test]
625 async fn test_empty_response_body() {
626 #[derive(Debug, Default, Deserialize)]
628 struct EmptyResponse {}
629
630 let mut server = Server::new_async().await;
631 let mock = server
632 .mock("POST", "/test")
633 .with_status(200)
634 .with_body("")
635 .create_async()
636 .await;
637
638 let client = create_test_client(&server.url());
639 let url = format!("{}/test", server.url());
640
641 let result: Result<EmptyResponse> = client.make_request("POST", &url, None::<&()>).await;
642
643 assert!(result.is_ok());
644 mock.assert_async().await;
645 }
646}