use crate::email::Email; use crate::error::{ApiError, Error, Result}; use crate::response::SendResponse; use crate::util::normalize_non_empty; use crate::wire::{WireEmail, WireSendResponse}; use reqwest::{Client as HttpClient, Url}; const DEFAULT_BASE_URL: &str = "https://api.useplunk.com/"; /// Async client for Plunk's transactional email API. /// /// ```no_run /// use plunk_rs::{Client, Email, EmailAddress}; /// /// # async fn demo() -> Result<(), Box> { /// let client = Client::new("sk_your_secret_key")?; /// /// let email = Email::html("user@example.com", "Welcome", "

Hello

")? /// .from(EmailAddress::named("My App", "hello@example.com")?)? /// .reply_to("support@example.com")?; /// /// let response = client.send(&email).await?; /// assert_eq!(response.deliveries().len(), 1); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug)] pub struct Client { http: HttpClient, api_key: String, base_url: Url, } impl Client { /// Creates a client that targets Plunk's hosted API. pub fn new(api_key: impl Into) -> Result { Self::builder(api_key).build() } /// Creates a builder for custom HTTP clients or self-hosted Plunk URLs. pub fn builder(api_key: impl Into) -> ClientBuilder { ClientBuilder::new(api_key) } /// Sends a validated email via `POST /v1/send`. pub async fn send(&self, email: &Email) -> Result { let response = self .http .post(self.base_url.join("v1/send").expect("valid send endpoint")) .bearer_auth(&self.api_key) .json(&WireEmail::from(email)) .send() .await?; let status = response.status(); if !status.is_success() { let body = response.text().await?; return Err(Error::Api(ApiError::from_response(status, body))); } let payload = response.json::().await?; if !payload.success { return Err(Error::UnexpectedResponse( "success=false in a successful API response".to_string(), )); } Ok(SendResponse::from(payload.data)) } pub fn base_url(&self) -> &Url { &self.base_url } } #[derive(Clone, Debug)] pub struct ClientBuilder { api_key: String, base_url: Url, http: HttpClient, } impl ClientBuilder { pub fn new(api_key: impl Into) -> Self { Self { api_key: api_key.into(), base_url: Url::parse(DEFAULT_BASE_URL).expect("default base URL must be valid"), http: HttpClient::new(), } } pub fn base_url(mut self, base_url: impl AsRef) -> Result { let mut parsed = Url::parse(base_url.as_ref())?; if !parsed.path().ends_with('/') { parsed.set_path(&format!("{}/", parsed.path())); } self.base_url = parsed; Ok(self) } pub fn http_client(mut self, http: HttpClient) -> Self { self.http = http; self } pub fn build(self) -> Result { let api_key = normalize_non_empty(self.api_key, Error::InvalidApiKey)?; Ok(Client { http: self.http, api_key, base_url: self.base_url, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn builder_normalizes_base_url_for_self_hosting() { let client = Client::builder("sk_test") .base_url("https://plunk.example.com/api") .unwrap() .build() .unwrap(); assert_eq!(client.base_url().as_str(), "https://plunk.example.com/api/"); } }