twiml_rust/
validation_warnings.rs

1/// Optional validation warnings for TwiML best practices
2///
3/// These are NOT errors - the generated TwiML is valid.
4/// These warnings help developers avoid common pitfalls.
5use crate::messaging::{MessagingResponse, MessagingVerb};
6
7/// Warning types for TwiML best practices
8#[derive(Debug, Clone, PartialEq)]
9pub enum TwiMLWarning {
10    /// A Redirect verb appears after a Message with an action attribute
11    /// The Redirect will be unreachable because control flow will transfer to the action URL
12    UnreachableRedirectAfterMessageWithAction {
13        message_index: usize,
14        redirect_index: usize,
15    },
16
17    /// An empty Redirect URL will create an infinite loop
18    EmptyRedirectUrl { redirect_index: usize },
19
20    /// Multiple verbs after a Redirect (they will never be reached)
21    UnreachableVerbsAfterRedirect {
22        redirect_index: usize,
23        unreachable_count: usize,
24    },
25}
26
27impl std::fmt::Display for TwiMLWarning {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            TwiMLWarning::UnreachableRedirectAfterMessageWithAction {
31                message_index,
32                redirect_index,
33            } => {
34                write!(
35                    f,
36                    "Warning: Redirect at index {} may be unreachable because Message at index {} has an action attribute",
37                    redirect_index, message_index
38                )
39            }
40            TwiMLWarning::EmptyRedirectUrl { redirect_index } => {
41                write!(
42                    f,
43                    "Warning: Redirect at index {} has an empty URL, which will create an infinite loop",
44                    redirect_index
45                )
46            }
47            TwiMLWarning::UnreachableVerbsAfterRedirect {
48                redirect_index,
49                unreachable_count,
50            } => {
51                write!(
52                    f,
53                    "Warning: {} verb(s) after Redirect at index {} will never be reached",
54                    unreachable_count, redirect_index
55                )
56            }
57        }
58    }
59}
60
61impl MessagingResponse {
62    /// Validate the TwiML response and return warnings (if any)
63    ///
64    /// This does NOT affect XML generation - the TwiML is still valid.
65    /// Warnings help identify potential logic issues.
66    ///
67    /// # Example
68    /// ```
69    /// use twiml_rust::messaging::MessagingResponse;
70    ///
71    /// let response = MessagingResponse::new()
72    ///     .message("Hello!")
73    ///     .redirect("https://example.com");
74    ///
75    /// let warnings = response.validate();
76    /// assert!(warnings.is_empty()); // No warnings - this is fine
77    /// ```
78    pub fn validate(&self) -> Vec<TwiMLWarning> {
79        let mut warnings = Vec::new();
80
81        // Check for unreachable verbs after Redirect
82        for (i, verb) in self.verbs.iter().enumerate() {
83            if matches!(verb, MessagingVerb::Redirect(_)) {
84                let remaining = self.verbs.len() - i - 1;
85                if remaining > 0 {
86                    warnings.push(TwiMLWarning::UnreachableVerbsAfterRedirect {
87                        redirect_index: i,
88                        unreachable_count: remaining,
89                    });
90                }
91            }
92        }
93
94        warnings
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::messaging::MessagingResponse;
102
103    #[test]
104    fn test_no_warnings_for_simple_message() {
105        let response = MessagingResponse::new().message("Hello!");
106        let warnings = response.validate();
107        assert!(warnings.is_empty());
108    }
109
110    #[test]
111    fn test_no_warnings_for_message_then_redirect() {
112        let response = MessagingResponse::new()
113            .message("Hello!")
114            .redirect("https://example.com");
115        let warnings = response.validate();
116        assert!(warnings.is_empty());
117    }
118
119    #[test]
120    fn test_warning_for_verbs_after_redirect() {
121        let response = MessagingResponse::new()
122            .redirect("https://example.com")
123            .message("This will never be sent");
124
125        let warnings = response.validate();
126        assert_eq!(warnings.len(), 1);
127        assert!(matches!(
128            warnings[0],
129            TwiMLWarning::UnreachableVerbsAfterRedirect {
130                redirect_index: 0,
131                unreachable_count: 1
132            }
133        ));
134    }
135
136    #[test]
137    fn test_warning_display() {
138        let warning = TwiMLWarning::UnreachableVerbsAfterRedirect {
139            redirect_index: 0,
140            unreachable_count: 2,
141        };
142        let display = format!("{}", warning);
143        assert!(display.contains("2 verb(s)"));
144        assert!(display.contains("index 0"));
145    }
146}