#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sample {
pub timestamp_ms: i64,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Series {
pub labels: Vec<(String, String)>,
pub samples: Vec<Sample>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Vector(pub Vec<Series>);
impl Vector {
pub fn new(series: Vec<Series>) -> Self {
Self(series)
}
pub fn series(&self) -> &[Series] {
&self.0
}
pub fn into_series(self) -> Vec<Series> {
self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<Vec<Series>> for Vector {
fn from(series: Vec<Series>) -> Self {
Self(series)
}
}
impl std::ops::Deref for Vector {
type Target = [Series];
fn deref(&self) -> &[Series] {
&self.0
}
}
impl IntoIterator for Vector {
type Item = Series;
type IntoIter = std::vec::IntoIter<Series>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<Series> for Vector {
fn from_iter<I: IntoIterator<Item = Series>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchOp {
Eq,
Ne,
EqRegex,
NeRegex,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Matcher {
pub label: String,
pub op: MatchOp,
pub value: String,
}
impl Matcher {
pub fn eq(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
op: MatchOp::Eq,
value: value.into(),
}
}
pub fn ne(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
op: MatchOp::Ne,
value: value.into(),
}
}
pub fn eq_regex(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
op: MatchOp::EqRegex,
value: value.into(),
}
}
pub fn matches(&self, labels: &[(String, String)]) -> bool {
let v = labels
.iter()
.find(|(k, _)| k == &self.label)
.map(|(_, v)| v.as_str())
.unwrap_or("");
match self.op {
MatchOp::Eq => v == self.value,
MatchOp::Ne => v != self.value,
MatchOp::EqRegex => regex_full_match(&self.value, v).unwrap_or(false),
MatchOp::NeRegex => !regex_full_match(&self.value, v).unwrap_or(true),
}
}
}
fn regex_full_match(pattern: &str, value: &str) -> Option<bool> {
let anchored = format!("^(?:{pattern})$");
regex::Regex::new(&anchored)
.ok()
.map(|re| re.is_match(value))
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn matcher_covers_all_four_ops() {
let ls = labels(&[("__name__", "errors_total"), ("phase", "saturate")]);
assert!(Matcher::eq("phase", "saturate").matches(&ls));
assert!(!Matcher::eq("phase", "rampup").matches(&ls));
assert!(Matcher::ne("phase", "rampup").matches(&ls));
assert!(!Matcher::ne("phase", "saturate").matches(&ls));
assert!(Matcher::eq_regex("phase", "sat.*").matches(&ls));
assert!(!Matcher::eq_regex("phase", "ramp.*").matches(&ls));
let ne_re = Matcher {
label: "phase".into(),
op: MatchOp::NeRegex,
value: "ramp.*".into(),
};
assert!(ne_re.matches(&ls));
}
#[test]
fn missing_label_is_empty_and_regex_is_anchored() {
let ls = labels(&[("__name__", "errors_total")]);
assert!(Matcher::eq("phase", "").matches(&ls));
assert!(!Matcher::eq("phase", "saturate").matches(&ls));
let ls2 = labels(&[("phase", "saturate")]);
assert!(!Matcher::eq_regex("phase", "sat").matches(&ls2));
assert!(Matcher::eq_regex("phase", "saturate").matches(&ls2));
}
}