Repository: clarkmcc/cel-rust (language: rust)

Issue:
Add `Context::new`
Currently `Context` must be default constructed but there's situations where one might want to disable `regex` or `chrono` dependencies, or switch them up with alternative implementations (e.g. oniguruma).

I agree having those methods is a good default (though I'd feature gate them to allow reducing dependencies), but adding a `Context::new` can't hurt and something like:

> Prefer using `Default` as it contains functions required for casting and good defaults, or at least copy the casting function from `Default`.

in `Context::new` documentation should be enough to point users in the right direction.

Or maybe include casting functions in `Context::new` and add `Context::empty` as well with a warning in the documentation.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index c206bc7..1d5c048 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -26,3 +26,5 @@ jobs:
         run: |
           cargo test --verbose
           cargo test --verbose --features json
+          cargo test --verbose --features regex
+          cargo test --verbose --features chrono
diff --git a/example/Cargo.toml b/example/Cargo.toml
index e87d76d..a0720ef 100644
--- a/example/Cargo.toml
+++ b/example/Cargo.toml
@@ -9,7 +9,7 @@ axum = { version = "0.7.5", default-features = false, features = [
     "json",
     "tokio",
 ] }
-cel-interpreter = { path = "../interpreter", features = ["json"] }
+cel-interpreter = { path = "../interpreter", features = ["json", "chrono", "regex"] }
 chrono = "0.4.26"
 serde = { version = "1.0.196", features = ["derive"] }
 serde_json = "1.0.124"
diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml
index df13fb6..ca587e8 100644
--- a/interpreter/Cargo.toml
+++ b/interpreter/Cargo.toml
@@ -11,11 +11,11 @@ categories = ["compilers"]
 [dependencies]
 cel-parser = { path = "../parser", version = "0.7.1 " }
 thiserror = "1.0.40"
-chrono = { version = "0.4.26", default-features = false, features = ["alloc"] }
+chrono = { version = "0.4.26", default-features = false, features = ["alloc"], optional = true }
 nom = "7.1.3"
 paste = "1.0.14"
 serde = "1.0.196"
-regex = "1.10.5"
+regex = { version = "1.10.5", optional = true }
 serde_json = { version = "1.0.124", optional = true }
 base64 = { version = "0.22.1", optional = true }
 
@@ -29,4 +29,5 @@ harness = false
 
 [features]
 json = ["dep:base64", "dep:serde_json"]
-
+regex = ["dep:regex"]
+chrono = ["dep:chrono"]
\ No newline at end of file
diff --git a/interpreter/src/context.rs b/interpreter/src/context.rs
index 5a48c47..3741939 100644
--- a/interpreter/src/context.rs
+++ b/interpreter/src/context.rs
@@ -138,6 +138,24 @@ impl<'a> Context<'a> {
             variables: Default::default(),
         }
     }
