use http::Method;
use serde::{Deserialize, Serialize};
use crate::core::operation::{Operation, encode_path_segment, json_body, push_opt};
use crate::core::pagination::{DEFAULT_PAGE_SIZE, Listing, Page, Pagination, Paginator};
use crate::error::Result;
use crate::types::{Contact, DeleteResponse};
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ListContactsResponse {
pub contacts: Vec<Contact>,
pub pagination: Option<Pagination>,
}
impl Listing for ListContactsResponse {
type Item = Contact;
fn into_page(self) -> Page<Self::Item> {
Page {
items: self.contacts,
pagination: self.pagination,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ContactCapabilities {
pub contact: Option<String>,
#[serde(rename = "type")]
pub kind: Option<String>,
pub capabilities: Option<Capabilities>,
pub last_checked: Option<i64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Capabilities {
pub imessage: Option<bool>,
pub sms: Option<bool>,
pub facetime: Option<bool>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ContactTag {
pub tag: Option<String>,
pub created_at: Option<i64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ContactTagsResponse {
pub tags: Vec<ContactTag>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct AddTagsResponse {
pub success: Option<bool>,
pub tags_added: Option<Vec<String>>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Default)]
pub struct ListContacts {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub q: Option<String>,
pub sort: Option<String>,
}
impl Operation for ListContacts {
type Output = ListContactsResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
"/contacts".into()
}
fn query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
push_opt(&mut q, "limit", self.limit);
push_opt(&mut q, "offset", self.offset);
push_opt(&mut q, "q", self.q.as_ref());
push_opt(&mut q, "sort", self.sort.as_ref());
q
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct CreateContact {
pub identifier: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl CreateContact {
pub fn new(identifier: impl Into<String>) -> Self {
CreateContact {
identifier: identifier.into(),
name: None,
}
}
#[must_use]
pub fn name(mut self, v: impl Into<String>) -> Self {
self.name = Some(v.into());
self
}
}
impl Operation for CreateContact {
type Output = Contact;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
"/contacts".into()
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetContact {
pub contact_id: String,
}
impl Operation for GetContact {
type Output = Contact;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/contacts/{}", encode_path_segment(&self.contact_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct UpdateContact {
#[serde(skip)]
pub contact_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Operation for UpdateContact {
type Output = Contact;
const METHOD: Method = Method::PATCH;
fn path(&self) -> String {
format!("/contacts/{}", encode_path_segment(&self.contact_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct DeleteContact {
pub contact_id: String,
}
impl Operation for DeleteContact {
type Output = DeleteResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!("/contacts/{}", encode_path_segment(&self.contact_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetContactCapabilities {
pub contact_id: String,
}
impl Operation for GetContactCapabilities {
type Output = ContactCapabilities;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!(
"/contacts/{}/capabilities",
encode_path_segment(&self.contact_id)
)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct ListContactTags {
pub contact_id: String,
}
impl Operation for ListContactTags {
type Output = ContactTagsResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/contacts/{}/tags", encode_path_segment(&self.contact_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct AddContactTags {
#[serde(skip)]
pub contact_id: String,
pub tags: Vec<String>,
}
impl Operation for AddContactTags {
type Output = AddTagsResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/contacts/{}/tags", encode_path_segment(&self.contact_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct RemoveContactTag {
pub contact_id: String,
pub tag: String,
}
impl Operation for RemoveContactTag {
type Output = DeleteResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!(
"/contacts/{}/tags/{}",
encode_path_segment(&self.contact_id),
encode_path_segment(&self.tag)
)
}
}
#[derive(Debug)]
pub struct Contacts<'c, C> {
pub(crate) client: &'c C,
}
#[cfg(feature = "async")]
impl crate::Client {
pub fn contacts(&self) -> Contacts<'_, crate::Client> {
Contacts { client: self }
}
}
#[cfg(feature = "sync")]
impl crate::BlockingClient {
pub fn contacts(&self) -> Contacts<'_, crate::BlockingClient> {
Contacts { client: self }
}
}
#[cfg(feature = "async")]
impl<'c> Contacts<'c, crate::Client> {
pub async fn list(&self) -> Result<ListContactsResponse> {
self.client.send(ListContacts::default()).await
}
pub async fn list_with(&self, query: ListContacts) -> Result<ListContactsResponse> {
self.client.send(query).await
}
pub fn list_all(
&self,
) -> Paginator<'c, crate::Client, impl Fn(u32, u32) -> ListContacts + use<'c>, ListContacts>
{
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| {
ListContacts {
offset: Some(offset),
limit: Some(limit),
..Default::default()
}
})
}
pub async fn create(&self, op: CreateContact) -> Result<Contact> {
self.client.send(op).await
}
pub async fn get(&self, contact_id: impl Into<String>) -> Result<Contact> {
self.client
.send(GetContact {
contact_id: contact_id.into(),
})
.await
}
pub async fn update(
&self,
contact_id: impl Into<String>,
name: Option<String>,
) -> Result<Contact> {
self.client
.send(UpdateContact {
contact_id: contact_id.into(),
name,
})
.await
}
pub async fn delete(&self, contact_id: impl Into<String>) -> Result<DeleteResponse> {
self.client
.send(DeleteContact {
contact_id: contact_id.into(),
})
.await
}
pub async fn capabilities(&self, contact_id: impl Into<String>) -> Result<ContactCapabilities> {
self.client
.send(GetContactCapabilities {
contact_id: contact_id.into(),
})
.await
}
pub async fn tags(&self, contact_id: impl Into<String>) -> Result<ContactTagsResponse> {
self.client
.send(ListContactTags {
contact_id: contact_id.into(),
})
.await
}
pub async fn add_tags(
&self,
contact_id: impl Into<String>,
tags: Vec<String>,
) -> Result<AddTagsResponse> {
self.client
.send(AddContactTags {
contact_id: contact_id.into(),
tags,
})
.await
}
pub async fn remove_tag(
&self,
contact_id: impl Into<String>,
tag: impl Into<String>,
) -> Result<DeleteResponse> {
self.client
.send(RemoveContactTag {
contact_id: contact_id.into(),
tag: tag.into(),
})
.await
}
}
#[cfg(feature = "sync")]
impl<'c> Contacts<'c, crate::BlockingClient> {
pub fn list(&self) -> Result<ListContactsResponse> {
self.client.send(ListContacts::default())
}
pub fn list_with(&self, query: ListContacts) -> Result<ListContactsResponse> {
self.client.send(query)
}
pub fn list_all(
&self,
) -> Paginator<
'c,
crate::BlockingClient,
impl Fn(u32, u32) -> ListContacts + use<'c>,
ListContacts,
> {
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| {
ListContacts {
offset: Some(offset),
limit: Some(limit),
..Default::default()
}
})
}
pub fn create(&self, op: CreateContact) -> Result<Contact> {
self.client.send(op)
}
pub fn get(&self, contact_id: impl Into<String>) -> Result<Contact> {
self.client.send(GetContact {
contact_id: contact_id.into(),
})
}
pub fn update(&self, contact_id: impl Into<String>, name: Option<String>) -> Result<Contact> {
self.client.send(UpdateContact {
contact_id: contact_id.into(),
name,
})
}
pub fn delete(&self, contact_id: impl Into<String>) -> Result<DeleteResponse> {
self.client.send(DeleteContact {
contact_id: contact_id.into(),
})
}
pub fn capabilities(&self, contact_id: impl Into<String>) -> Result<ContactCapabilities> {
self.client.send(GetContactCapabilities {
contact_id: contact_id.into(),
})
}
pub fn tags(&self, contact_id: impl Into<String>) -> Result<ContactTagsResponse> {
self.client.send(ListContactTags {
contact_id: contact_id.into(),
})
}
pub fn add_tags(
&self,
contact_id: impl Into<String>,
tags: Vec<String>,
) -> Result<AddTagsResponse> {
self.client.send(AddContactTags {
contact_id: contact_id.into(),
tags,
})
}
pub fn remove_tag(
&self,
contact_id: impl Into<String>,
tag: impl Into<String>,
) -> Result<DeleteResponse> {
self.client.send(RemoveContactTag {
contact_id: contact_id.into(),
tag: tag.into(),
})
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unreadable_literal
)]
mod tests {
use super::*;
use crate::core::operation::Operation;
#[test]
fn list_contacts_method_is_get() {
assert_eq!(ListContacts::METHOD, http::Method::GET);
}
#[test]
fn list_contacts_path() {
let op = ListContacts::default();
assert_eq!(op.path(), "/contacts");
}
#[test]
fn list_contacts_query_empty_when_no_filters() {
let op = ListContacts::default();
assert!(op.query().is_empty());
}
#[test]
fn list_contacts_query_with_limit_and_offset() {
let op = ListContacts {
limit: Some(10),
offset: Some(20),
q: None,
sort: None,
};
let q = op.query();
assert_eq!(q.len(), 2);
assert!(q.contains(&("limit", "10".to_string())));
assert!(q.contains(&("offset", "20".to_string())));
}
#[test]
fn list_contacts_query_with_all_fields() {
let op = ListContacts {
limit: Some(5),
offset: Some(0),
q: Some("alice".into()),
sort: Some("name".into()),
};
let q = op.query();
assert_eq!(q.len(), 4);
assert!(q.contains(&("limit", "5".to_string())));
assert!(q.contains(&("offset", "0".to_string())));
assert!(q.contains(&("q", "alice".to_string())));
assert!(q.contains(&("sort", "name".to_string())));
}
#[test]
fn list_contacts_query_omits_unset_optionals() {
let op = ListContacts {
limit: Some(3),
offset: None,
q: None,
sort: None,
};
let q = op.query();
assert_eq!(q.len(), 1);
assert_eq!(q[0], ("limit", "3".to_string()));
}
#[test]
fn create_contact_method_is_post() {
assert_eq!(CreateContact::METHOD, http::Method::POST);
}
#[test]
fn create_contact_path() {
let op = CreateContact {
identifier: "+15550001111".into(),
name: None,
};
assert_eq!(op.path(), "/contacts");
}
#[test]
fn create_contact_body_minimal_no_name() {
let op = CreateContact {
identifier: "+15550001111".into(),
name: None,
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "identifier": "+15550001111" }));
}
#[test]
fn create_contact_body_with_name() {
let op = CreateContact {
identifier: "+15550001111".into(),
name: Some("Alice".into()),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
v,
serde_json::json!({ "identifier": "+15550001111", "name": "Alice" })
);
}
#[test]
fn get_contact_method_is_get() {
assert_eq!(GetContact::METHOD, http::Method::GET);
}
#[test]
fn get_contact_path() {
let op = GetContact {
contact_id: "abc123".into(),
};
assert_eq!(op.path(), "/contacts/abc123");
}
#[test]
fn update_contact_method_is_patch() {
assert_eq!(UpdateContact::METHOD, http::Method::PATCH);
}
#[test]
fn update_contact_path() {
let op = UpdateContact {
contact_id: "abc123".into(),
name: None,
};
assert_eq!(op.path(), "/contacts/abc123");
}
#[test]
fn update_contact_body_no_name() {
let op = UpdateContact {
contact_id: "abc123".into(),
name: None,
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({}));
}
#[test]
fn update_contact_body_with_name() {
let op = UpdateContact {
contact_id: "abc123".into(),
name: Some("Bob".into()),
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "name": "Bob" }));
}
#[test]
fn delete_contact_method_is_delete() {
assert_eq!(DeleteContact::METHOD, http::Method::DELETE);
}
#[test]
fn delete_contact_path() {
let op = DeleteContact {
contact_id: "abc123".into(),
};
assert_eq!(op.path(), "/contacts/abc123");
}
#[test]
fn get_capabilities_method_is_get() {
assert_eq!(GetContactCapabilities::METHOD, http::Method::GET);
}
#[test]
fn get_capabilities_path() {
let op = GetContactCapabilities {
contact_id: "abc123".into(),
};
assert_eq!(op.path(), "/contacts/abc123/capabilities");
}
#[test]
fn list_tags_method_is_get() {
assert_eq!(ListContactTags::METHOD, http::Method::GET);
}
#[test]
fn list_tags_path() {
let op = ListContactTags {
contact_id: "abc123".into(),
};
assert_eq!(op.path(), "/contacts/abc123/tags");
}
#[test]
fn add_tags_method_is_post() {
assert_eq!(AddContactTags::METHOD, http::Method::POST);
}
#[test]
fn add_tags_path() {
let op = AddContactTags {
contact_id: "abc123".into(),
tags: vec!["vip".into()],
};
assert_eq!(op.path(), "/contacts/abc123/tags");
}
#[test]
fn add_tags_body_single_tag() {
let op = AddContactTags {
contact_id: "abc123".into(),
tags: vec!["vip".into()],
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "tags": ["vip"] }));
}
#[test]
fn add_tags_body_multiple_tags() {
let op = AddContactTags {
contact_id: "abc123".into(),
tags: vec!["vip".into(), "priority".into()],
};
let body = op.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "tags": ["vip", "priority"] }));
}
#[test]
fn remove_tag_method_is_delete() {
assert_eq!(RemoveContactTag::METHOD, http::Method::DELETE);
}
#[test]
fn remove_tag_path() {
let op = RemoveContactTag {
contact_id: "abc123".into(),
tag: "vip".into(),
};
assert_eq!(op.path(), "/contacts/abc123/tags/vip");
}
}