Skip to content

Commit 75596d2

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
Add versions to the 'optional' library to gradually expose new functions.
Add functions `list.first()` and `list.last()`, both returning `optional` PiperOrigin-RevId: 782140219
1 parent 57f8218 commit 75596d2

6 files changed

Lines changed: 153 additions & 10 deletions

File tree

bundle/src/main/java/dev/cel/bundle/CelEnvironment.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import dev.cel.compiler.CelCompilerBuilder;
4747
import dev.cel.compiler.CelCompilerLibrary;
4848
import dev.cel.extensions.CelExtensions;
49-
import dev.cel.extensions.CelOptionalLibrary;
5049
import dev.cel.parser.CelStandardMacro;
5150
import dev.cel.runtime.CelRuntimeBuilder;
5251
import dev.cel.runtime.CelRuntimeLibrary;
@@ -684,8 +683,8 @@ enum CanonicalCelExtension {
684683
(options, version) -> CelExtensions.math(options, version),
685684
(options, version) -> CelExtensions.math(options, version)),
686685
OPTIONAL(
687-
(options, version) -> CelOptionalLibrary.INSTANCE,
688-
(options, version) -> CelOptionalLibrary.INSTANCE),
686+
(options, version) -> CelExtensions.optional(version),
687+
(options, version) -> CelExtensions.optional(version)),
689688
STRINGS(
690689
(options, version) -> CelExtensions.strings(),
691690
(options, version) -> CelExtensions.strings()),

extensions/src/main/java/dev/cel/extensions/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ java_library(
2727
":sets_function",
2828
":strings",
2929
"//common:options",
30+
"//extensions:optional_library",
3031
"@maven//:com_google_guava_guava",
3132
],
3233
)

extensions/src/main/java/dev/cel/extensions/CelExtensions.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ public final class CelExtensions {
3838
private static final CelListsExtensions LISTS_EXTENSIONS_ALL = new CelListsExtensions();
3939
private static final CelRegexExtensions REGEX_EXTENSIONS = new CelRegexExtensions();
4040

41+
/**
42+
* Implementation of optional values.
43+
*
44+
* <p>Refer to README.md for available functions.
45+
*/
46+
public static CelOptionalLibrary optional() {
47+
return CelOptionalLibrary.INSTANCE;
48+
}
49+
50+
/**
51+
* Implementation of optional values.
52+
*
53+
* <p>Refer to README.md for available functions for each supported version.
54+
*/
55+
public static CelOptionalLibrary optional(int version) {
56+
return new CelOptionalLibrary(version);
57+
}
58+
4159
/**
4260
* Extended functions for string manipulation.
4361
*

extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.collect.ImmutableList.toImmutableList;
2020

2121
import com.google.common.collect.ImmutableList;
22+
import com.google.common.collect.Iterables;
2223
import com.google.common.primitives.Ints;
2324
import com.google.common.primitives.UnsignedLong;
2425
import com.google.protobuf.ByteString;
@@ -55,6 +56,7 @@
5556

5657
/** Internal implementation of CEL optional values. */
5758
public final class CelOptionalLibrary implements CelCompilerLibrary, CelInternalRuntimeLibrary {
59+
// TODO migrate from this constant to the CelExtensions.optional()
5860
public static final CelOptionalLibrary INSTANCE = new CelOptionalLibrary();
5961

6062
/** Enumerations of function names used for supporting optionals. */
@@ -66,7 +68,10 @@ public enum Function {
6668
OPTIONAL_UNWRAP("optional.unwrap"),
6769
OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"),
6870
OR("or"),
69-
OR_VALUE("orValue");
71+
OR_VALUE("orValue"),
72+
FIRST("first"),
73+
LAST("last");
74+
7075
private final String functionName;
7176

7277
public String getFunction() {
@@ -80,6 +85,16 @@ public String getFunction() {
8085

8186
private static final String UNUSED_ITER_VAR = "#unused";
8287

88+
private final int version;
89+
90+
CelOptionalLibrary() {
91+
this(Integer.MAX_VALUE);
92+
}
93+
94+
CelOptionalLibrary(int version) {
95+
this.version = version;
96+
}
97+
8398
@Override
8499
public void setParserOptions(CelParserBuilder parserBuilder) {
85100
if (!parserBuilder.getOptions().enableOptionalSyntax()) {
@@ -90,8 +105,10 @@ public void setParserOptions(CelParserBuilder parserBuilder) {
90105
}
91106
parserBuilder.addMacros(
92107
CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap));
93-
parserBuilder.addMacros(
94-
CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap));
108+
if (version >= 1) {
109+
parserBuilder.addMacros(
110+
CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap));
111+
}
95112
}
96113

97114
@Override
@@ -172,6 +189,23 @@ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
172189
optionalTypeV,
173190
OptionalType.create(mapTypeKv),
174191
paramTypeK)));
192+
if (version >= 2) {
193+
checkerBuilder.addFunctionDeclarations(
194+
CelFunctionDecl.newFunctionDeclaration(
195+
Function.FIRST.functionName,
196+
CelOverloadDecl.newMemberOverload(
197+
"optional_list_first",
198+
"Return the first value in a list if present, otherwise optional.none()",
199+
optionalTypeV,
200+
listTypeV)),
201+
CelFunctionDecl.newFunctionDeclaration(
202+
Function.LAST.functionName,
203+
CelOverloadDecl.newMemberOverload(
204+
"optional_list_last",
205+
"Return the last value in a list if present, otherwise optional.none()",
206+
optionalTypeV,
207+
listTypeV)));
208+
}
175209
}
176210

