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)]
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#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn parses_documented_tags() {
227        assert_eq!(Routing::from_str("none:").unwrap(), Routing::None);
228        assert_eq!(
229            Routing::from_str("account:100001_VoIP").unwrap(),
230            Routing::Account("100001_VoIP".into()),
231        );
232        assert_eq!(
233            Routing::from_str("fwd:15555").unwrap(),
234            Routing::Forward("15555".into()),
235        );
236        assert_eq!(
237            Routing::from_str("vm:101").unwrap(),
238            Routing::Voicemail("101".into()),
239        );
240        assert_eq!(
241            Routing::from_str("cb:2359").unwrap(),
242            Routing::Callback("2359".into()),
243        );
244    }
245
246    #[test]
247    fn preserves_unknown_tags() {
248        let r = Routing::from_str("future:abc").unwrap();
249        assert_eq!(
250            r,
251            Routing::Unknown {
252                tag: "future".into(),
253                value: "abc".into(),
254            },
255        );
256        assert_eq!(r.to_string(), "future:abc");
257    }
258
259    #[test]
260    fn sip_value_can_contain_colons() {
261        // Split is on the FIRST colon so sip URIs survive intact.
262        let r = Routing::from_str("sip:5552223333@sip.voip.ms:5060").unwrap();
263        assert_eq!(r, Routing::Sip("5552223333@sip.voip.ms:5060".into()));
264        assert_eq!(r.to_string(), "sip:5552223333@sip.voip.ms:5060");
265    }
266
267    #[test]
268    fn rejects_missing_colon() {
269        assert_eq!(
270            Routing::from_str("nocolon"),
271            Err(RoutingParseError::MissingColon),
272        );
273    }
274
275    #[test]
276    fn round_trips_through_serde() {
277        let r = Routing::Forward("19998887777".into());
278        let json = serde_json::to_string(&r).unwrap();
279        assert_eq!(json, "\"fwd:19998887777\"");
280        let back: Routing = serde_json::from_str(&json).unwrap();
281        assert_eq!(back, r);
282    }
283
284    #[test]
285    fn deserialize_none() {
286        let r: Routing = serde_json::from_str("\"none:\"").unwrap();
287        assert_eq!(r, Routing::None);
288    }
289}