#[cfg(any(feature = "ip", feature = "lists"))] use std::sync::Arc;
use cel::{
Context, ExecutionError, ResolveResult,
extractors::{Arguments, This},
objects::Value,
};
pub fn register(ctx: &mut Context<'_>) {
ctx.add_function("indexOf", index_of);
ctx.add_function("lastIndexOf", last_index_of);
ctx.add_function("isGreaterThan", is_greater_than);
ctx.add_function("isLessThan", is_less_than);
ctx.add_function("compareTo", compare_to);
#[cfg(feature = "ip")]
ctx.add_function("ip", ip_dispatch);
#[cfg(feature = "ip")]
ctx.add_function("string", string_dispatch);
#[cfg(any(feature = "strings", feature = "lists"))]
ctx.add_function("reverse", reverse);
#[cfg(feature = "lists")]
{
ctx.add_function("min", min_dispatch);
ctx.add_function("max", max_dispatch);
}
}
#[allow(unused_variables)]
fn index_of(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
match this {
#[cfg(feature = "strings")]
Value::String(s) => crate::strings::string_index_of(This(s), Arguments(args)),
#[cfg(feature = "lists")]
Value::List(list) => crate::lists::list_index_of(&list, &args),
_ => Err(ExecutionError::function_error(
"indexOf",
format!("indexOf not supported on type {:?}", this.type_of()),
)),
}
}
#[allow(unused_variables)]
fn last_index_of(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
match this {
#[cfg(feature = "strings")]
Value::String(s) => crate::strings::string_last_index_of(This(s), Arguments(args)),
#[cfg(feature = "lists")]
Value::List(list) => crate::lists::list_last_index_of(&list, &args),
_ => Err(ExecutionError::function_error(
"lastIndexOf",
format!("lastIndexOf not supported on type {:?}", this.type_of()),
)),
}
}
macro_rules! opaque_comparison_dispatch {
($fn_name:ident, $name:literal, $semver_fn:path, $quantity_fn:path) => {
#[allow(unused_variables)]
fn $fn_name(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
let arg = args
.first()
.cloned()
.ok_or_else(|| ExecutionError::function_error($name, "missing argument"))?;
match &this {
#[cfg(feature = "semver_funcs")]
Value::Opaque(o)
if o.downcast_ref::<crate::semver_funcs::KubeSemver>()
.is_some() =>
{
$semver_fn(This(this), arg)
}
#[cfg(feature = "quantity")]
Value::Opaque(o) if o.downcast_ref::<crate::quantity::KubeQuantity>().is_some() => {
$quantity_fn(This(this), arg)
}
_ => Err(ExecutionError::function_error(
$name,
format!("{} not supported on type {:?}", $name, this.type_of()),
)),
}
}
};
}
opaque_comparison_dispatch!(
is_greater_than,
"isGreaterThan",
crate::semver_funcs::semver_is_greater_than,
crate::quantity::cel_is_greater_than
);
opaque_comparison_dispatch!(
is_less_than,
"isLessThan",
crate::semver_funcs::semver_is_less_than,
crate::quantity::cel_is_less_than
);
opaque_comparison_dispatch!(
compare_to,
"compareTo",
crate::semver_funcs::semver_compare_to,
crate::quantity::cel_compare_to
);
#[cfg(feature = "ip")]
fn ip_dispatch(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
match &this {
Value::Opaque(o) if o.downcast_ref::<crate::ip::KubeCIDR>().is_some() => {
crate::ip::cidr_ip(This(this))
}
_ => {
let s = match args.first() {
Some(Value::String(s)) => s.clone(),
_ => match this {
Value::String(s) => s,
_ => {
return Err(ExecutionError::function_error(
"ip",
"expected string or CIDR argument",
));
}
},
};
let addr = crate::ip::parse_ip_addr(&s).map_err(|e| ExecutionError::function_error("ip", e))?;
Ok(Value::Opaque(std::sync::Arc::new(crate::ip::KubeIP::new(addr))))
}
}
}
#[cfg(feature = "ip")]
fn string_dispatch(This(this): This<Value>) -> ResolveResult {
match &this {
Value::Opaque(o) if o.downcast_ref::<crate::ip::KubeIP>().is_some() => {
crate::ip::ip_string(This(this))
}
Value::Opaque(o) if o.downcast_ref::<crate::ip::KubeCIDR>().is_some() => {
crate::ip::cidr_string(This(this))
}
_ => builtin_string_fallback(this),
}
}
#[cfg(feature = "ip")]
fn builtin_string_fallback(this: Value) -> ResolveResult {
match this {
Value::String(_) => Ok(this),
Value::Int(n) => Ok(Value::String(Arc::new(n.to_string()))),
Value::UInt(n) => Ok(Value::String(Arc::new(n.to_string()))),
Value::Float(f) => Ok(Value::String(Arc::new(f.to_string()))),
Value::Bytes(ref b) => Ok(Value::String(Arc::new(
String::from_utf8_lossy(b.as_slice()).into(),
))),
Value::Timestamp(ref t) => Ok(Value::String(Arc::new(t.to_rfc3339()))),
Value::Duration(ref d) => Ok(Value::String(Arc::new(format_cel_duration(
d.num_nanoseconds().unwrap_or(d.num_seconds() * 1_000_000_000),
)))),
_ => Err(ExecutionError::function_error(
"string",
format!("cannot convert {:?} to string", this.type_of()),
)),
}
}
#[cfg(feature = "ip")]
fn format_cel_duration(total_nanos: i64) -> String {
if total_nanos == 0 {
return "0s".into();
}
let neg = total_nanos < 0;
let u = total_nanos.unsigned_abs();
let mut result = String::new();
if neg {
result.push('-');
}
const NS_SECOND: u64 = 1_000_000_000;
const NS_MINUTE: u64 = 60 * NS_SECOND;
const NS_HOUR: u64 = 60 * NS_MINUTE;
if u >= NS_SECOND {
let hours = u / NS_HOUR;
let mins = (u % NS_HOUR) / NS_MINUTE;
let secs = (u % NS_MINUTE) / NS_SECOND;
let frac = u % NS_SECOND;
if hours > 0 {
result.push_str(&format!("{hours}h"));
}
if hours > 0 || mins > 0 {
result.push_str(&format!("{mins}m"));
}
if frac > 0 {
let frac_s = format!("{frac:09}");
let frac_s = frac_s.trim_end_matches('0');
result.push_str(&format!("{secs}.{frac_s}s"));
} else {
result.push_str(&format!("{secs}s"));
}
} else {
const NS_MILLISECOND: u64 = 1_000_000;
const NS_MICROSECOND: u64 = 1_000;
if u >= NS_MILLISECOND {
let ms = u as f64 / NS_MILLISECOND as f64;
let s = format!("{ms:.3}");
result.push_str(s.trim_end_matches('0').trim_end_matches('.'));
result.push_str("ms");
} else if u >= NS_MICROSECOND {
let us = u as f64 / NS_MICROSECOND as f64;
let s = format!("{us:.3}");
result.push_str(s.trim_end_matches('0').trim_end_matches('.'));
result.push_str("µs");
} else {
result.push_str(&format!("{u}ns"));
}
}
result
}
#[cfg(any(feature = "strings", feature = "lists"))]
#[allow(unused_variables)]
fn reverse(This(this): This<Value>) -> ResolveResult {
match this {
#[cfg(feature = "strings")]
Value::String(s) => crate::strings::string_reverse(This(s)),
#[cfg(feature = "lists")]
Value::List(list) => crate::lists::list_reverse_value(This(list)),
_ => Err(ExecutionError::function_error(
"reverse",
format!("reverse not supported on type {:?}", this.type_of()),
)),
}
}
#[cfg(feature = "lists")]
fn min_dispatch(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
match this {
Value::List(list) if args.is_empty() => crate::lists::list_min(This(list)),
_ => {
let mut all_args = vec![this];
all_args.extend(args.iter().cloned());
cel::functions::min(Arguments(Arc::new(all_args)))
}
}
}
#[cfg(feature = "lists")]
fn max_dispatch(This(this): This<Value>, Arguments(args): Arguments) -> ResolveResult {
match this {
Value::List(list) if args.is_empty() => crate::lists::list_max(This(list)),
_ => {
let mut all_args = vec![this];
all_args.extend(args.iter().cloned());
cel::functions::max(Arguments(Arc::new(all_args)))
}
}
}
#[cfg(test)]
mod tests {
#![allow(dead_code)]
use cel::{Context, Program, Value};
fn eval(expr: &str) -> Value {
let mut ctx = Context::default();
crate::register_all(&mut ctx);
Program::compile(expr).unwrap().execute(&ctx).unwrap()
}
fn eval_err(expr: &str) -> cel::ExecutionError {
let mut ctx = Context::default();
crate::register_all(&mut ctx);
Program::compile(expr).unwrap().execute(&ctx).unwrap_err()
}
#[test]
#[cfg(feature = "strings")]
fn test_index_of_unsupported_type() {
eval_err("true.indexOf('x')");
}
#[test]
#[cfg(feature = "strings")]
fn test_last_index_of_unsupported_type() {
eval_err("true.lastIndexOf('x')");
}
#[test]
#[cfg(feature = "semver_funcs")]
fn test_is_greater_than_unsupported_type() {
eval_err("'hello'.isGreaterThan('world')");
}
#[test]
#[cfg(feature = "semver_funcs")]
fn test_is_less_than_unsupported_type() {
eval_err("'hello'.isLessThan('world')");
}
#[test]
#[cfg(feature = "semver_funcs")]
fn test_compare_to_unsupported_type() {
eval_err("'hello'.compareTo('world')");
}
#[test]
#[cfg(feature = "ip")]
fn test_string_int() {
assert_eq!(eval("42.string()"), Value::String("42".to_string().into()));
}
#[test]
#[cfg(feature = "ip")]
fn test_string_uint() {
assert_eq!(eval("42u.string()"), Value::String("42".to_string().into()));
}
#[test]
#[cfg(feature = "ip")]
fn test_string_float() {
assert_eq!(eval("3.14.string()"), Value::String("3.14".to_string().into()));
}
#[test]
#[cfg(feature = "ip")]
fn test_string_string() {
assert_eq!(
eval("'hello'.string()"),
Value::String("hello".to_string().into())
);
}
#[test]
#[cfg(feature = "ip")]
fn test_string_bytes() {
assert_eq!(eval("b'abc'.string()"), Value::String("abc".to_string().into()));
}
#[test]
#[cfg(feature = "ip")]
fn test_string_unsupported_type() {
eval_err("true.string()");
}
#[test]
#[cfg(feature = "lists")]
fn test_min_list_method() {
assert_eq!(eval("[3, 1, 2].min()"), Value::Int(1));
}
#[test]
#[cfg(feature = "lists")]
fn test_max_list_method() {
assert_eq!(eval("[3, 1, 2].max()"), Value::Int(3));
}
#[test]
#[cfg(feature = "lists")]
fn test_min_global_variadic() {
assert_eq!(eval("min(5, 3)"), Value::Int(3));
}
#[test]
#[cfg(feature = "lists")]
fn test_max_global_variadic() {
assert_eq!(eval("max(5, 3)"), Value::Int(5));
}
}