Skip to main content

freeswitch_types/variables/
sip_geolocation.rs

1use std::fmt;
2
3/// A reference extracted from a SIP Geolocation header (RFC 6442).
4///
5/// Each entry is either a `cid:` reference to a multipart body part
6/// (typically containing PIDF-LO XML) or a URL for location dereference.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum SipGeolocationRef {
9    /// Content-ID reference to a MIME body part (e.g., `cid:uuid`).
10    Cid(String),
11    /// HTTP(S) or other URL for location dereference.
12    Url(String),
13}
14
15impl fmt::Display for SipGeolocationRef {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        match self {
18            Self::Cid(id) => write!(f, "<cid:{id}>"),
19            Self::Url(url) => write!(f, "<{url}>"),
20        }
21    }
22}
23
24/// Parsed SIP Geolocation header value (RFC 6442).
25///
26/// Contains one or more `<uri>` references, comma-separated. Each reference
27/// is classified as either a `cid:` body-part reference or a dereference URL.
28///
29/// ```
30/// use freeswitch_types::variables::SipGeolocation;
31///
32/// let raw = "<cid:abc-123>, <https://lis.example.com/held/abc>";
33/// let geo = SipGeolocation::parse(raw);
34/// assert_eq!(geo.len(), 2);
35/// assert!(geo.cid().is_some());
36/// assert!(geo.url().is_some());
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct SipGeolocation(Vec<SipGeolocationRef>);
40
41impl SipGeolocation {
42    /// Parse a raw Geolocation header value into typed references.
43    pub fn parse(raw: &str) -> Self {
44        let refs = raw
45            .split(',')
46            .filter_map(|entry| {
47                let entry = entry.trim();
48                let inner = entry
49                    .strip_prefix('<')?
50                    .strip_suffix('>')?;
51                if inner.is_empty() {
52                    return None;
53                }
54                if let Some(id) = inner.strip_prefix("cid:") {
55                    Some(SipGeolocationRef::Cid(id.to_string()))
56                } else {
57                    Some(SipGeolocationRef::Url(inner.to_string()))
58                }
59            })
60            .collect();
61        Self(refs)
62    }
63
64    /// The parsed references as a slice.
65    pub fn refs(&self) -> &[SipGeolocationRef] {
66        &self.0
67    }
68
69    /// Number of references.
70    pub fn len(&self) -> usize {
71        self.0
72            .len()
73    }
74
75    /// Returns `true` if there are no references.
76    pub fn is_empty(&self) -> bool {
77        self.0
78            .is_empty()
79    }
80
81    /// The first `cid:` reference, if any.
82    pub fn cid(&self) -> Option<&str> {
83        self.0
84            .iter()
85            .find_map(|r| match r {
86                SipGeolocationRef::Cid(id) => Some(id.as_str()),
87                _ => None,
88            })
89    }
90
91    /// The first URL reference, if any.
92    pub fn url(&self) -> Option<&str> {
93        self.0
94            .iter()
95            .find_map(|r| match r {
96                SipGeolocationRef::Url(url) => Some(url.as_str()),
97                _ => None,
98            })
99    }
100
101    /// Iterate over all `cid:` references.
102    pub fn cids(&self) -> impl Iterator<Item = &str> {
103        self.0
104            .iter()
105            .filter_map(|r| match r {
106                SipGeolocationRef::Cid(id) => Some(id.as_str()),
107                _ => None,
108            })
109    }
110
111    /// Iterate over all URL references.
112    pub fn urls(&self) -> impl Iterator<Item = &str> {
113        self.0
114            .iter()
115            .filter_map(|r| match r {
116                SipGeolocationRef::Url(url) => Some(url.as_str()),
117                _ => None,
118            })
119    }
120}
121
122impl fmt::Display for SipGeolocation {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        for (i, r) in self
125            .0
126            .iter()
127            .enumerate()
128        {
129            if i > 0 {
130                f.write_str(", ")?;
131            }
132            write!(f, "{r}")?;
133        }
134        Ok(())
135    }
136}
137
138impl<'a> IntoIterator for &'a SipGeolocation {
139    type Item = &'a SipGeolocationRef;
140    type IntoIter = std::slice::Iter<'a, SipGeolocationRef>;
141
142    fn into_iter(self) -> Self::IntoIter {
143        self.0
144            .iter()
145    }
146}
147
148impl IntoIterator for SipGeolocation {
149    type Item = SipGeolocationRef;
150    type IntoIter = std::vec::IntoIter<SipGeolocationRef>;
151
152    fn into_iter(self) -> Self::IntoIter {
153        self.0
154            .into_iter()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn parse_cid_and_url() {
164        let raw = "<cid:32863354-18b4-4069-bd00-7bced5fc6c9b>, <https://lis.example.com/api/v1/held/test>";
165        let geo = SipGeolocation::parse(raw);
166        assert_eq!(geo.len(), 2);
167        assert_eq!(geo.cid(), Some("32863354-18b4-4069-bd00-7bced5fc6c9b"));
168        assert!(geo
169            .url()
170            .unwrap()
171            .contains("lis.example.com"));
172    }
173
174    #[test]
175    fn single_cid() {
176        let geo = SipGeolocation::parse("<cid:abc-123>");
177        assert_eq!(geo.len(), 1);
178        assert_eq!(geo.cid(), Some("abc-123"));
179        assert!(geo
180            .url()
181            .is_none());
182    }
183
184    #[test]
185    fn single_url() {
186        let geo = SipGeolocation::parse("<https://lis.example.com/location>");
187        assert_eq!(geo.len(), 1);
188        assert!(geo
189            .cid()
190            .is_none());
191        assert_eq!(geo.url(), Some("https://lis.example.com/location"));
192    }
193
194    #[test]
195    fn empty_input() {
196        let geo = SipGeolocation::parse("");
197        assert!(geo.is_empty());
198    }
199
200    #[test]
201    fn empty_brackets_skipped() {
202        let geo = SipGeolocation::parse("<>, <cid:test>");
203        assert_eq!(geo.len(), 1);
204        assert_eq!(geo.cid(), Some("test"));
205    }
206
207    #[test]
208    fn display_roundtrip() {
209        let raw = "<cid:abc-123>, <https://lis.example.com/test>";
210        let geo = SipGeolocation::parse(raw);
211        assert_eq!(geo.to_string(), raw);
212    }
213
214    #[test]
215    fn multiple_cids() {
216        let raw = "<cid:first>, <cid:second>, <https://example.com/loc>";
217        let geo = SipGeolocation::parse(raw);
218        let cids: Vec<_> = geo
219            .cids()
220            .collect();
221        assert_eq!(cids, vec!["first", "second"]);
222        let urls: Vec<_> = geo
223            .urls()
224            .collect();
225        assert_eq!(urls, vec!["https://example.com/loc"]);
226    }
227}