pub mod account;
pub mod conversation;
pub mod participant_conversation;
pub mod serverless;
pub mod sync;
use std::fmt::{self};
use account::Accounts;
use conversation::Conversations;
use reqwest::{header::HeaderMap, Method, Response};
use serde::{Deserialize, Serialize};
use serverless::Serverless;
use strum_macros::{Display, EnumIter, EnumString};
use sync::Sync;
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct TwilioConfig {
pub account_sid: String,
pub auth_token: String,
}
impl TwilioConfig {
pub fn build(account_sid: String, auth_token: String) -> TwilioConfig {
if !account_sid.starts_with("AC") {
panic!("Account SID must start with AC");
} else if account_sid.len() != 34 {
panic!(
"Account SID should be 34 characters in length. Was {}",
account_sid.len()
)
}
if auth_token.len() != 32 {
panic!(
"Auth token should be 32 characters in length. Was {}",
auth_token.len()
)
}
TwilioConfig {
account_sid,
auth_token,
}
}
}
pub struct Client {
pub config: TwilioConfig,
client: reqwest::Client,
}
#[derive(Debug)]
pub struct TwilioError {
pub kind: ErrorKind,
}
impl fmt::Display for TwilioError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind.as_str())
}
}
#[derive(Debug)]
pub enum ErrorKind {
ValidationError(String),
NetworkError(reqwest::Error),
TwilioError(TwilioApiError),
ParsingError(reqwest::Error),
}
impl ErrorKind {
fn as_str(&self) -> String {
match self {
ErrorKind::ValidationError(error) => {
format!("Validation error for provided arguments: {}", error)
}
ErrorKind::NetworkError(error) => format!("Network error reaching Twilio: {}", &error),
ErrorKind::ParsingError(error) => format!("Unable to parse response: {}", &error),
ErrorKind::TwilioError(error) => {
format!("Error: {}", &error)
}
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TwilioApiError {
pub code: u32,
pub message: String,
pub more_info: String,
pub status: u16,
}
impl fmt::Display for TwilioApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} from Twilio. ({}) {}. For more info see: {}",
self.status, self.code, self.message, self.more_info
)
}
}
#[allow(dead_code)]
#[derive(Deserialize)]
pub struct PageMeta {
page: u16,
page_size: u16,
first_page_url: String,
previous_page_url: Option<String>,
next_page_url: Option<String>,
key: String,
}
#[derive(Display, EnumIter, EnumString, PartialEq)]
pub enum SubResource {
Account,
Conversations,
Sync,
Serverless,
}
impl Client {
pub fn new(config: &TwilioConfig) -> Self {
Self {
config: config.clone(),
client: reqwest::Client::new(),
}
}
async fn send_request<T, U>(
&self,
method: Method,
url: &str,
params: Option<&U>,
headers: Option<HeaderMap>,
) -> Result<T, TwilioError>
where
T: serde::de::DeserializeOwned,
U: Serialize + ?Sized,
{
let response = self.send_http_request(method, url, params, headers).await?;
match response.status().is_success() {
true => response.json::<T>().await.map_err(|error| TwilioError {
kind: ErrorKind::ParsingError(error),
}),
false => {
let parsed_twilio_error = response.json::<TwilioApiError>();
match parsed_twilio_error.await {
Ok(twilio_error) => Err(TwilioError {
kind: ErrorKind::TwilioError(twilio_error),
}),
Err(error) => Err(TwilioError {
kind: ErrorKind::ParsingError(error),
}),
}
}
}
}
async fn send_request_and_ignore_response<T>(
&self,
method: Method,
url: &str,
params: Option<&T>,
headers: Option<HeaderMap>,
) -> Result<(), TwilioError>
where
T: Serialize + ?Sized,
{
let response = self.send_http_request(method, url, params, headers).await?;
match response.status().is_success() {
true => Ok(()),
false => {
let parsed_twilio_error = response.json::<TwilioApiError>();
match parsed_twilio_error.await {
Ok(twilio_error) => Err(TwilioError {
kind: ErrorKind::TwilioError(twilio_error),
}),
Err(error) => Err(TwilioError {
kind: ErrorKind::ParsingError(error),
}),
}
}
}
}
async fn send_http_request<T>(
&self,
method: Method,
url: &str,
params: Option<&T>,
headers: Option<HeaderMap>,
) -> Result<Response, TwilioError>
where
T: Serialize + ?Sized,
{
match method {
Method::GET => {
self.client
.request(method, url)
.basic_auth(&self.config.account_sid, Some(&self.config.auth_token))
.headers(headers.unwrap_or_default())
.query(¶ms)
.send()
.await
}
_ => {
self.client
.request(method, url)
.basic_auth(&self.config.account_sid, Some(&self.config.auth_token))
.headers(headers.unwrap_or_default())
.form(¶ms)
.send()
.await
}
}
.map_err(|error| TwilioError {
kind: ErrorKind::NetworkError(error),
})
}
pub fn accounts(&self) -> Accounts {
Accounts { client: self }
}
pub fn conversations(&self) -> Conversations {
Conversations { client: self }
}
pub fn sync(&self) -> Sync {
Sync { client: self }
}
pub fn serverless(&self) -> Serverless {
Serverless { client: self }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Account SID must start with AC")]
fn account_sid_regex() {
TwilioConfig::build(String::from("ThisisnotanaccountSID"), String::from("1234"));
}
#[test]
#[should_panic(expected = "Account SID should be 34 characters in length. Was 23")]
fn account_sid_len() {
TwilioConfig::build(
String::from("ACThisisnotanaccountSID"),
String::from("1234"),
);
}
#[test]
#[should_panic(expected = "Auth token should be 32 characters in length. Was 20")]
fn auth_token_len() {
TwilioConfig::build(
String::from("AC11111111111111111111111111111111"),
String::from("11111111111111111111"),
);
}
#[test]
fn config_on_good_credentials() {
let account_sid = String::from("AC11111111111111111111111111111111");
let auth_token = String::from("11111111111111111111111111111111");
let config = TwilioConfig::build(account_sid.clone(), auth_token.clone());
assert_eq!(account_sid, config.account_sid);
assert_eq!(auth_token, config.auth_token);
}
}