diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 964ca0a58..3bdc7c894 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -47,7 +47,6 @@ import dev.cel.compiler.CelCompilerBuilder; import dev.cel.compiler.CelCompilerLibrary; import dev.cel.extensions.CelExtensions; -import dev.cel.extensions.CelOptionalLibrary; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelRuntimeBuilder; import dev.cel.runtime.CelRuntimeLibrary; @@ -694,8 +693,8 @@ enum CanonicalCelExtension { (options, version) -> CelExtensions.math(options, version), (options, version) -> CelExtensions.math(options, version)), OPTIONAL( - (options, version) -> CelOptionalLibrary.INSTANCE, - (options, version) -> CelOptionalLibrary.INSTANCE), + (options, version) -> CelExtensions.optional(version), + (options, version) -> CelExtensions.optional(version)), STRINGS( (options, version) -> CelExtensions.strings(), (options, version) -> CelExtensions.strings()), diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel index 3b8b84bb9..1be39fef2 100644 --- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel +++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel @@ -33,6 +33,7 @@ java_library( ":encoders", ":lists", ":math", + ":optional_library", ":protos", ":regex", ":sets", @@ -177,6 +178,7 @@ java_library( "//common/ast", "//common/types", "//compiler:compiler_builder", + "//extensions:extension_library", "//parser:macro", "//parser:operator", "//parser:parser_builder", diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java index cd97566f3..b7cb94297 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensionLibrary.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableSet; import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelVarDecl; import dev.cel.parser.CelMacro; import java.util.Comparator; @@ -66,6 +67,9 @@ default ImmutableSet macros() { return ImmutableSet.of(); } - // TODO - Add a method for variables. + /** Returns the set of variables defined by this extension library. */ + default ImmutableSet variables() { + return ImmutableSet.of(); + } } } diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java index 83bf3fa53..a8dc56ce4 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java +++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java @@ -36,6 +36,24 @@ public final class CelExtensions { private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions(); private static final CelRegexExtensions REGEX_EXTENSIONS = new CelRegexExtensions(); + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions. + */ + public static CelOptionalLibrary optional() { + return CelOptionalLibrary.library().latest(); + } + + /** + * Implementation of optional values. + * + *

Refer to README.md for available functions for each supported version. + */ + public static CelOptionalLibrary optional(int version) { + return CelOptionalLibrary.library().version(version); + } + /** * Extended functions for string manipulation. * @@ -311,6 +329,8 @@ public static CelExtensionLibrary getE return CelListsExtensions.library(); case "math": return CelMathExtensions.library(options); + case "optional": + return CelOptionalLibrary.library(); case "protos": return CelProtoExtensions.library(); case "regex": diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java index 7a99360cc..a1e850a55 100644 --- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java +++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java @@ -19,6 +19,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.ByteString; @@ -54,8 +56,8 @@ import java.util.Optional; /** Internal implementation of CEL optional values. */ -public final class CelOptionalLibrary implements CelCompilerLibrary, CelInternalRuntimeLibrary { - public static final CelOptionalLibrary INSTANCE = new CelOptionalLibrary(); +public final class CelOptionalLibrary + implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet { /** Enumerations of function names used for supporting optionals. */ public enum Function { @@ -66,7 +68,10 @@ public enum Function { OPTIONAL_UNWRAP("optional.unwrap"), OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"), OR("or"), - OR_VALUE("orValue"); + OR_VALUE("orValue"), + FIRST("first"), + LAST("last"); + private final String functionName; public String getFunction() { @@ -78,8 +83,188 @@ public String getFunction() { } } + private static final CelExtensionLibrary LIBRARY = + new CelExtensionLibrary() { + final TypeParamType paramTypeK = TypeParamType.create("K"); + final TypeParamType paramTypeV = TypeParamType.create("V"); + final OptionalType optionalTypeV = OptionalType.create(paramTypeV); + final ListType listTypeV = ListType.create(paramTypeV); + final MapType mapTypeKv = MapType.create(paramTypeK, paramTypeV); + + private final CelOptionalLibrary version0 = + new CelOptionalLibrary( + 0, + ImmutableSet.of( + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_OF.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_of", optionalTypeV, paramTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_ofNonZeroValue", optionalTypeV, paramTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_NONE.getFunction(), + CelOverloadDecl.newGlobalOverload("optional_none", optionalTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.VALUE.getFunction(), + CelOverloadDecl.newMemberOverload( + "optional_value", paramTypeV, optionalTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.HAS_VALUE.getFunction(), + CelOverloadDecl.newMemberOverload( + "optional_hasValue", SimpleType.BOOL, optionalTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.OPTIONAL_UNWRAP.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_unwrap_list", listTypeV, ListType.create(optionalTypeV))), + // Note: Implementation of "or" and "orValue" are special-cased inside the + // interpreter. Hence, their bindings are not provided here. + CelFunctionDecl.newFunctionDeclaration( + "or", + CelOverloadDecl.newMemberOverload( + "optional_or_optional", optionalTypeV, optionalTypeV, optionalTypeV)), + CelFunctionDecl.newFunctionDeclaration( + "orValue", + CelOverloadDecl.newMemberOverload( + "optional_orValue_value", paramTypeV, optionalTypeV, paramTypeV)), + // Note: Function bindings for optional field selection and indexer is defined + // in {@code StandardFunctions}. + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_SELECT.getFunction(), + CelOverloadDecl.newGlobalOverload( + "select_optional_field", + optionalTypeV, + SimpleType.DYN, + SimpleType.STRING)), + CelFunctionDecl.newFunctionDeclaration( + Operator.OPTIONAL_INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "list_optindex_optional_int", optionalTypeV, listTypeV, SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_list_optindex_optional_int", + optionalTypeV, + OptionalType.create(listTypeV), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "map_optindex_optional_value", optionalTypeV, mapTypeKv, paramTypeK), + CelOverloadDecl.newGlobalOverload( + "optional_map_optindex_optional_value", + optionalTypeV, + OptionalType.create(mapTypeKv), + paramTypeK)), + // Index overloads to accommodate using an optional value as the operand + CelFunctionDecl.newFunctionDeclaration( + Operator.INDEX.getFunction(), + CelOverloadDecl.newGlobalOverload( + "optional_list_index_int", + optionalTypeV, + OptionalType.create(listTypeV), + SimpleType.INT), + CelOverloadDecl.newGlobalOverload( + "optional_map_index_value", + optionalTypeV, + OptionalType.create(mapTypeKv), + paramTypeK))), + ImmutableSet.of( + CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap)), + ImmutableSet.of( + // Type declaration for optional_type -> type(optional_type(V)) + CelVarDecl.newVarDeclaration( + OptionalType.NAME, TypeType.create(optionalTypeV)))); + + private final CelOptionalLibrary version1 = + new CelOptionalLibrary( + 1, + version0.functions, + ImmutableSet.builder() + .addAll(version0.macros) + .add( + CelMacro.newReceiverMacro( + "optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap)) + .build(), + version0.variables); + + private final CelOptionalLibrary version2 = + new CelOptionalLibrary( + 2, + ImmutableSet.builder() + .addAll(version1.functions) + .add( + CelFunctionDecl.newFunctionDeclaration( + Function.FIRST.functionName, + CelOverloadDecl.newMemberOverload( + "optional_list_first", + "Return the first value in a list if present, otherwise" + + " optional.none()", + optionalTypeV, + listTypeV)), + CelFunctionDecl.newFunctionDeclaration( + Function.LAST.functionName, + CelOverloadDecl.newMemberOverload( + "optional_list_last", + "Return the last value in a list if present, otherwise" + + " optional.none()", + optionalTypeV, + listTypeV))) + .build(), + version1.macros, + version1.variables); + + @Override + public String name() { + return "optional"; + } + + @Override + public ImmutableSet versions() { + return ImmutableSet.of(version0, version1, version2); + } + }; + + static CelExtensionLibrary library() { + return LIBRARY; + } + + // TODO migrate from this constant to the CelExtensions.optional() + public static final CelOptionalLibrary INSTANCE = CelOptionalLibrary.library().latest(); + private static final String UNUSED_ITER_VAR = "#unused"; + private final int version; + private final ImmutableSet functions; + private final ImmutableSet macros; + private final ImmutableSet variables; + + CelOptionalLibrary( + int version, ImmutableSet functions, ImmutableSet macros, + ImmutableSet variables) { + this.version = version; + this.functions = functions; + this.macros = macros; + this.variables = variables; + } + + @Override + public int version() { + return version; + } + + @Override + public ImmutableSet functions() { + return functions; + } + + @Override + public ImmutableSet variables() { + return variables; + } + + @Override + public ImmutableSet macros() { + return macros; + } + @Override public void setParserOptions(CelParserBuilder parserBuilder) { if (!parserBuilder.getOptions().enableOptionalSyntax()) { @@ -88,90 +273,13 @@ public void setParserOptions(CelParserBuilder parserBuilder) { parserBuilder.setOptions( parserBuilder.getOptions().toBuilder().enableOptionalSyntax(true).build()); } - parserBuilder.addMacros( - CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap)); - parserBuilder.addMacros( - CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap)); + parserBuilder.addMacros(macros()); } @Override public void setCheckerOptions(CelCheckerBuilder checkerBuilder) { - TypeParamType paramTypeK = TypeParamType.create("K"); - TypeParamType paramTypeV = TypeParamType.create("V"); - OptionalType optionalTypeV = OptionalType.create(paramTypeV); - ListType listTypeV = ListType.create(paramTypeV); - MapType mapTypeKv = MapType.create(paramTypeK, paramTypeV); - - // Type declaration for optional_type -> type(optional_type(V)) - checkerBuilder.addVarDeclarations( - CelVarDecl.newVarDeclaration(OptionalType.NAME, TypeType.create(optionalTypeV))); - - checkerBuilder.addFunctionDeclarations( - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_of", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_OF_NON_ZERO_VALUE.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_ofNonZeroValue", optionalTypeV, paramTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_NONE.getFunction(), - CelOverloadDecl.newGlobalOverload("optional_none", optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.VALUE.getFunction(), - CelOverloadDecl.newMemberOverload("optional_value", paramTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.HAS_VALUE.getFunction(), - CelOverloadDecl.newMemberOverload("optional_hasValue", SimpleType.BOOL, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - Function.OPTIONAL_UNWRAP.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_unwrap_list", listTypeV, ListType.create(optionalTypeV))), - // Note: Implementation of "or" and "orValue" are special-cased inside the interpreter. - // Hence, their bindings are not provided here. - CelFunctionDecl.newFunctionDeclaration( - "or", - CelOverloadDecl.newMemberOverload( - "optional_or_optional", optionalTypeV, optionalTypeV, optionalTypeV)), - CelFunctionDecl.newFunctionDeclaration( - "orValue", - CelOverloadDecl.newMemberOverload( - "optional_orValue_value", paramTypeV, optionalTypeV, paramTypeV)), - // Note: Function bindings for optional field selection and indexer is defined in - // {@code StandardFunctions}. - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_SELECT.getFunction(), - CelOverloadDecl.newGlobalOverload( - "select_optional_field", optionalTypeV, SimpleType.DYN, SimpleType.STRING)), - CelFunctionDecl.newFunctionDeclaration( - Operator.OPTIONAL_INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "list_optindex_optional_int", optionalTypeV, listTypeV, SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_list_optindex_optional_int", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "map_optindex_optional_value", optionalTypeV, mapTypeKv, paramTypeK), - CelOverloadDecl.newGlobalOverload( - "optional_map_optindex_optional_value", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK)), - // Index overloads to accommodate using an optional value as the operand - CelFunctionDecl.newFunctionDeclaration( - Operator.INDEX.getFunction(), - CelOverloadDecl.newGlobalOverload( - "optional_list_index_int", - optionalTypeV, - OptionalType.create(listTypeV), - SimpleType.INT), - CelOverloadDecl.newGlobalOverload( - "optional_map_index_value", - optionalTypeV, - OptionalType.create(mapTypeKv), - paramTypeK))); + checkerBuilder.addVarDeclarations(variables()); + checkerBuilder.addFunctionDeclarations(functions()); } @Override @@ -241,6 +349,14 @@ public void setRuntimeOptions( Optional.class, Long.class, CelOptionalLibrary::indexOptionalList)); + + if (version >= 2) { + runtimeBuilder.addFunctionBindings( + CelFunctionBinding.from( + "optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst), + CelFunctionBinding.from( + "optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast)); + } } private static ImmutableList elideOptionalCollection(Collection> list) { @@ -372,5 +488,18 @@ private static Object indexOptionalList(Optional optionalList, long index) { return Optional.of(list.get(castIndex)); } - private CelOptionalLibrary() {} + @SuppressWarnings("rawtypes") + private static Object listOptionalFirst(Collection list) { + if (list.isEmpty()) { + return Optional.empty(); + } + if (list instanceof List) { + return Optional.ofNullable(((List) list).get(0)); + } + return Optional.ofNullable(Iterables.getFirst(list, null)); + } + + private static Object listOptionalLast(Collection list) { + return Optional.ofNullable(Iterables.getLast(list, null)); + } } diff --git a/extensions/src/main/java/dev/cel/extensions/README.md b/extensions/src/main/java/dev/cel/extensions/README.md index d204c5cf7..4bb2f68d8 100644 --- a/extensions/src/main/java/dev/cel/extensions/README.md +++ b/extensions/src/main/java/dev/cel/extensions/README.md @@ -801,6 +801,36 @@ Examples: ].sortBy(e, e.score).map(e, e.name) == ["bar", "foo", "baz"] +### Last + +Introduced in the 'optional' extension version 2 + +Returns an optional with the last value from the list or `optional.None` if the +list is empty. + + .last() -> + +Examples: + + [1, 2, 3].last().value() == 3 + [].last().orValue('test') == 'test' + +This is syntactic sugar for list[list.size()-1]. + +### First + +Introduced in the 'optional' extension version 2 + +Returns an optional with the first value from the list or `optional.None` if the +list is empty. + + .first() -> + +Examples: + + [1, 2, 3].first().value() == 1 + [].first().orValue('test') == 'test' + ## Regex Regex introduces support for regular expressions in CEL. @@ -881,4 +911,4 @@ regex.extractAll('id:123, id:456', 'id:\\d+') == ['id:123', 'id:456'] regex.extractAll('id:123, id:456', 'assa') == [] regex.extractAll('testuser@testdomain', '(.*)@([^.]*)') \\ Runtime Error multiple capture group -``` \ No newline at end of file +``` diff --git a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java index fb9bdec36..1e6bbded6 100644 --- a/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java +++ b/extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java @@ -35,6 +35,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelValidationException; +import dev.cel.common.CelVarDecl; import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelType; import dev.cel.common.types.ListType; @@ -45,6 +46,7 @@ import dev.cel.common.types.TypeType; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; +import dev.cel.parser.CelMacro; import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; @@ -57,7 +59,7 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -@SuppressWarnings("unchecked") +@SuppressWarnings({"unchecked", "SingleTestParameter"}) public class CelOptionalLibraryTest { @SuppressWarnings("ImmutableEnumChecker") // Test only @@ -94,13 +96,84 @@ private enum ConstantTestCases { } private static CelBuilder newCelBuilder() { + return newCelBuilder(Integer.MAX_VALUE); + } + + private static CelBuilder newCelBuilder(int version) { return CelFactory.standardCelBuilder() .setOptions(CelOptions.current().enableTimestampEpoch(true).build()) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .setContainer(CelContainer.ofName("cel.expr.conformance.proto3")) .addMessageTypes(TestAllTypes.getDescriptor()) - .addRuntimeLibraries(CelOptionalLibrary.INSTANCE) - .addCompilerLibraries(CelOptionalLibrary.INSTANCE); + .addRuntimeLibraries(CelExtensions.optional(version)) + .addCompilerLibraries(CelExtensions.optional(version)); + } + + @Test + public void library() { + CelExtensionLibrary library = + CelExtensions.getExtensionLibrary("optional", CelOptions.DEFAULT); + assertThat(library.name()).isEqualTo("optional"); + assertThat(library.latest().version()).isEqualTo(2); + + // Version 0 + assertThat(library.version(0).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]"); + assertThat(library.version(0).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap"); + assertThat(library.version(0).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); + + // Version 1 + assertThat(library.version(1).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]"); + assertThat(library.version(1).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap", "optFlatMap"); + assertThat(library.version(1).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); + + // Version 2 + assertThat(library.version(2).functions().stream().map(CelFunctionDecl::name)) + .containsExactly( + "optional.of", + "optional.ofNonZeroValue", + "optional.none", + "value", + "hasValue", + "optional.unwrap", + "or", + "orValue", + "_?._", + "_[?_]", + "_[_]", + "first", + "last"); + assertThat(library.version(2).macros().stream().map(CelMacro::getFunction)) + .containsExactly("optMap", "optFlatMap"); + assertThat(library.version(2).variables().stream().map(CelVarDecl::name)) + .containsExactly("optional_type"); } @Test @@ -1497,4 +1570,40 @@ public void optionalType_typeComparison() throws Exception { assertThat(cel.createProgram(ast).eval()).isEqualTo(true); } + + @Test + @TestParameters("{expression: '[].first().hasValue() == false'}") + @TestParameters("{expression: '[\"a\",\"b\",\"c\"].first().value() == \"a\"'}") + public void listFirst_success(String expression) throws Exception { + Cel cel = newCelBuilder().build(); + boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval(); + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[].last().hasValue() == false'}") + @TestParameters("{expression: '[1, 2, 3].last().value() == 3'}") + public void listLast_success(String expression) throws Exception { + Cel cel = newCelBuilder().build(); + boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval(); + assertThat(result).isTrue(); + } + + @Test + @TestParameters("{expression: '[1].first()', expectedError: 'undeclared reference to ''first'''}") + @TestParameters("{expression: '[2].last()', expectedError: 'undeclared reference to ''last'''}") + public void listFirstAndLast_throws_earlyVersion(String expression, String expectedError) + throws Exception { + // Configure Cel with an earlier version of the 'optional' library, which did not support + // 'first' and 'last' + Cel cel = newCelBuilder(1).build(); + assertThat( + assertThrows( + CelValidationException.class, + () -> { + cel.createProgram(cel.compile(expression).getAst()).eval(); + })) + .hasMessageThat() + .contains(expectedError); + } }