solti_model/domain/selector/
requirement.rs1use serde::{Deserialize, Serialize};
6
7use super::SelectorOperator;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SelectorRequirement {
21 pub key: String,
23 pub operator: SelectorOperator,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub values: Vec<String>,
29}
30
31impl SelectorRequirement {
32 pub fn validate(&self) -> crate::error::ModelResult<()> {
38 use std::borrow::Cow;
39
40 if self.key.is_empty() {
41 return Err(crate::ModelError::Invalid(Cow::Borrowed(
42 "selector requirement key must not be empty",
43 )));
44 }
45 match self.operator {
46 SelectorOperator::In | SelectorOperator::NotIn => {
47 if self.values.is_empty() {
48 return Err(crate::ModelError::Invalid(Cow::Owned(format!(
49 "selector requirement '{}' with operator {} must have non-empty values",
50 self.key, self.operator,
51 ))));
52 }
53 }
54 SelectorOperator::Exists | SelectorOperator::DoesNotExist => {
55 if !self.values.is_empty() {
56 return Err(crate::ModelError::Invalid(Cow::Owned(format!(
57 "selector requirement '{}' with operator {} must have empty values",
58 self.key, self.operator,
59 ))));
60 }
61 }
62 }
63 Ok(())
64 }
65
66 #[inline]
68 pub fn r#in(key: impl Into<String>, values: Vec<String>) -> Self {
69 Self {
70 key: key.into(),
71 operator: SelectorOperator::In,
72 values,
73 }
74 }
75
76 #[inline]
78 pub fn not_in(key: impl Into<String>, values: Vec<String>) -> Self {
79 Self {
80 key: key.into(),
81 operator: SelectorOperator::NotIn,
82 values,
83 }
84 }
85
86 #[inline]
88 pub fn exists(key: impl Into<String>) -> Self {
89 Self {
90 key: key.into(),
91 operator: SelectorOperator::Exists,
92 values: vec![],
93 }
94 }
95
96 #[inline]
98 pub fn does_not_exist(key: impl Into<String>) -> Self {
99 Self {
100 key: key.into(),
101 operator: SelectorOperator::DoesNotExist,
102 values: vec![],
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn in_constructor() {
113 let req = SelectorRequirement::r#in("gpu", vec!["a100".into(), "h100".into()]);
114 assert_eq!(req.key, "gpu");
115 assert_eq!(req.operator, SelectorOperator::In);
116 assert_eq!(req.values, vec!["a100", "h100"]);
117 }
118
119 #[test]
120 fn not_in_constructor() {
121 let req = SelectorRequirement::not_in("zone", vec!["us-west".into()]);
122 assert_eq!(req.operator, SelectorOperator::NotIn);
123 }
124
125 #[test]
126 fn exists_constructor() {
127 let req = SelectorRequirement::exists("gpu");
128 assert_eq!(req.operator, SelectorOperator::Exists);
129 assert!(req.values.is_empty());
130 }
131
132 #[test]
133 fn does_not_exist_constructor() {
134 let req = SelectorRequirement::does_not_exist("tainted");
135 assert_eq!(req.operator, SelectorOperator::DoesNotExist);
136 assert!(req.values.is_empty());
137 }
138
139 #[test]
140 fn serde_roundtrip() {
141 let req = SelectorRequirement::r#in("tier", vec!["prod".into(), "staging".into()]);
142 let json = serde_json::to_string(&req).unwrap();
143 let back: SelectorRequirement = serde_json::from_str(&json).unwrap();
144 assert_eq!(back, req);
145 }
146
147 #[test]
148 fn serde_skips_empty_values() {
149 let req = SelectorRequirement::exists("gpu");
150 let json = serde_json::to_string(&req).unwrap();
151 assert!(
152 !json.contains("values"),
153 "empty values should be skipped: {json}"
154 );
155 }
156}