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
31#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct SipGeolocation(Vec<SipGeolocationRef>);
47
48impl SipGeolocation {
49 pub fn parse(raw: &str) -> Self {
51 let refs = raw
52 .split(',')
53 .filter_map(|entry| {
54 let entry = entry.trim();
55 let inner = entry
56 .strip_prefix('<')?
57 .strip_suffix('>')?;
58 if inner.is_empty() {
59 return None;
60 }
61 if let Some(id) = inner.strip_prefix("cid:") {
62 Some(SipGeolocationRef::Cid(id.to_string()))
63 } else {
64 Some(SipGeolocationRef::Url(inner.to_string()))
65 }
66 })
67 .collect();
68 Self(refs)
69 }
70
71 pub fn refs(&self) -> &[SipGeolocationRef] {
73 &self.0
74 }
75
76 pub fn len(&self) -> usize {
78 self.0
79 .len()
80 }
81
82 pub fn is_empty(&self) -> bool {
84 self.0
85 .is_empty()
86 }
87
88 pub fn cid(&self) -> Option<&str> {
90 self.0
91 .iter()
92 .find_map(|r| match r {
93 SipGeolocationRef::Cid(id) => Some(id.as_str()),
94 _ => None,
95 })
96 }
97
98 pub fn url(&self) -> Option<&str> {
100 self.0
101 .iter()
102 .find_map(|r| match r {
103 SipGeolocationRef::Url(url) => Some(url.as_str()),
104 _ => None,
105 })
106 }
107
108 pub fn cids(&self) -> impl Iterator<Item = &str> {
110 self.0
111 .iter()
112 .filter_map(|r| match r {
113 SipGeolocationRef::Cid(id) => Some(id.as_str()),
114 _ => None,
115 })
116 }
117
118 pub fn urls(&self) -> impl Iterator<Item = &str> {
120 self.0
121 .iter()
122 .filter_map(|r| match r {
123 SipGeolocationRef::Url(url) => Some(url.as_str()),
124 _ => None,
125 })
126 }
127}
128
129impl fmt::Display for SipGeolocation {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 crate::fmt_joined(f, &self.0, ", ")
132 }
133}
134
135impl<'a> IntoIterator for &'a SipGeolocation {
136 type Item = &'a SipGeolocationRef;
137 type IntoIter = std::slice::Iter<'a, SipGeolocationRef>;
138
139 fn into_iter(self) -> Self::IntoIter {
140 self.0
141 .iter()
142 }
143}
144
145impl IntoIterator for SipGeolocation {
146 type Item = SipGeolocationRef;
147 type IntoIter = std::vec::IntoIter<SipGeolocationRef>;
148
149 fn into_iter(self) -> Self::IntoIter {
150 self.0
151 .into_iter()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn parse_cid_and_url() {
161 let raw = "<cid:32863354-18b4-4069-bd00-7bced5fc6c9b>, <https://lis.example.com/api/v1/held/test>";
162 let geo = SipGeolocation::parse(raw);
163 assert_eq!(geo.len(), 2);
164 assert_eq!(geo.cid(), Some("32863354-18b4-4069-bd00-7bced5fc6c9b"));
165 assert!(geo
166 .url()
167 .unwrap()
168 .contains("lis.example.com"));
169 }
170
171 #[test]
172 fn single_cid() {
173 let geo = SipGeolocation::parse("<cid:abc-123>");
174 assert_eq!(geo.len(), 1);
175 assert_eq!(geo.cid(), Some("abc-123"));
176 assert!(geo
177 .url()
178 .is_none());
179 }
180
181 #[test]
182 fn single_url() {
183 let geo = SipGeolocation::parse("<https://lis.example.com/location>");
184 assert_eq!(geo.len(), 1);
185 assert!(geo
186 .cid()
187 .is_none());
188 assert_eq!(geo.url(), Some("https://lis.example.com/location"));
189 }
190
191 #[test]
192 fn empty_input() {
193 let geo = SipGeolocation::parse("");
194 assert!(geo.is_empty());
195 }
196
197 #[test]
198 fn empty_brackets_skipped() {
199 let geo = SipGeolocation::parse("<>, <cid:test>");
200 assert_eq!(geo.len(), 1);
201 assert_eq!(geo.cid(), Some("test"));
202 }
203
204 #[test]
205 fn display_roundtrip() {
206 let raw = "<cid:abc-123>, <https://lis.example.com/test>";
207 let geo = SipGeolocation::parse(raw);
208 assert_eq!(geo.to_string(), raw);
209 }
210
211 #[test]
212 fn multiple_cids() {
213 let raw = "<cid:first>, <cid:second>, <https://example.com/loc>";
214 let geo = SipGeolocation::parse(raw);
215 let cids: Vec<_> = geo
216 .cids()
217 .collect();
218 assert_eq!(cids, vec!["first", "second"]);
219 let urls: Vec<_> = geo
220 .urls()
221 .collect();
222 assert_eq!(urls, vec!["https://example.com/loc"]);
223 }
224}