cel 0.14.0

A parser and interpreter for the Common Expression Language (CEL)
Documentation
use crate::common::traits::{self, Adder, Comparer, Sizer, Zeroer};
use crate::common::types::{CelBool, CelBytes, CelDouble, CelInt, CelUInt, Kind, Type};
#[cfg(feature = "chrono")]
use crate::common::types::{CelDuration, CelTimestamp};
use crate::common::value::{Downcast, Val};
use crate::ExecutionError;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ops::Deref;
use std::string::String as StdString;

#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct String(StdString);

impl String {
    pub fn into_inner(self) -> StdString {
        self.0
    }

    pub fn inner(&self) -> &str {
        &self.0
    }
}

impl Deref for String {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.inner()
    }
}

impl Val for String {
    fn get_type(&self) -> &Type {
        &super::STRING_TYPE
    }

    fn as_adder(&self) -> Option<&dyn Adder> {
        Some(self)
    }

    fn as_comparer(&self) -> Option<&dyn Comparer> {
        Some(self)
    }

    fn as_sizer(&self) -> Option<&dyn Sizer> {
        Some(self)
    }

    fn as_zeroer(&self) -> Option<&dyn Zeroer> {
        Some(self)
    }

    fn equals(&self, other: &dyn Val) -> bool {
        other
            .downcast_ref::<Self>()
            .is_some_and(|other| self.0 == other.0)
    }

    fn clone_as_boxed(&self) -> Box<dyn Val> {
        Box::new(String(self.0.clone()))
    }
}

impl Adder for String {
    fn add<'a>(&'a self, rhs: &dyn Val) -> Result<Cow<'a, dyn Val>, ExecutionError> {
        if let Some(rhs) = rhs.downcast_ref::<Self>() {
            let mut s = StdString::with_capacity(rhs.0.len() + self.0.len());
            s.push_str(&self.0);
            s.push_str(&rhs.0);
            Ok(Cow::<dyn Val>::Owned(Box::new(Self(s))))
        } else {
            Err(ExecutionError::UnsupportedBinaryOperator(
                "add",
                (self as &dyn Val).try_into()?,
                rhs.try_into()?,
            ))
        }
    }
}

impl Comparer for String {
    fn compare(&self, rhs: &dyn Val) -> Result<Ordering, ExecutionError> {
        if let Some(rhs) = rhs.downcast_ref::<Self>() {
            Ok(self.0.cmp(&rhs.0))
        } else {
            Err(ExecutionError::NoSuchOverload)
        }
    }
}

impl Sizer for String {
    fn size(&self) -> CelInt {
        (self.inner().len() as i64).into()
    }
}

impl Zeroer for String {
    fn is_zero_value(&self) -> bool {
        self.inner().is_empty()
    }
}

impl From<StdString> for String {
    fn from(v: StdString) -> Self {
        Self(v)
    }
}

impl From<String> for StdString {
    fn from(v: String) -> Self {
        v.0
    }
}

impl From<&str> for String {
    fn from(value: &str) -> Self {
        Self(StdString::from(value))
    }
}

impl TryFrom<Box<dyn Val>> for StdString {
    type Error = Box<dyn Val>;

    fn try_from(value: Box<dyn Val>) -> Result<Self, Self::Error> {
        super::cast_boxed::<String>(value).map(|s| s.into_inner())
    }
}

impl<'a> TryFrom<&'a dyn Val> for &'a str {
    type Error = &'a dyn Val;
    fn try_from(value: &'a dyn Val) -> Result<Self, Self::Error> {
        if let Some(s) = value.downcast_ref::<String>() {
            return Ok(s.inner());
        }
        Err(value)
    }
}

fn string_contains<'a>(args: Vec<Cow<'a, dyn Val>>) -> Result<Cow<'a, dyn Val>, ExecutionError> {
    let target = &args[0];
    let arg = &args[1];
    match target.downcast_ref::<String>() {
        None => Err(ExecutionError::UnexpectedType {
            got: target.get_type().name().to_string(),
            want: super::STRING_TYPE.name().to_string(),
        }),
        Some(s) => match arg.downcast_ref::<String>() {
            None => Err(ExecutionError::UnexpectedType {
                got: arg.get_type().name().to_string(),
                want: super::STRING_TYPE.name().to_string(),
            }),
            Some(needle) => Ok(Cow::<dyn Val>::Owned(Box::new(CelBool::from(
                s.contains(needle.inner()),
            )))),
        },
    }
}

fn ends_with_string<'a>(args: Vec<Cow<'a, dyn Val>>) -> Result<Cow<'a, dyn Val>, ExecutionError> {
    super::binary_fn(
        args,
        super::STRING_TYPE,
        super::STRING_TYPE,
        |target: &String, needle: &String| {
            Ok(Box::new(CelBool::from(target.ends_with(needle.inner()))))
        },
    )
}

fn starts_with_string<'a>(args: Vec<Cow<'a, dyn Val>>) -> Result<Cow<'a, dyn Val>, ExecutionError> {
    super::binary_fn(
        args,
        super::STRING_TYPE,
        super::STRING_TYPE,
        |target: &String, needle: &String| {
            Ok(Box::new(CelBool::from(target.starts_with(needle.inner()))))
        },
    )
}

