1use std::borrow::Cow;
46use std::cmp::Ordering;
47use std::hash::{Hash, Hasher};
48
49pub type Label = (Cow<'static, str>, Cow<'static, str>);
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct LabelSet {
65 pairs: Vec<Label>,
67}
68
69impl LabelSet {
70 pub const EMPTY: LabelSet = LabelSet { pairs: Vec::new() };
72
73 #[inline]
75 pub const fn new() -> Self {
76 Self::EMPTY
77 }
78
79 #[inline]
81 pub fn len(&self) -> usize {
82 self.pairs.len()
83 }
84
85 #[inline]
87 pub fn is_empty(&self) -> bool {
88 self.pairs.is_empty()
89 }
90
91 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
93 self.pairs.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
94 }
95
96 pub fn add(
101 &mut self,
102 key: impl Into<Cow<'static, str>>,
103 value: impl Into<Cow<'static, str>>,
104 ) -> &mut Self {
105 let key = key.into();
106 let value = value.into();
107 match self
108 .pairs
109 .binary_search_by(|(k, _)| (k.as_ref()).cmp(key.as_ref()))
110 {
111 Ok(idx) => self.pairs[idx].1 = value,
112 Err(idx) => self.pairs.insert(idx, (key, value)),
113 }
114 self
115 }
116
117 #[must_use]
120 pub fn with(
121 mut self,
122 key: impl Into<Cow<'static, str>>,
123 value: impl Into<Cow<'static, str>>,
124 ) -> Self {
125 self.add(key, value);
126 self
127 }
128
129 pub fn get(&self, key: &str) -> Option<&str> {
131 self.pairs
132 .binary_search_by(|(k, _)| (k.as_ref()).cmp(key))
133 .ok()
134 .map(|idx| self.pairs[idx].1.as_ref())
135 }
136
137 pub fn remove(&mut self, key: &str) -> bool {
139 if let Ok(idx) = self.pairs.binary_search_by(|(k, _)| (k.as_ref()).cmp(key)) {
140 self.pairs.remove(idx);
141 true
142 } else {
143 false
144 }
145 }
146
147 pub fn to_prometheus(&self) -> String {
154 if self.pairs.is_empty() {
155 return String::new();
156 }
157 let mut out = String::with_capacity(2 + self.pairs.len() * 16);
158 out.push('{');
159 for (i, (k, v)) in self.pairs.iter().enumerate() {
160 if i > 0 {
161 out.push(',');
162 }
163 out.push_str(k);
164 out.push_str("=\"");
165 escape_prometheus_value(&mut out, v);
166 out.push('"');
167 }
168 out.push('}');
169 out
170 }
171
172 pub fn to_statsd(&self) -> String {
178 if self.pairs.is_empty() {
179 return String::new();
180 }
181 let mut out = String::with_capacity(2 + self.pairs.len() * 16);
182 out.push_str("|#");
183 for (i, (k, v)) in self.pairs.iter().enumerate() {
184 if i > 0 {
185 out.push(',');
186 }
187 out.push_str(k);
188 out.push(':');
189 for c in v.chars() {
190 if matches!(c, '|' | ',' | '\n' | ':') {
191 out.push('_');
192 } else {
193 out.push(c);
194 }
195 }
196 }
197 out
198 }
199}
200
201impl Hash for LabelSet {
202 fn hash<H: Hasher>(&self, state: &mut H) {
203 self.pairs.len().hash(state);
206 for (k, v) in &self.pairs {
207 k.as_ref().hash(state);
208 v.as_ref().hash(state);
209 }
210 }
211}
212
213impl PartialOrd for LabelSet {
214 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
215 Some(self.cmp(other))
216 }
217}
218
219impl Ord for LabelSet {
220 fn cmp(&self, other: &Self) -> Ordering {
221 let a = self.pairs.iter().map(|(k, v)| (k.as_ref(), v.as_ref()));
222 let b = other.pairs.iter().map(|(k, v)| (k.as_ref(), v.as_ref()));
223 a.cmp(b)
224 }
225}
226
227impl<K, V> FromIterator<(K, V)> for LabelSet
228where
229 K: Into<Cow<'static, str>>,
230 V: Into<Cow<'static, str>>,
231{
232 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
233 let mut s = Self::new();
234 for (k, v) in iter {
235 s.add(k, v);
236 }
237 s
238 }
239}
240
241impl<K, V, const N: usize> From<[(K, V); N]> for LabelSet
242where
243 K: Into<Cow<'static, str>>,
244 V: Into<Cow<'static, str>>,
245{
246 fn from(arr: [(K, V); N]) -> Self {
247 arr.into_iter().collect()
248 }
249}
250
251#[cfg(feature = "serde")]
252impl serde::Serialize for LabelSet {
253 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254 use serde::ser::SerializeMap;
255 let mut map = serializer.serialize_map(Some(self.pairs.len()))?;
256 for (k, v) in &self.pairs {
257 map.serialize_entry(k.as_ref(), v.as_ref())?;
258 }
259 map.end()
260 }
261}
262
263fn escape_prometheus_value(out: &mut String, v: &str) {
264 for c in v.chars() {
265 match c {
266 '\\' => out.push_str("\\\\"),
267 '"' => out.push_str("\\\""),
268 '\n' => out.push_str("\\n"),
269 c => out.push(c),
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn empty_set_is_empty_and_renders_empty() {
280 let l = LabelSet::EMPTY;
281 assert!(l.is_empty());
282 assert_eq!(l.len(), 0);
283 assert_eq!(l.to_prometheus(), "");
284 assert_eq!(l.to_statsd(), "");
285 }
286
287 #[test]
288 fn add_keeps_sorted_and_deduplicates() {
289 let mut l = LabelSet::new();
290 l.add("status", "200");
291 l.add("method", "GET");
292 l.add("region", "us");
293 let pairs: Vec<_> = l.iter().collect();
296 assert_eq!(
297 pairs,
298 vec![("method", "GET"), ("region", "us"), ("status", "200")]
299 );
300
301 l.add("status", "500");
303 assert_eq!(l.get("status"), Some("500"));
304 assert_eq!(l.len(), 3);
305 }
306
307 #[test]
308 fn equal_sets_hash_equal_regardless_of_insertion_order() {
309 use std::collections::hash_map::DefaultHasher;
310 fn hash(l: &LabelSet) -> u64 {
311 let mut h = DefaultHasher::new();
312 l.hash(&mut h);
313 h.finish()
314 }
315 let a = LabelSet::from([("a", "1"), ("b", "2"), ("c", "3")]);
316 let b = LabelSet::from([("c", "3"), ("a", "1"), ("b", "2")]);
317 assert_eq!(a, b);
318 assert_eq!(hash(&a), hash(&b));
319 }
320
321 #[test]
322 fn remove_keeps_invariants() {
323 let mut l = LabelSet::from([("a", "1"), ("b", "2"), ("c", "3")]);
324 assert!(l.remove("b"));
325 assert!(!l.remove("b"));
326 assert_eq!(l.iter().collect::<Vec<_>>(), vec![("a", "1"), ("c", "3")]);
327 }
328
329 #[test]
330 fn prometheus_rendering_escapes_correctly() {
331 let l = LabelSet::from([("path", r#"/foo "bar"\baz"#), ("note", "line1\nline2")]);
332 let s = l.to_prometheus();
333 assert_eq!(s, r#"{note="line1\nline2",path="/foo \"bar\"\\baz"}"#);
335 }
336
337 #[test]
338 fn statsd_rendering_sanitises_specials() {
339 let l = LabelSet::from([("k1", "with|pipe"), ("k2", "with,comma:colon")]);
340 let s = l.to_statsd();
341 assert_eq!(s, "|#k1:with_pipe,k2:with_comma_colon");
342 }
343
344 #[test]
345 fn ordering_is_lexicographic_over_pairs() {
346 let a = LabelSet::from([("a", "1")]);
347 let b = LabelSet::from([("a", "2")]);
348 let c = LabelSet::from([("b", "0")]);
349 let mut v = vec![c.clone(), b.clone(), a.clone()];
350 v.sort();
351 assert_eq!(v, vec![a, b, c]);
352 }
353
354 #[cfg(feature = "serde")]
355 #[test]
356 fn serde_serializes_as_map() {
357 let l = LabelSet::from([("method", "GET"), ("status", "200")]);
358 let j = serde_json::to_string(&l).unwrap();
359 assert_eq!(j, r#"{"method":"GET","status":"200"}"#);
360 }
361}