solti_model/domain/selector/
requirement.rs1use serde::{Deserialize, Serialize};
7
8use super::SelectorOperator;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct SelectorRequirement {
28 pub key: String,
30 pub operator: SelectorOperator,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub values: Vec<String>,
37}
38
39#[cfg(feature = "schema")]
40impl schemars::JsonSchema for SelectorRequirement {
41 fn schema_name() -> std::borrow::Cow<'static, str> {
42 "SelectorRequirement".into()
43 }
44
45 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
46 crate::schema::selector_requirement(generator)
47 }
48}
49
50impl SelectorRequirement {
51 pub fn validate(&self) -> crate::error::ModelResult<()> {
66 use std::borrow::Cow;
67
68 crate::validation::validate_qualified_name("selector requirement key", &self.key)?;
69 match self.operator {
70 SelectorOperator::In | SelectorOperator::NotIn => {
71 if self.values.is_empty() {
72 return Err(crate::ModelError::Invalid(Cow::Owned(format!(
73 "selector requirement '{}' with operator {} must have non-empty values",
74 self.key, self.operator,
75 ))));
76 }
77 for value in &self.values {
78 crate::validation::validate_label_value("selector requirement value", value)?;
79 }
80 }
81 SelectorOperator::Exists | SelectorOperator::DoesNotExist => {
82 if !self.values.is_empty() {
83 return Err(crate::ModelError::Invalid(Cow::Owned(format!(
84 "selector requirement '{}' with operator {} must have empty values",
85 self.key, self.operator,
86 ))));
87 }
88 }
89 }
90 Ok(())
91 }
92
93 #[inline]
104 pub fn r#in(key: impl Into<String>, values: Vec<String>) -> Self {
105 Self {
106 key: key.into(),
107 operator: SelectorOperator::In,
108 values,
109 }
110 }
111
112 #[inline]
114 pub fn not_in(key: impl Into<String>, values: Vec<String>) -> Self {
115 Self {
116 key: key.into(),
117 operator: SelectorOperator::NotIn,
118 values,
119 }
120 }
121
122 #[inline]
124 pub fn exists(key: impl Into<String>) -> Self {
125 Self {
126 key: key.into(),
127 operator: SelectorOperator::Exists,
128 values: vec![],
129 }
130 }
131
132 #[inline]
134 pub fn does_not_exist(key: impl Into<String>) -> Self {
135 Self {
136 key: key.into(),
137 operator: SelectorOperator::DoesNotExist,
138 values: vec![],
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn constructors_set_operator_key_and_values() {
149 let included = SelectorRequirement::r#in("gpu", vec!["a100".into(), "h100".into()]);
150 assert_eq!(included.key, "gpu");
151 assert_eq!(included.operator, SelectorOperator::In);
152 assert_eq!(included.values, vec!["a100", "h100"]);
153
154 let excluded = SelectorRequirement::not_in("zone", vec!["us-west".into()]);
155 assert_eq!(excluded.key, "zone");
156 assert_eq!(excluded.operator, SelectorOperator::NotIn);
157 assert_eq!(excluded.values, vec!["us-west"]);
158
159 for (requirement, operator) in [
160 (SelectorRequirement::exists("gpu"), SelectorOperator::Exists),
161 (
162 SelectorRequirement::does_not_exist("tainted"),
163 SelectorOperator::DoesNotExist,
164 ),
165 ] {
166 assert_eq!(requirement.operator, operator);
167 assert!(requirement.values.is_empty());
168 }
169 }
170
171 #[test]
172 fn serde_roundtrip_and_empty_values_shape_are_stable() {
173 let req = SelectorRequirement::r#in("tier", vec!["prod".into(), "staging".into()]);
174 let json = serde_json::to_string(&req).unwrap();
175 let back: SelectorRequirement = serde_json::from_str(&json).unwrap();
176 assert_eq!(back, req);
177
178 let req = SelectorRequirement::exists("gpu");
179 let json = serde_json::to_string(&req).unwrap();
180 assert!(
181 !json.contains("values"),
182 "empty values should be skipped: {json}"
183 );
184 }
185
186 #[test]
187 fn validation_uses_kubernetes_label_rules() {
188 SelectorRequirement::r#in(
189 "workloads.example.io/class",
190 vec!["gpu_fast".into(), "".into()],
191 )
192 .validate()
193 .unwrap();
194
195 assert!(SelectorRequirement::exists("bad key").validate().is_err());
196 assert!(
197 SelectorRequirement::r#in("valid", vec!["-invalid".into()])
198 .validate()
199 .is_err()
200 );
201 }
202}