1use std::fmt;
28
29use crate::labels::Labels;
30
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct Selector {
41 clauses: Vec<Clause>,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
48enum Clause {
49 Eq(String, String),
51 Ne(String, String),
55 Glob(String, GlobPattern),
57 Present(String),
59 Absent(String),
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SelectorParseError {
67 message: String,
68}
69
70impl fmt::Display for SelectorParseError {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.message)
73 }
74}
75
76impl std::error::Error for SelectorParseError {}
77
78impl Selector {
79 pub fn new() -> Self {
81 Self {
82 clauses: Vec::new(),
83 }
84 }
85
86 pub fn parse(s: &str) -> Result<Self, SelectorParseError> {
90 let mut sel = Self::new();
91 for raw in s.split(',') {
92 let clause = raw.trim();
93 if clause.is_empty() {
94 continue;
95 }
96 sel.clauses.push(parse_clause(clause)?);
97 }
98 Ok(sel)
99 }
100
101 pub fn eq(mut self, key: &str, value: &str) -> Self {
103 self.clauses
104 .push(Clause::Eq(key.to_string(), value.to_string()));
105 self
106 }
107
108 pub fn ne(mut self, key: &str, value: &str) -> Self {
110 self.clauses
111 .push(Clause::Ne(key.to_string(), value.to_string()));
112 self
113 }
114
115 pub fn glob(mut self, key: &str, pattern: &str) -> Self {
117 self.clauses
118 .push(Clause::Glob(key.to_string(), GlobPattern::new(pattern)));
119 self
120 }
121
122 pub fn present(mut self, key: &str) -> Self {
124 self.clauses.push(Clause::Present(key.to_string()));
125 self
126 }
127
128 pub fn absent(mut self, key: &str) -> Self {
130 self.clauses.push(Clause::Absent(key.to_string()));
131 self
132 }
133
134 pub fn matches(&self, labels: &Labels) -> bool {
137 self.clauses.iter().all(|c| c.matches(labels))
138 }
139
140 pub fn len(&self) -> usize {
142 self.clauses.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.clauses.is_empty()
148 }
149}
150
151impl fmt::Display for Selector {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 let mut first = true;
157 for clause in &self.clauses {
158 if !first {
159 f.write_str(",")?;
160 }
161 first = false;
162 match clause {
163 Clause::Eq(k, v) => write!(f, "{k}={v}")?,
164 Clause::Ne(k, v) => write!(f, "{k}!={v}")?,
165 Clause::Glob(k, g) => write!(f, "{k}~={}", g.pattern)?,
166 Clause::Present(k) => write!(f, "{k}?")?,
167 Clause::Absent(k) => write!(f, "{k}!?")?,
168 }
169 }
170 Ok(())
171 }
172}
173
174impl Clause {
175 fn matches(&self, labels: &Labels) -> bool {
176 match self {
177 Self::Eq(k, v) => labels.get(k) == Some(v.as_str()),
178 Self::Ne(k, v) => labels.get(k) != Some(v.as_str()),
179 Self::Glob(k, g) => labels.get(k).is_some_and(|s| g.matches(s)),
180 Self::Present(k) => labels.get(k).is_some(),
181 Self::Absent(k) => labels.get(k).is_none(),
182 }
183 }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq)]
194struct GlobPattern {
195 pattern: String,
196}
197
198impl GlobPattern {
199 fn new(pattern: &str) -> Self {
200 Self {
201 pattern: pattern.to_string(),
202 }
203 }
204
205 fn matches(&self, s: &str) -> bool {
206 glob_matches(&self.pattern, s)
207 }
208}
209
210fn glob_matches(pattern: &str, s: &str) -> bool {
213 let p: Vec<char> = pattern.chars().collect();
214 let t: Vec<char> = s.chars().collect();
215 let (mut pi, mut ti) = (0usize, 0usize);
216 let mut star_pi: Option<usize> = None;
217 let mut star_ti: usize = 0;
218 while ti < t.len() {
219 if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
220 pi += 1;
221 ti += 1;
222 } else if pi < p.len() && p[pi] == '*' {
223 star_pi = Some(pi);
224 star_ti = ti;
225 pi += 1;
226 } else if let Some(sp) = star_pi {
227 pi = sp + 1;
228 star_ti += 1;
229 ti = star_ti;
230 } else {
231 return false;
232 }
233 }
234 while pi < p.len() && p[pi] == '*' {
235 pi += 1;
236 }
237 pi == p.len()
238}
239
240fn parse_clause(clause: &str) -> Result<Clause, SelectorParseError> {
245 if let Some(key) = clause.strip_suffix("!?") {
254 let key = key.trim();
255 validate_key(key)?;
256 return Ok(Clause::Absent(key.to_string()));
257 }
258 if let Some(key) = clause.strip_suffix('?') {
259 let key = key.trim();
260 validate_key(key)?;
261 return Ok(Clause::Present(key.to_string()));
262 }
263 if let Some(idx) = clause.find("~=") {
264 let (k, rest) = clause.split_at(idx);
265 let v = &rest[2..];
266 let k = k.trim();
267 let v = v.trim();
268 validate_key(k)?;
269 return Ok(Clause::Glob(k.to_string(), GlobPattern::new(v)));
270 }
271 if let Some(idx) = clause.find("!=") {
272 let (k, rest) = clause.split_at(idx);
273 let v = &rest[2..];
274 let k = k.trim();
275 let v = v.trim();
276 validate_key(k)?;
277 return Ok(Clause::Ne(k.to_string(), v.to_string()));
278 }
279 if let Some(idx) = clause.find('=') {
280 let (k, rest) = clause.split_at(idx);
281 let v = &rest[1..];
282 let k = k.trim();
283 let v = v.trim();
284 validate_key(k)?;
285 return Ok(Clause::Eq(k.to_string(), v.to_string()));
286 }
287 Err(SelectorParseError {
288 message: format!("clause '{clause}': no operator (expected one of =, !=, ~=, ?, !?)"),
289 })
290}
291
292fn validate_key(key: &str) -> Result<(), SelectorParseError> {
293 if key.is_empty() {
294 return Err(SelectorParseError {
295 message: "empty label key".to_string(),
296 });
297 }
298 if key.chars().any(char::is_whitespace) {
303 return Err(SelectorParseError {
304 message: format!("label key '{key}' contains whitespace"),
305 });
306 }
307 Ok(())
308}
309
310#[derive(Clone, Debug, PartialEq, Eq)]
316pub enum LookupError {
317 NotFound,
319 Ambiguous { count: usize },
322}
323
324impl fmt::Display for LookupError {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 match self {
327 Self::NotFound => f.write_str("no components matched selector"),
328 Self::Ambiguous { count } => {
329 write!(f, "selector matched {count} components, expected exactly 1")
330 }
331 }
332 }
333}
334
335impl std::error::Error for LookupError {}
336
337#[macro_export]
353macro_rules! selector {
354 ($($key:tt = $value:expr),* $(,)?) => {
355 {
356 let mut __sel = $crate::selector::Selector::new();
357 $( __sel = __sel.eq(stringify!($key), $value); )*
358 __sel
359 }
360 };
361}
362
363#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn lbls(pairs: &[(&str, &str)]) -> Labels {
372 let mut l = Labels::empty();
373 for (k, v) in pairs {
374 l = l.with(*k, *v);
375 }
376 l
377 }
378
379 #[test]
382 fn glob_empty_pattern_matches_only_empty() {
383 assert!(glob_matches("", ""));
384 assert!(!glob_matches("", "a"));
385 }
386
387 #[test]
388 fn glob_literal_exact() {
389 assert!(glob_matches("rampup", "rampup"));
390 assert!(!glob_matches("rampup", "ramp"));
391 assert!(!glob_matches("rampup", "rampups"));
392 }
393
394 #[test]
395 fn glob_star_only_matches_anything() {
396 assert!(glob_matches("*", ""));
397 assert!(glob_matches("*", "anything"));
398 assert!(glob_matches("*", "with spaces and $pecial_chars"));
399 }
400
401 #[test]
402 fn glob_star_prefix() {
403 assert!(glob_matches("*_00", "label_00"));
404 assert!(glob_matches("*_00", "_00"));
405 assert!(!glob_matches("*_00", "label_01"));
406 }
407
408 #[test]
409 fn glob_star_suffix() {
410 assert!(glob_matches("label_*", "label_00"));
411 assert!(glob_matches("label_*", "label_"));
412 assert!(!glob_matches("label_*", "other"));
413 }
414
415 #[test]
416 fn glob_star_middle() {
417 assert!(glob_matches("label_*_tail", "label_x_tail"));
418 assert!(glob_matches("label_*_tail", "label__tail"));
419 assert!(!glob_matches("label_*_tail", "label_tail"));
420 }
421
422 #[test]
423 fn glob_question_single_char() {
424 assert!(glob_matches("label_?", "label_0"));
425 assert!(glob_matches("label_?", "label_z"));
426 assert!(!glob_matches("label_?", "label_00"));
427 assert!(!glob_matches("label_?", "label_"));
428 }
429
430 #[test]
431 fn glob_multiple_stars_backtrack() {
432 assert!(glob_matches("*a*b*", "xyzaXYZb"));
433 assert!(glob_matches("*a*b*", "ab"));
434 assert!(!glob_matches("*a*b*", "ba"));
435 }
436
437 #[test]
438 fn glob_unicode_chars() {
439 assert!(glob_matches("*_μ", "prefix_μ"));
440 assert!(glob_matches("?_x", "λ_x"));
441 }
442
443 #[test]
446 fn eq_matches_exact_value() {
447 let l = lbls(&[("phase", "rampup")]);
448 assert!(Selector::new().eq("phase", "rampup").matches(&l));
449 assert!(!Selector::new().eq("phase", "teardown").matches(&l));
450 }
451
452 #[test]
453 fn eq_misses_on_absent_key() {
454 let l = lbls(&[("name", "x")]);
455 assert!(!Selector::new().eq("phase", "rampup").matches(&l));
456 }
457
458 #[test]
459 fn ne_matches_absent_key_by_design() {
460 let l = lbls(&[("name", "x")]);
464 assert!(Selector::new().ne("skip", "true").matches(&l));
465 }
466
467 #[test]
468 fn ne_matches_differing_value() {
469 let l = lbls(&[("phase", "rampup")]);
470 assert!(Selector::new().ne("phase", "teardown").matches(&l));
471 assert!(!Selector::new().ne("phase", "rampup").matches(&l));
472 }
473
474 #[test]
475 fn glob_clause_matches_value() {
476 let l = lbls(&[("profile", "label_03")]);
477 assert!(Selector::new().glob("profile", "label_*").matches(&l));
478 assert!(Selector::new().glob("profile", "label_0?").matches(&l));
479 assert!(!Selector::new().glob("profile", "other_*").matches(&l));
480 }
481
482 #[test]
483 fn glob_clause_misses_on_absent_key() {
484 let l = lbls(&[("name", "x")]);
485 assert!(!Selector::new().glob("profile", "*").matches(&l));
486 }
487
488 #[test]
489 fn present_and_absent_clauses() {
490 let l = lbls(&[("profile", "label_00")]);
491 assert!(Selector::new().present("profile").matches(&l));
492 assert!(!Selector::new().present("missing").matches(&l));
493 assert!(Selector::new().absent("missing").matches(&l));
494 assert!(!Selector::new().absent("profile").matches(&l));
495 }
496
497 #[test]
498 fn empty_value_is_present_not_absent() {
499 let l = lbls(&[("tag", "")]);
503 assert!(Selector::new().present("tag").matches(&l));
504 assert!(!Selector::new().absent("tag").matches(&l));
505 assert!(Selector::new().eq("tag", "").matches(&l));
506 }
507
508 #[test]
511 fn empty_selector_matches_everything() {
512 assert!(Selector::new().matches(&Labels::empty()));
513 assert!(Selector::new().matches(&lbls(&[("a", "b"), ("c", "d")])));
514 }
515
516 #[test]
517 fn multi_clause_conjunction() {
518 let l = lbls(&[("phase", "rampup"), ("k", "10"), ("profile", "label_00")]);
519 let sel = Selector::new()
520 .eq("phase", "rampup")
521 .eq("k", "10")
522 .glob("profile", "label_*");
523 assert!(sel.matches(&l));
524
525 let miss = Selector::new().eq("phase", "rampup").eq("k", "999");
527 assert!(!miss.matches(&l));
528 }
529
530 #[test]
531 fn contradictory_clauses_never_match() {
532 let sel = Selector::new().eq("phase", "x").ne("phase", "x");
534 assert!(!sel.matches(&lbls(&[("phase", "x")])));
535 assert!(!sel.matches(&lbls(&[("phase", "y")])));
536 assert!(!sel.matches(&Labels::empty()));
537 }
538
539 #[test]
542 fn parse_single_clause_eq() {
543 let s = Selector::parse("phase=rampup").unwrap();
544 assert_eq!(s.len(), 1);
545 assert!(s.matches(&lbls(&[("phase", "rampup")])));
546 }
547
548 #[test]
549 fn parse_multi_clause() {
550 let s = Selector::parse("phase=rampup,k=10,profile~=label_*").unwrap();
551 assert_eq!(s.len(), 3);
552 assert!(s.matches(&lbls(&[
553 ("phase", "rampup"),
554 ("k", "10"),
555 ("profile", "label_07"),
556 ])));
557 }
558
559 #[test]
560 fn parse_tolerates_whitespace() {
561 let s = Selector::parse(" phase = rampup , profile ~= label_* ").unwrap();
562 assert_eq!(s.len(), 2);
563 assert!(s.matches(&lbls(&[("phase", "rampup"), ("profile", "label_99")])));
564 }
565
566 #[test]
567 fn parse_present_and_absent() {
568 let s = Selector::parse("profile?,skip!?").unwrap();
569 assert_eq!(s.len(), 2);
570 assert!(s.matches(&lbls(&[("profile", "label_00")])));
571 assert!(!s.matches(&lbls(&[("skip", "true")])));
572 }
573
574 #[test]
575 fn parse_ne_disambiguates_from_eq() {
576 let s = Selector::parse("phase!=teardown").unwrap();
579 assert!(s.matches(&lbls(&[("phase", "rampup")])));
580 assert!(!s.matches(&lbls(&[("phase", "teardown")])));
581 }
582
583 #[test]
584 fn parse_glob_disambiguates_from_eq() {
585 let s = Selector::parse("profile~=label_*").unwrap();
586 assert!(s.matches(&lbls(&[("profile", "label_04")])));
587 assert!(!s.matches(&lbls(&[("profile", "other")])));
588 }
589
590 #[test]
591 fn parse_absent_disambiguates_from_present() {
592 let s = Selector::parse("skip!?").unwrap();
595 assert_eq!(s.len(), 1);
596 assert!(s.matches(&lbls(&[("other", "x")])));
597 assert!(!s.matches(&lbls(&[("skip", "y")])));
598 }
599
600 #[test]
601 fn parse_empty_input_is_empty_selector() {
602 assert!(Selector::parse("").unwrap().is_empty());
603 assert!(Selector::parse(" ").unwrap().is_empty());
604 assert!(Selector::parse(",,, ,").unwrap().is_empty());
606 }
607
608 #[test]
609 fn parse_missing_operator_errors() {
610 let e = Selector::parse("phase rampup").unwrap_err();
611 assert!(e.to_string().contains("no operator"), "got: {e}");
612 }
613
614 #[test]
615 fn parse_empty_key_errors() {
616 assert!(Selector::parse("=value").is_err());
617 assert!(Selector::parse("?").is_err());
618 assert!(Selector::parse("!?").is_err());
619 }
620
621 #[test]
622 fn parse_empty_value_is_fine() {
623 let s = Selector::parse("tag=").unwrap();
626 assert!(s.matches(&lbls(&[("tag", "")])));
627 assert!(!s.matches(&lbls(&[("tag", "x")])));
628 }
629
630 #[test]
633 fn display_round_trips_through_parse() {
634 for text in &[
635 "",
636 "phase=rampup",
637 "type=phase,name=rampup",
638 "profile~=label_*",
639 "skip!?",
640 "profile?",
641 "phase!=teardown",
642 "type=phase,profile~=label_*,skip!?,optimize_for=RECALL",
643 ] {
644 let parsed = Selector::parse(text).unwrap();
645 let rendered = parsed.to_string();
646 let reparsed = Selector::parse(&rendered).unwrap();
647 assert_eq!(parsed, reparsed, "text={text} rendered={rendered}");
648 }
649 }
650
651 #[test]
654 fn macro_basic_eq_chain() {
655 let sel: Selector = crate::selector!(phase = "ann_query", k = "10");
656 assert_eq!(sel.len(), 2);
657 assert!(sel.matches(&lbls(&[("phase", "ann_query"), ("k", "10"),])));
658 }
659
660 #[test]
661 fn macro_trailing_comma_accepted() {
662 let sel: Selector = crate::selector!(a = "1", b = "2",);
663 assert_eq!(sel.len(), 2);
664 }
665
666 #[test]
667 fn macro_zero_args_empty_selector() {
668 let sel: Selector = crate::selector!();
669 assert!(sel.is_empty());
670 }
671
672 #[test]
675 fn lookup_error_display() {
676 assert_eq!(
677 LookupError::NotFound.to_string(),
678 "no components matched selector",
679 );
680 assert_eq!(
681 LookupError::Ambiguous { count: 3 }.to_string(),
682 "selector matched 3 components, expected exactly 1",
683 );
684 }
685}