Skip to content

Commit 399d4ce

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 97667f5 commit 399d4ce

6 files changed

Lines changed: 155 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: 61 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;
@@ -66,7 +67,10 @@ public enum Function {
6667
OPTIONAL_UNWRAP("optional.unwrap"),
6768
OPTIONAL_OF_NON_ZERO_VALUE("optional.ofNonZeroValue"),
6869
OR("or"),
69-
OR_VALUE("orValue");
70+
OR_VALUE("orValue"),
71+
FIRST("first"),
72+
LAST("last");
73+
7074
private final String functionName;
7175

7276
public String getFunction() {
@@ -80,6 +84,16 @@ public String getFunction() {
8084

8185
private static final String UNUSED_ITER_VAR = "#unused";
8286

87+
private final int version;
88+
89+
CelOptionalLibrary() {
90+
this(Integer.MAX_VALUE);
91+
}
92+
93+
CelOptionalLibrary(int version) {
94+
this.version = version;
95+
}
96+
8397
@Override
8498
public void setParserOptions(CelParserBuilder parserBuilder) {
8599
if (!parserBuilder.getOptions().enableOptionalSyntax()) {
@@ -90,8 +104,10 @@ public void setParserOptions(CelParserBuilder parserBuilder) {
90104
}
91105
parserBuilder.addMacros(
92106
CelMacro.newReceiverMacro("optMap", 2, CelOptionalLibrary::expandOptMap));
93-
parserBuilder.addMacros(
94-
CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap));
107+
if (version >= 1) {
108+
parserBuilder.addMacros(
109+
CelMacro.newReceiverMacro("optFlatMap", 2, CelOptionalLibrary::expandOptFlatMap));
110+
}
95111
}
96112

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

177210
@Override
@@ -241,6 +274,14 @@ public void setRuntimeOptions(
241274
Optional.class,
242275
Long.class,
243276
CelOptionalLibrary::indexOptionalList));
277+
278+
if (version >= 2) {
279+
runtimeBuilder.addFunctionBindings(
280+
CelFunctionBinding.from(
281+
"optional_list_first", Collection.class, CelOptionalLibrary::listOptionalFirst),
282+
CelFunctionBinding.from(
283+
"optional_list_last", Collection.class, CelOptionalLibrary::listOptionalLast));
284+
}
244285
}
245286

246287
private static ImmutableList<Object> elideOptionalCollection(Collection<Optional<Object>> list) {
@@ -372,5 +413,21 @@ private static Object indexOptionalList(Optional<?> optionalList, long index) {
372413
return Optional.of(list.get(castIndex));
373414
}
374415

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

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)