1use std::{fmt, str::FromStr};
8
9use serde::{Deserialize, Serialize};
10
11use super::{SelectorOperator, SelectorRequirement};
12use crate::{Labels, ModelError, ModelResult};
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct LabelSelector {
43 #[serde(default, skip_serializing_if = "Labels::is_empty")]
45 pub match_labels: Labels,
46
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub match_expressions: Vec<SelectorRequirement>,
50}
51
52impl LabelSelector {
53 #[inline]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 #[inline]
86 pub fn from_labels(labels: Labels) -> Self {
87 Self {
88 match_labels: labels,
89 match_expressions: vec![],
90 }
91 }
92
93 #[inline]
109 pub fn from_expressions(expr: Vec<SelectorRequirement>) -> Self {
110 Self {
111 match_labels: Labels::new(),
112 match_expressions: expr,
113 }
114 }
115
116 #[inline]
118 pub fn is_empty(&self) -> bool {
119 self.match_labels.is_empty() && self.match_expressions.is_empty()
120 }
121
122 pub fn validate(&self) -> crate::ModelResult<()> {
128 self.match_labels.validate()?;
129 for requirement in &self.match_expressions {
130 requirement.validate()?;
131 }
132 Ok(())
133 }
134
135 pub fn matches(&self, labels: &Labels) -> bool {
155 for (key, expected) in &self.match_labels {
156 match labels.get(key) {
157 Some(actual) if actual == expected => {}
158 _ => return false,
159 }
160 }
161
162 for req in &self.match_expressions {
163 let value = labels.get(&req.key);
164 let ok = match req.operator {
165 SelectorOperator::In => match value {
166 Some(v) => req.values.iter().any(|x| x == v),
167 None => false,
168 },
169 SelectorOperator::NotIn => match value {
170 Some(v) => !req.values.iter().any(|x| x == v),
171 None => true,
172 },
173 SelectorOperator::Exists => value.is_some(),
174 SelectorOperator::DoesNotExist => value.is_none(),
175 };
176 if !ok {
177 return false;
178 }
179 }
180 true
181 }
182}
183
184impl FromStr for LabelSelector {
185 type Err = ModelError;
186
187 fn from_str(value: &str) -> ModelResult<Self> {
195 let value = trim_selector_whitespace(value);
196 if value.is_empty() {
197 return Ok(Self::new());
198 }
199
200 let requirements = split_requirements(value)?
201 .into_iter()
202 .map(parse_requirement)
203 .collect::<ModelResult<Vec<_>>>()?;
204 let selector = Self::from_expressions(requirements);
205 selector.validate()?;
206 Ok(selector)
207 }
208}
209
210impl fmt::Display for LabelSelector {
211 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212 let mut first = true;
213 let mut separator = |formatter: &mut fmt::Formatter<'_>| {
214 if first {
215 first = false;
216 Ok(())
217 } else {
218 formatter.write_str(",")
219 }
220 };
221
222 for (key, value) in &self.match_labels {
223 separator(formatter)?;
224 write!(formatter, "{key}={value}")?;
225 }
226 for requirement in &self.match_expressions {
227 separator(formatter)?;
228 match requirement.operator {
229 SelectorOperator::In => write!(
230 formatter,
231 "{} in ({})",
232 requirement.key,
233 requirement.values.join(",")
234 )?,
235 SelectorOperator::NotIn => write!(
236 formatter,
237 "{} notin ({})",
238 requirement.key,
239 requirement.values.join(",")
240 )?,
241 SelectorOperator::Exists => formatter.write_str(&requirement.key)?,
242 SelectorOperator::DoesNotExist => write!(formatter, "!{}", requirement.key)?,
243 }
244 }
245 Ok(())
246 }
247}
248
249fn split_requirements(value: &str) -> ModelResult<Vec<&str>> {
250 let mut result = Vec::new();
251 let mut start = 0;
252 let mut depth = 0_u8;
253
254 for (index, character) in value.char_indices() {
255 match character {
256 '(' => {
257 if depth > 0 {
258 return Err(invalid_selector("nested parentheses are not allowed"));
259 }
260 depth = 1;
261 }
262 ')' => {
263 if depth == 0 {
264 return Err(invalid_selector("unexpected closing parenthesis"));
265 }
266 depth = 0;
267 }
268 ',' if depth == 0 => {
269 let requirement = trim_selector_whitespace(&value[start..index]);
270 if requirement.is_empty() {
271 return Err(invalid_selector("empty requirement"));
272 }
273 result.push(requirement);
274 start = index + character.len_utf8();
275 }
276 _ => {}
277 }
278 }
279
280 if depth != 0 {
281 return Err(invalid_selector("unclosed parenthesis"));
282 }
283 let requirement = trim_selector_whitespace(&value[start..]);
284 if requirement.is_empty() {
285 return Err(invalid_selector("empty requirement"));
286 }
287 result.push(requirement);
288 Ok(result)
289}
290
291fn parse_requirement(value: &str) -> ModelResult<SelectorRequirement> {
292 if let Some(key) = value.strip_prefix('!') {
293 let key = trim_selector_whitespace(key);
294 if key.is_empty() {
295 return Err(invalid_selector("missing key after `!`"));
296 }
297 return Ok(SelectorRequirement::does_not_exist(key));
298 }
299
300 if let Some(open) = value.find('(') {
301 let close = value
302 .rfind(')')
303 .ok_or_else(|| invalid_selector("unclosed parenthesis"))?;
304 if !trim_selector_whitespace(&value[close + 1..]).is_empty() {
305 return Err(invalid_selector(
306 "unexpected text after closing parenthesis",
307 ));
308 }
309
310 let head = trim_selector_whitespace_end(&value[..open]);
311 let (key, operator) = if let Some(key) = head.strip_suffix("notin")
312 && key
313 .as_bytes()
314 .last()
315 .is_some_and(|byte| is_selector_whitespace(*byte))
316 {
317 (trim_selector_whitespace_end(key), SelectorOperator::NotIn)
318 } else if let Some(key) = head.strip_suffix("in")
319 && key
320 .as_bytes()
321 .last()
322 .is_some_and(|byte| is_selector_whitespace(*byte))
323 {
324 (trim_selector_whitespace_end(key), SelectorOperator::In)
325 } else {
326 return Err(invalid_selector("expected `in` or `notin` before `(`"));
327 };
328 if key.is_empty() {
329 return Err(invalid_selector("missing key before set operator"));
330 }
331
332 let values = trim_selector_whitespace(&value[open + 1..close]);
333 let values: Vec<_> = values
334 .split(',')
335 .map(trim_selector_whitespace)
336 .map(|value| value.to_owned())
337 .collect();
338 return Ok(SelectorRequirement {
339 key: key.to_owned(),
340 operator,
341 values,
342 });
343 }
344
345 for (token, operator) in [
346 ("!=", SelectorOperator::NotIn),
347 ("==", SelectorOperator::In),
348 ("=", SelectorOperator::In),
349 ] {
350 if let Some((key, selected)) = value.split_once(token) {
351 let key = trim_selector_whitespace(key);
352 if key.is_empty() {
353 return Err(invalid_selector("missing key before equality operator"));
354 }
355 return Ok(SelectorRequirement {
356 key: key.to_owned(),
357 operator,
358 values: vec![trim_selector_whitespace(selected).to_owned()],
359 });
360 }
361 }
362
363 Ok(SelectorRequirement::exists(trim_selector_whitespace(value)))
364}
365
366fn trim_selector_whitespace(value: &str) -> &str {
367 trim_selector_whitespace_end(value.trim_start_matches(|character: char| {
368 character.is_ascii() && is_selector_whitespace(character as u8)
369 }))
370}
371
372fn trim_selector_whitespace_end(value: &str) -> &str {
373 value.trim_end_matches(|character: char| {
374 character.is_ascii() && is_selector_whitespace(character as u8)
375 })
376}
377
378const fn is_selector_whitespace(byte: u8) -> bool {
379 matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
380}
381
382fn invalid_selector(message: &str) -> ModelError {
383 ModelError::Invalid(format!("invalid label selector: {message}").into())
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 fn labels(pairs: &[(&str, &str)]) -> Labels {
391 let mut labels = Labels::new();
392 for (key, value) in pairs {
393 labels.insert(*key, *value);
394 }
395 labels
396 }
397
398 #[test]
399 fn empty_and_exact_label_matching() {
400 let empty = LabelSelector::new();
401 assert!(empty.is_empty());
402 assert!(empty.matches(&labels(&[])));
403 assert!(empty.matches(&labels(&[("a", "b")])));
404
405 let selector = LabelSelector::from_labels(labels(&[("zone", "eu")]));
406 assert!(!selector.is_empty());
407 assert!(selector.matches(&labels(&[("zone", "eu"), ("extra", "x")])));
408 assert!(!selector.matches(&labels(&[("zone", "us")])));
409 assert!(!selector.matches(&labels(&[])));
410 }
411
412 #[test]
413 fn set_operators_follow_kubernetes_missing_key_semantics() {
414 let included = LabelSelector::from_expressions(vec![SelectorRequirement::r#in(
415 "gpu",
416 vec!["a100".into(), "h100".into()],
417 )]);
418 assert!(included.matches(&labels(&[("gpu", "a100")])));
419 assert!(included.matches(&labels(&[("gpu", "h100")])));
420 assert!(!included.matches(&labels(&[("gpu", "t4")])));
421 assert!(!included.matches(&labels(&[])));
422
423 let excluded = LabelSelector::from_expressions(vec![SelectorRequirement::not_in(
424 "tier",
425 vec!["dev".into()],
426 )]);
427 assert!(excluded.matches(&labels(&[("tier", "prod")])));
428 assert!(!excluded.matches(&labels(&[("tier", "dev")])));
429 assert!(excluded.matches(&labels(&[])));
430
431 assert!(
432 "tier!=frontend"
433 .parse::<LabelSelector>()
434 .unwrap()
435 .matches(&Labels::new())
436 );
437 assert!(
438 "tier notin (frontend)"
439 .parse::<LabelSelector>()
440 .unwrap()
441 .matches(&Labels::new())
442 );
443 }
444
445 #[test]
446 fn existence_operators_match_presence() {
447 let exists = LabelSelector::from_expressions(vec![SelectorRequirement::exists("gpu")]);
448 assert!(exists.matches(&labels(&[("gpu", "any")])));
449 assert!(!exists.matches(&labels(&[])));
450
451 let missing =
452 LabelSelector::from_expressions(vec![SelectorRequirement::does_not_exist("tainted")]);
453 assert!(missing.matches(&labels(&[])));
454 assert!(!missing.matches(&labels(&[("tainted", "true")])));
455 }
456
457 #[test]
458 fn labels_and_expressions_are_anded() {
459 let selector = LabelSelector {
460 match_labels: labels(&[("zone", "eu")]),
461 match_expressions: vec![SelectorRequirement::exists("gpu")],
462 };
463 assert!(selector.matches(&labels(&[("zone", "eu"), ("gpu", "a100")])));
464 assert!(!selector.matches(&labels(&[("zone", "us"), ("gpu", "a100")])));
465 assert!(!selector.matches(&labels(&[("zone", "eu")])));
466
467 let expressions = LabelSelector::from_expressions(vec![
468 SelectorRequirement::r#in("tier", vec!["prod".into(), "staging".into()]),
469 SelectorRequirement::does_not_exist("tainted"),
470 ]);
471 assert!(expressions.matches(&labels(&[("tier", "prod")])));
472 assert!(!expressions.matches(&labels(&[("tier", "prod"), ("tainted", "true")])));
473 assert!(!expressions.matches(&labels(&[("tier", "dev")])));
474 }
475
476 #[test]
477 fn serde_roundtrip_and_empty_shape_are_stable() {
478 let selector = LabelSelector {
479 match_labels: labels(&[("zone", "eu")]),
480 match_expressions: vec![SelectorRequirement::exists("gpu")],
481 };
482 let json = serde_json::to_string_pretty(&selector).unwrap();
483 let back: LabelSelector = serde_json::from_str(&json).unwrap();
484 assert_eq!(back, selector);
485
486 let empty = LabelSelector::new();
487 let json = serde_json::to_string(&empty).unwrap();
488 assert_eq!(json, "{}");
489 assert_eq!(serde_json::from_str::<LabelSelector>(&json).unwrap(), empty);
490 }
491
492 #[test]
493 fn validation_checks_labels_and_expressions() {
494 let mut invalid = Labels::new();
495 invalid.insert("bad key", "value");
496 assert!(LabelSelector::from_labels(invalid).validate().is_err());
497
498 let selector = LabelSelector::from_expressions(vec![SelectorRequirement::exists(
499 "example.io/capability",
500 )]);
501 selector.validate().unwrap();
502 }
503
504 #[test]
505 fn parser_and_display_use_kubernetes_selector_syntax() {
506 let selector: LabelSelector =
507 "environment=production,tier in (frontend,backend),track!=canary,!tainted,gpu"
508 .parse()
509 .unwrap();
510
511 assert!(selector.matches(&labels(&[
512 ("environment", "production"),
513 ("tier", "frontend"),
514 ("track", "stable"),
515 ("gpu", "h100"),
516 ])));
517 assert!(!selector.matches(&labels(&[
518 ("environment", "production"),
519 ("tier", "worker"),
520 ("track", "stable"),
521 ("gpu", "h100"),
522 ])));
523
524 let selector: LabelSelector = "release==stable".parse().unwrap();
525 assert!(selector.matches(&labels(&[("release", "stable")])));
526 assert!("".parse::<LabelSelector>().unwrap().is_empty());
527
528 let rendered = LabelSelector {
529 match_labels: labels(&[("environment", "production")]),
530 match_expressions: vec![
531 SelectorRequirement::r#in("tier", vec!["frontend".into(), "backend".into()]),
532 SelectorRequirement::does_not_exist("tainted"),
533 ],
534 };
535 assert_eq!(
536 rendered.to_string(),
537 "environment=production,tier in (frontend,backend),!tainted"
538 );
539 }
540
541 #[test]
542 fn empty_values_roundtrip_and_match_kubernetes_semantics() {
543 let selector: LabelSelector = "x in (foo,,baz),z notin ()".parse().unwrap();
544 assert!(selector.matches(&labels(&[("x", ""), ("z", "value")])));
545 assert!(!selector.matches(&labels(&[("x", "foo"), ("z", "")])));
546 assert!(
547 "key="
548 .parse::<LabelSelector>()
549 .unwrap()
550 .matches(&labels(&[("key", "")]))
551 );
552
553 for value in ["key=", "key in ()", "key in (foo,,baz)", "key notin ()"] {
554 let selector: LabelSelector = value.parse().unwrap();
555 let reparsed: LabelSelector = selector.to_string().parse().unwrap();
556 assert_eq!(reparsed, selector, "selector must round-trip: {value}");
557 }
558 }
559
560 #[test]
561 fn parser_accepts_ascii_whitespace_and_rejects_malformed_input() {
562 " \t\r\ntier\tin\n(frontend)\r\n"
563 .parse::<LabelSelector>()
564 .unwrap();
565
566 for value in [
567 "\u{00a0}tier in (frontend)",
568 "tier\u{00a0}in (frontend)",
569 "tier in (\u{00a0}frontend)",
570 ",environment=production",
571 "environment=production,",
572 "tier in (frontend",
573 "tier around (frontend)",
574 "!",
575 "bad key=value",
576 "tier in (front@end)",
577 ] {
578 assert!(
579 value.parse::<LabelSelector>().is_err(),
580 "selector must be rejected: {value:?}"
581 );
582 }
583 }
584}