use serde::{Deserialize, Serialize};
use super::SelectorOperator;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SelectorRequirement {
pub key: String,
pub operator: SelectorOperator,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<String>,
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for SelectorRequirement {
fn schema_name() -> std::borrow::Cow<'static, str> {
"SelectorRequirement".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
crate::schema::selector_requirement(generator)
}
}
impl SelectorRequirement {
pub fn validate(&self) -> crate::error::ModelResult<()> {
use std::borrow::Cow;
crate::validation::validate_qualified_name("selector requirement key", &self.key)?;
match self.operator {
SelectorOperator::In | SelectorOperator::NotIn => {
if self.values.is_empty() {
return Err(crate::ModelError::Invalid(Cow::Owned(format!(
"selector requirement '{}' with operator {} must have non-empty values",
self.key, self.operator,
))));
}
for value in &self.values {
crate::validation::validate_label_value("selector requirement value", value)?;
}
}
SelectorOperator::Exists | SelectorOperator::DoesNotExist => {
if !self.values.is_empty() {
return Err(crate::ModelError::Invalid(Cow::Owned(format!(
"selector requirement '{}' with operator {} must have empty values",
self.key, self.operator,
))));
}
}
}
Ok(())
}
#[inline]
pub fn r#in(key: impl Into<String>, values: Vec<String>) -> Self {
Self {
key: key.into(),
operator: SelectorOperator::In,
values,
}
}
#[inline]
pub fn not_in(key: impl Into<String>, values: Vec<String>) -> Self {
Self {
key: key.into(),
operator: SelectorOperator::NotIn,
values,
}
}
#[inline]
pub fn exists(key: impl Into<String>) -> Self {
Self {
key: key.into(),
operator: SelectorOperator::Exists,
values: vec![],
}
}
#[inline]
pub fn does_not_exist(key: impl Into<String>) -> Self {
Self {
key: key.into(),
operator: SelectorOperator::DoesNotExist,
values: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_set_operator_key_and_values() {
let included = SelectorRequirement::r#in("gpu", vec!["a100".into(), "h100".into()]);
assert_eq!(included.key, "gpu");
assert_eq!(included.operator, SelectorOperator::In);
assert_eq!(included.values, vec!["a100", "h100"]);
let excluded = SelectorRequirement::not_in("zone", vec!["us-west".into()]);
assert_eq!(excluded.key, "zone");
assert_eq!(excluded.operator, SelectorOperator::NotIn);
assert_eq!(excluded.values, vec!["us-west"]);
for (requirement, operator) in [
(SelectorRequirement::exists("gpu"), SelectorOperator::Exists),
(
SelectorRequirement::does_not_exist("tainted"),
SelectorOperator::DoesNotExist,
),
] {
assert_eq!(requirement.operator, operator);
assert!(requirement.values.is_empty());
}
}
#[test]
fn serde_roundtrip_and_empty_values_shape_are_stable() {
let req = SelectorRequirement::r#in("tier", vec!["prod".into(), "staging".into()]);
let json = serde_json::to_string(&req).unwrap();
let back: SelectorRequirement = serde_json::from_str(&json).unwrap();
assert_eq!(back, req);
let req = SelectorRequirement::exists("gpu");
let json = serde_json::to_string(&req).unwrap();
assert!(
!json.contains("values"),
"empty values should be skipped: {json}"
);
}
#[test]
fn validation_uses_kubernetes_label_rules() {
SelectorRequirement::r#in(
"workloads.example.io/class",
vec!["gpu_fast".into(), "".into()],
)
.validate()
.unwrap();
assert!(SelectorRequirement::exists("bad key").validate().is_err());
assert!(
SelectorRequirement::r#in("valid", vec!["-invalid".into()])
.validate()
.is_err()
);
}
}