muninn_query 0.5.0

Query langugage for muninn logging stack
Documentation
use chrono::FixedOffset;

use crate::{
  ast::{visit::AstVisitor, *},
  is_valid_identifier,
};

fn is_comparison_operator(op: BinaryOperator) -> bool {
  match op {
    BinaryOperator::And | BinaryOperator::Or => false,
    _ => true,
  }
}

fn need_parens(parent: &Expression, child: &Expression) -> bool {
  match (parent, child) {
    (Expression::BinaryExpression(parent), Expression::BinaryExpression(child)) => {
      if parent.op.precedence() == child.op.precedence() {
        is_comparison_operator(parent.op) && is_comparison_operator(child.op)
      } else {
        parent.op.precedence() > child.op.precedence()
      }
    }
    (Expression::UnaryExpression(_), Expression::BinaryExpression(_)) => true,
    (_, _) => false,
  }
}

struct QueryWriter<'a> {
  query: String,
  stack: Vec<&'a Expression>,
}

impl<'a> QueryWriter<'a> {
  fn new() -> QueryWriter<'a> {
    QueryWriter {
      query: String::new(),
      stack: vec![],
    }
  }

  fn push_escape_str(&mut self, str: &str) {
    self.query.push_str("\"");
    for c in str.chars() {
      match c {
        '"' => {
          self.query.push_str("\\\"");
        }
        '\\' => {
          self.query.push_str("\\\\");
        }
        '\n' => {
          self.query.push_str("\\n");
        }
        '\r' => {
          self.query.push_str("\\r");
        }
        '\t' => {
          self.query.push_str("\\t");
        }
        '\u{1B}' => {
          self.query.push_str("\\");
        }
        '\u{0}'..='\u{1F}' => {
          for c in c.escape_unicode() {
            self.query.push(c);
          }
        }
        c => {
          self.query.push(c);
        }
      }
    }
    self.query.push_str("\"");
  }
}

impl<'a> AstVisitor<'a> for QueryWriter<'a> {
  fn visit_expression(&mut self, expression: &'a Expression) {
    let parent = self.stack.last().copied();
    self.stack.push(expression);

    let need_parens = parent.map_or(false, |parent| need_parens(parent, expression));

    if need_parens {
      self.query.push_str("(");
    }
    visit::walk_expression(self, expression);
    if need_parens {
      self.query.push_str(")");
    }

    self.stack.pop();
  }

  fn visit_unary_operator(&mut self, op: &'a UnaryOperator) {
    match *op {
      UnaryOperator::Not => self.query.push_str("not "),
    }
  }

  fn visit_binary_operator(&mut self, op: &'a BinaryOperator) {
    let parent = self.stack.last().copied();
    let is_time = parent.map_or(false, |parent| match parent {
      Expression::BinaryExpression(expr) => match expr.rhs {
        Expression::Value(Value::Date(_)) => true,
        Expression::Value(Value::DateTime(_)) => true,
        Expression::Value(Value::NaiveDateTime(_)) => true,
        Expression::Value(Value::RelativeTime(_)) => true,
        _ => false,
      },
      _ => false,
    });

    match *op {
      BinaryOperator::Eq => self.query.push_str(" == "),
      BinaryOperator::Ne => self.query.push_str(" != "),
      BinaryOperator::Lt => self.query.push_str(" < "),
      BinaryOperator::Gt => self.query.push_str(" > "),
      BinaryOperator::Lte if is_time => self.query.push_str(" before "),
      BinaryOperator::Lte => self.query.push_str(" <= "),
      BinaryOperator::Gte if is_time => self.query.push_str(" after "),
      BinaryOperator::Gte => self.query.push_str(" >= "),
      BinaryOperator::Match => self.query.push_str(" match "),
      BinaryOperator::IMatch => self.query.push_str(" imatch "),
      BinaryOperator::And => self.query.push_str(" and "),
      BinaryOperator::Or => self.query.push_str(" or "),
    }
  }

  fn visit_absolute_path(&mut self, path: &AbsolutePath) {
    for path_element in path {
      if is_valid_identifier(&path_element) {
        self.query.push_str(".");
        self.query.push_str(&path_element);
      } else {
        self.query.push_str(".");
        self.push_escape_str(&path_element);
      }
    }
  }

  fn visit_wildcard_path(&mut self, path: &WildcardPath) {
    for path_element in &path.0 {
      match path_element {
        PathElement::Field(field) => {
          if is_valid_identifier(&field) {
            self.query.push_str(".");
            self.query.push_str(&field);
          } else {
            self.query.push_str(".");
            self.push_escape_str(&field);
          }
        }
        PathElement::Wildcard => self.query.push_str(".*"),
        PathElement::RecursiveWildcard => self.query.push_str(".**"),
      }
    }
  }

  fn visit_value(&mut self, value: &Value) {
    match *value {
      Value::Null => {
        self.query.push_str("null");
      }
      Value::Bool(bool) => {
        if bool {
          self.query.push_str("true");
        } else {
          self.query.push_str("false");
        }
      }
      Value::Int(num) => {
        self.query.push_str(&num.to_string());
      }
      Value::Float(num) => {
        self.query.push_str(&num.to_string());
      }
      Value::String(ref str) => {
        self.push_escape_str(str);
      }
      Value::Date(ref date) => self.query.push_str(&date.format("%Y-%m-%d").to_string()),
      Value::DateTime(ref date) => {
        if date.offset() == &FixedOffset::east(0) {
          self
            .query
            .push_str(&date.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string())
        } else {
          self
            .query
            .push_str(&date.format("%Y-%m-%dT%H:%M:%S%.f%:z").to_string())
        }
      }
      Value::NaiveDateTime(ref date) => self
        .query
        .push_str(&date.format("%Y-%m-%dT%H:%M:%S%.f").to_string()),
      Value::RelativeTime(ref date) => match date {
        RelativeTime::Now => self.query.push_str("now"),
        RelativeTime::Duration(duration, anchor) => {
          match duration {
            Duration::Seconds(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" second");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Minutes(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" minute");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Hours(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" hour");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Days(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" day");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Weeks(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" week");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Months(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" month");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
            Duration::Years(v) => {
              self.query.push_str(&v.to_string());
              self.query.push_str(" year");
              if v.abs() > 1 {
                self.query.push_str("s");
              }
            }
          }
          self.query.push_str(" ");
          match anchor {
            TimeAnchor::Ago => self.query.push_str("ago"),
          }
        }
      },
    }
  }
}

