1
/// A single label filter: either key-existence (`KEY`) or exact-value (`KEY=VALUE`).
2
#[derive(Debug, Clone, PartialEq, Eq)]
3
pub 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

            
10
impl LabelFilter {
11
  /// Parse a filter string. `"KEY"` becomes `Exists`, `"KEY=VALUE"` becomes `Equals`.
12
86
  pub fn parse(s: &str) -> Self {
13
86
    if let Some((key, value)) = s.split_once('=') {
14
76
      Self::Equals(key.to_owned(), value.to_owned())
15
    } else {
16
10
      Self::Exists(s.to_owned())
17
    }
18
86
  }
19

            
20
  /// Returns true when this filter matches `labels`.
21
212
  pub fn matches(&self, labels: &hashbrown::HashMap<String, String>) -> bool {
22
212
    match self {
23
32
      Self::Exists(key) => labels.contains_key(key),
24
180
      Self::Equals(key, value) => labels.get(key).map(|v| v == value).unwrap_or(false),
25
    }
26
212
  }
27
}
28

            
29
/// Returns true when all `filters` match `labels` (AND semantics).
30
/// An empty filter list always returns true.
31
184
pub fn matches_all(filters: &[LabelFilter], labels: &hashbrown::HashMap<String, String>) -> bool {
32
202
  filters.iter().all(|f| f.matches(labels))
33
184
}
34

            
35
#[cfg(test)]
36
mod tests {
37
  use super::*;
38
  use hashbrown::HashMap;
39

            
40
18
  fn map(pairs: &[(&str, &str)]) -> HashMap<String, String> {
41
18
    pairs
42
18
      .iter()
43
18
      .map(|(k, v)| (k.to_string(), v.to_string()))
44
18
      .collect()
45
18
  }
46

            
47
  #[test]
48
2
  fn parse_key_only() {
49
2
    assert_eq!(LabelFilter::parse("area"), LabelFilter::Exists("area".into()));
50
2
  }
51

            
52
  #[test]
53
2
  fn parse_key_value() {
54
2
    assert_eq!(
55
2
      LabelFilter::parse("area=ci"),
56
2
      LabelFilter::Equals("area".into(), "ci".into())
57
    );
58
2
  }
59

            
60
  #[test]
61
2
  fn parse_key_value_with_equals_in_value() {
62
    // only split on the first '='
63
2
    assert_eq!(
64
2
      LabelFilter::parse("key=a=b"),
65
2
      LabelFilter::Equals("key".into(), "a=b".into())
66
    );
67
2
  }
68

            
69
  #[test]
70
2
  fn exists_filter_matches_present_key() {
71
2
    let labels = map(&[("area", "ci")]);
72
2
    assert!(LabelFilter::Exists("area".into()).matches(&labels));
73
2
  }
74

            
75
  #[test]
76
2
  fn exists_filter_rejects_missing_key() {
77
2
    let labels = map(&[("other", "x")]);
78
2
    assert!(!LabelFilter::Exists("area".into()).matches(&labels));
79
2
  }
80

            
81
  #[test]
82
2
  fn equals_filter_matches_exact_value() {
83
2
    let labels = map(&[("area", "ci")]);
84
2
    assert!(LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
85
2
  }
86

            
87
  #[test]
88
2
  fn equals_filter_rejects_wrong_value() {
89
2
    let labels = map(&[("area", "build")]);
90
2
    assert!(!LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
91
2
  }
92

            
93
  #[test]
94
2
  fn equals_filter_rejects_missing_key() {
95
2
    let labels = map(&[]);
96
2
    assert!(!LabelFilter::Equals("area".into(), "ci".into()).matches(&labels));
97
2
  }
98

            
99
  #[test]
100
2
  fn matches_all_empty_filters() {
101
2
    let labels = map(&[]);
102
2
    assert!(matches_all(&[], &labels));
103
2
  }
104

            
105
  #[test]
106
2
  fn matches_all_multiple_filters_all_match() {
107
2
    let labels = map(&[("area", "ci"), ("kind", "test")]);
108
2
    let filters = vec![
109
2
      LabelFilter::Equals("area".into(), "ci".into()),
110
2
      LabelFilter::Exists("kind".into()),
111
    ];
112
2
    assert!(matches_all(&filters, &labels));
113
2
  }
114

            
115
  #[test]
116
2
  fn matches_all_multiple_filters_one_fails() {
117
2
    let labels = map(&[("area", "ci")]);
118
2
    let filters = vec![
119
2
      LabelFilter::Equals("area".into(), "ci".into()),
120
2
      LabelFilter::Exists("kind".into()),
121
    ];
122
2
    assert!(!matches_all(&filters, &labels));
123
2
  }
124

            
125
  #[test]
126
2
  fn matches_all_no_match() {
127
2
    let labels = map(&[("area", "build")]);
128
2
    let filters = vec![LabelFilter::Equals("area".into(), "ci".into())];
129
2
    assert!(!matches_all(&filters, &labels));
130
2
  }
131
}