solti_model/domain/
label.rs1use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct Labels(BTreeMap<String, String>);
13
14impl Labels {
15 #[inline]
17 pub fn new() -> Self {
18 Self(BTreeMap::new())
19 }
20
21 #[inline]
23 pub fn len(&self) -> usize {
24 self.0.len()
25 }
26
27 #[inline]
29 pub fn is_empty(&self) -> bool {
30 self.0.is_empty()
31 }
32
33 #[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 #[inline]
46 pub fn get(&self, key: &str) -> Option<&str> {
47 self.0.get(key).map(|s| s.as_str())
48 }
49
50 #[inline]
52 pub fn contains_key(&self, key: &str) -> bool {
53 self.0.contains_key(key)
54 }
55
56 #[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
73pub 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}