Implement most of the builtin time module (#82)

* Implement builtin `time.add_date` method

* Implement builtin `time.clock` method

* Implement builtin `time.date` method

* Implement builtin `time.diff` method

* Implement builtin `time.format` method

* Migrate `time.now_ns` to `chrono`

* Implement builtin `time.parse_ns` method

* Implement builtin `time.parse_rfc3339_ns` method

* Implement builtin `time.weekday` method

* Add conditional `test` module for OPA tests

* Cache result of `time.now_ns`

* Fail in strict mode if timestamp is outside of range

* Add `ensure_i32` util

* Move `diff_between_datetimes` into its own file and include appropriate license
This commit is contained in:
Burak
2024-01-01 01:07:39 +00:00
committed by GitHub
parent 2292774446
commit d2eb3ecd1f
14 changed files with 1002 additions and 12 deletions

View File

@@ -33,6 +33,9 @@ mod utils;
#[cfg(feature = "uuid")]
mod uuid;
#[cfg(feature = "opa-testutil")]
mod test;
use crate::ast::{Expr, Ref};
use crate::lexer::Span;
use crate::value::Value;
@@ -76,6 +79,7 @@ lazy_static! {
encoding::register(&mut m);
//token_signing::register(&mut m);
//token_verification::register(&mut m);
#[cfg(feature = "time")]
time::register(&mut m);
#[cfg(feature = "crypto")]
@@ -92,6 +96,10 @@ lazy_static! {
debugging::register(&mut m);
tracing::register(&mut m);
units::register(&mut m);
#[cfg(feature = "opa-testutil")]
test::register(&mut m);
m
};
}
@@ -100,6 +108,7 @@ pub fn must_cache(path: &str) -> Option<&'static str> {
match path {
"rand.intn" => Some("rand.intn"),
"uuid.rfc4122" => Some("uuid.rfc4122"),
"time.now_ns" => Some("time.now_ns"),
_ => None,
}
}

40
src/builtins/test.rs Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::{ensure_args_count, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
use anyhow::{bail, Ok, Result};
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("test.sleep", (sleep, 1));
}
fn sleep(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "test.sleep";
ensure_args_count(span, name, params, args, 1)?;
let val = ensure_string(name, &params[0], &args[0])?;
let duration = if let Some(millis) = val.strip_suffix("ms").and_then(|v| v.parse().ok()) {
Duration::from_millis(millis)
} else if let Some(secs) = val.strip_suffix("s").and_then(|v| v.parse().ok()) {
Duration::from_secs(secs)
} else {
bail!(params[0].span().error(
format!("`{name}` expects a simple duration ends with `ms` or `s`. Got {val} instead")
.as_str()
))
};
thread::sleep(duration);
Ok(Value::Null)
}

View File

