use std::collections::BTreeSet;
use std::fmt;
use crate::error::DomainError;
use crate::value_objects::entry_labels::EntryLabels;
use crate::value_objects::memory_dimension_identity::MemoryDimensionIdentity;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LabelSelectorOperator {
In,
NotIn,
Exists,
NotExists,
}
impl LabelSelectorOperator {
pub fn as_str(self) -> &'static str {
match self {
Self::In => "in",
Self::NotIn => "notin",
Self::Exists => "exists",
Self::NotExists => "notexists",
}
}
fn takes_values(self) -> bool {
matches!(self, Self::In | Self::NotIn)
}
}
impl fmt::Display for LabelSelectorOperator {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LabelSelector {
key: String,
operator: LabelSelectorOperator,
values: BTreeSet<String>,
}
impl LabelSelector {
pub fn new(
key: impl Into<String>,
operator: LabelSelectorOperator,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, DomainError> {
let key = key.into().trim().to_string();
if key.is_empty() {
return Err(DomainError::EmptyValue("label selector key"));
}
let values = values
.into_iter()
.map(Into::into)
.map(|value| bare_label_value(&value))
.filter(|value| !value.is_empty())
.collect::<BTreeSet<_>>();
if operator.takes_values() && values.is_empty() {
return Err(DomainError::InvalidState(format!(
"label selector `{key} {operator}` requires at least one value"
)));
}
if !operator.takes_values() && !values.is_empty() {
return Err(DomainError::InvalidState(format!(
"label selector `{key} {operator}` takes no values"
)));
}
Ok(Self {
key,
operator,
values,
})
}
pub fn key(&self) -> &str {
&self.key
}
pub fn operator(&self) -> LabelSelectorOperator {
self.operator
}
pub fn values(&self) -> &BTreeSet<String> {
&self.values
}
pub fn is_positive(&self) -> bool {
matches!(
self.operator,
LabelSelectorOperator::In | LabelSelectorOperator::Exists
)
}
pub fn admits(&self, labels: &EntryLabels) -> bool {
match self.operator {
LabelSelectorOperator::Exists => labels.has_key(&self.key),
LabelSelectorOperator::NotExists => !labels.has_key(&self.key),
LabelSelectorOperator::In => labels
.values(&self.key)
.is_some_and(|values| values.iter().any(|value| self.values.contains(value))),
LabelSelectorOperator::NotIn => !labels
.values(&self.key)
.is_some_and(|values| values.iter().any(|value| self.values.contains(value))),
}
}
}
impl fmt::Display for LabelSelector {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.operator {
LabelSelectorOperator::Exists => formatter.write_str(&self.key),
LabelSelectorOperator::NotExists => write!(formatter, "!{}", self.key),
LabelSelectorOperator::In | LabelSelectorOperator::NotIn => {
let values = self.values.iter().cloned().collect::<Vec<_>>().join(", ");
write!(formatter, "{} {} ({values})", self.key, self.operator)
}
}
}
}
pub fn bare_label_value(value: &str) -> String {
MemoryDimensionIdentity::parse(value)
.map(|identity| identity.dimension_id().to_string())
.unwrap_or_else(|| value.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(pairs: &[(&str, &str)]) -> EntryLabels {
EntryLabels::from_pairs(pairs.iter().map(|(key, value)| (*key, *value)))
}
#[test]
fn in_and_notin_read_the_values_under_a_key() {
let selector = LabelSelector::new("env", LabelSelectorOperator::In, ["prod", "staging"])
.expect("selector");
assert!(selector.admits(&labels(&[("env", "prod"), ("task", "t-1")])));
assert!(!selector.admits(&labels(&[("env", "dev")])));
assert!(!selector.admits(&labels(&[("task", "t-1")])));
let selector =
LabelSelector::new("env", LabelSelectorOperator::NotIn, ["prod"]).expect("selector");
assert!(selector.admits(&labels(&[("env", "dev")])));
assert!(!selector.admits(&labels(&[("env", "prod")])));
}
#[test]
fn an_absent_key_satisfies_notin_as_kubernetes_reads_it() {
let selector =
LabelSelector::new("env", LabelSelectorOperator::NotIn, ["prod"]).expect("selector");
assert!(selector.admits(&labels(&[("task", "t-1")])));
}
#[test]
fn exists_and_notexists_ask_only_for_the_key() {
let exists = LabelSelector::new(
"incident",
LabelSelectorOperator::Exists,
Vec::<String>::new(),
)
.expect("selector");
assert!(exists.admits(&labels(&[("incident", "north-outage")])));
assert!(!exists.admits(&labels(&[("task", "t-1")])));
let absent = LabelSelector::new(
"task",
LabelSelectorOperator::NotExists,
Vec::<String>::new(),
)
.expect("selector");
assert!(absent.admits(&labels(&[("agentic_process", "p-1")])));
assert!(!absent.admits(&labels(&[("agentic_process", "p-1"), ("task", "t-1")])));
}
#[test]
fn a_namespaced_value_is_read_as_its_bare_value() {
let selector = LabelSelector::new(
"incident",
LabelSelectorOperator::In,
["about:project:kmp:dimension:north-outage"],
)
.expect("selector");
assert_eq!(
selector.values().iter().cloned().collect::<Vec<_>>(),
vec!["north-outage".to_string()]
);
assert!(selector.admits(&labels(&[("incident", "north-outage")])));
}
#[test]
fn the_shapes_that_mean_nothing_are_refused() {
assert!(
LabelSelector::new(" ", LabelSelectorOperator::Exists, Vec::<String>::new()).is_err()
);
assert!(
LabelSelector::new("env", LabelSelectorOperator::In, Vec::<String>::new()).is_err()
);
assert!(LabelSelector::new("env", LabelSelectorOperator::In, [" "]).is_err());
assert!(LabelSelector::new("env", LabelSelectorOperator::Exists, ["prod"]).is_err());
}
#[test]
fn a_selector_reads_back_the_way_a_caller_would_write_it() {
assert_eq!(
LabelSelector::new("env", LabelSelectorOperator::In, ["staging", "prod"])
.expect("selector")
.to_string(),
"env in (prod, staging)"
);
assert_eq!(
LabelSelector::new(
"task",
LabelSelectorOperator::NotExists,
Vec::<String>::new()
)
.expect("selector")
.to_string(),
"!task"
);
}
}