1use anyhow::{Context, Result};
4use serde::{de::DeserializeOwned, Serialize};
5
6pub struct Client {
7 base_url: String,
8 client: reqwest::Client,
9}
10
11impl Client {
12 pub fn new(base_url: &str) -> Result<Self> {
14 let client = reqwest::Client::builder()
15 .timeout(std::time::Duration::from_secs(30))
16 .build()
17 .context("Failed to create HTTP client")?;
18
19 Ok(Self {
20 base_url: base_url.to_string(),
21 client,
22 })
23 }
24
25 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
27 let url = format!("{}{}", self.base_url, path);
28 let response = self
29 .client
30 .get(&url)
31 .send()
32 .await
33 .context("Failed to send GET request")?;
34
35 if !response.status().is_success() {
36 let status = response.status();
37 let text = response.text().await.unwrap_or_default();
38 anyhow::bail!("Request failed ({}): {}", status, text);
39 }
40
41 response
42 .json()
43 .await
44 .context("Failed to parse JSON response")
45 }
46
47 pub async fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
49 let url = format!("{}{}", self.base_url, path);
50 let response = self
51 .client
52 .post(&url)
53 .json(body)
54 .send()
55 .await
56 .context("Failed to send POST request")?;
57
58 if !response.status().is_success() {
59 let status = response.status();
60 let text = response.text().await.unwrap_or_default();
61 anyhow::bail!("Request failed ({}): {}", status, text);
62 }
63
64 response
65 .json()
66 .await
67 .context("Failed to parse JSON response")
68 }
69
70 pub async fn put<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
72 let url = format!("{}{}", self.base_url, path);
73 let response = self
74 .client
75 .put(&url)
76 .json(body)
77 .send()
78 .await
79 .context("Failed to send PUT request")?;
80
81 if !response.status().is_success() {
82 let status = response.status();
83 let text = response.text().await.unwrap_or_default();
84 anyhow::bail!("Request failed ({}): {}", status, text);
85 }
86
87 response
88 .json()
89 .await
90 .context("Failed to parse JSON response")
91 }
92
93 pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
95 let url = format!("{}{}", self.base_url, path);
96 let response = self
97 .client
98 .delete(&url)
99 .send()
100 .await
101 .context("Failed to send DELETE request")?;
102
103 if !response.status().is_success() {
104 let status = response.status();
105 let text = response.text().await.unwrap_or_default();
106 anyhow::bail!("Request failed ({}): {}", status, text);
107 }
108
109 response
110 .json()
111 .await
112 .context("Failed to parse JSON response")
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_client_creation() {
122 let client = Client::new("http://localhost:8080").unwrap();
123 assert_eq!(client.base_url, "http://localhost:8080");
124 }
125
126 #[test]
127 fn test_url_formatting() {
128 let client = Client::new("http://localhost:8080").unwrap();
129 let url = format!("{}{}", client.base_url, "/api/users");
130 assert_eq!(url, "http://localhost:8080/api/users");
131 }
132
133 #[test]
134 fn test_client_with_https() {
135 let client = Client::new("https://mail.example.com:8080").unwrap();
136 assert_eq!(client.base_url, "https://mail.example.com:8080");
137 }
138
139 #[test]
140 fn test_url_formatting_with_query() {
141 let client = Client::new("http://localhost:8080").unwrap();
142 let url = format!("{}{}", client.base_url, "/api/queue?status=pending");
143 assert_eq!(url, "http://localhost:8080/api/queue?status=pending");
144 }
145
146 #[test]
147 fn test_client_with_trailing_slash() {
148 let client = Client::new("http://localhost:8080/").unwrap();
149 assert_eq!(client.base_url, "http://localhost:8080/");
150 }
151
152 #[test]
153 fn test_multiple_path_segments() {
154 let client = Client::new("http://localhost:8080").unwrap();
155 let url = format!(
156 "{}{}",
157 client.base_url, "/api/users/test@example.com/mailboxes"
158 );
159 assert_eq!(
160 url,
161 "http://localhost:8080/api/users/test@example.com/mailboxes"
162 );
163 }
164}