Skip to main content

mk_lib/
label_filter.rs

1/// A single label filter: either key-existence (`KEY`) or exact-value (`KEY=VALUE`).
2#[derive(Debug, Clone, PartialEq, Eq)]
3pub enum LabelFilter {
4  /// Match tasks that have the key, regardless of value.
5  Exists(String),
6  /// Match tasks whose label key equals the given value exactly.
7  Equals(String, String),
8}
9
10impl LabelFilter {
11  /// Parse a filter string. `"KEY"` becomes `Exists`, `"KEY=VALUE"` becomes `Equals`.
12  pub fn parse(s: &str) -> Self {
13    if let Some((key, value)) = s.split_once('=') {
14      Self::Equals(key.to_owned(), value.to_owned())
15    } else {
16      Self::Exists(s.to_owned())
17    }
18  }
19
20  /// Returns true when this filter matches `labels`.
21  pub fn matches(&self, labels: &hashbrown::HashMap<String, String>) -> bool {
22    match self {
23      Self::Exists(key) => labels.contains_key(key),
24      Self::Equals(key, value) => labels.get(key).map(|v| v == value).unwrap_or(false),
25    }
26  }
27}
28
29/// Returns true when all `filters` match `labels` (AND semantics).
30/// An empty filter list always returns true.
31pub fn matches_all(filters: &[LabelFilter], labels: &hashbrown::HashMap<String, String>) -> bool {
32  filters.iter().all(|f| f.matches(labels))
33}
34
35#[cfg(test)]
36mod tests {
37  use super::*;
38  use hashbrown::HashMap;
39
40  fn map(pairs: &[(&str, &str)]) -> HashMap<String, String> {
41    pairs
42      .iter()
43      .map(|(k, v)| (k.to_string(), v.to_string()))
44      .collect()
45  }
46
47  #[test]
48  fn parse_key_only() {
49    assert_eq!(LabelFilter::parse("area"), LabelFilter::Exists("area".into()));
50  }
51
52  #[test]
53  fn parse_key_value() {
54    assert_eq!(
55      LabelFilter::parse("area=ci"),
56      LabelFilter::Equals("area".into(), "ci".into())
57    );
58  }
59
60  #[test]
61  fn parse_key_value_with_equals_in_value() {
62    // only split on the first '='
63    assert_eq!(
64      LabelFilter::parse("key=a=b"),
65      LabelFilter::Equals("key".into(), "a=b".into())
66    );
67  }
68
69  #[test]
70  fn exists_filter_matches_present_key() {
71    let labels = map(&[("area", "ci")]);
72    assert!(LabelFilter::Exists("area".into()).matches(&labels));
73  }
74
75  #[test]
76  fn exists_filter_rejects_missing_key() {
77    let labels = map(&[("other", "x")]);
78    assert!(!LabelFilter::Exists("area".into()).matches(&labels));
79  }
80
81  #[test]
82  fn equals_filter_matches_exact_value() {
83    let labels = map(&[("area", "ci")]);
84    assert!(LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
85  }
86
87  #[test]
88  fn equals_filter_rejects_wrong_value() {
89    let labels = map(&[("area", "build")]);
90    assert!(!LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
91  }
92
93  #[test]
94  fn equals_filter_rejects_missing_key() {
95    let labels = map(&[]);
96    assert!(!LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
97  }
98
99  #[test]
100  fn matches_all_empty_filters() {
101    let labels = map(&[]);
102    assert!(matches_all(&[], &labels));
103  }
104
105  #[test]
106  fn matches_all_multiple_filters_all_match() {
107    let labels = map(&[("area", "ci"), ("kind", "test")]);
108    let filters = vec![
109      LabelFilter::Equals("area".into(), "ci".into()),
110      LabelFilter::Exists("kind".into()),
111    ];
112    assert!(matches_all(&filters, &labels));
113  }
114
115  #[test]
116  fn matches_all_multiple_filters_one_fails() {
117    let labels = map(&[("area", "ci")]);
118    let filters = vec![
119      LabelFilter::Equals("area".into(), "ci".into()),
120      LabelFilter::Exists("kind".into()),
121    ];
122    assert!(!matches_all(&filters, &labels));
123  }
124
125  #[test]
126  fn matches_all_no_match() {
127    let labels = map(&[("area", "build")]);
128    let filters = vec![LabelFilter::Equals("area".into(), "ci".into())];
129    assert!(!matches_all(&filters, &labels));
130  }
131}