1use reqwest::Client;
4use serde::de::DeserializeOwned;
5
6use crate::error::{Error, new_api_error, new_connection_error};
7
8pub(crate) const DEFAULT_BASE_URL: &str = "https://api.emailit.com";
9pub(crate) const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
10
11pub(crate) struct BaseClient {
12 pub api_key: String,
13 pub base_url: String,
14 pub http_client: Client,
15}
16
17impl BaseClient {
18 pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
19 Self {
20 api_key: api_key.into(),
21 base_url: base_url.into(),
22 http_client: Client::new(),
23 }
24 }
25
26 pub async fn request<T: DeserializeOwned>(
27 &self,
28 method: &str,
29 path: &str,
30 body: Option<serde_json::Value>,
31 query: Option<&[(&str, String)]>,
32 ) -> Result<T, Error> {
33 let url = format!("{}{}", self.base_url, path);
34
35 let mut req = match method {
36 "GET" => self.http_client.get(&url),
37 "POST" => self.http_client.post(&url),
38 "PATCH" => self.http_client.patch(&url),
39 "DELETE" => self.http_client.delete(&url),
40 "PUT" => self.http_client.put(&url),
41 _ => self.http_client.get(&url),
42 };
43
44 req = req
45 .header("Authorization", format!("Bearer {}", self.api_key))
46 .header("Content-Type", "application/json")
47 .header("User-Agent", format!("emailit-rust/{}", SDK_VERSION));
48
49 if let Some(q) = query {
50 req = req.query(q);
51 }
52
53 if let Some(b) = body {
54 req = req.json(&b);
55 }
56
57 let resp = req
58 .send()
59 .await
60 .map_err(|e| new_connection_error(e.to_string()))?;
61
62 let status = resp.status().as_u16();
63 let body_text = resp
64 .text()
65 .await
66 .map_err(|e| new_connection_error(e.to_string()))?;
67
68 if status >= 400 {
69 let message = extract_error_message(&body_text, status);
70 return Err(new_api_error(status, message, body_text));
71 }
72
73 serde_json::from_str(&body_text)
74 .map_err(|e| new_connection_error(format!("Failed to parse response: {}", e)))
75 }
76
77 pub async fn request_raw(&self, method: &str, path: &str) -> Result<RawResponse, Error> {
78 let url = format!("{}{}", self.base_url, path);
79
80 let req = match method {
81 "GET" => self.http_client.get(&url),
82 _ => self.http_client.get(&url),
83 };
84
85 let resp = req
86 .header("Authorization", format!("Bearer {}", self.api_key))
87 .header("User-Agent", format!("emailit-rust/{}", SDK_VERSION))
88 .send()
89 .await
90 .map_err(|e| new_connection_error(e.to_string()))?;
91
92 let status = resp.status().as_u16();
93 let headers: Vec<(String, String)> = resp
94 .headers()
95 .iter()
96 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
97 .collect();
98
99 let body = resp
100 .bytes()
101 .await
102 .map_err(|e| new_connection_error(e.to_string()))?
103 .to_vec();
104
105 if status >= 400 {
106 let body_str = String::from_utf8_lossy(&body).to_string();
107 let message = extract_error_message(&body_str, status);
108 return Err(new_api_error(status, message, body_str));
109 }
110
111 Ok(RawResponse {
112 status,
113 headers,
114 body,
115 })
116 }
117}
118
119#[derive(Debug)]
122pub struct RawResponse {
123 pub status: u16,
125 pub headers: Vec<(String, String)>,
127 pub body: Vec<u8>,
129}
130
131fn extract_error_message(body: &str, status_code: u16) -> String {
132 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(body) {
133 if let Some(error) = parsed.get("error") {
134 if let Some(e) = error.as_str() {
135 let mut msg = e.to_string();
136 if let Some(m) = parsed.get("message").and_then(|v| v.as_str()) {
137 msg.push_str(": ");
138 msg.push_str(m);
139 }
140 return msg;
141 }
142 if let Some(m) = error.get("message").and_then(|v| v.as_str()) {
143 return m.to_string();
144 }
145 }
146 }
147
148 format!("API request failed with status {}", status_code)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_extract_error_string() {
157 let body = r#"{"error":"Unauthorized","message":"Invalid API key"}"#;
158 assert_eq!(
159 extract_error_message(body, 401),
160 "Unauthorized: Invalid API key"
161 );
162 }
163
164 #[test]
165 fn test_extract_error_nested() {
166 let body = r#"{"error":{"message":"Validation failed"}}"#;
167 assert_eq!(extract_error_message(body, 422), "Validation failed");
168 }
169
170 #[test]
171 fn test_extract_error_fallback() {
172 assert_eq!(
173 extract_error_message("not json", 500),
174 "API request failed with status 500"
175 );
176 }
177
178 #[test]
179 fn test_extract_error_string_only() {
180 let body = r#"{"error":"Not Found"}"#;
181 assert_eq!(extract_error_message(body, 404), "Not Found");
182 }
183}