#[cfg(feature = "regex")]
fn matches<'a>(args: Vec<Cow<'a, dyn Val>>) -> Result<Cow<'a, dyn Val>, ExecutionError> {
    super::binary_fn(
        args,
        super::STRING_TYPE,
        super::STRING_TYPE,
        |this: &String, regex: &String| match regex::Regex::new(regex.inner()) {
            Ok(re) => Ok(Box::new(CelBool::from(re.is_match(this.inner())))),
            Err(err) => Err(ExecutionError::FunctionError {
                function: "matches".to_string(),
                message: format!("'{}' not a valid regex:\n{err}", regex.inner()),
            }),
        },
    )
}

fn string<'a>(args: Vec<Cow<'a, dyn Val>>) -> Result<Cow<'a, dyn Val>, ExecutionError> {
    let mut args = args;
    let arg = args.remove(0).into_owned();
    let ret: Result<Box<String>, Box<dyn Val>> = match arg.get_type().kind() {
        Kind::String => arg.downcast::<String>(),
        Kind::Int => arg
            .downcast::<CelInt>()
            .map(|arg| Box::new(String::from(arg.to_string()))),
        Kind::UInt => arg
            .downcast::<CelUInt>()
            .map(|arg| Box::new(String::from(arg.to_string()))),
        Kind::Double => arg
            .downcast::<CelDouble>()
            .map(|arg| Box::new(String::from(arg.to_string()))),
        Kind::Bytes => arg.downcast::<CelBytes>().map(|arg| {
            Box::new(String::from(
                StdString::from_utf8_lossy(arg.inner()).as_ref(),
            ))
        }),
        #[cfg(feature = "chrono")]
        Kind::Timestamp => arg
            .downcast::<CelTimestamp>()
            .map(|ts| Box::new(String::from(ts.inner().to_rfc3339()))),
        #[cfg(feature = "chrono")]
        Kind::Duration => arg
            .downcast::<CelDuration>()
            .map(|arg| Box::new(String::from(crate::duration::format_duration(arg.inner())))),
        _ => Err(arg),
    };
    match ret {
        Ok(ret) => Ok(Cow::<dyn Val>::Owned(ret)),
        Err(arg) => Err(ExecutionError::FunctionError {
            function: "string".to_owned(),
            message: format!("cannot convert {arg:?} to string"),
        }),
    }
}

pub(crate) fn stdlib(env: &mut crate::Env) {
    env.add_overload(
        "string",
        "string_to_string",
        vec![super::STRING_TYPE],
        string,
    )
    .expect("Must be unique id");
    env.add_overload("string", "int64_to_string", vec![super::INT_TYPE], string)
        .expect("Must be unique id");
    env.add_overload("string", "uint64_to_string", vec![super::UINT_TYPE], string)
        .expect("Must be unique id");
    env.add_overload(
        "string",
        "double_to_string",
        vec![super::DOUBLE_TYPE],
        string,
    )
    .expect("Must be unique id");
    env.add_overload("string", "bytes_to_string", vec![super::BYTES_TYPE], string)
        .expect("Must be unique id");

    #[cfg(feature = "chrono")]
    {
        env.add_overload(
            "string",
            "timestamp_to_string",
            vec![super::TIMESTAMP_TYPE],
            string,
        )
        .expect("Must be unique id");
        env.add_overload(
            "string",
            "duration_to_string",
            vec![super::DURATION_TYPE],
            string,
        )
        .expect("Must be unique id");
    }

    env.add_member_overload(
        "contains",
        "contains_string",
        super::STRING_TYPE,
        vec![super::STRING_TYPE],
        string_contains,
    )
    .expect("Must be unique id");
    env.add_member_overload(
        "endsWith",
        "ends_with_string",
        super::STRING_TYPE,
        vec![super::STRING_TYPE],
        ends_with_string,
    )
    .expect("Must be unique id");
    env.add_overload(
        "size",
        "size_string",
        vec![super::STRING_TYPE],
        traits::adapter::sizer_size,
    )
    .expect("Must be unique id");
    env.add_member_overload(
        "size",
        "string_size",
        super::STRING_TYPE,
        vec![],
        traits::adapter::sizer_size,
    )
    .expect("Must be unique id");
    env.add_member_overload(
        "startsWith",
        "starts_with_string",
        super::STRING_TYPE,
        vec![super::STRING_TYPE],
        starts_with_string,
    )
    .expect("Must be unique id");
    #[cfg(feature = "regex")]
    env.add_member_overload(
        "matches",
        "matches",
        super::STRING_TYPE,
        vec![super::STRING_TYPE],
        matches,
    )
    .expect("Must be unique id");
}

#[cfg(test)]
mod tests {
    use super::StdString;
    use super::String;
    use crate::common::value::Val;

    #[test]
    fn test_try_into_string() {
        let str: Box<dyn Val> = Box::new(String::from("cel-rust"));
        assert_eq!(Ok(StdString::from("cel-rust")), str.try_into())
    }

    #[test]
    fn test_try_into_str() {
        let str: Box<dyn Val> = Box::new(String::from("cel-rust"));
        assert_eq!(Ok("cel-rust"), str.as_ref().try_into())
    }
}