+
+    /// Constructs a new empty context with no variables or functions.
+    ///
+    /// If you're looking for a context that has all the standard methods, functions
+    /// and macros already added to the context, use [`Context::default`] instead.
+    ///
+    /// # Example
+    /// ```
+    /// use cel_interpreter::Context;
+    /// let mut context = Context::empty();
+    /// context.add_function("add", |a: i64, b: i64| a + b);
+    /// ```
+    pub fn empty() -> Self {
+        Context::Root {
+            variables: Default::default(),
+            functions: Default::default(),
+        }
+    }
 }
 
 impl<'a> Default for Context<'a> {
@@ -146,6 +164,7 @@ impl<'a> Default for Context<'a> {
             variables: Default::default(),
             functions: Default::default(),
         };
+
         ctx.add_function("contains", functions::contains);
         ctx.add_function("size", functions::size);
         ctx.add_function("has", functions::has);
@@ -155,9 +174,6 @@ impl<'a> Default for Context<'a> {
         ctx.add_function("max", functions::max);
         ctx.add_function("startsWith", functions::starts_with);
         ctx.add_function("endsWith", functions::ends_with);
-        ctx.add_function("matches", functions::matches);
-        ctx.add_function("duration", functions::duration);
-        ctx.add_function("timestamp", functions::timestamp);
         ctx.add_function("string", functions::string);
         ctx.add_function("bytes", functions::bytes);
         ctx.add_function("double", functions::double);
@@ -165,6 +181,14 @@ impl<'a> Default for Context<'a> {
         ctx.add_function("exists_one", functions::exists_one);
         ctx.add_function("int", functions::int);
         ctx.add_function("uint", functions::uint);
+
+        #[cfg(feature = "regex")]
+        ctx.add_function("matches", functions::matches);
+        #[cfg(feature = "chrono")]
+        ctx.add_function("duration", functions::duration);
+        #[cfg(feature = "chrono")]
+        ctx.add_function("timestamp", functions::timestamp);
+
         ctx
     }
 }
diff --git a/interpreter/src/functions.rs b/interpreter/src/functions.rs
index 69f48d2..5ac6591 100644
--- a/interpreter/src/functions.rs
+++ b/interpreter/src/functions.rs
@@ -1,12 +1,9 @@
 use crate::context::Context;
-use crate::duration::{format_duration, parse_duration};
 use crate::magic::{Arguments, Identifier, This};
 use crate::objects::{Value, ValueType};
 use crate::resolvers::{Argument, Resolver};
 use crate::ExecutionError;
 use cel_parser::Expression;
-use chrono::{DateTime, Duration, FixedOffset};
-use regex::Regex;
 use std::cmp::Ordering;
 use std::convert::TryInto;
 use std::sync::Arc;
@@ -152,8 +149,10 @@ pub fn contains(This(this): This<Value>, arg: Value) -> Result<Value> {
 pub fn string(ftx: &FunctionContext, This(this): This<Value>) -> Result<Value> {
     Ok(match this {
         Value::String(v) => Value::String(v.clone()),
+        #[cfg(feature = "chrono")]
         Value::Timestamp(t) => Value::String(t.to_rfc3339().into()),
-        Value::Duration(v) => Value::String(format_duration(&v).into()),
+        #[cfg(feature = "chrono")]
+        Value::Duration(v) => Value::String(crate::duration::format_duration(&v).into()),
         Value::Int(v) => Value::String(v.to_string().into()),
         Value::UInt(v) => Value::String(v.to_string().into()),
         Value::Float(v) => Value::String(v.to_string().into()),
@@ -247,12 +246,13 @@ pub fn ends_with(This(this): This<Arc<String>>, suffix: Arc<String>) -> bool {
 /// ```cel
 /// "abc".matches("^[a-z]*$") == true
 /// ```
+#[cfg(feature = "regex")]
 pub fn matches(
     ftx: &FunctionContext,
     This(this): This<Arc<String>>,
     regex: Arc<String>,
 ) -> Result<bool> {
-    match Regex::new(&regex) {
+    match regex::Regex::new(&regex) {
         Ok(re) => Ok(re.is_match(&this)),
         Err(err) => Err(ftx.error(format!("'{regex}' not a valid regex:\n{err}"))),
     }
@@ -499,19 +499,36 @@ pub fn exists_one(
 /// - `1.5ms` parses as 1 millisecond and 500 microseconds
 /// - `1ns` parses as 1 nanosecond
 /// - `1.5ns` parses as 1 nanosecond (sub-nanosecond durations not supported)
+#[cfg(feature = "chrono")]
 pub fn duration(value: Arc<String>) -> Result<Value> {
     Ok(Value::Duration(_duration(value.as_str())?))
 }
 
 /// Timestamp parses the provided argument into a [`Value::Timestamp`] value.
 /// The
+#[cfg(feature = "chrono")]
 pub fn timestamp(value: Arc<String>) -> Result<Value> {
     Ok(Value::Timestamp(
-        DateTime::parse_from_rfc3339(value.as_str())
+        chrono::DateTime::parse_from_rfc3339(value.as_str())
             .map_err(|e| ExecutionError::function_error("timestamp", e.to_string().as_str()))?,
     ))
 }
 
+/// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`].
+/// and only returns the duration, rather than returning the remaining input.
+#[cfg(feature = "chrono")]
+fn _duration(i: &str) -> Result<chrono::Duration> {
+    let (_, duration) = crate::duration::parse_duration(i)
+        .map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
+    Ok(duration)
+}
+
+#[cfg(feature = "chrono")]
+fn _timestamp(i: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
+    chrono::DateTime::parse_from_rfc3339(i)
+        .map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
+}
+
 pub fn max(Arguments(args): Arguments) -> Result<Value> {
     // If items is a list of values, then operate on the list
     let items = if args.len() == 1 {
@@ -536,25 +553,12 @@ pub fn max(Arguments(args): Arguments) -> Result<Value> {
         .cloned()
 }
 
-/// A wrapper around [`parse_duration`] that converts errors into [`ExecutionError`].
-/// and only returns the duration, rather than returning the remaining input.
-fn _duration(i: &str) -> Result<Duration> {
-    let (_, duration) =
-        parse_duration(i).map_err(|e| ExecutionError::function_error("duration", e.to_string()))?;
-    Ok(duration)
-}
-
-fn _timestamp(i: &str) -> Result<DateTime<FixedOffset>> {
-    DateTime::parse_from_rfc3339(i)
-        .map_err(|e| ExecutionError::function_error("timestamp", e.to_string()))
-}
-
 #[cfg(test)]
 mod tests {
     use crate::context::Context;
     use crate::testing::test_script;
-    use crate::{Program, Value};
-    use chrono::{DateTime, FixedOffset};
+    #[cfg(feature = "regex")]
+    use crate::ExecutionError::FunctionError;
     use std::collections::HashMap;
 
     fn assert_script(input: &(&str, &str)) {
@@ -661,27 +665,6 @@ mod tests {
         .for_each(assert_script);
     }
 
-    #[test]
-    fn test_duration() {
-        [
-            ("duration equal 1", "duration('1s') == duration('1000ms')"),
-            ("duration equal 2", "duration('1m') == duration('60s')"),
-            ("duration equal 3", "duration('1h') == duration('60m')"),
-            ("duration comparison 1", "duration('1m') > duration('1s')"),
-            ("duration comparison 2", "duration('1m') < duration('1h')"),
-            (
-                "duration subtraction",
-                "duration('1h') - duration('1m') == duration('59m')",
-            ),
-            (
-                "duration addition",
-                "duration('1h') + duration('1m') == duration('1h1m')",
-            ),
-        ]
-        .iter()
-        .for_each(assert_script);
-    }
-
     #[test]
     fn test_starts_with() {
         [
@@ -702,6 +685,7 @@ mod tests {
         .for_each(assert_script);
     }
 
+    #[cfg(feature = "chrono")]
     #[test]
     fn test_timestamp() {
         [(
@@ -732,26 +716,109 @@ mod tests {
         .for_each(assert_script);
     }
 
+    #[cfg(feature = "chrono")]
+    #[test]
+    fn test_duration() {
+        [
+            ("duration equal 1", "duration('1s') == duration('1000ms')"),
+            ("duration equal 2", "duration('1m') == duration('60s')"),
+            ("duration equal 3", "duration('1h') == duration('60m')"),
+            ("duration comparison 1", "duration('1m') > duration('1s')"),
+            ("duration comparison 2", "duration('1m') < duration('1h')"),
+            (
+                "duration subtraction",
+                "duration('1h') - duration('1m') == duration('59m')",
+            ),
+            (
+                "duration addition",
+                "duration('1h') + duration('1m') == duration('1h1m')",
+            ),
+        ]
+        .iter()
+        .for_each(assert_script);
+    }
+
+    #[cfg(feature = "chrono")]
     #[test]
     fn test_timestamp_variable() {
         let mut context = Context::default();
-        let ts: DateTime<FixedOffset> =
-            DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap();
-        context.add_variable("ts", Value::Timestamp(ts)).unwrap();
+        let ts: chrono::DateTime<chrono::FixedOffset> =
+            chrono::DateTime::parse_from_rfc3339("2023-05-29T00:00:00Z").unwrap();
+        context
+            .add_variable("ts", crate::Value::Timestamp(ts))
+            .unwrap();
 
-        let program = Program::compile("ts == timestamp('2023-05-29T00:00:00Z')").unwrap();
+        let program = crate::Program::compile("ts == timestamp('2023-05-29T00:00:00Z')").unwrap();
         let result = program.execute(&context).unwrap();
         assert_eq!(result, true.into());
     }
 
+    #[cfg(feature = "chrono")]
     #[test]
-    fn test_string() {
+    fn test_chrono_string() {
         [
             ("duration", "duration('1h30m').string() == '1h30m0s'"),
             (
                 "timestamp",
                 "timestamp('2023-05-29T00:00:00Z').string() == '2023-05-29T00:00:00+00:00'",
             ),
+        ]
+        .iter()
+        .for_each(assert_script);
+    }
+
+    #[test]
+    fn test_contains() {
+        let tests = vec![
+            ("list", "[1, 2, 3].contains(3) == true"),
+            ("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
+            ("string", "'foobar'.contains('bar') == true"),
+            ("bytes", "b'foobar'.contains(b'o') == true"),
+        ];
+
+        for (name, script) in tests {
+            assert_eq!(test_script(script, None), Ok(true.into()), "{}", name);
+        }
+    }
+
+    #[cfg(feature = "regex")]
+    #[test]
+    fn test_matches() {
+        let tests = vec![
+            ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"),
+            (
+                "map",
+                "{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false",
+            ),
+        ];
+
+        for (name, script) in tests {
+            assert_eq!(
+                test_script(script, None),
+                Ok(true.into()),
+                ".matches failed for '{name}'"
+            );
+        }
+    }
+
+    #[cfg(feature = "regex")]
+    #[test]
+    fn test_matches_err() {
+        assert_eq!(
+            test_script(
+                "'foobar'.matches('(foo') == true", None),
+            Err(
+                FunctionError {
+                    function: "matches".to_string(),
+                    message: "'(foo' not a valid regex:\nregex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
+                }
+            )
+        );
+    }
+
+    #[test]
+    fn test_string() {
+        [
             ("string", "'foo'.string() == 'foo'"),
             ("int", "10.string() == '10'"),
             ("float", "10.5.string() == '10.5'"),
diff --git a/interpreter/src/json.rs b/interpreter/src/json.rs
index f899a2f..ac06794 100644
--- a/interpreter/src/json.rs
+++ b/interpreter/src/json.rs
@@ -1,5 +1,6 @@
 use crate::Value;
 use base64::prelude::*;
+#[cfg(feature = "chrono")]
 use chrono::Duration;
 use thiserror::Error;
 
@@ -11,6 +12,7 @@ pub enum ConvertToJsonError<'a> {
     #[error("unable to convert value to json: {0:?}")]
     Value(&'a Value),
 
+    #[cfg(feature = "chrono")]
     /// The duration is too large to convert to nanoseconds. Any duration of 2^63
     /// nanoseconds or more will overflow. We'll return the duration type in the
     /// error message.
@@ -50,9 +52,11 @@ impl Value {
             Value::Float(f) => f.into(),
             Value::String(ref s) => s.to_string().into(),
             Value::Bool(b) => b.into(),
-            Value::Timestamp(ref dt) => dt.to_rfc3339().into(),
             Value::Bytes(ref b) => BASE64_STANDARD.encode(b.as_slice()).to_string().into(),
             Value::Null => serde_json::Value::Null,
+            #[cfg(feature = "chrono")]
+            Value::Timestamp(ref dt) => dt.to_rfc3339().into(),
+            #[cfg(feature = "chrono")]
             Value::Duration(ref v) => serde_json::Value::Number(serde_json::Number::from(
                 v.num_nanoseconds()
                     .ok_or(ConvertToJsonError::DurationOverflow(v))?,
@@ -66,22 +70,19 @@ impl Value {
 mod tests {
     use crate::objects::Map;
     use crate::Value as CelValue;
+    #[cfg(feature = "chrono")]
     use chrono::Duration;
     use serde_json::json;
     use std::collections::HashMap;
 
     #[test]
     fn test_cel_value_to_json() {
-        let tests = [
+        let mut tests = vec![
             (json!("hello"), CelValue::String("hello".to_string().into())),
             (json!(42), CelValue::Int(42)),
             (json!(42.0), CelValue::Float(42.0)),
             (json!(true), CelValue::Bool(true)),
             (json!(null), CelValue::Null),
-            (
-                json!(1_000_000_000),
-                CelValue::Duration(Duration::seconds(1)),
-            ),
             (
                 json!([true, null]),
                 CelValue::List(vec![CelValue::Bool(true), CelValue::Null].into()),
@@ -95,6 +96,14 @@ mod tests {
             ),
         ];
 
+        #[cfg(feature = "chrono")]
+        if true {
+            tests.push((
+                json!(1_000_000_000),
+                CelValue::Duration(Duration::seconds(1)),
+            ));
+        }
+
         for (expected, value) in tests.iter() {
             assert_eq!(
                 value.json().unwrap(),
diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs
index f4cc648..7a2067f 100644
--- a/interpreter/src/lib.rs
+++ b/interpreter/src/lib.rs
@@ -12,6 +12,7 @@ pub use cel_parser::Expression;
 pub use context::Context;
 pub use functions::FunctionContext;
 pub use objects::{ResolveResult, Value};
+#[cfg(feature = "chrono")]
 mod duration;
 pub mod functions;
 mod magic;
@@ -184,7 +185,6 @@ mod tests {
     use crate::context::Context;
     use crate::objects::{ResolveResult, Value};
     use crate::testing::test_script;
-    use crate::ExecutionError::FunctionError;
     use crate::{ExecutionError, Program};
     use std::collections::HashMap;
     use std::convert::TryInto;
@@ -230,53 +230,6 @@ mod tests {
         );
     }
 
-    #[test]
-    fn test_contains() {
-        let tests = vec![
-            ("list", "[1, 2, 3].contains(3) == true"),
-            ("map", "{1: true, 2: true, 3: true}.contains(3) == true"),
-            ("string", "'foobar'.contains('bar') == true"),
-            ("bytes", "b'foobar'.contains(b'o') == true"),
-        ];
-
-        for (name, script) in tests {
-            assert_eq!(test_script(script, None), Ok(true.into()), "{}", name);
-        }
-    }
-
-    #[test]
-    fn test_matches() {
-        let tests = vec![
-            ("string", "'foobar'.matches('^[a-zA-Z]*$') == true"),
-            (
-                "map",
-                "{'1': 'abc', '2': 'def', '3': 'ghi'}.all(key, key.matches('^[a-zA-Z]*$')) == false",
-            ),
-        ];
-
-        for (name, script) in tests {
-            assert_eq!(
-                test_script(script, None),
-                Ok(true.into()),
-                ".matches failed for '{name}'"
-            );
-        }
-    }
-
-    #[test]
-    fn test_matches_err() {
-        assert_eq!(
-            test_script(
-                "'foobar'.matches('(foo') == true", None),
-            Err(
-                FunctionError {
-                    function: "matches".to_string(),
-                    message: "'(foo' not a valid regex:\nregex parse error:\n    (foo\n    ^\nerror: unclosed group".to_string()
-                }
-            )
-        );
-    }
-
     #[test]
     fn test_execution_errors() {
         let tests = vec![
diff --git a/interpreter/src/magic.rs b/interpreter/src/magic.rs
index 605876a..eaed8be 100644
--- a/interpreter/src/magic.rs
+++ b/interpreter/src/magic.rs
@@ -2,7 +2,6 @@ use crate::macros::{impl_conversions, impl_handler};
 use crate::resolvers::{AllArguments, Argument};
 use crate::{ExecutionError, FunctionContext, ResolveResult, Value};
 use cel_parser::Expression;
-use chrono::{DateTime, Duration, FixedOffset};
 use std::collections::HashMap;
 use std::marker::PhantomData;
 use std::sync::Arc;
@@ -14,11 +13,15 @@ impl_conversions!(
     Arc<String> => Value::String,
     Arc<Vec<u8>> => Value::Bytes,
     bool => Value::Bool,
-    Duration => Value::Duration,
-    DateTime<FixedOffset> => Value::Timestamp,
     Arc<Vec<Value>> => Value::List
 );
 
+#[cfg(feature = "chrono")]
+impl_conversions!(
+    chrono::Duration => Value::Duration,
+    chrono::DateTime<chrono::FixedOffset> => Value::Timestamp,
+);
+
 impl From<i32> for Value {
     fn from(value: i32) -> Self {
         Value::Int(value as i64)
diff --git a/interpreter/src/objects.rs b/interpreter/src/objects.rs
index 4b81a08..ff0f5af 100644
--- a/interpreter/src/objects.rs
+++ b/interpreter/src/objects.rs
@@ -4,7 +4,6 @@ use crate::ser::SerializationError;
 use crate::ExecutionError::NoSuchKey;
 use crate::{to_value, ExecutionError};
 use cel_parser::{ArithmeticOp, Atom, Expression, Member, RelationOp, UnaryOp};
-use chrono::{DateTime, Duration, FixedOffset};
 use core::ops;
 use serde::{Serialize, Serializer};
 use std::cmp::Ordering;
@@ -174,8 +173,10 @@ pub enum Value {
     String(Arc<String>),
     Bytes(Arc<Vec<u8>>),
     Bool(bool),
-    Duration(Duration),
-    Timestamp(DateTime<FixedOffset>),
+    #[cfg(feature = "chrono")]
+    Duration(chrono::Duration),
+    #[cfg(feature = "chrono")]
+    Timestamp(chrono::DateTime<chrono::FixedOffset>),
     Null,
 }
 
@@ -225,7 +226,9 @@ impl Value {
             Value::String(_) => ValueType::String,
             Value::Bytes(_) => ValueType::Bytes,
             Value::Bool(_) => ValueType::Bool,
+            #[cfg(feature = "chrono")]
             Value::Duration(_) => ValueType::Duration,
+            #[cfg(feature = "chrono")]
             Value::Timestamp(_) => ValueType::Timestamp,
             Value::Null => ValueType::Null,
         }
@@ -258,7 +261,9 @@ impl PartialEq for Value {
             (Value::Bytes(a), Value::Bytes(b)) => a == b,
             (Value::Bool(a), Value::Bool(b)) => a == b,
             (Value::Null, Value::Null) => true,
+            #[cfg(feature = "chrono")]
             (Value::Duration(a), Value::Duration(b)) => a == b,
+            #[cfg(feature = "chrono")]
             (Value::Timestamp(a), Value::Timestamp(b)) => a == b,
             // Allow different numeric types to be compared without explicit casting.
             (Value::Int(a), Value::UInt(b)) => a
@@ -291,7 +296,9 @@ impl PartialOrd for Value {
             (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
             (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
             (Value::Null, Value::Null) => Some(Ordering::Equal),
+            #[cfg(feature = "chrono")]
             (Value::Duration(a), Value::Duration(b)) => Some(a.cmp(b)),
+            #[cfg(feature = "chrono")]
             (Value::Timestamp(a), Value::Timestamp(b)) => Some(a.cmp(b)),
             // Allow different numeric types to be compared without explicit casting.
             (Value::Int(a), Value::UInt(b)) => Some(
@@ -644,7 +651,9 @@ impl<'a> Value {
             Value::Bytes(v) => !v.is_empty(),
             Value::Bool(v) => *v,
             Value::Null => false,
+            #[cfg(feature = "chrono")]
             Value::Duration(v) => v.num_nanoseconds().map(|n| n != 0).unwrap_or(false),
+            #[cfg(feature = "chrono")]
             Value::Timestamp(v) => v.timestamp_nanos_opt().unwrap_or_default() > 0,
             Value::Function(_, _) => false,
         }
@@ -702,8 +711,11 @@ impl ops::Add<Value> for Value {
                 }
                 Value::Map(Map { map: Arc::new(new) }).into()
             }
+            #[cfg(feature = "chrono")]
             (Value::Duration(l), Value::Duration(r)) => Value::Duration(l + r).into(),
+            #[cfg(feature = "chrono")]
             (Value::Timestamp(l), Value::Duration(r)) => Value::Timestamp(l + r).into(),
+            #[cfg(feature = "chrono")]
             (Value::Duration(l), Value::Timestamp(r)) => Value::Timestamp(r + l).into(),
             (left, right) => Err(ExecutionError::UnsupportedBinaryOperator(
                 "add", left, right,
@@ -728,8 +740,11 @@ impl ops::Sub<Value> for Value {
             (Value::UInt(l), Value::Float(r)) => Value::Float(l as f64 - r).into(),
             (Value::Float(l), Value::UInt(r)) => Value::Float(l - r as f64).into(),
             // todo: implement checked sub for these over-flowable operations
+            #[cfg(feature = "chrono")]
             (Value::Duration(l), Value::Duration(r)) => Value::Duration(l - r).into(),
+            #[cfg(feature = "chrono")]
             (Value::Timestamp(l), Value::Duration(r)) => Value::Timestamp(l - r).into(),
+            #[cfg(feature = "chrono")]
             (Value::Timestamp(l), Value::Timestamp(r)) => Value::Duration(l - r).into(),
             (left, right) => Err(ExecutionError::UnsupportedBinaryOperator(
                 "sub", left, right,


Test patch: