1#![warn(missing_docs)]
6#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
7#![cfg_attr(feature = "cargo-clippy", allow(clippy::needless_lifetimes))]
8
9use core::fmt::{self, Write};
10
11use serde::Deserialize;
12
13mod encoder;
14mod ser;
15
16pub const REST_API_URL: &str = "api.twilio.com/2010-04-01/Accounts";
18pub const REST_API_SMS_ENDPOINT: &str = "Messages.json";
20pub const REST_API_CALL_ENDPOINT: &str = "Calls.json";
22
23const URL_BUFFER_SIZE: usize = 92;
26pub type UrlBuffer = str_buf::StrBuf<URL_BUFFER_SIZE>;
28
29pub const fn get_sms_base(account_sid: &str) -> UrlBuffer {
33 UrlBuffer::new().and("https://").and(REST_API_URL).and("/").and(account_sid).and("/").and("Messages")
34}
35
36pub const fn sms_resource_url(account_sid: &str) -> UrlBuffer {
38 UrlBuffer::new().and("https://").and(REST_API_URL).and("/").and(account_sid).and("/").and(REST_API_SMS_ENDPOINT)
39}
40
41pub const fn get_call_base(account_sid: &str) -> UrlBuffer {
45 UrlBuffer::new().and("https://").and(REST_API_URL).and("/").and(account_sid).and("/").and("Calls")
46}
47
48pub const fn call_resource_url(account_sid: &str) -> UrlBuffer {
50 UrlBuffer::new().and("https://").and(REST_API_URL).and("/").and(account_sid).and("/").and(REST_API_CALL_ENDPOINT)
51}
52
53pub enum TwilioMethod {
55 GET,
57 POST
59}
60
61impl TwilioMethod {
62 fn as_str(&self) -> &'static str {
63 match self {
64 TwilioMethod::GET => "GET",
65 TwilioMethod::POST => "POST",
66 }
67 }
68}
69
70impl Default for TwilioMethod {
71 #[inline(always)]
72 fn default() -> Self {
73 TwilioMethod::POST
74 }
75}
76
77pub struct TwilioRequest {
87 buffer: Vec<u8>,
88 len: usize,
89}
90
91impl TwilioRequest {
92 pub const CONTENT_TYPE: &'static str = "application/x-www-form-urlencode";
96
97 pub const fn new() -> Self {
99 Self {
100 buffer: Vec::new(),
101 len: 0
102 }
103 }
104
105 #[inline]
106 pub fn into_bytes(self) -> Vec<u8> {
108 self.buffer
109 }
110
111 #[inline]
112 pub fn into_string(self) -> String {
114 unsafe {
115 String::from_utf8_unchecked(self.buffer)
116 }
117 }
118
119 #[inline]
120 pub fn as_form(&self) -> &str {
122 unsafe {
123 core::str::from_utf8_unchecked(&self.buffer)
124 }
125 }
126
127 fn add_pair(&mut self, field: &str, value: &str) -> &mut Self {
128 self.len += 1;
129 encoder::push_pair(field, value, &mut self.buffer);
130 self
131 }
132
133 #[inline]
134 pub fn account_sid(&mut self, sid: &str) -> &mut Self {
136 self.add_pair("AccountSid", sid)
137 }
138
139 #[inline]
140 pub fn from(&mut self, from: &str) -> &mut Self {
144 self.add_pair("From", from)
145 }
146
147 #[inline]
148 pub fn to(&mut self, to: &str) -> &mut Self {
152 self.add_pair("To", to)
153 }
154
155 #[inline]
156 pub fn body(&mut self, body: &str) -> &mut Self {
158 debug_assert!(body.len() <= 1_600, "Text body cannot exceed 1600 characters");
159 self.add_pair("Body", body)
160 }
161
162 #[inline]
163 pub fn media_url(&mut self, media_url: &str) -> &mut Self {
165 self.add_pair("MediaUrl", media_url)
166 }
167
168 #[inline]
169 pub fn post_status_callback(&mut self, url: &str) -> &mut Self {
171 self.add_pair("StatusCallback", url)
172 }
173
174 #[inline]
175 pub fn provide_feedback(&mut self, value: bool) -> &mut Self {
177 match value {
178 true => self.add_pair("ProvideFeedback", "true"),
179 false => self.add_pair("ProvideFeedback", "false"),
180 }
181 }
182
183 #[inline]
184 pub fn attempt(&mut self, attempt: u32) -> &mut Self {
186 let mut buf = str_buf::StrBuf::<10>::new();
187 let _ = write!(buf, "{}", attempt);
188 self.add_pair("Attempt", buf.as_str())
189 }
190
191 #[inline]
192 pub fn validity_period(&mut self, attempt: u16) -> &mut Self {
196 let mut buf = str_buf::StrBuf::<5>::new();
197 let _ = write!(buf, "{}", attempt);
198 self.add_pair("ValidityPeriod", buf.as_str())
199 }
200
201 #[inline]
202 pub fn send_at(&mut self, date: &str) -> &mut Self {
204 self.add_pair("SendAt", date)
205 }
206
207 #[inline]
208 pub fn twiml(&mut self, twiml: &str) -> &mut Self {
210 self.add_pair("Twiml", twiml)
211 }
212
213 #[inline]
214 pub fn url(&mut self, url: &str) -> &mut Self {
216 self.add_pair("Url", url)
217 }
218
219 #[inline]
220 pub fn url_with_method(&mut self, method: TwilioMethod, url: &str) -> &mut Self {
224 self.add_pair("Method", method.as_str()).add_pair("Url", url)
225 }
226
227 #[inline]
228 pub fn status_url(&mut self, url: &str) -> &mut Self {
230 self.add_pair("StatusCallback", url)
231 }
232
233 #[inline]
234 pub fn status_url_with_method(&mut self, method: TwilioMethod, url: &str) -> &mut Self {
238 self.add_pair("StatusCallbackMethod", method.as_str()).add_pair("StatusCallback", url)
239 }
240
241
242 #[inline]
243 pub fn caller_id(&mut self, id: &str) -> &mut Self {
245 self.add_pair("CallerId", id)
246 }
247
248 #[inline]
249 pub fn send_digits(&mut self, digits: &str) -> &mut Self {
251 debug_assert!(digits.len() <= 32, "SendDigits cannot exceed 32");
252 self.add_pair("SendDigits", digits)
253 }
254
255 #[inline]
256 pub fn page_size(&mut self, size: u32) -> &mut Self {
258 debug_assert_ne!(size, 0);
259 let mut buf = str_buf::StrBuf::<10>::new();
260 let _ = write!(buf, "{}", size);
261 self.add_pair("PageSize", buf.as_str())
262 }
263
264 #[inline]
265 pub fn start_date(&mut self, date: &str) -> &mut Self {
267 self.add_pair("StartDate", date)
268 }
269
270 #[inline]
271 pub fn end_date(&mut self, date: &str) -> &mut Self {
273 self.add_pair("EndDate", date)
274 }
275
276 #[inline]
277 pub fn date_sent(&mut self, date: &str) -> &mut Self {
279 self.add_pair("DateSent", date)
280 }
281}
282
283#[derive(Debug)]
284pub enum CallInstruction<'a> {
286 Twiml(&'a str),
288 Url(&'a str),
290}
291
292#[derive(Debug)]
293pub struct Call<'a> {
295 pub from: &'a str,
297 pub to: &'a str,
299 pub instruction: CallInstruction<'a>,
301}
302
303impl<'a> Call<'a> {
304 #[inline]
305 pub fn request(&self) -> TwilioRequest {
307 let mut res = TwilioRequest::new();
308 res.from(self.from).to(self.to);
309 match self.instruction {
310 CallInstruction::Twiml(twiml) => res.twiml(twiml),
311 CallInstruction::Url(url) => res.url(url),
312 };
313 res
314 }
315}
316
317impl<'a> fmt::Display for Call<'a> {
318 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
319 encoder::format_pair("From", self.from, fmt)?;
320 fmt.write_str(encoder::SEP)?;
321 encoder::format_pair("To", self.to, fmt)?;
322 fmt.write_str(encoder::SEP)?;
323 match self.instruction {
324 CallInstruction::Twiml(twiml) => encoder::format_pair("Twiml", twiml, fmt)?,
325 CallInstruction::Url(url) => encoder::format_pair("Url", url, fmt)?,
326 }
327
328 Ok(())
329 }
330}
331
332impl<'a> Into<TwilioRequest> for Call<'a> {
333 #[inline(always)]
334 fn into(self) -> TwilioRequest {
335 self.request()
336 }
337}
338
339#[derive(Debug)]
340pub struct Sms<'a> {
342 pub from: &'a str,
344 pub to: &'a str,
346 pub body: &'a str,
348}
349
350impl<'a> Sms<'a> {
351 #[inline]
352 pub fn request(&self) -> TwilioRequest {
354 let mut res = TwilioRequest::new();
355 res.from(self.from).to(self.to).body(self.body);
356 res
357 }
358}
359
360impl<'a> fmt::Display for Sms<'a> {
361 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
362 encoder::format_pair("From", self.from, fmt)?;
363 fmt.write_str(encoder::SEP)?;
364 encoder::format_pair("To", self.to, fmt)?;
365 fmt.write_str(encoder::SEP)?;
366 encoder::format_pair("Body", self.body, fmt)?;
367
368 Ok(())
369 }
370}
371
372impl<'a> Into<TwilioRequest> for Sms<'a> {
373 #[inline(always)]
374 fn into(self) -> TwilioRequest {
375 self.request()
376 }
377}
378
379#[derive(Debug)]
380pub struct Mms<'a> {
382 pub sms: Sms<'a>,
384 pub media_url: &'a str
389}
390
391impl<'a> Mms<'a> {
392 #[inline]
393 pub fn request(&self) -> TwilioRequest {
395 let mut res = self.sms.request();
396 res.media_url(self.media_url);
397 res
398 }
399}
400
401impl<'a> fmt::Display for Mms<'a> {
402 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
403 self.sms.fmt(fmt)?;
404 fmt.write_str(encoder::SEP)?;
405 encoder::format_pair("MediaUrl", self.media_url, fmt)?;
406
407 Ok(())
408 }
409}
410
411impl<'a> Into<TwilioRequest> for Mms<'a> {
412 #[inline(always)]
413 fn into(self) -> TwilioRequest {
414 self.request()
415 }
416}
417
418#[derive(Debug, Deserialize)]
419#[serde(rename_all = "kebab-case")]
420pub enum SmsStatus {
422 Queued,
424 Sending,
426 Sent,
428 Failed,
430 Delivered,
432 Undelivered,
434 Receiving,
436 Received,
438}
439
440#[derive(Debug, Deserialize)]
441#[serde(rename_all = "kebab-case")]
442pub enum CallStatus {
444 Queued,
446 Ringing,
448 InProgress,
450 Canceled,
452 Completed,
454 Busy,
456 NoAnswer,
458 Failed,
460}
461
462#[derive(Debug, Deserialize)]
463#[serde(rename_all = "kebab-case")]
464pub enum CallDirection {
466 Inbound,
468 OutboundApi,
470 OutboundDial,
472 TrunkingTerminating,
474 TrunkingOriginating,
476}
477
478#[derive(Debug, Deserialize)]
479pub struct SmsResult {
481 pub from: String,
483 pub to: String,
485 pub body: String,
487 pub sid: String,
492 pub status: SmsStatus,
494 pub media_url: Option<String>,
496 pub price: Option<String>,
498 pub price_unit: String,
500 pub date_created: Option<String>,
504 pub date_sent: Option<String>,
506 pub date_updated: String,
508}
509
510fn deserialize_number_from_any<'de, D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
511 #[derive(Deserialize)]
512 #[serde(untagged)]
513 enum StringOrInt {
514 String(String),
515 Number(i64),
516 }
517
518 match StringOrInt::deserialize(deserializer)? {
519 StringOrInt::String(s) => s.parse::<i64>().map_err(serde::de::Error::custom),
520 StringOrInt::Number(i) => Ok(i),
521 }
522}
523
524#[derive(Debug, Deserialize)]
525pub struct CallResult {
527 pub from: String,
529 pub to: String,
531 pub sid: String,
536 pub status: CallStatus,
538 pub caller_name: Option<String>,
540 pub duration: Option<i64>,
542 pub price: Option<String>,
544 pub price_unit: String,
546 pub date_created: Option<String>,
550 pub start_time: Option<String>,
552 pub end_time: Option<String>,
554 pub direction: Option<CallDirection>,
556 #[serde(deserialize_with = "deserialize_number_from_any")]
557 pub queue_time: i64
559}
560
561#[derive(Debug, Deserialize)]
562pub struct TwilioError {
564 pub code: usize,
566 pub message: String,
568 pub status: usize,
570}
571
572impl fmt::Display for TwilioError {
573 #[inline(always)]
574 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
575 fmt.write_fmt(format_args!("Twilio API responded with status={}, code={}, message: {}", self.status, self.code, self.message))
576 }
577}
578
579impl std::error::Error for TwilioError {
580}