1use std::sync::Arc;
2
3use reqwest::Method;
4
5use crate::types::{
6 CreateTemplateOptions, CreateTemplateResponse, DeleteTemplateResponse,
7 DuplicateTemplateResponse, PublishTemplateResponse, Template, UpdateTemplateOptions,
8 UpdateTemplateResponse,
9};
10use crate::{
11 Config, Result,
12 list_opts::{ListOptions, ListResponse},
13};
14
15#[derive(Clone, Debug)]
17pub struct TemplateSvc(pub(crate) Arc<Config>);
18
19impl TemplateSvc {
20 #[maybe_async::maybe_async]
24 #[allow(clippy::needless_pass_by_value)]
25 pub async fn create(&self, template: CreateTemplateOptions) -> Result<CreateTemplateResponse> {
26 let request = self.0.build(Method::POST, "/templates");
27 let response = self.0.send(request.json(&template)).await?;
28 let content = response.json::<CreateTemplateResponse>().await?;
29
30 Ok(content)
31 }
32
33 #[maybe_async::maybe_async]
37 pub async fn get(&self, id_or_alias: &str) -> Result<Template> {
38 let path = format!("/templates/{id_or_alias}");
39
40 let request = self.0.build(Method::GET, &path);
41 let response = self.0.send(request).await?;
42 let content = response.json::<Template>().await?;
43
44 Ok(content)
45 }
46
47 #[maybe_async::maybe_async]
51 #[allow(clippy::needless_pass_by_value)]
52 pub async fn update(
53 &self,
54 id_or_alias: &str,
55 update: UpdateTemplateOptions,
56 ) -> Result<UpdateTemplateResponse> {
57 let path = format!("/templates/{id_or_alias}");
58
59 let request = self.0.build(Method::PATCH, &path);
60 let response = self.0.send(request.json(&update)).await?;
61 let content = response.json::<UpdateTemplateResponse>().await?;
62
63 Ok(content)
64 }
65
66 #[maybe_async::maybe_async]
70 pub async fn publish(&self, id_or_alias: &str) -> Result<PublishTemplateResponse> {
71 let path = format!("/templates/{id_or_alias}/publish");
72
73 let request = self.0.build(Method::POST, &path);
74 let response = self.0.send(request).await?;
75 let content = response.json::<PublishTemplateResponse>().await?;
76
77 Ok(content)
78 }
79
80 #[maybe_async::maybe_async]
84 pub async fn duplicate(&self, id_or_alias: &str) -> Result<DuplicateTemplateResponse> {
85 let path = format!("/templates/{id_or_alias}/duplicate");
86
87 let request = self.0.build(Method::POST, &path);
88 let response = self.0.send(request).await?;
89 let content = response.json::<DuplicateTemplateResponse>().await?;
90
91 Ok(content)
92 }
93
94 #[maybe_async::maybe_async]
98 pub async fn delete(&self, id_or_alias: &str) -> Result<DeleteTemplateResponse> {
99 let path = format!("/templates/{id_or_alias}");
100
101 let request = self.0.build(Method::DELETE, &path);
102 let response = self.0.send(request).await?;
103 let content = response.json::<DeleteTemplateResponse>().await?;
104
105 Ok(content)
106 }
107
108 #[maybe_async::maybe_async]
114 #[allow(clippy::needless_pass_by_value)]
115 pub async fn list<T>(&self, list_opts: ListOptions<T>) -> Result<ListResponse<Template>> {
116 let request = self.0.build(Method::GET, "/templates").query(&list_opts);
117 let response = self.0.send(request).await?;
118 let content = response.json::<ListResponse<Template>>().await?;
119
120 Ok(content)
121 }
122}
123
124#[allow(unreachable_pub)]
125pub mod types {
126 use serde::{Deserialize, Deserializer, Serialize};
127 crate::define_id_type!(TemplateId);
128
129 #[must_use]
133 #[derive(Debug, Clone, Serialize)]
134 pub struct CreateTemplateOptions {
135 name: String,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 alias: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 from: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 subject: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 reply_to: Option<Vec<String>>,
144 html: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 text: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 variables: Option<Vec<Variable>>,
149 }
150
151 #[must_use]
155 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156 pub struct Variable {
157 key: String,
158 #[serde(rename = "type")]
159 ttype: VariableType,
160 fallback_value: Option<serde_json::Value>,
161 }
162
163 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
164 #[must_use]
165 #[serde(rename_all = "snake_case")]
166 pub enum VariableType {
167 String,
168 Number,
169 }
170
171 impl CreateTemplateOptions {
172 pub fn new(name: impl Into<String>, html: impl Into<String>) -> Self {
177 Self {
178 name: name.into(),
179 alias: None,
180 from: None,
181 subject: None,
182 reply_to: None,
183 html: html.into(),
184 text: None,
185 variables: None,
186 }
187 }
188
189 #[inline]
191 pub fn with_alias(mut self, alias: &str) -> Self {
192 self.alias = Some(alias.to_owned());
193 self
194 }
195
196 #[inline]
202 pub fn with_from(mut self, from: &str) -> Self {
203 self.from = Some(from.to_owned());
204 self
205 }
206
207 #[inline]
211 pub fn with_subject(mut self, subject: &str) -> Self {
212 self.subject = Some(subject.to_owned());
213 self
214 }
215
216 #[inline]
220 pub fn with_reply_to(mut self, reply_to: &str) -> Self {
221 let reply_to_vec = self.reply_to.get_or_insert_with(Vec::new);
222 reply_to_vec.push(reply_to.to_owned());
223 self
224 }
225
226 #[inline]
230 pub fn with_reply_tos(mut self, reply_tos: &[String]) -> Self {
231 let reply_to_vec = self.reply_to.get_or_insert_with(Vec::new);
232 reply_to_vec.extend_from_slice(reply_tos);
233 self
234 }
235
236 #[inline]
241 pub fn with_text(mut self, text: &str) -> Self {
242 self.text = Some(text.to_owned());
243 self
244 }
245
246 #[inline]
250 #[allow(clippy::needless_pass_by_value)]
251 pub fn with_variable(mut self, variable: Variable) -> Self {
252 let variables = self.variables.get_or_insert_with(Vec::new);
253 variables.push(variable);
254 self
255 }
256
257 #[inline]
261 #[allow(clippy::needless_pass_by_value)]
262 pub fn with_variables(mut self, variables: &[Variable]) -> Self {
263 let variables_vec = self.variables.get_or_insert_with(Vec::new);
264 variables_vec.extend_from_slice(variables);
265 self
266 }
267 }
268
269 impl Variable {
270 pub fn new(key: impl Into<String>, ttype: VariableType) -> Self {
276 Self {
277 key: key.into(),
278 ttype,
279 fallback_value: None,
280 }
281 }
282
283 #[inline]
292 #[allow(clippy::needless_pass_by_value)]
293 pub fn with_fallback(mut self, fallback: impl Into<serde_json::Value>) -> Self {
294 self.fallback_value = Some(fallback.into());
295 self
296 }
297 }
298
299 #[derive(Debug, Clone, Deserialize)]
300 pub struct CreateTemplateResponse {
301 pub id: TemplateId,
303 }
304
305 #[must_use]
307 #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
308 pub struct Template {
309 pub id: TemplateId,
310 pub alias: Option<String>,
311 pub name: String,
312 pub created_at: String,
313 pub updated_at: String,
314 pub status: TemplateEvent,
315 pub published_at: Option<String>,
316 pub from: Option<String>,
317 pub subject: Option<String>,
318 pub reply_to: Option<Vec<String>>,
319 pub html: Option<String>,
320 pub text: Option<String>,
321 #[serde(deserialize_with = "parse_nullable_vec")]
322 #[serde(default)]
323 pub variables: Vec<Variable>,
324 }
325
326 fn parse_nullable_vec<'de, D>(deserializer: D) -> Result<Vec<Variable>, D::Error>
330 where
331 D: Deserializer<'de>,
332 {
333 let opt = Option::deserialize(deserializer)?;
334 Ok(opt.unwrap_or_else(Vec::new))
335 }
336
337 #[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq)]
339 #[serde(rename_all = "snake_case")]
340 pub enum TemplateEvent {
341 Draft,
342 Published,
343 }
344
345 #[must_use]
347 #[derive(Debug, Default, Clone, Serialize)]
348 pub struct UpdateTemplateOptions {
349 name: String,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 alias: Option<String>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 from: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 subject: Option<String>,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 reply_to: Option<Vec<String>>,
358 html: String,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 text: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 variables: Option<Vec<Variable>>,
363 }
364
365 impl UpdateTemplateOptions {
366 pub fn new(name: impl Into<String>, html: impl Into<String>) -> Self {
367 Self {
368 name: name.into(),
369 alias: None,
370 from: None,
371 subject: None,
372 reply_to: None,
373 html: html.into(),
374 text: None,
375 variables: None,
376 }
377 }
378
379 #[inline]
380 pub fn with_alias(mut self, alias: &str) -> Self {
381 self.alias = Some(alias.to_owned());
382 self
383 }
384
385 #[inline]
386 pub fn with_from(mut self, from: &str) -> Self {
387 self.from = Some(from.to_owned());
388 self
389 }
390
391 #[inline]
392 pub fn with_subject(mut self, subject: &str) -> Self {
393 self.subject = Some(subject.to_owned());
394 self
395 }
396
397 #[inline]
398 pub fn with_reply_to(mut self, reply_to: &str) -> Self {
399 let reply_tos = self.reply_to.get_or_insert_with(Vec::new);
400 reply_tos.push(reply_to.to_owned());
401 self
402 }
403
404 #[inline]
405 pub fn with_reply_tos(mut self, reply_tos: &[String]) -> Self {
406 let reply_tos_vec = self.reply_to.get_or_insert_with(Vec::new);
407 reply_tos_vec.extend_from_slice(reply_tos);
408 self
409 }
410
411 #[inline]
412 pub fn with_text(mut self, text: &str) -> Self {
413 self.text = Some(text.to_owned());
414 self
415 }
416
417 #[inline]
418 #[allow(clippy::needless_pass_by_value)]
419 pub fn with_variable(mut self, variable: Variable) -> Self {
420 let variables_vec = self.variables.get_or_insert_with(Vec::new);
421 variables_vec.push(variable);
422 self
423 }
424
425 #[inline]
426 #[allow(clippy::needless_pass_by_value)]
427 pub fn with_variables(mut self, variables: &[Variable]) -> Self {
428 let variables_vec = self.variables.get_or_insert_with(Vec::new);
429 variables_vec.extend_from_slice(variables);
430 self
431 }
432 }
433
434 #[derive(Debug, Clone, Deserialize)]
435 pub struct UpdateTemplateResponse {
436 pub id: TemplateId,
438 }
439
440 #[derive(Debug, Clone, Deserialize)]
441 pub struct PublishTemplateResponse {
442 pub id: TemplateId,
444 }
445
446 #[derive(Debug, Clone, Deserialize)]
447 pub struct DuplicateTemplateResponse {
448 pub id: TemplateId,
450 }
451
452 #[derive(Debug, Clone, Deserialize)]
453 pub struct DeleteTemplateResponse {
454 pub id: TemplateId,
456 pub deleted: bool,
458 }
459}
460
461#[cfg(test)]
462#[allow(clippy::unwrap_used)]
463#[allow(clippy::needless_return)]
464mod test {
465 use crate::{
466 templates::Template,
467 test::{CLIENT, DebugResult},
468 types::CreateTemplateOptions,
469 };
470
471 #[tokio_shared_rt::test(shared = true)]
472 #[cfg(not(feature = "blocking"))]
473 async fn all() -> DebugResult<()> {
474 use crate::{list_opts::ListOptions, types::UpdateTemplateOptions};
475
476 let resend = &*CLIENT;
477
478 let name = "my template";
479 let html = "<p>hello</p>";
480 let alias = "alias";
481
482 let template = CreateTemplateOptions::new(name, html).with_alias(alias);
484
485 let template = resend.templates.create(template).await?;
486 let id = template.id;
487
488 std::thread::sleep(std::time::Duration::from_secs(1));
489
490 let get_alias = resend.templates.get(alias).await?;
491 let get_id = resend.templates.get(&id).await?;
492 assert_eq!(get_alias, get_id);
493
494 let alias = "alias updated";
496 let template = resend.templates.get(alias).await;
497 assert!(template.is_err());
498
499 let update = UpdateTemplateOptions::new(name, html).with_alias(alias);
500 let _update = resend.templates.update("alias", update).await?;
501 std::thread::sleep(std::time::Duration::from_secs(1));
502
503 let template = resend.templates.get(alias).await;
505 assert!(template.is_ok());
506
507 let template = resend.templates.get(alias).await?;
509 assert!(template.published_at.is_none());
510
511 let template = resend.templates.publish(alias).await?;
512 std::thread::sleep(std::time::Duration::from_secs(1));
513
514 let template = resend.templates.get(&template.id).await?;
515 assert!(template.published_at.is_some());
516
517 let templates = resend.templates.list(ListOptions::default()).await?;
519 assert!(templates.len() == 1);
520
521 let duplicate = resend.templates.duplicate(alias).await?;
523 assert!(duplicate.id != template.id);
524 std::thread::sleep(std::time::Duration::from_secs(1));
525 let templates = resend.templates.list(ListOptions::default()).await?;
526 assert!(templates.len() == 2);
527
528 let deleted = resend.templates.delete(alias).await?;
530 assert!(deleted.deleted);
531 let deleted = resend.templates.delete(&duplicate.id).await;
532 assert!(deleted.is_ok());
533 std::thread::sleep(std::time::Duration::from_secs(1));
534
535 let deleted = resend.templates.delete(&duplicate.id).await;
536 assert!(deleted.is_err());
537
538 Ok(())
539 }
540
541 #[test]
542 fn deserialize_test() {
543 let template = r#"{
544 "object": "template",
545 "id": "34a080c9-b17d-4187-ad80-5af20266e535",
546 "alias": "reset-password",
547 "name": "reset-password",
548 "created_at": "2023-10-06T23:47:56.678Z",
549 "updated_at": "2023-10-06T23:47:56.678Z",
550 "status": "published",
551 "published_at": "2023-10-06T23:47:56.678Z",
552 "from": "John Doe <john.doe@example.com>",
553 "subject": "Hello, world!",
554 "reply_to": null,
555 "html": "<h1>Hello, world!</h1>",
556 "text": "Hello, world!",
557 "variables": [
558 {
559 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
560 "key": "user_name",
561 "type": "string",
562 "fallback_value": "John Doe",
563 "created_at": "2023-10-06T23:47:56.678Z",
564 "updated_at": "2023-10-06T23:47:56.678Z"
565 }
566 ]
567}"#;
568
569 let res = serde_json::from_str::<Template>(template);
570 assert!(res.is_ok());
571
572 let res = res.unwrap();
573 assert!(!res.variables.is_empty());
574
575 let template = r#"{
576 "object": "template",
577 "id": "34a080c9-b17d-4187-ad80-5af20266e535",
578 "alias": "reset-password",
579 "name": "reset-password",
580 "created_at": "2023-10-06T23:47:56.678Z",
581 "updated_at": "2023-10-06T23:47:56.678Z",
582 "status": "published",
583 "published_at": "2023-10-06T23:47:56.678Z",
584 "from": "John Doe <john.doe@example.com>",
585 "subject": "Hello, world!",
586 "reply_to": null,
587 "html": "<h1>Hello, world!</h1>",
588 "text": "Hello, world!"
589}"#;
590
591 let res = serde_json::from_str::<Template>(template);
592 assert!(res.is_ok());
593
594 let res = res.unwrap();
595 assert!(res.variables.is_empty());
596 }
597}