1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::{
6 Config, Result,
7 list_opts::{ListOptions, ListResponse},
8 types::{
9 CreateTopicOptions, CreateTopicResponse, DeleteTopicResponse, Topic, UpdateTopicOptions,
10 UpdateTopicResponse,
11 },
12};
13
14#[derive(Clone, Debug)]
16pub struct TopicsSvc(pub(crate) Arc<Config>);
17
18impl TopicsSvc {
19 #[maybe_async::maybe_async]
23 pub async fn create(&self, topic: CreateTopicOptions) -> Result<CreateTopicResponse> {
24 let request = self.0.build(Method::POST, "/topics");
25 let response = self.0.send(request.json(&topic)).await?;
26 let content = response.json::<CreateTopicResponse>().await?;
27
28 Ok(content)
29 }
30
31 #[maybe_async::maybe_async]
35 pub async fn get(&self, topic_id: &str) -> Result<Topic> {
36 let path = format!("/topics/{topic_id}");
37
38 let request = self.0.build(Method::GET, &path);
39 let response = self.0.send(request).await?;
40 let content = response.json::<Topic>().await?;
41
42 Ok(content)
43 }
44
45 #[maybe_async::maybe_async]
49 pub async fn update(
50 &self,
51 topic_id: &str,
52 update: UpdateTopicOptions,
53 ) -> Result<UpdateTopicResponse> {
54 let path = format!("/topics/{topic_id}");
55
56 let request = self.0.build(Method::PATCH, &path);
57 let response = self.0.send(request.json(&update)).await?;
58 let content = response.json::<UpdateTopicResponse>().await?;
59
60 Ok(content)
61 }
62
63 #[maybe_async::maybe_async]
67 pub async fn delete(&self, topic_id: &str) -> Result<DeleteTopicResponse> {
68 let path = format!("/topics/{topic_id}");
69
70 let request = self.0.build(Method::DELETE, &path);
71 let response = self.0.send(request).await?;
72 let content = response.json::<DeleteTopicResponse>().await?;
73
74 Ok(content)
75 }
76
77 #[maybe_async::maybe_async]
83 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Topic>> {
84 let request = self.0.build(Method::GET, "/topics").query(&list_opts);
85 let response = self.0.send(request).await?;
86 let content = response.json::<ListResponse<Topic>>().await?;
87
88 Ok(content)
89 }
90}
91
92#[allow(unreachable_pub)]
93pub mod types {
94 use serde::{Deserialize, Serialize};
95
96 crate::define_id_type!(TopicId);
97
98 #[must_use]
102 #[derive(Debug, Clone, Serialize)]
103 pub struct CreateTopicOptions {
104 name: String,
105 default_subscription: SubscriptionType,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 description: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 visibility: Option<TopicVisibility>,
110 }
111
112 #[must_use]
113 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
114 #[serde(rename_all = "snake_case")]
115 pub enum SubscriptionType {
116 OptIn,
117 OptOut,
118 }
119
120 impl CreateTopicOptions {
121 pub fn new(name: impl Into<String>, default_subscription: SubscriptionType) -> Self {
126 Self {
127 name: name.into(),
128 default_subscription,
129 description: None,
130 visibility: None,
131 }
132 }
133
134 #[inline]
136 pub fn with_description(mut self, description: String) -> Self {
137 self.description = Some(description);
138 self
139 }
140
141 #[inline]
143 pub const fn with_visibility(mut self, visibility: TopicVisibility) -> Self {
144 self.visibility = Some(visibility);
145 self
146 }
147 }
148
149 #[must_use]
150 #[derive(Debug, Clone, Serialize, Deserialize)]
151 pub struct CreateTopicResponse {
152 pub id: TopicId,
154 }
155
156 #[must_use]
158 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159 pub struct Topic {
160 pub id: TopicId,
161 pub name: String,
162 pub description: Option<String>,
163 pub default_subscription: SubscriptionType,
164 pub visibility: TopicVisibility,
165 pub created_at: String,
166 }
167
168 #[must_use]
169 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
170 #[serde(rename_all = "kebab-case")]
171 pub enum TopicVisibility {
172 Public,
174 Private,
176 }
177
178 #[must_use]
180 #[derive(Debug, Default, Clone, Serialize)]
181 pub struct UpdateTopicOptions {
182 name: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 description: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 visibility: Option<TopicVisibility>,
187 }
188
189 impl UpdateTopicOptions {
190 pub const fn new() -> Self {
191 Self {
192 name: None,
193 description: None,
194 visibility: None,
195 }
196 }
197
198 #[inline]
200 pub fn with_name(mut self, name: impl Into<String>) -> Self {
201 self.name = Some(name.into());
202 self
203 }
204
205 #[inline]
207 pub fn with_description(mut self, description: impl Into<String>) -> Self {
208 self.description = Some(description.into());
209 self
210 }
211
212 #[inline]
214 pub const fn with_visibility(mut self, visibility: TopicVisibility) -> Self {
215 self.visibility = Some(visibility);
216 self
217 }
218 }
219
220 #[derive(Debug, Clone, Serialize, Deserialize)]
221 pub struct UpdateTopicResponse {
222 pub id: TopicId,
224 }
225
226 #[derive(Debug, Clone, Serialize, Deserialize)]
227 pub struct DeleteTopicResponse {
228 pub id: TopicId,
230 pub deleted: bool,
232 }
233}
234
235#[cfg(test)]
236#[allow(clippy::unwrap_used)]
237#[allow(clippy::needless_return)]
238mod test {
239 use crate::types::Topic;
240 #[cfg(not(feature = "blocking"))]
241 use crate::{
242 list_opts::ListOptions,
243 test::{CLIENT, DebugResult},
244 types::{CreateTopicOptions, SubscriptionType, TopicVisibility, UpdateTopicOptions},
245 };
246
247 #[tokio_shared_rt::test(shared = true)]
248 #[serial_test::serial]
249 #[cfg(not(feature = "blocking"))]
250 async fn all() -> DebugResult<()> {
251 let resend = &*CLIENT;
252
253 let topic = CreateTopicOptions::new("Weekly Newsletter", SubscriptionType::OptIn)
255 .with_visibility(TopicVisibility::Public);
256 let topic = resend.topics.create(topic).await?;
257 std::thread::sleep(std::time::Duration::from_secs(1));
258
259 let topic = resend.topics.get(&topic.id).await?;
261 assert_eq!(topic.visibility, TopicVisibility::Public);
262
263 let update = UpdateTopicOptions::new()
265 .with_name("Weekly Newsletter")
266 .with_description("Weekly newsletter for our subscribers")
267 .with_visibility(TopicVisibility::Private);
268 let topic = resend.topics.update(&topic.id, update).await?;
269 std::thread::sleep(std::time::Duration::from_secs(4));
270
271 let topics = resend.topics.list(ListOptions::default()).await?;
273 assert!(topics.len() == 1, "{}", format!("Was {}", topics.len()));
274 assert_eq!(
275 topics.data.first().unwrap().visibility,
276 TopicVisibility::Private
277 );
278
279 let deleted = resend.topics.delete(&topic.id).await?;
281 assert!(deleted.deleted);
282
283 std::thread::sleep(std::time::Duration::from_secs(4));
284
285 let topics = resend.topics.list(ListOptions::default()).await?;
286 assert!(topics.is_empty());
287
288 Ok(())
289 }
290
291 #[test]
292 fn deserialize_test() {
293 let topic = r#"{
294 "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
295 "name": "Weekly Newsletter",
296 "description": "Weekly newsletter for our subscribers",
297 "default_subscription": "opt_in",
298 "visibility": "public",
299 "created_at": "2023-04-08 00:11:13.110779+00"
300}"#;
301
302 let res = serde_json::from_str::<Topic>(topic);
303 assert!(res.is_ok());
304 }
305}