use super::references::{Reference, cel_children, cel_reference};
use super::*;
use crate::predicate::Rfc3339Timestamp;
#[derive(Clone, Debug)]
pub(crate) struct TimeBoundary {
pub(crate) instant: String,
pub(crate) timestamp: Rfc3339Timestamp,
pub(crate) comparison: String,
}
impl Expression {
pub(crate) fn time_boundaries(&self) -> Vec<TimeBoundary> {
let mut boundaries = Vec::new();
collect_boundaries(&self.cel_ast, &mut boundaries);
boundaries
}
}
const TIME_COMPARISONS: [&str; 8] = [
"timeAfter",
"time_after",
"timeAtOrAfter",
"time_at_or_after",
"timeBefore",
"time_before",
"timeAtOrBefore",
"time_at_or_before",
];
fn collect_boundaries(expr: &IdedExpr, boundaries: &mut Vec<TimeBoundary>) {
if let Expr::Call(call) = &expr.expr {
let name = call.func_name.as_str();
if TIME_COMPARISONS.contains(&name) && call.args.len() == 2 {
push_pair(name, &call.args[0], &call.args[1], boundaries);
} else if (name == "timeBetween" || name == "time_between")
&& call.args.len() == 3
&& is_env_now(&call.args[0])
{
push_literal(name, &call.args[1], boundaries);
push_literal(name, &call.args[2], boundaries);
} else if let Some(operator) = comparison_operator(name)
&& call.args.len() == 2
{
push_pair(operator, &call.args[0], &call.args[1], boundaries);
}
}
for child in cel_children(expr) {
collect_boundaries(child, boundaries);
}
}
fn push_pair(comparison: &str, a: &IdedExpr, b: &IdedExpr, boundaries: &mut Vec<TimeBoundary>) {
if is_env_now(a) {
push_literal(comparison, b, boundaries);
} else if is_env_now(b) {
push_literal(comparison, a, boundaries);
}
}
fn push_literal(comparison: &str, expr: &IdedExpr, boundaries: &mut Vec<TimeBoundary>) {
let Expr::Literal(LiteralValue::String(instant)) = &expr.expr else {
return;
};
let Some(timestamp) = parse_rfc3339_timestamp(instant) else {
return;
};
boundaries.push(TimeBoundary {
instant: instant.to_string(),
timestamp,
comparison: comparison.to_owned(),
});
}
fn is_env_now(expr: &IdedExpr) -> bool {
matches!(cel_reference(expr), Some(Reference::EnvNow))
}
fn comparison_operator(func_name: &str) -> Option<&'static str> {
match func_name {
name if name == operators::EQUALS => Some("=="),
name if name == operators::NOT_EQUALS => Some("!="),
name if name == operators::LESS => Some("<"),
name if name == operators::LESS_EQUALS => Some("<="),
name if name == operators::GREATER => Some(">"),
name if name == operators::GREATER_EQUALS => Some(">="),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::super::Expression;
fn boundaries(source: &str) -> Vec<(String, String)> {
let expression = Expression::parse(source).unwrap();
expression
.time_boundaries()
.into_iter()
.map(|boundary| (boundary.comparison, boundary.instant))
.collect()
}
#[test]
fn finds_time_function_boundaries_on_env_now() {
assert_eq!(
boundaries(r#"timeAtOrAfter(env.now, "2027-10-01T00:00:00Z")"#),
vec![(
"timeAtOrAfter".to_owned(),
"2027-10-01T00:00:00Z".to_owned()
)]
);
assert_eq!(
boundaries(r#"timeBefore("2027-01-01T00:00:00Z", env.now)"#),
vec![("timeBefore".to_owned(), "2027-01-01T00:00:00Z".to_owned())]
);
assert_eq!(
boundaries(r#"timeBetween(env.now, "2027-01-01T00:00:00Z", "2027-02-01T00:00:00Z")"#),
vec![
("timeBetween".to_owned(), "2027-01-01T00:00:00Z".to_owned()),
("timeBetween".to_owned(), "2027-02-01T00:00:00Z".to_owned()),
]
);
}
#[test]
fn finds_bare_comparison_boundaries() {
assert_eq!(
boundaries(r#"env.now >= "2027-06-01T00:00:00Z""#),
vec![(">=".to_owned(), "2027-06-01T00:00:00Z".to_owned())]
);
assert_eq!(
boundaries(
r#"context.tier == "premium" && (env.now < "2027-06-01T00:00:00Z" || variables["beta"])"#
),
vec![("<".to_owned(), "2027-06-01T00:00:00Z".to_owned())]
);
}
#[test]
fn ignores_non_boundaries() {
assert!(boundaries(r#"context.starts_at >= "2027-06-01T00:00:00Z""#).is_empty());
assert!(boundaries(r#"env.now >= "someday""#).is_empty());
assert!(boundaries(r#"env.now >= context.starts_at"#).is_empty());
}
}