1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6 Config, Result,
7 list_opts::{ListOptions, ListResponse},
8 types::{
9 CreateWebhookOptions, CreateWebhookResponse, DeleteWebhookResponse, UpdateWebhookOptions,
10 UpdateWebhookResponse, Webhook,
11 },
12};
13
14#[derive(Clone, Debug)]
16pub struct WebhookSvc(pub(crate) Arc<Config>);
17
18impl WebhookSvc {
19 #[maybe_async::maybe_async]
23 pub async fn create(&self, webhook: CreateWebhookOptions) -> Result<CreateWebhookResponse> {
24 let request = self.0.build(Method::POST, "/webhooks");
25 let response = self.0.send(request.json(&webhook)).await?;
26 let content = response.json::<CreateWebhookResponse>().await?;
27
28 Ok(content)
29 }
30
31 #[maybe_async::maybe_async]
35 pub async fn get(&self, webhook_id: &str) -> Result<Webhook> {
36 let path = format!("/webhooks/{webhook_id}");
37
38 let request = self.0.build(Method::GET, &path);
39 let response = self.0.send(request).await?;
40 let content = response.json::<Webhook>().await?;
41
42 Ok(content)
43 }
44
45 #[maybe_async::maybe_async]
47 pub async fn update(
48 &self,
49 webhook_id: &str,
50 update: UpdateWebhookOptions,
51 ) -> Result<UpdateWebhookResponse> {
52 let path = format!("/webhooks/{webhook_id}");
53
54 let request = self.0.build(Method::PATCH, &path);
55 let response = self.0.send(request.json(&update)).await?;
56 let content = response.json::<UpdateWebhookResponse>().await?;
57
58 Ok(content)
59 }
60
61 #[maybe_async::maybe_async]
65 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Webhook>> {
66 let request = self.0.build(Method::GET, "/webhooks").query(&list_opts);
67 let response = self.0.send(request).await?;
68 let content = response.json::<ListResponse<Webhook>>().await?;
69
70 Ok(content)
71 }
72
73 #[maybe_async::maybe_async]
77 pub async fn delete(&self, webhook_id: &str) -> Result<bool> {
78 let path = format!("/webhooks/{webhook_id}");
79
80 let request = self.0.build(Method::DELETE, &path);
81 let response = self.0.send(request).await?;
82 let content = response.json::<DeleteWebhookResponse>().await?;
83
84 Ok(content.deleted)
85 }
86}
87
88#[allow(unreachable_pub)]
89pub mod types {
90 use serde::{Deserialize, Serialize};
91
92 use crate::events::EventType;
93
94 crate::define_id_type!(WebhookId);
95
96 #[must_use]
97 #[derive(Debug, Clone, Serialize)]
98 pub struct CreateWebhookOptions {
99 endpoint: String,
100 events: Vec<EventType>,
101 }
102
103 impl CreateWebhookOptions {
104 pub fn new(
105 endpoint: impl Into<String>,
106 events: impl IntoIterator<Item = impl Into<EventType>>,
107 ) -> Self {
108 Self {
109 endpoint: endpoint.into(),
110 events: events.into_iter().map(Into::into).collect::<Vec<_>>(),
111 }
112 }
113 }
114
115 #[must_use]
116 #[derive(Debug, Clone, Serialize, Deserialize)]
117 pub struct CreateWebhookResponse {
118 pub id: WebhookId,
119 pub signing_secret: String,
120 }
121
122 #[must_use]
123 #[derive(Debug, Clone, Serialize, Deserialize)]
124 pub struct Webhook {
125 pub id: WebhookId,
126 pub created_at: String,
127 pub status: String,
128 pub endpoint: String,
129 #[serde(default)]
130 pub events: Vec<EventType>,
131 }
132
133 #[derive(Debug, Clone, Copy, Serialize)]
134 #[serde(rename_all = "lowercase")]
135 pub enum WebhookStatus {
136 Enabled,
137 Disabled,
138 }
139
140 #[must_use]
141 #[derive(Debug, Clone, Serialize, Default)]
142 pub struct UpdateWebhookOptions {
143 #[serde(skip_serializing_if = "Option::is_none")]
144 endpoint: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 events: Option<Vec<EventType>>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 status: Option<WebhookStatus>,
149 }
150
151 impl UpdateWebhookOptions {
152 #[inline]
153 pub fn with_endpoint(mut self, endpoint: &str) -> Self {
154 self.endpoint = Some(endpoint.to_owned());
155 self
156 }
157
158 #[inline]
159 pub fn with_events(
160 mut self,
161 events: impl IntoIterator<Item = impl Into<EventType>>,
162 ) -> Self {
163 self.events = Some(events.into_iter().map(Into::into).collect());
164 self
165 }
166
167 #[inline]
168 pub fn with_status(mut self, status: WebhookStatus) -> Self {
169 self.status = Some(status);
170 self
171 }
172 }
173
174 #[derive(Debug, Clone, Serialize, Deserialize)]
175 pub struct UpdateWebhookResponse {
176 pub id: WebhookId,
178 }
179
180 #[derive(Debug, Clone, Serialize, Deserialize)]
181 pub struct DeleteWebhookResponse {
182 #[allow(dead_code)]
184 pub id: WebhookId,
185 pub deleted: bool,
187 }
188}
189
190#[cfg(test)]
191mod test {
192 use crate::{
193 events::EmailEventType,
194 types::Webhook,
195 types::{CreateWebhookOptions, CreateWebhookResponse},
196 };
197 #[cfg(not(feature = "blocking"))]
198 use crate::{
199 list_opts::ListOptions,
200 test::{CLIENT, DebugResult},
201 types::{UpdateWebhookOptions, WebhookStatus},
202 };
203
204 #[tokio_shared_rt::test(shared = true)]
205 #[serial_test::serial]
206 #[cfg(not(feature = "blocking"))]
207 async fn all() -> DebugResult<()> {
208 let resend = &*CLIENT;
209
210 let events = [
211 EmailEventType::EmailSent,
212 EmailEventType::EmailDelivered,
213 EmailEventType::EmailBounced,
214 ];
215 let webhook = CreateWebhookOptions::new("https://webhook.example.com/handler", events);
216 let id = resend.webhooks.create(webhook).await?.id;
217
218 std::thread::sleep(std::time::Duration::from_secs(1));
219
220 let webhook = resend.webhooks.get(&id).await?;
221 assert_eq!(webhook.events.len(), 3);
222 let webhooks = resend.webhooks.list(ListOptions::default()).await?;
223 assert!(!webhooks.is_empty());
224
225 let update = UpdateWebhookOptions::default()
226 .with_endpoint("https://new-webhook.example.com/handler")
227 .with_events([EmailEventType::EmailSent, EmailEventType::EmailDelivered])
228 .with_status(WebhookStatus::Enabled);
229 let _webhook = resend.webhooks.update(&id, update).await?;
230 std::thread::sleep(std::time::Duration::from_secs(1));
231 let webhook = resend.webhooks.get(&id).await?;
232 assert_eq!(webhook.events.len(), 2);
233
234 let deleted = resend.webhooks.delete(&id).await?;
235 assert!(deleted);
236 std::thread::sleep(std::time::Duration::from_secs(1));
237
238 let webhooks = resend.webhooks.list(ListOptions::default()).await?;
239 assert!(webhooks.is_empty());
240
241 Ok(())
242 }
243
244 #[test]
245 fn serialize_test() {
246 let events = [EmailEventType::EmailSent];
247
248 let webhook =
249 CreateWebhookOptions::new("https://webhook.example.com/handler".to_owned(), events);
250
251 let res = serde_json::to_string(&webhook);
252 assert!(res.is_ok());
253 }
254
255 #[test]
256 fn deserialize_test() {
257 let create_webhook_res = r#"{
258 "object": "webhook",
259 "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
260 "signing_secret": "whsec_xxxxxxxxxx"
261}"#;
262
263 let res = serde_json::from_str::<CreateWebhookResponse>(create_webhook_res);
264 assert!(res.is_ok());
265
266 let webhook = r#"{
267 "object": "webhook",
268 "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
269 "created_at": "2023-08-22 15:28:00+00",
270 "status": "enabled",
271 "endpoint": "https://webhook.example.com/handler",
272 "events": ["email.sent", "email.received"],
273 "signing_secret": "whsec_xxxxxxxxxx"
274}"#;
275
276 let res = serde_json::from_str::<Webhook>(webhook);
277 assert!(res.is_ok());
278 }
279}