Skip to main content

vonage_sms/
types.rs

1//! SMS types and data structures
2
3use crate::status::SmsStatus;
4use serde::{Deserialize, Serialize, Deserializer};
5
6/// Custom deserializer for fields that can be either string or number
7fn deserialize_string_or_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
8where
9    D: Deserializer<'de>,
10{
11    use serde::de::Error;
12    
13    match serde_json::Value::deserialize(deserializer)? {
14        serde_json::Value::String(s) => {
15            s.parse::<u32>().map_err(|_| D::Error::custom(format!("invalid number string: {}", s)))
16        }
17        serde_json::Value::Number(n) => {
18            n.as_u64()
19                .and_then(|n| if n <= u32::MAX as u64 { Some(n as u32) } else { None })
20                .ok_or_else(|| D::Error::custom("number out of range for u32"))
21        }
22        _ => Err(D::Error::custom("expected string or number")),
23    }
24}
25
26/// SMS message type
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum MessageType {
30    /// Plain text message
31    Text,
32    /// Binary message
33    Binary,
34    /// WAP push message  
35    Wappush,
36    /// Unicode message
37    Unicode,
38}
39
40impl std::fmt::Display for MessageType {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            MessageType::Text => write!(f, "text"),
44            MessageType::Binary => write!(f, "binary"),
45            MessageType::Wappush => write!(f, "wappush"),
46            MessageType::Unicode => write!(f, "unicode"),
47        }
48    }
49}
50
51/// SMS message class
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub enum MessageClass {
54    /// Flash SMS (class 0)
55    #[serde(rename = "0")]
56    Flash = 0,
57    /// ME specific (class 1)
58    #[serde(rename = "1")]
59    MeSpecific = 1,
60    /// SIM specific (class 2)  
61    #[serde(rename = "2")]
62    SimSpecific = 2,
63    /// TE specific (class 3)
64    #[serde(rename = "3")]
65    TeSpecific = 3,
66}
67
68/// SMS request structure for sending messages
69#[derive(Debug, Clone, Serialize)]
70pub struct SmsRequest {
71    /// Sender ID or phone number
72    pub from: String,
73    /// Recipient phone number
74    pub to: String,
75    /// Message text (for text messages)
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub text: Option<String>,
78    /// Message type (defaults to text)
79    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
80    pub message_type: Option<MessageType>,
81    /// Binary message body (hex encoded)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub body: Option<String>,
84    /// User Data Header for binary messages
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub udh: Option<String>,
87    /// Protocol identifier
88    #[serde(rename = "protocol-id", skip_serializing_if = "Option::is_none")]
89    pub protocol_id: Option<u8>,
90    /// Message class
91    #[serde(rename = "message-class", skip_serializing_if = "Option::is_none")]
92    pub message_class: Option<MessageClass>,
93    /// Time-to-live in milliseconds
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub ttl: Option<u32>,
96    /// Delivery receipt callback URL
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub callback: Option<String>,
99    /// Request delivery receipt
100    #[serde(rename = "status-report-req", skip_serializing_if = "Option::is_none")]
101    pub status_report_req: Option<bool>,
102    /// Client reference
103    #[serde(rename = "client-ref", skip_serializing_if = "Option::is_none")]
104    pub client_ref: Option<String>,
105    /// Account reference for billing
106    #[serde(rename = "account-ref", skip_serializing_if = "Option::is_none")]
107    pub account_ref: Option<String>,
108    /// Entity ID for regulatory requirements
109    #[serde(rename = "entity-id", skip_serializing_if = "Option::is_none")]
110    pub entity_id: Option<String>,
111    /// Content ID for regulatory requirements
112    #[serde(rename = "content-id", skip_serializing_if = "Option::is_none")]
113    pub content_id: Option<String>,
114}
115
116impl SmsRequest {
117    /// Create a new text message request
118    pub fn text(from: impl Into<String>, to: impl Into<String>, text: impl Into<String>) -> Self {
119        Self {
120            from: from.into(),
121            to: to.into(),
122            text: Some(text.into()),
123            message_type: Some(MessageType::Text),
124            body: None,
125            udh: None,
126            protocol_id: None,
127            message_class: None,
128            ttl: None,
129            callback: None,
130            status_report_req: None,
131            client_ref: None,
132            account_ref: None,
133            entity_id: None,
134            content_id: None,
135        }
136    }
137    
138    /// Create a new binary message request
139    pub fn binary(from: impl Into<String>, to: impl Into<String>, body: impl Into<String>) -> Self {
140        Self {
141            from: from.into(),
142            to: to.into(),
143            text: None,
144            message_type: Some(MessageType::Binary),
145            body: Some(body.into()),
146            udh: None,
147            protocol_id: None,
148            message_class: None,
149            ttl: None,
150            callback: None,
151            status_report_req: None,
152            client_ref: None,
153            account_ref: None,
154            entity_id: None,
155            content_id: None,
156        }
157    }
158    
159    /// Set the client reference
160    pub fn with_client_ref(mut self, client_ref: impl Into<String>) -> Self {
161        self.client_ref = Some(client_ref.into());
162        self
163    }
164    
165    /// Set the callback URL for delivery receipts
166    pub fn with_callback(mut self, callback: impl Into<String>) -> Self {
167        self.callback = Some(callback.into());
168        self
169    }
170    
171    /// Enable delivery receipts
172    pub fn with_status_report(mut self, enabled: bool) -> Self {
173        self.status_report_req = Some(enabled);
174        self
175    }
176    
177    /// Set time-to-live in milliseconds
178    pub fn with_ttl(mut self, ttl: u32) -> Self {
179        self.ttl = Some(ttl);
180        self
181    }
182}
183
184/// Individual SMS message status in the response
185#[derive(Debug, Clone, Deserialize)]
186pub struct SmsMessage {
187    /// Recipient number (optional in error responses)
188    pub to: Option<String>,
189    /// Message ID (optional in error responses)
190    #[serde(rename = "message-id")]
191    pub message_id: Option<String>,
192    /// Message status
193    pub status: SmsStatus,
194    /// Error text if status is not success
195    #[serde(rename = "error-text")]
196    pub error_text: Option<String>,
197    /// Remaining account balance
198    #[serde(rename = "remaining-balance")]
199    pub remaining_balance: Option<String>,
200    /// Message price
201    #[serde(rename = "message-price")]
202    pub message_price: Option<String>,
203    /// Network code
204    pub network: Option<String>,
205    /// Account reference
206    #[serde(rename = "account-ref")]
207    pub account_ref: Option<String>,
208}
209
210/// Message status for checking delivery status
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum MessageStatus {
213    /// Message was successfully delivered
214    Delivered,
215    /// Message failed to deliver
216    Failed,
217    /// Message is still pending delivery
218    Pending,
219    /// Message status is unknown
220    Unknown,
221}
222
223/// SMS response from the API
224#[derive(Debug, Clone, Deserialize)]
225pub struct SmsResponse {
226    /// Number of messages sent
227    #[serde(rename = "message-count", deserialize_with = "deserialize_string_or_u32")]
228    pub message_count: u32,
229    /// Individual message details
230    pub messages: Vec<SmsMessage>,
231}
232
233impl SmsResponse {
234    /// Check if all messages were sent successfully
235    pub fn all_successful(&self) -> bool {
236        self.messages.iter().all(|msg| msg.status.is_success())
237    }
238    
239    /// Get count of successful messages
240    pub fn success_count(&self) -> u32 {
241        self.messages.iter().filter(|msg| msg.status.is_success()).count() as u32
242    }
243    
244    /// Get count of failed messages
245    pub fn failure_count(&self) -> u32 {
246        self.messages.iter().filter(|msg| msg.status.is_error()).count() as u32
247    }
248    
249    /// Get all failed messages
250    pub fn failed_messages(&self) -> Vec<&SmsMessage> {
251        self.messages.iter().filter(|msg| msg.status.is_error()).collect()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_sms_response_deserialize_string_count() {
261        let json = r#"{
262            "message-count": "1",
263            "messages": [{
264                "to": "447700900000",
265                "message-id": "0A0000000123ABCD1",
266                "status": "0",
267                "remaining-balance": "3.14159265",
268                "message-price": "0.03330000",
269                "network": "12345"
270            }]
271        }"#;
272        
273        let response: SmsResponse = serde_json::from_str(json).unwrap();
274        assert_eq!(response.message_count, 1);
275        assert_eq!(response.messages.len(), 1);
276    }
277
278    #[test]
279    fn test_sms_response_deserialize_number_count() {
280        let json = r#"{
281            "message-count": 2,
282            "messages": [{
283                "to": "447700900000",
284                "message-id": "0A0000000123ABCD1",
285                "status": "0",
286                "remaining-balance": "3.14159265",
287                "message-price": "0.03330000",
288                "network": "12345"
289            }]
290        }"#;
291        
292        let response: SmsResponse = serde_json::from_str(json).unwrap();
293        assert_eq!(response.message_count, 2);
294        assert_eq!(response.messages.len(), 1);
295    }
296}