Skip to main content

voip_ms/
types.rs

1//! Hand-written domain types used in place of `String` in selected
2//! generated request and response fields.
3//!
4//! Types here are wired into `src/generated.rs` by `xtask` through the
5//! field-name override table in `xtask/src/field_overrides.rs`.
6
7use serde::de::{Deserializer, Error as DeError, Visitor};
8use serde::ser::Serializer;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::str::FromStr;
12
13/// A VoIP.ms routing target encoded on the wire as `tag:payload`.
14///
15/// VoIP.ms uses this `tag:payload` scheme across all routing-like fields
16/// (`routing`, `failover_busy`, `routing_match`, the `fail_over_routing_*`
17/// family, …). Documented tags are mapped to named variants; anything else
18/// is preserved verbatim in [`Routing::Unknown`] so that VoIP.ms adding a
19/// new tag does not break deserialization or round-tripping.
20///
21/// `none:` (no routing) is represented by [`Routing::None`].
22///
23/// # Wire format
24///
25/// * `account:100001_VoIP` → [`Routing::Account`]
26/// * `fwd:15555` → [`Routing::Forward`]
27/// * `vm:101` → [`Routing::Voicemail`]
28/// * `sip:user@host` → [`Routing::Sip`]
29/// * `sys:5` → [`Routing::System`]
30/// * `grp:42` → [`Routing::Group`]
31/// * `queue:7` → [`Routing::Queue`]
32/// * `ivr:3` → [`Routing::Ivr`]
33/// * `cb:2359` → [`Routing::Callback`]
34/// * `tc:11` → [`Routing::TimeCondition`]
35/// * `disa:1` → [`Routing::Disa`]
36/// * `did:5551234567` → [`Routing::Did`]
37/// * `phone:5551234567` → [`Routing::Phone`]
38/// * `none:` → [`Routing::None`]
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum Routing {
41    /// No routing (wire: `none:`).
42    None,
43    /// Sub-account by name (wire: `account:NAME`).
44    Account(String),
45    /// Forwarding entry by id (wire: `fwd:ID`).
46    Forward(String),
47    /// Voicemail box (wire: `vm:MAILBOX`).
48    Voicemail(String),
49    /// External SIP URI (wire: `sip:user@host`).
50    Sip(String),
51    /// System recording / system action (wire: `sys:ID`).
52    System(String),
53    /// Ring group by id (wire: `grp:ID`).
54    Group(String),
55    /// Queue by id (wire: `queue:ID`).
56    Queue(String),
57    /// IVR menu by id (wire: `ivr:ID`).
58    Ivr(String),
59    /// Callback entry by id (wire: `cb:ID`).
60    Callback(String),
61    /// Time condition by id (wire: `tc:ID`).
62    TimeCondition(String),
63    /// DISA entry by id (wire: `disa:ID`).
64    Disa(String),
65    /// DID number (wire: `did:NUMBER`).
66    Did(String),
67    /// Outbound phone number (wire: `phone:NUMBER`).
68    Phone(String),
69    /// Any tag this crate doesn't recognize. The original wire form is
70    /// preserved as `tag:value` so it round-trips unchanged.
71    Unknown { tag: String, value: String },
72}
73
74impl Routing {
75    /// The wire tag (the substring before the `:`).
76    pub fn tag(&self) -> &str {
77        match self {
78            Routing::None => "none",
79            Routing::Account(_) => "account",
80            Routing::Forward(_) => "fwd",
81            Routing::Voicemail(_) => "vm",
82            Routing::Sip(_) => "sip",
83            Routing::System(_) => "sys",
84            Routing::Group(_) => "grp",
85            Routing::Queue(_) => "queue",
86            Routing::Ivr(_) => "ivr",
87            Routing::Callback(_) => "cb",
88            Routing::TimeCondition(_) => "tc",
89            Routing::Disa(_) => "disa",
90            Routing::Did(_) => "did",
91            Routing::Phone(_) => "phone",
92            Routing::Unknown { tag, .. } => tag,
93        }
94    }
95
96    /// The wire payload (the substring after the `:`).
97    pub fn value(&self) -> &str {
98        match self {
99            Routing::None => "",
100            Routing::Account(v)
101            | Routing::Forward(v)
102            | Routing::Voicemail(v)
103            | Routing::Sip(v)
104            | Routing::System(v)
105            | Routing::Group(v)
106            | Routing::Queue(v)
107            | Routing::Ivr(v)
108            | Routing::Callback(v)
109            | Routing::TimeCondition(v)
110            | Routing::Disa(v)
111            | Routing::Did(v)
112            | Routing::Phone(v) => v,
113            Routing::Unknown { value, .. } => value,
114        }
115    }
116}
117
118impl fmt::Display for Routing {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{}:{}", self.tag(), self.value())
121    }
122}
123
124/// Parse a `tag:value` string into a [`Routing`].
125///
126/// An empty string is rejected; use [`Option::None`] in the surrounding
127/// struct to represent an absent value.
128impl FromStr for Routing {
129    type Err = RoutingParseError;
130
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        let (tag, value) = match s.find(':') {
133            Some(i) => (&s[..i], &s[i + 1..]),
134            None => return Err(RoutingParseError::MissingColon),
135        };
136
137        Ok(match tag {
138            "none" => Routing::None,
139            "account" => Routing::Account(value.into()),
140            "fwd" => Routing::Forward(value.into()),
141            "vm" => Routing::Voicemail(value.into()),
142            "sip" => Routing::Sip(value.into()),
143            "sys" => Routing::System(value.into()),
144            "grp" => Routing::Group(value.into()),
145            "queue" => Routing::Queue(value.into()),
146            "ivr" => Routing::Ivr(value.into()),
147            "cb" => Routing::Callback(value.into()),
148            "tc" => Routing::TimeCondition(value.into()),
149            "disa" => Routing::Disa(value.into()),
150            "did" => Routing::Did(value.into()),
151            "phone" => Routing::Phone(value.into()),
152            other => Routing::Unknown {
153                tag: other.to_string(),
154                value: value.to_string(),
155            },
156        })
157    }
158}
159
160/// Error from parsing a [`Routing`] from a string.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum RoutingParseError {
163    /// The input contained no `:` separator.
164    MissingColon,
165}
166
167impl fmt::Display for RoutingParseError {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            RoutingParseError::MissingColon => {
171                f.write_str("routing string is missing required `:` separator")
172            }
173        }
174    }
175}
176
177impl std::error::Error for RoutingParseError {}
178
179impl Serialize for Routing {
180    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
181    where
182        S: Serializer,
183    {
184        serializer.collect_str(self)
185    }
186}
187
188impl<'de> Deserialize<'de> for Routing {
189    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
190    where
191        D: Deserializer<'de>,
192    {
193        struct RoutingVisitor;
194
195        impl<'de> Visitor<'de> for RoutingVisitor {
196            type Value = Routing;
197
198            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199                f.write_str("a VoIP.ms routing string of the form `tag:value`")
200            }
201
202            fn visit_str<E>(self, v: &str) -> Result<Routing, E>
203            where
204                E: DeError,
205            {
206                Routing::from_str(v).map_err(E::custom)
207            }
208
209            fn visit_string<E>(self, v: String) -> Result<Routing, E>
210            where
211                E: DeError,
212            {
213                Routing::from_str(&v).map_err(E::custom)
214            }
215        }
216
217        deserializer.deserialize_str(RoutingVisitor)
218    }
219}
220
221/// A duration in seconds, or an "unbounded" sentinel.
222///
223/// Several VoIP.ms queue/announcement fields take a number of seconds *or* a
224/// word meaning no limit (`none` / `unlimited`), so a bare `u64` can't hold the
225/// sentinel. [`Seconds`] serializes the sentinel as `none`; [`WaitTime`] as
226/// `unlimited` (the word `maximum_wait_time` documents). Both deserialize
227/// tolerantly: a number, a numeric string, or either sentinel word.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229pub enum Seconds {
230    /// A concrete number of seconds.
231    Value(u64),
232    /// No limit (wire: `none`).
233    Unlimited,
234}
235
236/// A wait time in seconds, or unlimited.
237///
238/// Like [`Seconds`] but serializes the unbounded case as `unlimited`, the word
239/// `maximum_wait_time` documents.
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
241pub enum WaitTime {
242    /// A concrete number of seconds.
243    Value(u64),
244    /// No limit (wire: `unlimited`).
245    Unlimited,
246}
247
248macro_rules! impl_seconds {
249    ($name:ident, $unlimited_wire:literal, $expecting:literal) => {
250        impl From<u64> for $name {
251            fn from(v: u64) -> Self {
252                $name::Value(v)
253            }
254        }
255
256        impl Serialize for $name {
257            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
258            where
259                S: Serializer,
260            {
261                match self {
262                    $name::Value(v) => serializer.serialize_str(&v.to_string()),
263                    $name::Unlimited => serializer.serialize_str($unlimited_wire),
264                }
265            }
266        }
267
268        impl<'de> Deserialize<'de> for $name {
269            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
270            where
271                D: Deserializer<'de>,
272            {
273                struct SecondsVisitor;
274
275                impl<'de> Visitor<'de> for SecondsVisitor {
276                    type Value = $name;
277
278                    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279                        f.write_str($expecting)
280                    }
281
282                    fn visit_u64<E>(self, v: u64) -> Result<$name, E>
283                    where
284                        E: DeError,
285                    {
286                        Ok($name::Value(v))
287                    }
288
289                    fn visit_str<E>(self, v: &str) -> Result<$name, E>
290                    where
291                        E: DeError,
292                    {
293                        let t = v.trim();
294                        match t.to_ascii_lowercase().as_str() {
295                            "none" | "unlimited" => Ok($name::Unlimited),
296                            _ => t
297                                .parse::<u64>()
298                                .map($name::Value)
299                                .map_err(|_| E::custom(format!("invalid seconds value {v}"))),
300                        }
301                    }
302                }
303
304                deserializer.deserialize_any(SecondsVisitor)
305            }
306        }
307    };
308}
309
310impl_seconds!(Seconds, "none", "a number of seconds or `none`");
311impl_seconds!(WaitTime, "unlimited", "a number of seconds or `unlimited`");
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn parses_documented_tags() {
319        assert_eq!(Routing::from_str("none:").unwrap(), Routing::None);
320        assert_eq!(
321            Routing::from_str("account:100001_VoIP").unwrap(),
322            Routing::Account("100001_VoIP".into()),
323        );
324        assert_eq!(
325            Routing::from_str("fwd:15555").unwrap(),
326            Routing::Forward("15555".into()),
327        );
328        assert_eq!(
329            Routing::from_str("vm:101").unwrap(),
330            Routing::Voicemail("101".into()),
331        );
332        assert_eq!(
333            Routing::from_str("cb:2359").unwrap(),
334            Routing::Callback("2359".into()),
335        );
336    }
337
338    #[test]
339    fn preserves_unknown_tags() {
340        let r = Routing::from_str("future:abc").unwrap();
341        assert_eq!(
342            r,
343            Routing::Unknown {
344                tag: "future".into(),
345                value: "abc".into(),
346            },
347        );
348        assert_eq!(r.to_string(), "future:abc");
349    }
350
351    #[test]
352    fn sip_value_can_contain_colons() {
353        // Split is on the FIRST colon so sip URIs survive intact.
354        let r = Routing::from_str("sip:5552223333@sip.voip.ms:5060").unwrap();
355        assert_eq!(r, Routing::Sip("5552223333@sip.voip.ms:5060".into()));
356        assert_eq!(r.to_string(), "sip:5552223333@sip.voip.ms:5060");
357    }
358
359    #[test]
360    fn rejects_missing_colon() {
361        assert_eq!(
362            Routing::from_str("nocolon"),
363            Err(RoutingParseError::MissingColon),
364        );
365    }
366
367    #[test]
368    fn round_trips_through_serde() {
369        let r = Routing::Forward("19998887777".into());
370        let json = serde_json::to_string(&r).unwrap();
371        assert_eq!(json, "\"fwd:19998887777\"");
372        let back: Routing = serde_json::from_str(&json).unwrap();
373        assert_eq!(back, r);
374    }
375
376    #[test]
377    fn deserialize_none() {
378        let r: Routing = serde_json::from_str("\"none:\"").unwrap();
379        assert_eq!(r, Routing::None);
380    }
381
382    #[test]
383    fn seconds_serializes_value_and_sentinel() {
384        assert_eq!(
385            serde_json::to_string(&Seconds::Value(30)).unwrap(),
386            "\"30\""
387        );
388        assert_eq!(
389            serde_json::to_string(&Seconds::Unlimited).unwrap(),
390            "\"none\""
391        );
392        assert_eq!(
393            serde_json::to_string(&WaitTime::Unlimited).unwrap(),
394            "\"unlimited\""
395        );
396    }
397
398    #[test]
399    fn seconds_deserializes_number_string_and_sentinels() {
400        // A bare number, a numeric string, and either sentinel word all parse.
401        assert_eq!(
402            serde_json::from_str::<Seconds>("45").unwrap(),
403            Seconds::Value(45)
404        );
405        assert_eq!(
406            serde_json::from_str::<Seconds>("\"45\"").unwrap(),
407            Seconds::Value(45)
408        );
409        for s in ["\"none\"", "\"NONE\"", "\"unlimited\""] {
410            assert_eq!(
411                serde_json::from_str::<Seconds>(s).unwrap(),
412                Seconds::Unlimited,
413                "{s}"
414            );
415        }
416        // WaitTime shares the tolerant parse.
417        assert_eq!(
418            serde_json::from_str::<WaitTime>("\"unlimited\"").unwrap(),
419            WaitTime::Unlimited
420        );
421    }
422}