1use crate::email::Email;
2use crate::error::{ApiError, Error, Result};
3use crate::response::SendResponse;
4use crate::util::normalize_non_empty;
5use crate::wire::{WireEmail, WireSendResponse};
6use reqwest::{Client as HttpClient, Url};
7
8const DEFAULT_BASE_URL: &str = "https://api.useplunk.com/";
9
10#[derive(Clone, Debug)]
28pub struct Client {
29 http: HttpClient,
30 api_key: String,
31 base_url: Url,
32}
33
34impl Client {
35 pub fn new(api_key: impl Into<String>) -> Result<Self> {
37 Self::builder(api_key).build()
38 }
39
40 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
42 ClientBuilder::new(api_key)
43 }
44
45 pub async fn send(&self, email: &Email) -> Result<SendResponse> {
47 let response = self
48 .http
49 .post(self.base_url.join("v1/send").expect("valid send endpoint"))
50 .bearer_auth(&self.api_key)
51 .json(&WireEmail::from(email))
52 .send()
53 .await?;
54
55 let status = response.status();
56
57 if !status.is_success() {
58 let body = response.text().await?;
59 return Err(Error::Api(ApiError::from_response(status, body)));
60 }
61
62 let payload = response.json::<WireSendResponse>().await?;
63 if !payload.success {
64 return Err(Error::UnexpectedResponse(
65 "success=false in a successful API response".to_string(),
66 ));
67 }
68
69 Ok(SendResponse::from(payload.data))
70 }
71
72 pub fn base_url(&self) -> &Url {
73 &self.base_url
74 }
75}
76
77#[derive(Clone, Debug)]
78pub struct ClientBuilder {
79 api_key: String,
80 base_url: Url,
81 http: HttpClient,
82}
83
84impl ClientBuilder {
85 pub fn new(api_key: impl Into<String>) -> Self {
86 Self {
87 api_key: api_key.into(),
88 base_url: Url::parse(DEFAULT_BASE_URL).expect("default base URL must be valid"),
89 http: HttpClient::new(),
90 }
91 }
92
93 pub fn base_url(mut self, base_url: impl AsRef<str>) -> Result<Self> {
94 let mut parsed = Url::parse(base_url.as_ref())?;
95 if !parsed.path().ends_with('/') {
96 parsed.set_path(&format!("{}/", parsed.path()));
97 }
98 self.base_url = parsed;
99 Ok(self)
100 }
101
102 pub fn http_client(mut self, http: HttpClient) -> Self {
103 self.http = http;
104 self
105 }
106
107 pub fn build(self) -> Result<Client> {
108 let api_key = normalize_non_empty(self.api_key, Error::InvalidApiKey)?;
109
110 Ok(Client {
111 http: self.http,
112 api_key,
113 base_url: self.base_url,
114 })
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn builder_normalizes_base_url_for_self_hosting() {
124 let client = Client::builder("sk_test")
125 .base_url("https://plunk.example.com/api")
126 .unwrap()
127 .build()
128 .unwrap();
129
130 assert_eq!(client.base_url().as_str(), "https://plunk.example.com/api/");
131 }
132}