impl ToString for WildcardPath {
  fn to_string(&self) -> String {
    let mut writer = QueryWriter::new();
    writer.visit_wildcard_path(self);
    writer.query
  }
}

impl ToString for Expression {
  fn to_string(&self) -> String {
    let mut writer = QueryWriter::new();
    writer.visit_expression(self);
    writer.query
  }
}

pub fn encode_absolute_path(path: &AbsolutePath) -> String {
  let mut writer = QueryWriter::new();
  writer.visit_absolute_path(path);
  writer.query
}

#[cfg(test)]
mod test {
  use crate::parser::parse_filter;

  fn test_query_format(input: &str, expected: &str) {
    let (query, errors) = parse_filter(input);
    assert_eq!(errors, vec![]);

    let output = query.to_string();
    assert_eq!(output, expected);
  }

  fn test_query(input: &str) {
    let (query, errors) = parse_filter(input);
    assert_eq!(errors, vec![]);

    let output = query.to_string();
    assert_eq!(output, input);
  }

  #[test]
  fn test_precedence() {
    test_query(".a and .b or .c and .d");
    test_query(".a and (.b or .c) and .d");
    test_query(".a or .level < 5 or .b");
    test_query("1 < 2 == 2 > 3");
    test_query_format("1 == 2 == false", "(1 == 2) == false");
    test_query("false == (1 == 2)");
    test_query("false == not (1 == 2)");
    test_query("not .a or .b");
    test_query(".a and .b match \"*\\*foo\"");
    test_query(".a and .b imatch \"foo?\\?\"");
  }

  #[test]
  fn test_types() {
    test_query(".a");
    test_query(".data.a == null");
    test_query(".data.a != null");
    test_query(".data.a and true");
    test_query(".data.a or false");
    test_query(".level == .data.a");
    test_query(".data.a == .level");
    test_query("42 == .data.a");
    test_query(".data.a == 42");
    test_query("3.14 == .data.a");
    test_query(".data.a == 3.14");
    test_query("42 == 3.14");
    test_query(".message == .data.a");
    test_query(".data.a == .message");
    test_query("42 <= 3.14");
    test_query(".time before 2020-01-01");
  }

  #[test]
  fn test_time() {
    test_query("2020-01-02");

    test_query_format("2020-01-02T03:04", "2020-01-02T03:04:00");
    test_query("2020-01-02T03:04:05");
    test_query_format("2020-01-02T03:04:05.000", "2020-01-02T03:04:05");
    test_query("2020-01-02T03:04:05.123");
    test_query("2020-01-02T03:04:05.123456");
    test_query("2020-01-02T03:04:05.123456789");

    test_query_format("2020-01-02T03:04Z", "2020-01-02T03:04:00Z");
    test_query("2020-01-02T03:04:05Z");
    test_query_format("2020-01-02T03:04:05.000Z", "2020-01-02T03:04:05Z");
    test_query("2020-01-02T03:04:05.123Z");
    test_query("2020-01-02T03:04:05.123456Z");
    test_query("2020-01-02T03:04:05.123456789Z");

    test_query_format("2020-01-02T03:04+05:00", "2020-01-02T03:04:00+05:00");
    test_query("2020-01-02T03:04:05+05:00");
    test_query_format("2020-01-02T03:04:05.000+05:00", "2020-01-02T03:04:05+05:00");
    test_query("2020-01-02T03:04:05.123+05:00");
    test_query("2020-01-02T03:04:05.123456+05:00");
    test_query("2020-01-02T03:04:05.123456789+05:00");

    test_query_format("2020-01-02T03:04-05:00", "2020-01-02T03:04:00-05:00");
    test_query("2020-01-02T03:04:05-05:00");
    test_query_format("2020-01-02T03:04:05.000-05:00", "2020-01-02T03:04:05-05:00");
    test_query("2020-01-02T03:04:05.123-05:00");
    test_query("2020-01-02T03:04:05.123456-05:00");
    test_query("2020-01-02T03:04:05.123456789-05:00");

    test_query("now");
    test_query("1 second ago");
    test_query("2 seconds ago");
    test_query("1 minute ago");
    test_query("2 minutes ago");
    test_query("1 hour ago");
    test_query("2 hours ago");
    test_query("1 day ago");
    test_query("2 days ago");
    test_query("1 week ago");
    test_query("2 weeks ago");
    test_query("1 month ago");
    test_query("2 months ago");
    test_query("1 year ago");
    test_query("2 years ago");
  }

  #[test]
  fn test_paths() {
    test_query(".a.b.c.d");
    test_query_format(".a.\"b\".\"c\"", ".a.b.c");
    test_query(".a.\"b c\"");
    test_query(".a.\"b \\\"foo\\\" c\"");
    test_query(".a.\"b \\\\\\\"foo\\\\\\\" c\"");
    test_query(".a.\"b\\n\\r\\t\\u{10}c\"");
  }
}