nmbrs_metrics/queryapi/
shapes.rs1#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Sample {
20 pub timestamp_ms: i64,
21 pub value: f64,
22}
23
24#[derive(Debug, Clone, PartialEq)]
27pub struct Series {
28 pub labels: Vec<(String, String)>,
29 pub samples: Vec<Sample>,
30}
31
32#[derive(Debug, Clone, Default, PartialEq)]
42pub struct Vector(pub Vec<Series>);
43
44impl Vector {
45 pub fn new(series: Vec<Series>) -> Self {
47 Self(series)
48 }
49 pub fn series(&self) -> &[Series] {
51 &self.0
52 }
53 pub fn into_series(self) -> Vec<Series> {
55 self.0
56 }
57 pub fn len(&self) -> usize {
58 self.0.len()
59 }
60 pub fn is_empty(&self) -> bool {
61 self.0.is_empty()
62 }
63}
64
65impl From<Vec<Series>> for Vector {
66 fn from(series: Vec<Series>) -> Self {
67 Self(series)
68 }
69}
70
71impl std::ops::Deref for Vector {
74 type Target = [Series];
75 fn deref(&self) -> &[Series] {
76 &self.0
77 }
78}
79
80impl IntoIterator for Vector {
81 type Item = Series;
82 type IntoIter = std::vec::IntoIter<Series>;
83 fn into_iter(self) -> Self::IntoIter {
84 self.0.into_iter()
85 }
86}
87
88impl FromIterator<Series> for Vector {
89 fn from_iter<I: IntoIterator<Item = Series>>(iter: I) -> Self {
90 Self(iter.into_iter().collect())
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum MatchOp {
98 Eq,
100 Ne,
102 EqRegex,
104 NeRegex,
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub struct Matcher {
111 pub label: String,
112 pub op: MatchOp,
113 pub value: String,
114}
115
116impl Matcher {
117 pub fn eq(label: impl Into<String>, value: impl Into<String>) -> Self {
119 Self {
120 label: label.into(),
121 op: MatchOp::Eq,
122 value: value.into(),
123 }
124 }
125 pub fn ne(label: impl Into<String>, value: impl Into<String>) -> Self {
127 Self {
128 label: label.into(),
129 op: MatchOp::Ne,
130 value: value.into(),
131 }
132 }
133 pub fn eq_regex(label: impl Into<String>, value: impl Into<String>) -> Self {
135 Self {
136 label: label.into(),
137 op: MatchOp::EqRegex,
138 value: value.into(),
139 }
140 }
141
142 pub fn matches(&self, labels: &[(String, String)]) -> bool {
147 let v = labels
148 .iter()
149 .find(|(k, _)| k == &self.label)
150 .map(|(_, v)| v.as_str())
151 .unwrap_or("");
152 match self.op {
153 MatchOp::Eq => v == self.value,
154 MatchOp::Ne => v != self.value,
155 MatchOp::EqRegex => regex_full_match(&self.value, v).unwrap_or(false),
156 MatchOp::NeRegex => !regex_full_match(&self.value, v).unwrap_or(true),
157 }
158 }
159}
160
161fn regex_full_match(pattern: &str, value: &str) -> Option<bool> {
164 let anchored = format!("^(?:{pattern})$");
165 regex::Regex::new(&anchored)
166 .ok()
167 .map(|re| re.is_match(value))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn labels(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
175 pairs
176 .iter()
177 .map(|(k, v)| (k.to_string(), v.to_string()))
178 .collect()
179 }
180
181 #[test]
182 fn matcher_covers_all_four_ops() {
183 let ls = labels(&[("__name__", "errors_total"), ("phase", "saturate")]);
184 assert!(Matcher::eq("phase", "saturate").matches(&ls));
185 assert!(!Matcher::eq("phase", "rampup").matches(&ls));
186 assert!(Matcher::ne("phase", "rampup").matches(&ls));
187 assert!(!Matcher::ne("phase", "saturate").matches(&ls));
188 assert!(Matcher::eq_regex("phase", "sat.*").matches(&ls));
189 assert!(!Matcher::eq_regex("phase", "ramp.*").matches(&ls));
190 let ne_re = Matcher {
191 label: "phase".into(),
192 op: MatchOp::NeRegex,
193 value: "ramp.*".into(),
194 };
195 assert!(ne_re.matches(&ls));
196 }
197
198 #[test]
199 fn missing_label_is_empty_and_regex_is_anchored() {
200 let ls = labels(&[("__name__", "errors_total")]);
201 assert!(Matcher::eq("phase", "").matches(&ls));
202 assert!(!Matcher::eq("phase", "saturate").matches(&ls));
203 let ls2 = labels(&[("phase", "saturate")]);
204 assert!(!Matcher::eq_regex("phase", "sat").matches(&ls2));
206 assert!(Matcher::eq_regex("phase", "saturate").matches(&ls2));
207 }
208}