@@ -3,28 +3,258 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::utils::ensure_args_count;
use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use std::collections::HashMap;
use std::time::SystemTime;
use anyhow::{bail, Result};
use anyhow::{anyhow, bail, Result};
use chrono::{
DateTime, Datelike, Days, FixedOffset, Local, Months, NaiveDateTime, SecondsFormat, TimeZone,
Timelike, Utc, Weekday,
};
use chrono_tz::Tz;
mod diff;
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("time.add_date", (add_date, 4));
m.insert("time.clock", (clock, 1));
m.insert("time.date", (date, 1));
m.insert("time.diff", (diff, 2));
m.insert("time.format", (format, 1));
m.insert("time.now_ns", (now_ns, 0));
m.insert("time.parse_ns", (parse_ns, 2));
m.insert("time.parse_rfc3339_ns", (parse_rfc3339_ns, 1));
m.insert("time.weekday", (weekday, 1));
}
fn now_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
fn add_date(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "time.add_date";
ensure_args_count(span, name, params, args, 4)?;
let (datetime, _) = parse_epoch(name, &params[0], &args[0])?;
let years = ensure_i32(name, &params[1], &args[1])?;
let months = ensure_i32(name, &params[2], &args[2])?;
let days = ensure_i32(name, &params[3], &args[3])?;
let Some(new_year) = datetime.year().checked_add(years) else {
return Ok(Value::Undefined);
};
datetime
.with_year(new_year)
.and_then(|d| {
let rhs = Months::new(months.unsigned_abs());
if months >= 0 {
d.checked_add_months(rhs)
} else {
d.checked_sub_months(rhs)
}
})
.and_then(|d| {
let rhs = Days::new(days.unsigned_abs() as u64);
if days >= 0 {
d.checked_add_days(rhs)
} else {
d.checked_sub_days(rhs)
}
})
.map_or(Ok(Value::Undefined), |d| {
safe_timestamp_nanos(span, strict, d.timestamp_nanos_opt())
})
}
fn clock(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "time.clock";
ensure_args_count(span, name, params, args, 1)?;
let (datetime, _) = parse_epoch(name, &params[0], &args[0])?;
Ok(Vec::from([
(datetime.hour() as u64).into(),
(datetime.minute() as u64).into(),
(datetime.second() as u64).into(),
])
.into())
}
fn date(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "time.date";
ensure_args_count(span, name, params, args, 1)?;
let (datetime, _) = parse_epoch(name, &params[0], &args[0])?;
Ok(Vec::from([
(datetime.year() as u64).into(),
(datetime.month() as u64).into(),
(datetime.day() as u64).into(),
])
.into())
}
fn diff(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "time.diff";
ensure_args_count(span, name, params, args, 2)?;
let (datetime1, _) = parse_epoch(name, &params[0], &args[0])?;
let (datetime2, _) = parse_epoch(name, &params[1], &args[1])?;
let (year, month, day, hour, min, sec) = diff::diff_between_datetimes(datetime1, datetime2)?;
Ok(Vec::from([
(year as i64).into(),
(month as i64).into(),
(day as i64).into(),
(hour as i64).into(),
(min as i64).into(),
(sec as i64).into(),
])
.into())
}
fn format(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "time.format";
ensure_args_count(span, name, params, args, 1)?;
let (datetime, format) = parse_epoch(name, &params[0], &args[0])?;
let result = match format {
Some(format) => datetime.format(&format).to_string(),
None => datetime.to_rfc3339_opts(SecondsFormat::AutoSi, true),
};
Ok(Value::String(result.into()))
}
fn now_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "time.now_ns";
ensure_args_count(span, name, params, args, 0)?;
let now = SystemTime::now();
let elapsed = match now.duration_since(SystemTime::UNIX_EPOCH) {
Ok(e) => e,
Err(e) => bail!(span.error(format!("could not fetch elapsed time. {e}").as_str())),
};
let nanos = elapsed.as_nanos();
Ok(Value::from(nanos))
safe_timestamp_nanos(span, strict, Utc::now().timestamp_nanos_opt())
}
fn parse_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "time.parse_ns";
ensure_args_count(span, name, params, args, 2)?;
let layout = ensure_string(name, &params[0], &args[0])?;
let value = ensure_string(name, &params[1], &args[1])?;
let datetime = NaiveDateTime::parse_from_str(&value, &layout)?;
safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt())
}
fn parse_rfc3339_ns(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "time.parse_rfc3339_ns";
ensure_args_count(span, name, params, args, 1)?;
let value = ensure_string(name, &params[0], &args[0])?;
let datetime = DateTime::parse_from_rfc3339(&value)?;
safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt())
}
fn weekday(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "time.weekday";
ensure_args_count(span, name, params, args, 1)?;
let (datetime, _) = parse_epoch(name, &params[0], &args[0])?;
let weekday = match datetime.weekday() {
Weekday::Mon => "Monday",
Weekday::Tue => "Tuesday",
Weekday::Wed => "Wednesday",
Weekday::Thu => "Thursday",
Weekday::Fri => "Friday",
Weekday::Sat => "Saturday",
Weekday::Sun => "Sunday",
};
Ok(Value::String(weekday.into()))
}
fn ensure_i32(name: &str, arg: &Expr, v: &Value) -> Result<i32> {
ensure_numeric(name, arg, v)?
.as_i64()
.and_then(|n| n.try_into().ok())
.ok_or_else(|| arg.span().error("could not convert to int32"))
}
fn safe_timestamp_nanos(span: &Span, strict: bool, nanos: Option<i64>) -> Result<Value> {
match nanos {
Some(ns) => Ok(Value::Number(ns.into())),
None if strict => {
bail!(span.error("time outside of valid range"))
}
None => Ok(Value::Undefined),
}
}
fn parse_epoch(
fcn: &str,
arg: &Expr,
val: &Value,
) -> Result<(DateTime<FixedOffset>, Option<String>)> {
match val {
Value::Number(num) => {
let ns = num.as_i64().ok_or_else(|| {
arg.span()
.error("could not convert numeric value of `ns` to int64")
})?;
return Ok((Utc.timestamp_nanos(ns).fixed_offset(), None));
}
Value::Array(arr) => match arr.as_slice() {
[Value::Number(num)] => {
let ns = num.as_i64().ok_or_else(|| {
arg.span()
.error("could not convert numeric value of `ns` to int64")
})?;
return Ok((Utc.timestamp_nanos(ns).fixed_offset(), None));
}
[Value::Number(num), Value::String(tz), rest @ ..] => {
let ns = num.as_i64().ok_or_else(|| {
arg.span()
.error("could not convert numeric value of `ns` to int64")
})?;
let datetime = match tz.as_ref() {
"UTC" | "" => Utc.timestamp_nanos(ns).fixed_offset(),
"Local" => Local.timestamp_nanos(ns).fixed_offset(),
_ => {
let tz: Tz = tz.parse().map_err(|err: String| anyhow!(err))?;
tz.timestamp_nanos(ns).fixed_offset()
}
};
let format = match rest.first() {
Some(Value::String(format)) => Some(format.to_string()),
Some(other) => {
bail!(arg.span().error(&format!(
"`{fcn}` expects 3rd element of `ns` to be a `string`. Got `{other}` instead"
)))
}
None => None,
};
return Ok((datetime, format));
}
_ => {}
},
_ => {}
}
bail!(arg.span().error(&format!(
"`{fcn}` expects `ns` to be a `number` or `array[number, string]`. Got `{val}` instead"
)))
}

