sip_header/
geolocation.rs1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum SipGeolocationRef {
16 Cid(String),
18 Url(String),
20}
21
22impl fmt::Display for SipGeolocationRef {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Cid(id) => write!(f, "<cid:{id}>"),
26 Self::Url(url) => write!(f, "<{url}>"),
27 }
28 }
29}
30
31fn parse_ref(entry: &str) -> Option<SipGeolocationRef> {
34 let (inner, _) = entry
35 .trim()
36 .strip_prefix('<')?
37 .split_once('>')?;
38 if inner.is_empty() {
39 return None;
40 }
41 Some(
42 match inner
43 .get(..4)
44 .filter(|scheme| scheme.eq_ignore_ascii_case("cid:"))
45 {
46 Some(_) => SipGeolocationRef::Cid(inner[4..].to_string()),
47 None => SipGeolocationRef::Url(inner.to_string()),
48 },
49 )
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct SipGeolocation(Vec<SipGeolocationRef>);
68
69impl SipGeolocation {
70 pub fn parse(raw: &str) -> Self {
74 Self::from_entries(crate::split_comma_entries(raw))
75 }
76
77 pub fn from_entries<'a>(entries: impl IntoIterator<Item = &'a str>) -> Self {
81 Self(
82 entries
83 .into_iter()
84 .filter_map(parse_ref)
85 .collect(),
86 )
87 }
88
89 pub fn refs(&self) -> &[SipGeolocationRef] {
91 &self.0
92 }
93
94 pub fn len(&self) -> usize {
96 self.0
97 .len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.0
103 .is_empty()
104 }
105
106 pub fn cid(&self) -> Option<&str> {
108 self.0
109 .iter()
110 .find_map(|r| match r {
111 SipGeolocationRef::Cid(id) => Some(id.as_str()),
112 _ => None,
113 })
114 }
115
116 pub fn url(&self) -> Option<&str> {
118 self.0
119 .iter()
120 .find_map(|r| match r {
121 SipGeolocationRef::Url(url) => Some(url.as_str()),
122 _ => None,
123 })
124 }
125
126 pub fn cids(&self) -> impl Iterator<Item = &str> {
128 self.0
129 .iter()
130 .filter_map(|r| match r {
131 SipGeolocationRef::Cid(id) => Some(id.as_str()),
132 _ => None,
133 })
134 }
135
136 pub fn urls(&self) -> impl Iterator<Item = &str> {
138 self.0
139 .iter()
140 .filter_map(|r| match r {
141 SipGeolocationRef::Url(url) => Some(url.as_str()),
142 _ => None,
143 })
144 }
145}
146
147impl fmt::Display for SipGeolocation {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 crate::fmt_joined(f, &self.0, ", ")
150 }
151}
152
153impl<'a> IntoIterator for &'a SipGeolocation {
154 type Item = &'a SipGeolocationRef;
155 type IntoIter = std::slice::Iter<'a, SipGeolocationRef>;
156
157 fn into_iter(self) -> Self::IntoIter {
158 self.0
159 .iter()
160 }
161}
162
163impl IntoIterator for SipGeolocation {
164 type Item = SipGeolocationRef;
165 type IntoIter = std::vec::IntoIter<SipGeolocationRef>;
166
167 fn into_iter(self) -> Self::IntoIter {
168 self.0
169 .into_iter()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn parse_cid_and_url() {
179 let raw = "<cid:32863354-18b4-4069-bd00-7bced5fc6c9b>, <https://lis.example.com/api/v1/held/test>";
180 let geo = SipGeolocation::parse(raw);
181 assert_eq!(geo.len(), 2);
182 assert_eq!(geo.cid(), Some("32863354-18b4-4069-bd00-7bced5fc6c9b"));
183 assert!(geo
184 .url()
185 .unwrap()
186 .contains("lis.example.com"));
187 }
188
189 #[test]
190 fn single_cid() {
191 let geo = SipGeolocation::parse("<cid:abc-123>");
192 assert_eq!(geo.len(), 1);
193 assert_eq!(geo.cid(), Some("abc-123"));
194 assert!(geo
195 .url()
196 .is_none());
197 }
198
199 #[test]
200 fn single_url() {
201 let geo = SipGeolocation::parse("<https://lis.example.com/location>");
202 assert_eq!(geo.len(), 1);
203 assert!(geo
204 .cid()
205 .is_none());
206 assert_eq!(geo.url(), Some("https://lis.example.com/location"));
207 }
208
209 #[test]
210 fn empty_input() {
211 let geo = SipGeolocation::parse("");
212 assert!(geo.is_empty());
213 }
214
215 #[test]
216 fn empty_brackets_skipped() {
217 let geo = SipGeolocation::parse("<>, <cid:test>");
218 assert_eq!(geo.len(), 1);
219 assert_eq!(geo.cid(), Some("test"));
220 }
221
222 #[test]
223 fn display_roundtrip() {
224 let raw = "<cid:abc-123>, <https://lis.example.com/test>";
225 let geo = SipGeolocation::parse(raw);
226 assert_eq!(geo.to_string(), raw);
227 }
228
229 #[test]
230 fn geoloc_params_dropped() {
231 let geo = SipGeolocation::parse("<cid:x@example.com>;inserted-by=y");
232 assert_eq!(
233 geo.refs(),
234 &[SipGeolocationRef::Cid("x@example.com".into())]
235 );
236 }
237
238 #[test]
239 fn comma_inside_brackets_not_split() {
240 let geo = SipGeolocation::parse("<https://example.com/a,b>");
241 assert_eq!(
242 geo.refs(),
243 &[SipGeolocationRef::Url("https://example.com/a,b".into())]
244 );
245 }
246
247 #[test]
248 fn cid_scheme_case_insensitive() {
249 let geo = SipGeolocation::parse("<CID:x@example.com>");
250 assert_eq!(geo.cid(), Some("x@example.com"));
251 }
252
253 #[test]
254 fn unbracketed_entry_skipped() {
255 let geo = SipGeolocation::parse("cid:x@example.com, <https://example.com/loc>");
256 assert_eq!(geo.len(), 1);
257 assert_eq!(geo.url(), Some("https://example.com/loc"));
258 }
259
260 #[test]
261 fn from_entries_matches_parse() {
262 let split =
263 SipGeolocation::from_entries(["<cid:a>;inserted-by=x", "<https://example.com/a,b>"]);
264 let joined = SipGeolocation::parse("<cid:a>;inserted-by=x, <https://example.com/a,b>");
265 assert_eq!(split, joined);
266 assert_eq!(split.len(), 2);
267 }
268
269 #[test]
270 fn multiple_cids() {
271 let raw = "<cid:first>, <cid:second>, <https://example.com/loc>";
272 let geo = SipGeolocation::parse(raw);
273 let cids: Vec<_> = geo
274 .cids()
275 .collect();
276 assert_eq!(cids, vec!["first", "second"]);
277 let urls: Vec<_> = geo
278 .urls()
279 .collect();
280 assert_eq!(urls, vec!["https://example.com/loc"]);
281 }
282}