solti_model/domain/
label.rs1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{ModelResult, validation};
12
13#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "schema", schemars(schema_with = "crate::schema::labels"))]
32#[serde(transparent)]
33pub struct Labels(BTreeMap<String, String>);
34
35impl Labels {
36 #[inline]
47 pub fn new() -> Self {
48 Self(BTreeMap::new())
49 }
50
51 #[inline]
53 pub fn len(&self) -> usize {
54 self.0.len()
55 }
56
57 #[inline]
59 pub fn is_empty(&self) -> bool {
60 self.0.is_empty()
61 }
62
63 #[inline]
76 pub fn insert<K, V>(&mut self, key: K, val: V) -> &mut Self
77 where
78 K: Into<String>,
79 V: Into<String>,
80 {
81 self.0.insert(key.into(), val.into());
82 self
83 }
84
85 #[inline]
87 pub fn get(&self, key: &str) -> Option<&str> {
88 self.0.get(key).map(|s| s.as_str())
89 }
90
91 #[inline]
93 pub fn contains_key(&self, key: &str) -> bool {
94 self.0.contains_key(key)
95 }
96
97 #[inline]
112 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
113 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
114 }
115
116 pub fn validate(&self) -> ModelResult<()> {
122 for (key, value) in &self.0 {
123 validation::validate_qualified_name("label key", key)?;
124 validation::validate_label_value("label value", value)?;
125 }
126 Ok(())
127 }
128}
129
130impl<'a> IntoIterator for &'a Labels {
131 type Item = (&'a str, &'a str);
132 type IntoIter = LabelsIter<'a>;
133
134 #[inline]
135 fn into_iter(self) -> Self::IntoIter {
136 LabelsIter(self.0.iter())
137 }
138}
139
140pub struct LabelsIter<'a>(std::collections::btree_map::Iter<'a, String, String>);
142
143impl<'a> Iterator for LabelsIter<'a> {
144 type Item = (&'a str, &'a str);
145
146 #[inline]
147 fn next(&mut self) -> Option<Self::Item> {
148 self.0.next().map(|(k, v)| (k.as_str(), v.as_str()))
149 }
150
151 #[inline]
152 fn size_hint(&self) -> (usize, Option<usize>) {
153 self.0.size_hint()
154 }
155}
156
157impl ExactSizeIterator for LabelsIter<'_> {}
158
159#[cfg(test)]
160mod tests {
161 use super::Labels;
162
163 #[test]
164 fn insertion_lookup_overwrite_and_iteration_are_deterministic() {
165 let mut labels = Labels::new();
166 assert!(labels.is_empty());
167 assert_eq!(labels.len(), 0);
168
169 labels.insert("z", "last").insert("a", "first");
170 labels.insert("env", "dev");
171 labels.insert("env", "prod");
172
173 assert_eq!(labels.len(), 3);
174 assert_eq!(labels.get("env"), Some("prod"));
175 assert_eq!(
176 labels.iter().collect::<Vec<_>>(),
177 vec![("a", "first"), ("env", "prod"), ("z", "last")]
178 );
179 }
180
181 #[test]
182 fn serde_is_transparent() {
183 let mut labels = Labels::new();
184 labels.insert("runner-tag", "prod");
185
186 let json = serde_json::to_string(&labels).unwrap();
187 assert_eq!(json, r#"{"runner-tag":"prod"}"#);
188 let back: Labels = serde_json::from_str(&json).unwrap();
189 assert_eq!(back, labels);
190 }
191
192 #[test]
193 fn validation_uses_kubernetes_label_rules() {
194 let mut labels = Labels::new();
195 labels
196 .insert("app.kubernetes.io/name", "solti_agent-1")
197 .insert("empty", "");
198 labels.validate().unwrap();
199
200 let mut invalid_key = Labels::new();
201 invalid_key.insert("example.io/bad key", "value");
202 assert!(invalid_key.validate().is_err());
203
204 let mut invalid_value = Labels::new();
205 invalid_value.insert("example.io/name", "-value");
206 assert!(invalid_value.validate().is_err());
207 }
208}