177211
@Override
@@ -241,6 +275,14 @@ public void setRuntimeOptions(
241275
Optional.class,
242276
Long.class,
243277
CelOptionalLibrary::indexOptionalList));
278+
279+
if (version >= 2) {
280+
runtimeBuilder.addFunctionBindings(
281+
CelFunctionBinding.from(
282+
"optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst),
283+
CelFunctionBinding.from(
284+
"optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast));
285+
}
244286
}
245287

246288
private static ImmutableList<Object> elideOptionalCollection(Collection<Optional<Object>> list) {
@@ -372,5 +414,18 @@ private static Object indexOptionalList(Optional<?> optionalList, long index) {
372414
return Optional.of(list.get(castIndex));
373415
}
374416

375-
private CelOptionalLibrary() {}
417+
@SuppressWarnings("rawtypes")
418+
private static Object listOptionalFirst(Collection<Object> list) {
419+
if (list.isEmpty()) {
420+
return Optional.empty();
421+
}
422+
if (list instanceof List) {
423+
return Optional.ofNullable(((List) list).get(0));
424+
}
425+
return Optional.ofNullable(Iterables.getFirst(list, null));
426+
}
427+
428+
private static Object listOptionalLast(Collection<Object> list) {
429+
return Optional.ofNullable(Iterables.getLast(list, null));
430+
}
376431
}

extensions/src/main/java/dev/cel/extensions/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,36 @@ lists.range(5) -> [0, 1, 2, 3, 4]
722722
lists.range(0) -> []
723723
```
724724

725+
### Last
726+
727+
Introduced in the 'optional' extension version 2
728+
729+
Returns an optional with the last value from the list or `optional.None` if the
730+
list is empty.
731+
732+
<list(T)>.last() -> <Optional(T)>
733+
734+
Examples:
735+
736+
[1, 2, 3].last().value() == 3
737+
[].last().orValue('test') == 'test'
738+
739+
This is syntactic sugar for list[list.size()-1].
740+
741+
### First
742+
743+
Introduced in the 'optional' extension version 2
744+
745+
Returns an optional with the first value from the list or `optional.None` if the
746+
list is empty.
747+
748+
<list(T)>.first() -> <Optional(T)>
749+
750+
Examples:
751+
752+
[1, 2, 3].first().value() == 1
753+
[].first().orValue('test') == 'test'
754+
725755
## Regex
726756

727757
Regex introduces support for regular expressions in CEL.

extensions/src/test/java/dev/cel/extensions/CelOptionalLibraryTest.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
import org.junit.runner.RunWith;
5757

5858
@RunWith(TestParameterInjector.class)
59-
@SuppressWarnings("unchecked")
59+
@SuppressWarnings({"unchecked", "SingleTestParameter"})
6060
public class CelOptionalLibraryTest {
6161

6262
@SuppressWarnings("ImmutableEnumChecker") // Test only
@@ -93,13 +93,17 @@ private enum ConstantTestCases {
9393
}
9494

9595
private static CelBuilder newCelBuilder() {
96+
return newCelBuilder(Integer.MAX_VALUE);
97+
}
98+
99+
private static CelBuilder newCelBuilder(int version) {
96100
return CelFactory.standardCelBuilder()
97101
.setOptions(CelOptions.current().enableTimestampEpoch(true).build())
98102
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
99103
.setContainer("cel.expr.conformance.proto3")
100104
.addMessageTypes(TestAllTypes.getDescriptor())
101-
.addRuntimeLibraries(CelOptionalLibrary.INSTANCE)
102-
.addCompilerLibraries(CelOptionalLibrary.INSTANCE);
105+
.addRuntimeLibraries(CelExtensions.optional(version))
106+
.addCompilerLibraries(CelExtensions.optional(version));
103107
}
104108

105109
@Test
@@ -1496,4 +1500,40 @@ public void optionalType_typeComparison() throws Exception {
14961500

14971501
assertThat(cel.createProgram(ast).eval()).isEqualTo(true);
14981502
}
1503+
1504+
@Test
1505+
@TestParameters("{expression: '[].first().hasValue() == false'}")
1506+
@TestParameters("{expression: '[\"a\",\"b\",\"c\"].first().value() == \"a\"'}")
1507+
public void listFirst_success(String expression) throws Exception {
1508+
Cel cel = newCelBuilder().build();
1509+
boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval();
1510+
assertThat(result).isTrue();
1511+
}
1512+
1513+
@Test
1514+
@TestParameters("{expression: '[].last().hasValue() == false'}")
1515+
@TestParameters("{expression: '[1, 2, 3].last().value() == 3'}")
1516+
public void listLast_success(String expression) throws Exception {
1517+
Cel cel = newCelBuilder().build();
1518+
boolean result = (boolean) cel.createProgram(cel.compile(expression).getAst()).eval();
1519+
assertThat(result).isTrue();
1520+
}
1521+
1522+
@Test
1523+
@TestParameters("{expression: '[1].first()', expectedError: 'undeclared reference to ''first'''}")
1524+
@TestParameters("{expression: '[2].last()', expectedError: 'undeclared reference to ''last'''}")
1525+
public void listFirstAndLast_throws_earlyVersion(String expression, String expectedError)
1526+
throws Exception {
1527+
// Configure Cel with an earlier version of the 'optional' library, which did not support
1528+
// 'first' and 'last'
1529+
Cel cel = newCelBuilder(1).build();
1530+
assertThat(
1531+
assertThrows(
1532+
CelValidationException.class,
1533+
() -> {
1534+
cel.createProgram(cel.compile(expression).getAst()).eval();
1535+
}))
1536+
.hasMessageThat()
1537+
.contains(expectedError);
1538+
}
14991539
}

0 commit comments

Comments
 (0)