69
src/builtins/time/diff.rs Normal file
View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT and Apache 2.0 License.
use anyhow::{anyhow, Result};
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike, Utc};
// Adapted from the official Go implementation:
// https://github.com/open-policy-agent/opa/blob/eb17a716b97720a27c6569395ba7c4b7409aae87/topdown/time.go#L179-L243
pub fn diff_between_datetimes(
datetime1: DateTime<FixedOffset>,
datetime2: DateTime<FixedOffset>,
) -> Result<(i32, i32, i32, i32, i32, i32)> {
// The following implementation of this function is taken
// from https://github.com/icza/gox licensed under Apache 2.0.
// The only modification made is to variable names.
//
// For details, see https://stackoverflow.com/a/36531443/1705598
//
// Copyright 2021 icza
// BEGIN REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT
// Make sure both datetimes in the same timezone
let datetime2 = datetime2.with_timezone(&datetime1.timezone());
// Make sure `datetime1` is always the smallest one
let (datetime1, datetime2) = if datetime1 > datetime2 {
(datetime2, datetime1)
} else {
(datetime1, datetime2)
};
let mut year = datetime2.year() - datetime1.year();
let mut month = datetime2.month() as i32 - datetime1.month() as i32;
let mut day = datetime2.day() as i32 - datetime1.day() as i32;
let mut hour = datetime2.hour() as i32 - datetime1.hour() as i32;
let mut min = datetime2.minute() as i32 - datetime1.minute() as i32;
let mut sec = datetime2.second() as i32 - datetime1.second() as i32;
// Normalize negative values
if sec < 0 {
sec += 60;
min -= 1;
}
if min < 0 {
min += 60;
hour -= 1;
}
if hour < 0 {
hour += 24;
day -= 1;
}
if day < 0 {
// Days in month:
let t = Utc
.with_ymd_and_hms(datetime1.year(), datetime1.month(), 32, 0, 0, 0)
.single()
.ok_or(anyhow!("Could not convert `ns1` to datetime"))?;
day += 32 - t.day() as i32;
month -= 1;
}
if month < 0 {
month += 12;
year -= 1;
}
// END REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT
Ok((year, month, day, hour, min, sec))
}