Skip to main content

smsru/domain/
request.rs

1use std::collections::BTreeMap;
2use std::net::IpAddr;
3
4use crate::domain::validation::ValidationError;
5use crate::domain::value::{
6    MessageText, PartnerId, RawPhoneNumber, SenderId, SmsId, TtlMinutes, UnixTimestamp,
7};
8
9/// SMS.RU "send SMS" API limit: maximum number of recipients per request.
10pub const SEND_SMS_MAX_RECIPIENTS: usize = 100;
11/// SMS.RU "check status" API limit: maximum number of ids per request.
12pub const CHECK_STATUS_MAX_SMS_IDS: usize = 100;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15/// Response format mode requested from SMS.RU.
16///
17/// The client currently supports only [`JsonMode::Json`].
18pub enum JsonMode {
19    #[default]
20    /// Request JSON responses (`json=1`).
21    Json,
22    /// Request plain-text responses (`json=0`).
23    Plain,
24}
25
26#[derive(Debug, Clone, Default)]
27/// Optional parameters for the "send SMS" request.
28///
29/// These map to SMS.RU form fields; most are optional and default to "not set".
30pub struct SendOptions {
31    /// Response format requested from SMS.RU (defaults to JSON).
32    pub json: JsonMode,
33    /// Sender id (`from=`). Must be enabled in your SMS.RU account.
34    pub from: Option<SenderId>,
35    /// End user IP (`ip=`), used by SMS.RU for anti-fraud/limits in some modes.
36    pub ip: Option<IpAddr>,
37    /// Scheduled send time as a unix timestamp in seconds (`time=`).
38    pub time: Option<UnixTimestamp>,
39    /// Per-recipient TTL in minutes (`ttl=`). See [`TtlMinutes`] for range.
40    pub ttl: Option<TtlMinutes>,
41    /// Send only during daytime (`daytime=1`).
42    pub daytime: bool,
43    /// Transliterate message (`translit=1`).
44    pub translit: bool,
45    /// Test mode (`test=1`): validate request without sending an SMS.
46    pub test: bool,
47    /// Optional partner identifier (`partner_id=`).
48    pub partner_id: Option<PartnerId>,
49}
50
51#[derive(Debug, Clone)]
52/// A validated "send SMS" request.
53///
54/// Use [`SendSms::to_many`] to send one message to many recipients, or
55/// [`SendSms::per_recipient`] to send per-recipient messages.
56pub enum SendSms {
57    /// One message to many recipients.
58    ToMany(ToMany),
59    /// Different messages per recipient.
60    PerRecipient(PerRecipient),
61}
62
63#[derive(Debug, Clone)]
64/// "One message to many recipients" request shape.
65pub struct ToMany {
66    recipients: Vec<RawPhoneNumber>,
67    msg: MessageText,
68    options: SendOptions,
69}
70
71#[derive(Debug, Clone)]
72/// "Per-recipient message" request shape.
73pub struct PerRecipient {
74    messages: BTreeMap<RawPhoneNumber, MessageText>,
75    options: SendOptions,
76}
77
78#[derive(Debug, Clone)]
79/// A validated "check status" request.
80///
81/// Use [`CheckStatus::new`] for one or many ids or [`CheckStatus::one`] as a convenience.
82pub struct CheckStatus {
83    sms_ids: Vec<SmsId>,
84}
85
86impl SendSms {
87    /// Create a "one message to many recipients" request.
88    ///
89    /// Constraints:
90    /// - `recipients` must be non-empty
91    /// - `recipients.len()` must be `<= SEND_SMS_MAX_RECIPIENTS` (100)
92    pub fn to_many(
93        recipients: Vec<RawPhoneNumber>,
94        msg: MessageText,
95        options: SendOptions,
96    ) -> Result<Self, ValidationError> {
97        if recipients.is_empty() {
98            return Err(ValidationError::Empty {
99                field: RawPhoneNumber::FIELD,
100            });
101        }
102        if recipients.len() > SEND_SMS_MAX_RECIPIENTS {
103            return Err(ValidationError::TooManyRecipients {
104                max: SEND_SMS_MAX_RECIPIENTS,
105                actual: recipients.len(),
106            });
107        }
108        Ok(Self::ToMany(ToMany {
109            recipients,
110            msg,
111            options,
112        }))
113    }
114
115    /// Create a "per-recipient message" request.
116    ///
117    /// Constraints:
118    /// - `messages` must be non-empty
119    /// - `messages.len()` must be `<= SEND_SMS_MAX_RECIPIENTS` (100)
120    pub fn per_recipient(
121        messages: BTreeMap<RawPhoneNumber, MessageText>,
122        options: SendOptions,
123    ) -> Result<Self, ValidationError> {
124        if messages.is_empty() {
125            return Err(ValidationError::Empty {
126                field: RawPhoneNumber::FIELD,
127            });
128        }
129        if messages.len() > SEND_SMS_MAX_RECIPIENTS {
130            return Err(ValidationError::TooManyRecipients {
131                max: SEND_SMS_MAX_RECIPIENTS,
132                actual: messages.len(),
133            });
134        }
135        Ok(Self::PerRecipient(PerRecipient { messages, options }))
136    }
137}
138
139impl ToMany {
140    /// Recipient phone numbers as provided (not normalized).
141    pub fn recipients(&self) -> &[RawPhoneNumber] {
142        &self.recipients
143    }
144
145    /// Message text (must be non-empty; see [`MessageText`]).
146    pub fn msg(&self) -> &MessageText {
147        &self.msg
148    }
149
150    /// Request options.
151    pub fn options(&self) -> &SendOptions {
152        &self.options
153    }
154}
155
156impl PerRecipient {
157    /// Per-recipient messages.
158    pub fn messages(&self) -> &BTreeMap<RawPhoneNumber, MessageText> {
159        &self.messages
160    }
161
162    /// Request options.
163    pub fn options(&self) -> &SendOptions {
164        &self.options
165    }
166}
167
168impl CheckStatus {
169    /// Create a "check status" request.
170    ///
171    /// Constraints:
172    /// - `sms_ids` must be non-empty
173    /// - `sms_ids.len()` must be `<= CHECK_STATUS_MAX_SMS_IDS` (100)
174    pub fn new(sms_ids: Vec<SmsId>) -> Result<Self, ValidationError> {
175        if sms_ids.is_empty() {
176            return Err(ValidationError::Empty {
177                field: SmsId::FIELD,
178            });
179        }
180        if sms_ids.len() > CHECK_STATUS_MAX_SMS_IDS {
181            return Err(ValidationError::TooManySmsIds {
182                max: CHECK_STATUS_MAX_SMS_IDS,
183                actual: sms_ids.len(),
184            });
185        }
186        Ok(Self { sms_ids })
187    }
188
189    /// Create a "check status" request for one message id.
190    pub fn one(sms_id: SmsId) -> Self {
191        Self {
192            sms_ids: vec![sms_id],
193        }
194    }
195
196    /// Message ids to query.
197    pub fn sms_ids(&self) -> &[SmsId] {
198        &self.sms_ids
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    fn make_recipients(count: usize) -> Vec<RawPhoneNumber> {
207        (0..count)
208            .map(|idx| RawPhoneNumber::new(format!("+792512300{idx:02}")).unwrap())
209            .collect()
210    }
211
212    #[test]
213    fn to_many_rejects_empty_recipients() {
214        let msg = MessageText::new("hi").unwrap();
215        let err = SendSms::to_many(Vec::new(), msg, SendOptions::default()).unwrap_err();
216        assert_eq!(
217            err,
218            ValidationError::Empty {
219                field: RawPhoneNumber::FIELD
220            }
221        );
222    }
223
224    #[test]
225    fn to_many_rejects_too_many_recipients() {
226        let msg = MessageText::new("hi").unwrap();
227        let recipients = make_recipients(SEND_SMS_MAX_RECIPIENTS + 1);
228        let err = SendSms::to_many(recipients, msg, SendOptions::default()).unwrap_err();
229        assert_eq!(
230            err,
231            ValidationError::TooManyRecipients {
232                max: SEND_SMS_MAX_RECIPIENTS,
233                actual: SEND_SMS_MAX_RECIPIENTS + 1
234            }
235        );
236    }
237
238    #[test]
239    fn per_recipient_rejects_empty_messages() {
240        let err = SendSms::per_recipient(BTreeMap::new(), SendOptions::default()).unwrap_err();
241        assert_eq!(
242            err,
243            ValidationError::Empty {
244                field: RawPhoneNumber::FIELD
245            }
246        );
247    }
248
249    #[test]
250    fn per_recipient_rejects_too_many_messages() {
251        let mut messages = BTreeMap::new();
252        for idx in 0..(SEND_SMS_MAX_RECIPIENTS + 1) {
253            messages.insert(
254                RawPhoneNumber::new(format!("+792512310{idx:02}")).unwrap(),
255                MessageText::new("hi").unwrap(),
256            );
257        }
258        let err = SendSms::per_recipient(messages, SendOptions::default()).unwrap_err();
259        assert_eq!(
260            err,
261            ValidationError::TooManyRecipients {
262                max: SEND_SMS_MAX_RECIPIENTS,
263                actual: SEND_SMS_MAX_RECIPIENTS + 1
264            }
265        );
266    }
267
268    #[test]
269    fn to_many_exposes_fields() {
270        let recipients = make_recipients(2);
271        let msg = MessageText::new("hello").unwrap();
272        let options = SendOptions::default();
273        let req = SendSms::to_many(recipients.clone(), msg.clone(), options.clone()).unwrap();
274
275        match req {
276            SendSms::ToMany(to_many) => {
277                assert_eq!(to_many.recipients(), recipients.as_slice());
278                assert_eq!(to_many.msg(), &msg);
279                assert_eq!(to_many.options().json, options.json);
280            }
281            SendSms::PerRecipient(_) => panic!("expected to_many request"),
282        }
283    }
284
285    #[test]
286    fn per_recipient_exposes_fields() {
287        let mut messages = BTreeMap::new();
288        let p1 = RawPhoneNumber::new("+79251234567").unwrap();
289        let msg = MessageText::new("hello").unwrap();
290        messages.insert(p1.clone(), msg.clone());
291        let options = SendOptions::default();
292
293        let req = SendSms::per_recipient(messages.clone(), options.clone()).unwrap();
294        match req {
295            SendSms::PerRecipient(per_recipient) => {
296                assert_eq!(per_recipient.messages(), &messages);
297                assert_eq!(per_recipient.options().json, options.json);
298            }
299            SendSms::ToMany(_) => panic!("expected per_recipient request"),
300        }
301    }
302
303    #[test]
304    fn check_status_rejects_empty_sms_ids() {
305        let err = CheckStatus::new(Vec::new()).unwrap_err();
306        assert_eq!(
307            err,
308            ValidationError::Empty {
309                field: SmsId::FIELD
310            }
311        );
312    }
313
314    #[test]
315    fn check_status_rejects_too_many_sms_ids() {
316        let sms_ids = (0..(CHECK_STATUS_MAX_SMS_IDS + 1))
317            .map(|idx| SmsId::new(format!("000000-{:06}", idx)).unwrap())
318            .collect::<Vec<_>>();
319        let err = CheckStatus::new(sms_ids).unwrap_err();
320        assert_eq!(
321            err,
322            ValidationError::TooManySmsIds {
323                max: CHECK_STATUS_MAX_SMS_IDS,
324                actual: CHECK_STATUS_MAX_SMS_IDS + 1
325            }
326        );
327    }
328
329    #[test]
330    fn check_status_one_creates_single_id_request() {
331        let sms_id = SmsId::new("000000-000001").unwrap();
332        let request = CheckStatus::one(sms_id.clone());
333        assert_eq!(request.sms_ids(), &[sms_id]);
334    }
335
336    #[test]
337    fn check_status_new_exposes_sms_ids() {
338        let ids = vec![
339            SmsId::new("000000-000001").unwrap(),
340            SmsId::new("000000-000002").unwrap(),
341        ];
342        let request = CheckStatus::new(ids.clone()).unwrap();
343        assert_eq!(request.sms_ids(), ids.as_slice());
344    }
345}