muninn_query 0.5.0

Query langugage for muninn logging stack
Documentation
use crate::{
  ast::Value,
  parser::{
    error::Error,
    utils::{IResult, LocatedSpan, ToRange},
  },
};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use nom::{
  branch::alt,
  character::complete::{char, digit1, one_of},
  combinator::{map, map_res, opt, recognize, value},
  sequence::tuple,
};

pub(crate) fn date_time(i: LocatedSpan) -> IResult<Value> {
  map(
    tuple((date, char('T'), time, opt(timezone))),
    |(date, _, time, timezone)| match timezone {
      Some(tz) => Value::DateTime(DateTime::from_utc(NaiveDateTime::new(date, time) - tz, tz)),
      None => Value::NaiveDateTime(NaiveDateTime::new(date, time)),
    },
  )(i)
}

pub(crate) fn date_value(i: LocatedSpan) -> IResult<Value> {
  map(date, Value::Date)(i)
}

fn date(i: LocatedSpan) -> IResult<NaiveDate> {
  map_res(
    recognize(tuple((year, char('-'), month, char('-'), day))),
    |res| {
      NaiveDate::parse_from_str(*res, "%Y-%m-%d").map_err(|err| {
        res
          .extra
          .report_error(Error(res.to_range(), "Invalid date".to_string()));
        err
      })
    },
  )(i)
}

fn time(i: LocatedSpan) -> IResult<NaiveTime> {
  alt((
    time_subsecond_precision,
    time_second_precision,
    time_minute_precision,
  ))(i)
}
fn time_subsecond_precision(i: LocatedSpan) -> IResult<NaiveTime> {
  map_res(
    recognize(tuple((
      hour,
      char(':'),
      minute,
      char(':'),
      second,
      char('.'),
      digit1,
    ))),
    |res| {
      NaiveTime::parse_from_str(*res, "%H:%M:%S%.f").map_err(|err| {
        res
          .extra
          .report_error(Error(res.to_range(), "Invalid time".to_string()));
        err
      })
    },
  )(i)
}
fn time_second_precision(i: LocatedSpan) -> IResult<NaiveTime> {
  map_res(
    recognize(tuple((hour, char(':'), minute, char(':'), second))),
    |res| {
      NaiveTime::parse_from_str(*res, "%H:%M:%S").map_err(|err| {
        res
          .extra
          .report_error(Error(res.to_range(), "Invalid time".to_string()));
        err
      })
    },
  )(i)
}
fn time_minute_precision(i: LocatedSpan) -> IResult<NaiveTime> {
  map_res(recognize(tuple((hour, char(':'), minute))), |res| {
    NaiveTime::parse_from_str(*res, "%H:%M").map_err(|err| {
      res
        .extra
        .report_error(Error(res.to_range(), "Invalid time".to_string()));
      err
    })
  })(i)
}

fn year(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(tuple((dec_digit, dec_digit, dec_digit, dec_digit)))(i)
}
fn month(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(alt((
    tuple((char('1'), one_of("012"))),
    tuple((char('0'), dec_digit)),
  )))(i)
}
fn day(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(alt((
    tuple((char('3'), one_of("01"))),
    tuple((one_of("012"), dec_digit)),
  )))(i)
}
fn hour(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(alt((
    tuple((char('2'), one_of("0123"))),
    tuple((one_of("01"), dec_digit)),
  )))(i)
}
fn minute(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(tuple((one_of("012345"), dec_digit)))(i)
}
fn second(i: LocatedSpan) -> IResult<LocatedSpan> {
  recognize(alt((
    tuple((char('6'), char('0'))),
    tuple((one_of("012345"), dec_digit)),
  )))(i)
}

fn dec_digit(i: LocatedSpan) -> IResult<char> {
  one_of("0123456789")(i)
}

fn timezone(i: LocatedSpan) -> IResult<FixedOffset> {
  alt((timezone_utc, timezone_offset))(i)
}
fn timezone_utc(i: LocatedSpan) -> IResult<FixedOffset> {
  value(FixedOffset::east(0), char('Z'))(i)
}
fn timezone_offset(i: LocatedSpan) -> IResult<FixedOffset> {
  map_res(
    tuple((
      one_of("+-"),
      // TODO: Optional : in timezone
      // recognize(tuple((hour, opt(char(':')), minute))),
      time_minute_precision,
    )),
    |(sign, offset)| match sign {
      '+' => Ok(FixedOffset::east(offset.num_seconds_from_midnight() as i32)),
      '-' => Ok(FixedOffset::west(offset.num_seconds_from_midnight() as i32)),
      _ => Err("Invalid sign"),
    },
  )(i)
}

#[cfg(test)]
mod tests {
  use crate::parser::{
    absolute_time::date,
    utils::{span, unwrap_span},
  };
  use chrono::NaiveDate;

  #[test]

  fn test_date() {
    assert_eq!(
      date(span("2020-01-01")).map(unwrap_span),
      Ok(("", NaiveDate::from_ymd(2020, 01, 01)))
    );

    assert_eq!(
      date(span("2024-02-29")).map(unwrap_span),
      Ok(("", NaiveDate::from_ymd(2024, 02, 29)))
    );

    assert!(date(span("2023-02-29")).is_err());
  }
}