twilio_data/
lib.rs

1//! Twilio API data structs
2//!
3//! To be used as building blocks.
4
5#![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
16///Twilio REST API base url
17pub const REST_API_URL: &str = "api.twilio.com/2010-04-01/Accounts";
18///Twilio REST API endpoint for SMS
19pub const REST_API_SMS_ENDPOINT: &str = "Messages.json";
20///Twilio REST API endpoint for calls
21pub const REST_API_CALL_ENDPOINT: &str = "Calls.json";
22
23//Fetch SMS link probably max
24//https://api.twilio.com/2010-04-01/Accounts/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages.json
25const URL_BUFFER_SIZE: usize = 92;
26///URL storage for `const fn` creation.
27pub type UrlBuffer = str_buf::StrBuf<URL_BUFFER_SIZE>;
28
29///Creates base URL to fetch SMS onto.
30///
31///To fetch SMS you need to call `<base>/<id>.json`
32pub 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
36///Creates URL to post SMS onto or fetch multiple SMS
37pub 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
41///Creates base URL to fetch Call onto.
42///
43///To fetch Call you need to call `<base>/<id>.json`
44pub 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
48///Creates URL to post Call onto or fetch multiple Call
49pub 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
53///Describes possible http methods, twilio can use to invoke callback.
54pub enum TwilioMethod {
55    ///Get
56    GET,
57    ///POST is default method
58    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
77///Generic Twilio request builder.
78///
79///Data is encoded as `application/x-www-form-urlencode`.
80///
81///When performing `GET` should be appended to URL as query string.
82///
83///When performing `POST` should be placed as body of HTTP Request with `Content-Type` equal to `application/x-www-form-urlencode`.
84///
85///While it implements `Serialize`, there is no need to employ it as internally data is already encoded.
86pub struct TwilioRequest {
87    buffer: Vec<u8>,
88    len: usize,
89}
90
91impl TwilioRequest {
92    ///Content type of Twilio API request.
93    ///
94    ///To be used for HTTP Post requests
95    pub const CONTENT_TYPE: &'static str = "application/x-www-form-urlencode";
96
97    ///Creates new request.
98    pub const fn new() -> Self {
99        Self {
100            buffer: Vec::new(),
101            len: 0
102        }
103    }
104
105    #[inline]
106    ///Returns raw `application/x-www-form-urlencoded` data.
107    pub fn into_bytes(self) -> Vec<u8> {
108        self.buffer
109    }
110
111    #[inline]
112    ///Returns string `application/x-www-form-urlencoded` data.
113    pub fn into_string(self) -> String {
114        unsafe {
115            String::from_utf8_unchecked(self.buffer)
116        }
117    }
118
119    #[inline]
120    ///Returns reference as string `application/x-www-form-urlencoded` data.
121    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    ///Adds `AccountSid` to specify owner of the resource.
135    pub fn account_sid(&mut self, sid: &str) -> &mut Self {
136        self.add_pair("AccountSid", sid)
137    }
138
139    #[inline]
140    ///Adds `From` field, which is identifier of caller.
141    ///
142    ///Type should be the same as for `To`
143    pub fn from(&mut self, from: &str) -> &mut Self {
144        self.add_pair("From", from)
145    }
146
147    #[inline]
148    ///Adds `To` field, which is identifier of callee.
149    ///
150    ///Type should be the same as for `To`
151    pub fn to(&mut self, to: &str) -> &mut Self {
152        self.add_pair("To", to)
153    }
154
155    #[inline]
156    ///Adds `Body` field.
157    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    ///Adds `MediaUrl` field.
164    pub fn media_url(&mut self, media_url: &str) -> &mut Self {
165        self.add_pair("MediaUrl", media_url)
166    }
167
168    #[inline]
169    ///Adds `StatusCallback` field, which is url where to perform POST request
170    pub fn post_status_callback(&mut self, url: &str) -> &mut Self {
171        self.add_pair("StatusCallback", url)
172    }
173
174    #[inline]
175    ///Sets `ProvideFeedback` field, to specify whether message delivery should be tracked.
176    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    ///Sets `Attempt` field, to indicate total number of attempts to post message.
185    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    ///Sets `ValidityPeriod` field, to indicate number of seconds allowed in waiting queue.
193    ///
194    ///If message is enqueuedfor longer, it is discarded by Twilio
195    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    ///Sets `SendAt` field, to indicate where message is to be sent.
203    pub fn send_at(&mut self, date: &str) -> &mut Self {
204        self.add_pair("SendAt", date)
205    }
206
207    #[inline]
208    ///Sets `Twiml` field, to provide call's content as xml string.
209    pub fn twiml(&mut self, twiml: &str) -> &mut Self {
210        self.add_pair("Twiml", twiml)
211    }
212
213    #[inline]
214    ///Sets `Url` field, to provide call's content via GET to the provided url
215    pub fn url(&mut self, url: &str) -> &mut Self {
216        self.add_pair("Url", url)
217    }
218
219    #[inline]
220    ///Sets `Url` field, to provide call's content via GET to the provided url.
221    ///
222    ///With option of setting HTTP method to access URL.
223    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    ///Sets `StatusCallback` field, to provide URL where to post status information.
229    pub fn status_url(&mut self, url: &str) -> &mut Self {
230        self.add_pair("StatusCallback", url)
231    }
232
233    #[inline]
234    ///Sets `StatusCallback` field, to provide URL where to post status information.
235    ///
236    ///With option of setting HTTP method to access URL.
237    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    ///Sets `CallerId` field, to provide caller identification.
244    pub fn caller_id(&mut self, id: &str) -> &mut Self {
245        self.add_pair("CallerId", id)
246    }
247
248    #[inline]
249    ///Sets `SendDigits` field, to provide set of keys to dial after call is established.
250    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    ///Sets `PageSize` field, to provide number of resources max for when reading multiple resources
257    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    ///Sets `StartDate` field, to provide starting date for when reading multiple calls
266    pub fn start_date(&mut self, date: &str) -> &mut Self {
267        self.add_pair("StartDate", date)
268    }
269
270    #[inline]
271    ///Sets `EndDate` field, to provide ending date for when reading multiple calls
272    pub fn end_date(&mut self, date: &str) -> &mut Self {
273        self.add_pair("EndDate", date)
274    }
275
276    #[inline]
277    ///Sets `DateSent` field, to provide message date for when reading multiple message
278    pub fn date_sent(&mut self, date: &str) -> &mut Self {
279        self.add_pair("DateSent", date)
280    }
281}
282
283#[derive(Debug)]
284///Call instruction.
285pub enum CallInstruction<'a> {
286    ///Provides xml with Twiml instructions
287    Twiml(&'a str),
288    ///Provides URL pointing to xml file with Twiml instructions
289    Url(&'a str),
290}
291
292#[derive(Debug)]
293///Describes minimal Call request, suitable for urlencoded serialization
294pub struct Call<'a> {
295    ///Phone number of source
296    pub from: &'a str,
297    ///Phone number of destination
298    pub to: &'a str,
299    ///Call content
300    pub instruction: CallInstruction<'a>,
301}
302
303impl<'a> Call<'a> {
304    #[inline]
305    ///Converts to generic TwilioRequest
306    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)]
340///Describes SMS, suitable for urlencoded serialization
341pub struct Sms<'a> {
342    ///Phone number of source
343    pub from: &'a str,
344    ///Phone number of destination
345    pub to: &'a str,
346    ///Text body
347    pub body: &'a str,
348}
349
350impl<'a> Sms<'a> {
351    #[inline]
352    ///Converts to generic TwilioRequest
353    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)]
380///Describes MMS, suitable for urlencoded serialization
381pub struct Mms<'a> {
382    ///Flattened SMS part
383    pub sms: Sms<'a>,
384    ///Url with media content.
385    ///
386    ///Twilio generally accepts `.gif`, `.png` and `.jpeg` images so it formats it for device.
387    ///Other formats are sent as it is, but MMS is limited to 5mb.
388    pub media_url: &'a str
389}
390
391impl<'a> Mms<'a> {
392    #[inline]
393    ///Converts to generic TwilioRequest
394    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")]
420///Status of message.
421pub enum SmsStatus {
422    ///In queue for sending.
423    Queued,
424    ///Sending is in progress.
425    Sending,
426    ///Sent
427    Sent,
428    ///Failed to send
429    Failed,
430    ///Successfully delivered.
431    Delivered,
432    ///Not delivered yet.
433    Undelivered,
434    ///Receiving.
435    Receiving,
436    ///Received.
437    Received,
438}
439
440#[derive(Debug, Deserialize)]
441#[serde(rename_all = "kebab-case")]
442///Status of call.
443pub enum CallStatus {
444    ///In queue for sending.
445    Queued,
446    ///The call is ringing.
447    Ringing,
448    ///The call is ongoing.
449    InProgress,
450    ///The call is cancelled.
451    Canceled,
452    ///The call is finished.
453    Completed,
454    ///The callee is busy.
455    Busy,
456    ///The callee is not answering.
457    NoAnswer,
458    ///Cannot perform call.
459    Failed,
460}
461
462#[derive(Debug, Deserialize)]
463#[serde(rename_all = "kebab-case")]
464///Status of call.
465pub enum CallDirection {
466    ///Inbound
467    Inbound,
468    ///Output API
469    OutboundApi,
470    ///Output Dial
471    OutboundDial,
472    ///Trunking Terminating
473    TrunkingTerminating,
474    ///Trunking Originating
475    TrunkingOriginating,
476}
477
478#[derive(Debug, Deserialize)]
479///Result of correct SMS request.
480pub struct SmsResult {
481    ///Originator of message.
482    pub from: String,
483    ///Destination of message.
484    pub to: String,
485    ///Message content.
486    pub body: String,
487    ///ID of message
488    ///
489    ///Can be used to query SMS information via following link:
490    ///`/2010-04-01/Accounts/{account_sid}/Messages/{sid}.json`
491    pub sid: String,
492    ///Status of message.
493    pub status: SmsStatus,
494    ///URL of optional media attachment
495    pub media_url: Option<String>,
496    ///Cost of message
497    pub price: Option<String>,
498    ///Currency unit of `cost`.
499    pub price_unit: String,
500    ///Timestamp (including zone) of when message is created.
501    ///
502    ///Can be None, despite it obviously not making sense
503    pub date_created: Option<String>,
504    ///Timestamp (including zone) of when message is sent.
505    pub date_sent: Option<String>,
506    ///Timestamp (including zone) of when message is updated.
507    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)]
525///Result of correct SMS request.
526pub struct CallResult {
527    ///Originator of message.
528    pub from: String,
529    ///Destination of message.
530    pub to: String,
531    ///ID of call
532    ///
533    ///Can be used to query Call information via following link:
534    ///`/2010-04-01/Accounts/{account_sid}/Calls/{sid}.json`
535    pub sid: String,
536    ///Status of message.
537    pub status: CallStatus,
538    ///Caller's name
539    pub caller_name: Option<String>,
540    ///Call's duration.
541    pub duration: Option<i64>,
542    ///Cost of call
543    pub price: Option<String>,
544    ///Currency unit of `cost`.
545    pub price_unit: String,
546    ///Timestamp (including zone) of when call is created.
547    ///
548    ///Can be None, despite it obviously not making sense
549    pub date_created: Option<String>,
550    ///Timestamp (including zone) of when call is established.
551    pub start_time: Option<String>,
552    ///Timestamp (including zone) of when call is finished.
553    pub end_time: Option<String>,
554    ///Call's direction.
555    pub direction: Option<CallDirection>,
556    #[serde(deserialize_with = "deserialize_number_from_any")]
557    ///The wait time in milliseconds before call is started.
558    pub queue_time: i64
559}
560
561#[derive(Debug, Deserialize)]
562///Error returned by Twilio REST API.
563pub struct TwilioError {
564    ///Error code
565    pub code: usize,
566    ///Error description
567    pub message: String,
568    ///Corresponding HTTP status code
569    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}