use reqwest::Client;
use serde::de::DeserializeOwned;
use crate::error::{Error, new_api_error, new_connection_error};
pub(crate) const DEFAULT_BASE_URL: &str = "https://api.emailit.com";
pub(crate) const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) struct BaseClient {
pub api_key: String,
pub base_url: String,
pub http_client: Client,
}
impl BaseClient {
pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: base_url.into(),
http_client: Client::new(),
}
}
pub async fn request<T: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<serde_json::Value>,
query: Option<&[(&str, String)]>,
) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
let mut req = match method {
"GET" => self.http_client.get(&url),
"POST" => self.http_client.post(&url),
"PATCH" => self.http_client.patch(&url),
"DELETE" => self.http_client.delete(&url),
"PUT" => self.http_client.put(&url),
_ => self.http_client.get(&url),
};
req = req
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.header("User-Agent", format!("emailit-rust/{}", SDK_VERSION));
if let Some(q) = query {
req = req.query(q);
}
if let Some(b) = body {
req = req.json(&b);
}
let resp = req
.send()
.await
.map_err(|e| new_connection_error(e.to_string()))?;
let status = resp.status().as_u16();
let body_text = resp
.text()
.await
.map_err(|e| new_connection_error(e.to_string()))?;
if status >= 400 {
let message = extract_error_message(&body_text, status);
return Err(new_api_error(status, message, body_text));
}
serde_json::from_str(&body_text)
.map_err(|e| new_connection_error(format!("Failed to parse response: {}", e)))
}
pub async fn request_raw(&self, method: &str, path: &str) -> Result<RawResponse, Error> {
let url = format!("{}{}", self.base_url, path);
let req = match method {
"GET" => self.http_client.get(&url),
_ => self.http_client.get(&url),
};
let resp = req
.header("Authorization", format!("Bearer {}", self.api_key))
.header("User-Agent", format!("emailit-rust/{}", SDK_VERSION))
.send()
.await
.map_err(|e| new_connection_error(e.to_string()))?;
let status = resp.status().as_u16();
let headers: Vec<(String, String)> = resp
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = resp
.bytes()
.await
.map_err(|e| new_connection_error(e.to_string()))?
.to_vec();
if status >= 400 {
let body_str = String::from_utf8_lossy(&body).to_string();
let message = extract_error_message(&body_str, status);
return Err(new_api_error(status, message, body_str));
}
Ok(RawResponse {
status,
headers,
body,
})
}
}
#[derive(Debug)]
pub struct RawResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
fn extract_error_message(body: &str, status_code: u16) -> String {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(body) {
if let Some(error) = parsed.get("error") {
if let Some(e) = error.as_str() {
let mut msg = e.to_string();
if let Some(m) = parsed.get("message").and_then(|v| v.as_str()) {
msg.push_str(": ");
msg.push_str(m);
}
return msg;
}
if let Some(m) = error.get("message").and_then(|v| v.as_str()) {
return m.to_string();
}
}
}
format!("API request failed with status {}", status_code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_error_string() {
let body = r#"{"error":"Unauthorized","message":"Invalid API key"}"#;
assert_eq!(
extract_error_message(body, 401),
"Unauthorized: Invalid API key"
);
}
#[test]
fn test_extract_error_nested() {
let body = r#"{"error":{"message":"Validation failed"}}"#;
assert_eq!(extract_error_message(body, 422), "Validation failed");
}
#[test]
fn test_extract_error_fallback() {
assert_eq!(
extract_error_message("not json", 500),
"API request failed with status 500"
);
}
#[test]
fn test_extract_error_string_only() {
let body = r#"{"error":"Not Found"}"#;
assert_eq!(extract_error_message(body, 404), "Not Found");
}
}