twiml_rust/
messaging.rs

1//! TwiML generation for SMS and MMS messaging responses.
2//!
3//! This module provides types and builders for creating TwiML responses
4//! for SMS and MMS messages. The main entry point is [`MessagingResponse`],
5//! which can contain [`Message`] verbs with [`Body`] and [`Media`] nouns.
6//!
7//! # Examples
8//!
9//! ## Simple SMS
10//!
11//! ```rust
12//! use twiml_rust::{MessagingResponse, TwiML};
13//!
14//! let response = MessagingResponse::new()
15//!     .message("Thanks for your message!");
16//!
17//! println!("{}", response.to_xml());
18//! ```
19//!
20//! ## MMS with Multiple Media
21//!
22//! ```rust
23//! use twiml_rust::{MessagingResponse, messaging::{Message, MessageAttributes, Body, Media}, TwiML};
24//!
25//! let message = Message::with_nouns(MessageAttributes::new())
26//!     .body(Body::new("Check out these photos!"))
27//!     .add_media(Media::new("https://example.com/photo1.jpg"))
28//!     .add_media(Media::new("https://example.com/photo2.jpg"));
29//!
30//! let response = MessagingResponse::new()
31//!     .message_with_nouns(message);
32//!
33//! println!("{}", response.to_xml());
34//! ```
35
36use crate::xml_escape::{escape_xml_attr, escape_xml_text};
37use crate::TwiML;
38
39/// Attributes to pass to message
40#[derive(Debug, Clone, Default)]
41pub struct MessageAttributes {
42    /// action - A URL specifying where Twilio should send status callbacks for the created outbound message.
43    pub action: Option<String>,
44    /// from - Phone Number to send Message from
45    pub from: Option<String>,
46    /// method - Action URL Method
47    pub method: Option<String>,
48    /// statusCallback - Status callback URL. Deprecated in favor of action.
49    pub status_callback: Option<String>,
50    /// to - Phone Number to send Message to
51    pub to: Option<String>,
52}
53
54impl MessageAttributes {
55    /// Create a new MessageAttributes
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Set the action URL
61    pub fn action(mut self, action: impl Into<String>) -> Self {
62        self.action = Some(action.into());
63        self
64    }
65
66    /// Set the from phone number
67    pub fn from(mut self, from: impl Into<String>) -> Self {
68        self.from = Some(from.into());
69        self
70    }
71
72    /// Set the HTTP method
73    pub fn method(mut self, method: impl Into<String>) -> Self {
74        self.method = Some(method.into());
75        self
76    }
77
78    /// Set the status callback URL
79    pub fn status_callback(mut self, status_callback: impl Into<String>) -> Self {
80        self.status_callback = Some(status_callback.into());
81        self
82    }
83
84    /// Set the to phone number
85    pub fn to(mut self, to: impl Into<String>) -> Self {
86        self.to = Some(to.into());
87        self
88    }
89}
90
91/// Attributes to pass to redirect
92#[derive(Debug, Clone, Default)]
93pub struct RedirectAttributes {
94    /// method - Redirect URL method
95    pub method: Option<String>,
96}
97
98impl RedirectAttributes {
99    /// Create a new RedirectAttributes
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Set the HTTP method
105    pub fn method(mut self, method: impl Into<String>) -> Self {
106        self.method = Some(method.into());
107        self
108    }
109}
110
111/// <Body> TwiML Noun
112#[derive(Debug, Clone)]
113pub struct Body {
114    message: String,
115}
116
117impl Body {
118    /// Create a new Body noun
119    pub fn new(message: impl Into<String>) -> Self {
120        Self {
121            message: message.into(),
122        }
123    }
124
125    fn to_xml(&self) -> String {
126        format!("<Body>{}</Body>", escape_xml_text(&self.message))
127    }
128}
129
130/// <Media> TwiML Noun
131#[derive(Debug, Clone)]
132pub struct Media {
133    url: String,
134}
135
136impl Media {
137    /// Create a new Media noun
138    pub fn new(url: impl Into<String>) -> Self {
139        Self { url: url.into() }
140    }
141
142    fn to_xml(&self) -> String {
143        format!("<Media>{}</Media>", escape_xml_text(&self.url))
144    }
145}
146
147/// <Message> TwiML Verb
148#[derive(Debug, Clone)]
149pub struct Message {
150    attributes: MessageAttributes,
151    body: Option<Body>,
152    media: Vec<Media>,
153}
154
155impl Message {
156    /// Create a new Message verb with plain text (backward compatible)
157    pub(crate) fn new(attributes: MessageAttributes, body: Option<String>) -> Self {
158        Self {
159            attributes,
160            body: body.map(Body::new),
161            media: Vec::new(),
162        }
163    }
164
165    /// Create a new Message verb with Body and Media nouns
166    pub fn with_nouns(attributes: MessageAttributes) -> Self {
167        Self {
168            attributes,
169            body: None,
170            media: Vec::new(),
171        }
172    }
173
174    /// Add a Body noun to the Message
175    pub fn body(mut self, body: Body) -> Self {
176        self.body = Some(body);
177        self
178    }
179
180    /// Add a Media noun to the Message
181    pub fn add_media(mut self, media: Media) -> Self {
182        self.media.push(media);
183        self
184    }
185
186    /// Add multiple Media nouns to the Message
187    pub fn media(mut self, media: Vec<Media>) -> Self {
188        self.media = media;
189        self
190    }
191
192    fn to_xml(&self) -> String {
193        let mut xml = String::from("<Message");
194
195        // Add attributes
196        if let Some(ref action) = self.attributes.action {
197            xml.push_str(&format!(" action=\"{}\"", escape_xml_attr(action)));
198        }
199        if let Some(ref from) = self.attributes.from {
200            xml.push_str(&format!(" from=\"{}\"", escape_xml_attr(from)));
201        }
202        if let Some(ref method) = self.attributes.method {
203            xml.push_str(&format!(" method=\"{}\"", escape_xml_attr(method)));
204        }
205        if let Some(ref status_callback) = self.attributes.status_callback {
206            xml.push_str(&format!(
207                " statusCallback=\"{}\"",
208                escape_xml_attr(status_callback)
209            ));
210        }
211        if let Some(ref to) = self.attributes.to {
212            xml.push_str(&format!(" to=\"{}\"", escape_xml_attr(to)));
213        }
214
215        // Check if we have body or media
216        if self.body.is_some() || !self.media.is_empty() {
217            xml.push('>');
218
219            // Add Body noun if present
220            if let Some(ref body) = self.body {
221                xml.push_str(&body.to_xml());
222            }
223
224            // Add Media nouns
225            for media in &self.media {
226                xml.push_str(&media.to_xml());
227            }
228
229            xml.push_str("</Message>");
230        } else {
231            xml.push_str(" />");
232        }
233
234        xml
235    }
236}
237
238/// <Redirect> TwiML Verb
239#[derive(Debug, Clone)]
240pub struct Redirect {
241    attributes: RedirectAttributes,
242    url: String,
243}
244
245impl Redirect {
246    /// Create a new Redirect verb
247    pub(crate) fn new(attributes: RedirectAttributes, url: impl Into<String>) -> Self {
248        Self {
249            attributes,
250            url: url.into(),
251        }
252    }
253
254    fn to_xml(&self) -> String {
255        let mut xml = String::from("<Redirect");
256
257        if let Some(ref method) = self.attributes.method {
258            xml.push_str(&format!(" method=\"{}\"", escape_xml_attr(method)));
259        }
260
261        xml.push_str(&format!(">{}</Redirect>", escape_xml_text(&self.url)));
262        xml
263    }
264}
265
266/// Top-level TwiML verbs for messaging
267#[derive(Debug, Clone)]
268pub(crate) enum MessagingVerb {
269    Message(Message),
270    Redirect(Redirect),
271}
272
273/// <Response> TwiML for Messages
274#[derive(Debug, Clone, Default)]
275pub struct MessagingResponse {
276    pub(crate) verbs: Vec<MessagingVerb>,
277    comments_before: Vec<String>,
278    comments: Vec<String>,
279    comments_after: Vec<String>,
280}
281
282impl MessagingResponse {
283    /// Create a new MessagingResponse
284    ///
285    /// <Response> TwiML for Messages
286    pub fn new() -> Self {
287        Self::default()
288    }
289
290    /// <Message> TwiML Verb
291    ///
292    /// Supports two calling patterns:
293    /// - `message(body)` - Simple message with just body text
294    /// - Use `message_with_attributes` for attributes + body
295    ///
296    /// # Arguments
297    /// * `body` - Message Body
298    ///
299    /// # Returns
300    /// Returns self for method chaining
301    pub fn message(mut self, body: impl Into<String>) -> Self {
302        let message = Message::new(MessageAttributes::default(), Some(body.into()));
303        self.verbs.push(MessagingVerb::Message(message));
304        self
305    }
306
307    /// <Message> TwiML Verb with attributes
308    ///
309    /// # Arguments
310    /// * `attributes` - TwiML attributes
311    /// * `body` - Message Body
312    ///
313    /// # Returns
314    /// Returns self for method chaining
315    pub fn message_with_attributes(
316        mut self,
317        attributes: MessageAttributes,
318        body: impl Into<String>,
319    ) -> Self {
320        let message = Message::new(attributes, Some(body.into()));
321        self.verbs.push(MessagingVerb::Message(message));
322        self
323    }
324
325    /// <Message> TwiML Verb with Body and Media nouns
326    ///
327    /// This allows you to create messages with proper <Body> and <Media> nouns,
328    /// supporting multiple media attachments for MMS.
329    ///
330    /// # Arguments
331    /// * `message` - Pre-configured Message with Body and/or Media nouns
332    ///
333    /// # Returns
334    /// Returns self for method chaining
335    ///
336    /// # Example
337    /// ```
338    /// use twiml_rust::{MessagingResponse, Message, MessageAttributes, Body, Media, TwiML};
339    ///
340    /// let message = Message::with_nouns(MessageAttributes::new())
341    ///     .body(Body::new("Check out these images!"))
342    ///     .add_media(Media::new("https://example.com/image1.jpg"))
343    ///     .add_media(Media::new("https://example.com/image2.jpg"));
344    ///
345    /// let response = MessagingResponse::new().message_with_nouns(message);
346    /// let xml = response.to_xml();
347    /// ```
348    pub fn message_with_nouns(mut self, message: Message) -> Self {
349        self.verbs.push(MessagingVerb::Message(message));
350        self
351    }
352
353    /// <Redirect> TwiML Verb
354    ///
355    /// Supports two calling patterns:
356    /// - `redirect(url)` - Simple redirect with just URL
357    /// - Use `redirect_with_attributes` for attributes + URL
358    ///
359    /// # Arguments
360    /// * `url` - Redirect URL
361    ///
362    /// # Returns
363    /// Returns self for method chaining
364    pub fn redirect(mut self, url: impl Into<String>) -> Self {
365        let redirect = Redirect::new(RedirectAttributes::default(), url);
366        self.verbs.push(MessagingVerb::Redirect(redirect));
367        self
368    }
369
370    /// <Redirect> TwiML Verb with attributes
371    ///
372    /// # Arguments
373    /// * `attributes` - TwiML attributes
374    /// * `url` - Redirect URL
375    ///
376    /// # Returns
377    /// Returns self for method chaining
378    pub fn redirect_with_attributes(
379        mut self,
380        attributes: RedirectAttributes,
381        url: impl Into<String>,
382    ) -> Self {
383        let redirect = Redirect::new(attributes, url);
384        self.verbs.push(MessagingVerb::Redirect(redirect));
385        self
386    }
387
388    /// Comments in <Response>
389    ///
390    /// # Arguments
391    /// * `comment` - XML Comment
392    ///
393    /// # Returns
394    /// Returns self for method chaining
395    pub fn comment(mut self, comment: impl Into<String>) -> Self {
396        self.comments.push(comment.into());
397        self
398    }
399
400    /// Comments after <Response>
401    ///
402    /// # Arguments
403    /// * `comment` - XML Comment
404    ///
405    /// # Returns
406    /// Returns self for method chaining
407    pub fn comment_after(mut self, comment: impl Into<String>) -> Self {
408        self.comments_after.push(comment.into());
409        self
410    }
411
412    /// Comments before <Response>
413    ///
414    /// # Arguments
415    /// * `comment` - XML Comment
416    ///
417    /// # Returns
418    /// Returns self for method chaining
419    pub fn comment_before(mut self, comment: impl Into<String>) -> Self {
420        self.comments_before.push(comment.into());
421        self
422    }
423}
424
425impl TwiML for MessagingResponse {
426    fn to_xml(&self) -> String {
427        let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
428
429        // Add comments before Response
430        for comment in &self.comments_before {
431            xml.push_str(&format!("<!-- {} -->\n", escape_xml_text(comment)));
432        }
433
434        xml.push_str("<Response>");
435
436        // Add comments inside Response
437        for comment in &self.comments {
438            xml.push_str(&format!("\n  <!-- {} -->", escape_xml_text(comment)));
439        }
440
441        for verb in &self.verbs {
442            match verb {
443                MessagingVerb::Message(message) => {
444                    xml.push_str(&message.to_xml());
445                }
446                MessagingVerb::Redirect(redirect) => {
447                    xml.push_str(&redirect.to_xml());
448                }
449            }
450        }
451
452        xml.push_str("</Response>");
453
454        // Add comments after Response
455        for comment in &self.comments_after {
456            xml.push_str(&format!("\n<!-- {} -->", escape_xml_text(comment)));
457        }
458
459        xml
460    }
461}