Skip to main content

solti_model/domain/
label.rs

1//! # Key-value metadata labels.
2//!
3//! [`Labels`] is an ordered map used for runner routing and task filtering.
4
5use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9/// Structured key–value metadata based on [`BTreeMap`].
10#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct Labels(BTreeMap<String, String>);
13
14impl Labels {
15    /// Create an empty set of labels.
16    #[inline]
17    pub fn new() -> Self {
18        Self(BTreeMap::new())
19    }
20
21    /// Returns the number of labels.
22    #[inline]
23    pub fn len(&self) -> usize {
24        self.0.len()
25    }
26
27    /// Returns `true` if no labels are present.
28    #[inline]
29    pub fn is_empty(&self) -> bool {
30        self.0.is_empty()
31    }
32
33    /// Insert or overwrite a label.
34    #[inline]
35    pub fn insert<K, V>(&mut self, key: K, val: V) -> &mut Self
36    where
37        K: Into<String>,
38        V: Into<String>,
39    {
40        self.0.insert(key.into(), val.into());
41        self
42    }
43
44    /// Get the value for a key, if present.
45    #[inline]
46    pub fn get(&self, key: &str) -> Option<&str> {
47        self.0.get(key).map(|s| s.as_str())
48    }
49
50    /// Check whether a key exists (regardless of its value).
51    #[inline]
52    pub fn contains_key(&self, key: &str) -> bool {
53        self.0.contains_key(key)
54    }
55
56    /// Iterate through all labels as `(&str, &str)` pairs.
57    #[inline]
58    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
59        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
60    }
61}
62
63impl<'a> IntoIterator for &'a Labels {
64    type Item = (&'a str, &'a str);
65    type IntoIter = LabelsIter<'a>;
66
67    #[inline]
68    fn into_iter(self) -> Self::IntoIter {
69        LabelsIter(self.0.iter())
70    }
71}
72
73/// Iterator over `Labels` yielding `(&str, &str)` pairs.
74pub struct LabelsIter<'a>(std::collections::btree_map::Iter<'a, String, String>);
75
76impl<'a> Iterator for LabelsIter<'a> {
77    type Item = (&'a str, &'a str);
78
79    #[inline]
80    fn next(&mut self) -> Option<Self::Item> {
81        self.0.next().map(|(k, v)| (k.as_str(), v.as_str()))
82    }
83
84    #[inline]
85    fn size_hint(&self) -> (usize, Option<usize>) {
86        self.0.size_hint()
87    }
88}
89
90impl ExactSizeIterator for LabelsIter<'_> {}
91
92#[cfg(test)]
93mod tests {
94    use super::Labels;
95
96    #[test]
97    fn new_is_empty() {
98        let labels = Labels::new();
99        assert!(labels.is_empty());
100        assert_eq!(labels.len(), 0);
101        assert!(labels.get("any").is_none());
102    }
103
104    #[test]
105    fn insert_and_get() {
106        let mut labels = Labels::new();
107        labels.insert("region", "us-east-1");
108
109        assert!(!labels.is_empty());
110        assert_eq!(labels.len(), 1);
111        assert_eq!(labels.get("region"), Some("us-east-1"));
112        assert!(labels.get("zone").is_none());
113    }
114
115    #[test]
116    fn insert_overwrites() {
117        let mut labels = Labels::new();
118        labels.insert("env", "dev");
119        labels.insert("env", "prod");
120
121        assert_eq!(labels.get("env"), Some("prod"));
122    }
123
124    #[test]
125    fn insert_chaining() {
126        let mut labels = Labels::new();
127        labels.insert("a", "1").insert("b", "2");
128
129        assert_eq!(labels.get("a"), Some("1"));
130        assert_eq!(labels.get("b"), Some("2"));
131    }
132
133    #[test]
134    fn iter_returns_sorted_pairs() {
135        let mut labels = Labels::new();
136        labels.insert("z", "last");
137        labels.insert("a", "first");
138
139        let pairs: Vec<_> = labels.iter().collect();
140        assert_eq!(pairs, vec![("a", "first"), ("z", "last")]);
141    }
142
143    #[test]
144    fn serde_transparent_roundtrip() {
145        let mut labels = Labels::new();
146        labels.insert("runner-tag", "prod");
147
148        let json = serde_json::to_string(&labels).unwrap();
149        assert!(json.contains("\"runner-tag\":\"prod\""));
150
151        let back: Labels = serde_json::from_str(&json).unwrap();
152        assert_eq!(back.get("runner-tag"), Some("prod"));
153    }
154}