Skip to main content

emailit/
client.rs

1//! Low-level HTTP client used internally by all service modules.
2
3use 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/// A raw HTTP response returned by endpoints that don't have a typed JSON body
120/// (e.g. CSV exports).
121#[derive(Debug)]
122pub struct RawResponse {
123    /// HTTP status code (e.g. 200).
124    pub status: u16,
125    /// Response headers as `(name, value)` pairs.
126    pub headers: Vec<(String, String)>,
127    /// Raw response body bytes.
128    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}