Skip to main content

sendly/
contacts.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use crate::client::Sendly;
5use crate::error::Result;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Contact {
9    pub id: String,
10    #[serde(alias = "phoneNumber")]
11    pub phone_number: String,
12    #[serde(default)]
13    pub name: Option<String>,
14    #[serde(default)]
15    pub email: Option<String>,
16    #[serde(default)]
17    pub metadata: Option<HashMap<String, serde_json::Value>>,
18    #[serde(default, alias = "createdAt")]
19    pub created_at: Option<String>,
20    #[serde(default, alias = "updatedAt")]
21    pub updated_at: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ContactList {
26    pub id: String,
27    pub name: String,
28    #[serde(default)]
29    pub description: Option<String>,
30    #[serde(default, alias = "contactCount")]
31    pub contact_count: i32,
32    #[serde(default, alias = "createdAt")]
33    pub created_at: Option<String>,
34    #[serde(default, alias = "updatedAt")]
35    pub updated_at: Option<String>,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub struct ContactListResponse {
40    pub contacts: Vec<Contact>,
41    #[serde(default)]
42    pub total: i32,
43    #[serde(default)]
44    pub limit: i32,
45    #[serde(default)]
46    pub offset: i32,
47}
48
49#[derive(Debug, Clone, Deserialize)]
50pub struct ContactListsResponse {
51    pub lists: Vec<ContactList>,
52    #[serde(default)]
53    pub total: i32,
54    #[serde(default)]
55    pub limit: i32,
56    #[serde(default)]
57    pub offset: i32,
58}
59
60#[derive(Debug, Clone, Serialize)]
61pub struct CreateContactRequest {
62    #[serde(rename = "phone_number")]
63    pub phone_number: String,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub name: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub email: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub metadata: Option<HashMap<String, serde_json::Value>>,
70}
71
72impl CreateContactRequest {
73    pub fn new(phone_number: impl Into<String>) -> Self {
74        Self {
75            phone_number: phone_number.into(),
76            name: None,
77            email: None,
78            metadata: None,
79        }
80    }
81
82    pub fn name(mut self, name: impl Into<String>) -> Self {
83        self.name = Some(name.into());
84        self
85    }
86
87    pub fn email(mut self, email: impl Into<String>) -> Self {
88        self.email = Some(email.into());
89        self
90    }
91
92    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
93        self.metadata = Some(metadata);
94        self
95    }
96}
97
98#[derive(Debug, Clone, Serialize, Default)]
99pub struct UpdateContactRequest {
100    #[serde(skip_serializing_if = "Option::is_none", rename = "phone_number")]
101    pub phone_number: Option<String>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub name: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub email: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub metadata: Option<HashMap<String, serde_json::Value>>,
108}
109
110impl UpdateContactRequest {
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    pub fn phone_number(mut self, phone_number: impl Into<String>) -> Self {
116        self.phone_number = Some(phone_number.into());
117        self
118    }
119
120    pub fn name(mut self, name: impl Into<String>) -> Self {
121        self.name = Some(name.into());
122        self
123    }
124
125    pub fn email(mut self, email: impl Into<String>) -> Self {
126        self.email = Some(email.into());
127        self
128    }
129
130    pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
131        self.metadata = Some(metadata);
132        self
133    }
134}
135
136#[derive(Debug, Clone, Default)]
137pub struct ListContactsOptions {
138    pub limit: Option<u32>,
139    pub offset: Option<u32>,
140    pub search: Option<String>,
141    pub list_id: Option<String>,
142}
143
144impl ListContactsOptions {
145    pub fn new() -> Self {
146        Self::default()
147    }
148
149    pub fn limit(mut self, limit: u32) -> Self {
150        self.limit = Some(limit.min(100));
151        self
152    }
153
154    pub fn offset(mut self, offset: u32) -> Self {
155        self.offset = Some(offset);
156        self
157    }
158
159    pub fn search(mut self, search: impl Into<String>) -> Self {
160        self.search = Some(search.into());
161        self
162    }
163
164    pub fn list_id(mut self, list_id: impl Into<String>) -> Self {
165        self.list_id = Some(list_id.into());
166        self
167    }
168
169    pub(crate) fn to_query_params(&self) -> Vec<(String, String)> {
170        let mut params = Vec::new();
171        if let Some(limit) = self.limit {
172            params.push(("limit".to_string(), limit.to_string()));
173        }
174        if let Some(offset) = self.offset {
175            params.push(("offset".to_string(), offset.to_string()));
176        }
177        if let Some(ref search) = self.search {
178            params.push(("search".to_string(), search.clone()));
179        }
180        if let Some(ref list_id) = self.list_id {
181            params.push(("list_id".to_string(), list_id.clone()));
182        }
183        params
184    }
185}
186
187#[derive(Debug, Clone, Serialize)]
188pub struct CreateContactListRequest {
189    pub name: String,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub description: Option<String>,
192}
193
194impl CreateContactListRequest {
195    pub fn new(name: impl Into<String>) -> Self {
196        Self {
197            name: name.into(),
198            description: None,
199        }
200    }
201
202    pub fn description(mut self, description: impl Into<String>) -> Self {
203        self.description = Some(description.into());
204        self
205    }
206}
207
208#[derive(Debug, Clone, Serialize, Default)]
209pub struct UpdateContactListRequest {
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub name: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub description: Option<String>,
214}
215
216impl UpdateContactListRequest {
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    pub fn name(mut self, name: impl Into<String>) -> Self {
222        self.name = Some(name.into());
223        self
224    }
225
226    pub fn description(mut self, description: impl Into<String>) -> Self {
227        self.description = Some(description.into());
228        self
229    }
230}
231
232#[derive(Debug, Clone, Serialize)]
233pub struct AddContactsRequest {
234    #[serde(rename = "contact_ids")]
235    pub contact_ids: Vec<String>,
236}
237
238#[derive(Debug, Clone, Serialize)]
239pub struct ImportContactItem {
240    pub phone: String,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub name: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub email: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none", rename = "optedInAt")]
246    pub opted_in_at: Option<String>,
247}
248
249impl ImportContactItem {
250    pub fn new(phone: impl Into<String>) -> Self {
251        Self {
252            phone: phone.into(),
253            name: None,
254            email: None,
255            opted_in_at: None,
256        }
257    }
258
259    pub fn name(mut self, name: impl Into<String>) -> Self {
260        self.name = Some(name.into());
261        self
262    }
263
264    pub fn email(mut self, email: impl Into<String>) -> Self {
265        self.email = Some(email.into());
266        self
267    }
268}
269
270#[derive(Debug, Clone, Serialize)]
271pub struct ImportContactsRequest {
272    pub contacts: Vec<ImportContactItem>,
273    #[serde(skip_serializing_if = "Option::is_none", rename = "listId")]
274    pub list_id: Option<String>,
275    #[serde(skip_serializing_if = "Option::is_none", rename = "optedInAt")]
276    pub opted_in_at: Option<String>,
277}
278
279#[derive(Debug, Clone, Deserialize)]
280pub struct ImportContactsError {
281    pub index: i32,
282    pub phone: String,
283    pub error: String,
284}
285
286#[derive(Debug, Clone, Deserialize)]
287pub struct ImportContactsResponse {
288    pub imported: i32,
289    #[serde(rename = "skippedDuplicates")]
290    pub skipped_duplicates: i32,
291    #[serde(default)]
292    pub errors: Vec<ImportContactsError>,
293    #[serde(default, rename = "totalErrors")]
294    pub total_errors: i32,
295}
296
297pub struct ContactsResource<'a> {
298    client: &'a Sendly,
299}
300
301impl<'a> ContactsResource<'a> {
302    pub fn new(client: &'a Sendly) -> Self {
303        Self { client }
304    }
305
306    pub fn lists(&self) -> ContactListsResource<'a> {
307        ContactListsResource::new(self.client)
308    }
309
310    pub async fn list(&self, options: ListContactsOptions) -> Result<ContactListResponse> {
311        let params = options.to_query_params();
312        let response = self.client.get("/contacts", &params).await?;
313        Ok(response.json().await?)
314    }
315
316    pub async fn get(&self, id: &str) -> Result<Contact> {
317        let response = self.client.get(&format!("/contacts/{}", id), &[]).await?;
318        Ok(response.json().await?)
319    }
320
321    pub async fn create(&self, request: CreateContactRequest) -> Result<Contact> {
322        let response = self.client.post("/contacts", &request).await?;
323        Ok(response.json().await?)
324    }
325
326    pub async fn update(&self, id: &str, request: UpdateContactRequest) -> Result<Contact> {
327        let response = self
328            .client
329            .patch(&format!("/contacts/{}", id), &request)
330            .await?;
331        Ok(response.json().await?)
332    }
333
334    pub async fn delete(&self, id: &str) -> Result<()> {
335        self.client.delete(&format!("/contacts/{}", id)).await?;
336        Ok(())
337    }
338
339    pub async fn import(&self, request: ImportContactsRequest) -> Result<ImportContactsResponse> {
340        let response = self.client.post("/contacts/import", &request).await?;
341        Ok(response.json().await?)
342    }
343}
344
345pub struct ContactListsResource<'a> {
346    client: &'a Sendly,
347}
348
349impl<'a> ContactListsResource<'a> {
350    pub fn new(client: &'a Sendly) -> Self {
351        Self { client }
352    }
353
354    pub async fn list(&self) -> Result<ContactListsResponse> {
355        let response = self.client.get("/contact-lists", &[]).await?;
356        Ok(response.json().await?)
357    }
358
359    pub async fn get(&self, id: &str) -> Result<ContactList> {
360        let response = self
361            .client
362            .get(&format!("/contact-lists/{}", id), &[])
363            .await?;
364        Ok(response.json().await?)
365    }
366
367    pub async fn create(&self, request: CreateContactListRequest) -> Result<ContactList> {
368        let response = self.client.post("/contact-lists", &request).await?;
369        Ok(response.json().await?)
370    }
371
372    pub async fn update(&self, id: &str, request: UpdateContactListRequest) -> Result<ContactList> {
373        let response = self
374            .client
375            .patch(&format!("/contact-lists/{}", id), &request)
376            .await?;
377        Ok(response.json().await?)
378    }
379
380    pub async fn delete(&self, id: &str) -> Result<()> {
381        self.client
382            .delete(&format!("/contact-lists/{}", id))
383            .await?;
384        Ok(())
385    }
386
387    pub async fn add_contacts(&self, list_id: &str, contact_ids: Vec<String>) -> Result<()> {
388        let request = AddContactsRequest { contact_ids };
389        self.client
390            .post(&format!("/contact-lists/{}/contacts", list_id), &request)
391            .await?;
392        Ok(())
393    }
394
395    pub async fn remove_contact(&self, list_id: &str, contact_id: &str) -> Result<()> {
396        self.client
397            .delete(&format!(
398                "/contact-lists/{}/contacts/{}",
399                list_id, contact_id
400            ))
401            .await?;
402        Ok(())
403    }
404}