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