use std::fmt;
use serde::de::DeserializeOwned;
use crate::error::Error;
use crate::generated::routes;
use crate::generated::types::{
ConflictErrorResponseContent, Contact, ContactDetail, ContactNote, ContactNotePayload,
ContactNoteRequestContent, ContactPayload, ContactRequestContent, CreateContactRequestContent,
UnprocessableEntityErrorResponseContent, UpdateContactClearanceRequestContent,
};
use crate::operation::Operation;
use crate::services::clearances::ClearanceStatus;
use crate::types::SensitiveString;
pub use crate::generated::services::contacts::*;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ContactParams {
pub name: String,
pub email_address: String,
pub alias_email_addresses: Option<Vec<String>>,
pub account_user_id: Option<i64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContactConflict {
pub contact_id: i64,
pub conflicting_contact_ids: Vec<i64>,
}
impl ContactConflict {
pub fn from_error(error: &Error) -> Option<&ContactConflict> {
std::error::Error::source(error)?.downcast_ref::<ContactConflict>()
}
}
impl<'a> Contacts<'a> {
pub async fn create_contact(&self, params: &ContactParams) -> Result<Contact, Error> {
let body = CreateContactRequestContent {
acting_user_id: self.acting_user_id(params.account_user_id).await?,
contact: contact_payload(params),
};
let mut operation = self.client().operation(&routes::CREATE_CONTACT, &[]);
operation.json(&body)?;
self.write(operation).await
}
pub async fn update_contact(
&self,
contact_id: i64,
params: &ContactParams,
) -> Result<Contact, Error> {
let current = self.get(contact_id, &GetContactParams::default()).await?;
let body = ContactRequestContent {
contact: merged_payload(params, ¤t),
};
let mut operation = self
.client()
.operation(&routes::UPDATE_CONTACT, &[&contact_id]);
operation.resource_id(contact_id);
operation.json(&body)?;
self.write(operation).await
}
pub async fn screen(&self, contact_id: i64, status: ClearanceStatus) -> Result<(), Error> {
let body = UpdateContactClearanceRequestContent {
status: status.as_str().to_string(),
};
self.update_clearance(contact_id, &body).await
}
pub async fn set_note(&self, contact_id: i64, note: &str) -> Result<ContactNote, Error> {
let body = ContactNoteRequestContent {
contact: ContactNotePayload {
note: note.to_string(),
},
};
let mut operation = self
.client()
.operation(&routes::UPDATE_CONTACT_NOTE, &[&contact_id]);
operation.json(&body)?;
self.write(operation).await
}
async fn acting_user_id(&self, chosen: Option<i64>) -> Result<Option<i64>, Error> {
match self.client().account_id() {
None => Ok(chosen),
Some(account_id) => {
let account_user_id = self.client().account_user_id().await?;
match chosen {
Some(chosen) if chosen != account_user_id => Err(Error::usage(format!(
"account user {chosen} does not belong to selected account {account_id}"
))),
_ => Ok(Some(account_user_id)),
}
}
}
}
async fn write<T: DeserializeOwned>(&self, operation: Operation) -> Result<T, Error> {
match self.client().execute(operation).await {
Ok(response) => response.json(),
Err(error) if error.http_status() == Some(409) => Err(clash(&error)),
Err(error) if error.http_status() == Some(422) => Err(rejection(&error)),
Err(error) => Err(error),
}
}
}
fn contact_payload(params: &ContactParams) -> ContactPayload {
let email_address = if params.email_address.is_empty() {
None
} else {
Some(SensitiveString::new(params.email_address.as_str()))
};
ContactPayload {
name: params.name.clone(),
email_address,
alias_email_addresses: params.alias_email_addresses.clone(),
}
}
fn merged_payload(params: &ContactParams, current: &ContactDetail) -> ContactPayload {
let mut payload = contact_payload(params);
if payload.name.is_empty() {
payload.name = current.name.clone().unwrap_or_default();
}
if payload.email_address.is_none() {
payload.email_address = current.email_address.clone();
}
if payload.alias_email_addresses.is_none() {
payload.alias_email_addresses = Some(current_aliases(current));
}
payload
}
fn current_aliases(current: &ContactDetail) -> Vec<String> {
current
.aliases
.iter()
.flatten()
.filter_map(|alias| alias.email_address.as_ref())
.map(|address| address.expose().to_string())
.collect()
}
fn clash(error: &Error) -> Error {
let payload: ConflictErrorResponseContent = error.body_json().unwrap_or_default();
Error::conflict(conflict_message(&payload)).with_source(ContactConflict {
contact_id: payload.contact_id.unwrap_or_default(),
conflicting_contact_ids: payload.conflicting_contact_ids.unwrap_or_default(),
})
}
fn rejection(error: &Error) -> Error {
let payload: UnprocessableEntityErrorResponseContent = error.body_json().unwrap_or_default();
Error::validation(&payload.errors.unwrap_or_default())
}
fn conflict_message(payload: &ConflictErrorResponseContent) -> String {
let messages = payload.errors.as_deref().unwrap_or_default();
if !messages.is_empty() {
messages.join("; ")
} else if let Some(message) = &payload.error {
message.clone()
} else {
"the contact conflicts with one that already exists".to_string()
}
}
impl fmt::Display for ContactConflict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.conflicting_contact_ids.is_empty() {
write!(
f,
"contact {} conflicts with one that already exists",
self.contact_id
)
} else {
let ids: Vec<String> = self
.conflicting_contact_ids
.iter()
.map(|id| id.to_string())
.collect();
write!(
f,
"contact {} conflicts with {}",
self.contact_id,
ids.join(", ")
)
}
}
}
impl std::error::Error for ContactConflict {}