use std::{collections::HashMap, fmt::Display};
use indexmap::IndexMap;
use lazy_static::lazy_static;
use promql_parser::{label::Matcher, parser::ast::Expr as PromExpr};
use serde::{de::Visitor, Deserialize};
use super::error::SchemaError;
#[derive(Deserialize, Debug, Clone)]
pub struct Document {
pub name: String,
pub description: Option<String>,
pub metrics: OutcomeMetrics,
}
pub type OutcomeMetrics = IndexMap<String, OutcomeExpectation>;
#[derive(Deserialize, Debug, Clone)]
pub struct OutcomeExpectation {
pub query: Option<PromQuery>,
#[serde(flatten)]
pub validation: OutcomeValidation,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum OutcomeValidation {
Range {
min: Option<f64>,
max: Option<f64>,
},
Eq {
eq: f64,
epsilon: Option<f64>,
},
}
impl OutcomeValidation {
pub fn validate(&self, value: f64) -> bool {
match self {
Self::Range { min, max } => {
if matches!(min, Some(min) if value.lt(min)) {
return false;
}
if matches!(max, Some(max) if value.gt(max)) {
return false;
}
true
}
Self::Eq { eq, epsilon } => {
let epsilon = epsilon.unwrap_or(f64::EPSILON);
((eq - epsilon)..=(eq + epsilon)).contains(&value)
}
}
}
pub fn show_validation(&self, value: f64) -> String {
if self.validate(value) {
format!("✅ pass, {value} is {self}")
} else {
format!("⚠️ expected {value} to be {self}")
}
}
}
impl Display for OutcomeValidation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use OutcomeValidation::*;
match self {
Range { min, max } => match (min, max) {
(Some(min), Some(max)) => write!(f, "between {min} and {max}"),
(Some(min), None) => write!(f, "at least {min}"),
(None, Some(max)) => write!(f, "at most {max}"),
(None, None) => write!(f, "anything"),
},
Eq { eq, epsilon } => match epsilon {
Some(epsilon) => write!(f, "equal to {eq} ± {epsilon}"),
None => write!(f, "equal to {eq}"),
},
}
}
}
#[derive(Debug, Clone)]
pub struct PromQuery(PromExpr);
impl PromQuery {
pub fn new(query: &str) -> Result<Self, SchemaError> {
promql_parser::parser::parse(query)
.map(Self)
.map_err(SchemaError::QueryParse)
}
pub fn builtin(name: &str) -> Option<&Self> {
macro_rules! builtins {
{ $($name:literal : $query:literal),+ , } => {
lazy_static! {
static ref QUERY_BUILTINS: HashMap<&'static str, PromQuery> = [
$(($name, PromQuery::new($query).unwrap())),+
]
.into_iter()
.collect();
}
}
}
builtins! {
"network/tps": "avg(rate(snarkos_blocks_transactions_total[10s]))", }
QUERY_BUILTINS.get(name)
}
pub fn into_inner(self) -> PromExpr {
self.0
}
pub fn add_matchers(&mut self, matchers: &[Matcher]) {
Self::inject_matchers(&mut self.0, matchers);
}
fn inject_matchers(expr: &mut PromExpr, matchers: &[Matcher]) {
macro_rules! inject {
($into:expr) => {
Self::inject_matchers(&mut $into, matchers)
};
($into:expr, $($into2:expr),+) => {
{
inject!($into);
inject!($($into2),+);
}
};
}
match expr {
PromExpr::Aggregate(expr) => inject!(expr.expr),
PromExpr::Unary(expr) => inject!(expr.expr),
PromExpr::Binary(expr) => inject!(expr.lhs, expr.rhs),
PromExpr::Paren(expr) => inject!(expr.expr),
PromExpr::Subquery(expr) => inject!(expr.expr),
PromExpr::NumberLiteral(_) => (),
PromExpr::StringLiteral(_) => (),
PromExpr::VectorSelector(selector) => {
selector.matchers.matchers.extend(matchers.iter().cloned());
}
PromExpr::MatrixSelector(selector) => selector
.vs
.matchers
.matchers
.extend(matchers.iter().cloned()),
PromExpr::Call(call) => {
call.args
.args
.iter_mut()
.for_each(|arg| Self::inject_matchers(arg, matchers));
}
PromExpr::Extension(_) => (),
}
}
}
impl<'de> Deserialize<'de> for PromQuery {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PromQueryVisitor;
impl<'de> Visitor<'de> for PromQueryVisitor {
type Value = PromQuery;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a PromQL query")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
PromQuery::new(v).map_err(E::custom)
}
}
deserializer.deserialize_str(PromQueryVisitor)
}
}
pub type OutcomeResults<'a> = Vec<OutcomeResult<'a>>;
pub struct OutcomeResult<'a> {
pub name: &'a str,
pub pass: bool, }