Skip to content

Commit eb2e557

Browse files
l46kokcopybara-github
authored andcommitted
Add ListsExtension with flatten function (single level only)
Recursive implementation will be added in the future. PiperOrigin-RevId: 666372924
1 parent 502830e commit eb2e557

6 files changed

Lines changed: 256 additions & 5 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ java_library(
1616
deps = [
1717
":bindings",
1818
":encoders",
19+
":lists",
1920
":math",
2021
":protos",
2122
":sets",
@@ -148,3 +149,18 @@ java_library(
148149
"@maven//:com_google_guava_guava",
149150
],
150151
)
152+
153+
java_library(
154+
name = "lists",
155+
srcs = ["CelListsExtensions.java"],
156+
tags = [
157+
],
158+
deps = [
159+
"//checker:checker_builder",
160+
"//common:compiler_common",
161+
"//common/types",
162+
"//compiler:compiler_builder",
163+
"//runtime",
164+
"@maven//:com_google_guava_guava",
165+
],
166+
)

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

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.common.collect.ImmutableSet;
2121
import com.google.common.collect.Streams;
2222
import dev.cel.common.CelOptions;
23+
import dev.cel.extensions.CelListsExtensions.Function;
2324
import java.util.Set;
2425

2526
/**
@@ -34,6 +35,7 @@ public final class CelExtensions {
3435
private static final CelProtoExtensions PROTO_EXTENSIONS = new CelProtoExtensions();
3536
private static final CelBindingsExtensions BINDINGS_EXTENSIONS = new CelBindingsExtensions();
3637
private static final CelEncoderExtensions ENCODER_EXTENSIONS = new CelEncoderExtensions();
38+
private static final CelListsExtensions LISTS_EXTENSIONS_ALL = new CelListsExtensions();
3739

3840
/**
3941
* Extended functions for string manipulation.
@@ -187,9 +189,9 @@ public static CelSetsExtensions sets() {
187189
*
188190
* <p>Refer to README.md for available functions.
189191
*
190-
* <p>This will include all functions denoted in {@link CelSetExtensions.Function}, including any
191-
* future additions. To expose only a subset of functions, use {@link
192-
* #sets(CelSetExtensions.Function...)} instead.
192+
* <p>This will include all functions denoted in {@link CelSetsExtensions.Function}, including any
193+
* future additions. To expose only a subset of functions, use {@link #sets(CelOptions,
194+
* CelSetsExtensions.Function...)} instead.
193195
*/
194196
public static CelSetsExtensions sets(CelOptions celOptions) {
195197
return new CelSetsExtensions(celOptions);
@@ -219,6 +221,43 @@ public static CelSetsExtensions sets(
219221
return new CelSetsExtensions(celOptions, functions);
220222
}
221223

224+
/**
225+
* Extended functions for List manipulation.
226+
*
227+
* <p>Refer to README.md for available functions.
228+
*
229+
* <p>This will include only the specific functions denoted by {@link
230+
* CelListsExtensions.Function}.
231+
*/
232+
public static CelListsExtensions lists() {
233+
return LISTS_EXTENSIONS_ALL;
234+
}
235+
236+
/**
237+
* Extended functions for List manipulation.
238+
*
239+
* <p>Refer to README.md for available functions.
240+
*
241+
* <p>This will include only the specific functions denoted by {@link
242+
* CelListsExtensions.Function}.
243+
*/
244+
public static CelListsExtensions lists(CelListsExtensions.Function... functions) {
245+
return lists(ImmutableSet.copyOf(functions));
246+
}
247+
248+
/**
249+
* Extended functions for List manipulation.
250+
*
251+
* <p>Refer to README.md for available functions.
252+
*
253+
* <p>This will include all functions denoted in {@link CelListsExtensions.Function}, including
254+
* any future additions. To expose only a subset of functions, use {@link #lists(Function...)}
255+
* instead.
256+
*/
257+
public static CelListsExtensions lists(Set<CelListsExtensions.Function> functions) {
258+
return new CelListsExtensions(functions);
259+
}
260+
222261
/**
223262
* Retrieves all function names used by every extension libraries.
224263
*
@@ -235,7 +274,9 @@ public static ImmutableSet<String> getAllFunctionNames() {
235274
stream(CelSetsExtensions.Function.values())
236275
.map(CelSetsExtensions.Function::getFunction),
237276
stream(CelEncoderExtensions.Function.values())
238-
.map(CelEncoderExtensions.Function::getFunction))
277+
.map(CelEncoderExtensions.Function::getFunction),
278+
stream(CelListsExtensions.Function.values())
279+
.map(CelListsExtensions.Function::getFunction))
239280
.collect(toImmutableSet());
240281
}
241282

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.extensions;
16+
17+
import com.google.common.base.Preconditions;
18+
import com.google.common.collect.ImmutableList;
19+
import com.google.common.collect.ImmutableSet;
20+
import dev.cel.checker.CelCheckerBuilder;
21+
import dev.cel.common.CelFunctionDecl;
22+
import dev.cel.common.CelOverloadDecl;
23+
import dev.cel.common.types.ListType;
24+
import dev.cel.common.types.TypeParamType;
25+
import dev.cel.compiler.CelCompilerLibrary;
26+
import dev.cel.runtime.CelRuntime;
27+
import dev.cel.runtime.CelRuntime.CelFunctionBinding;
28+
import dev.cel.runtime.CelRuntimeBuilder;
29+
import dev.cel.runtime.CelRuntimeLibrary;
30+
import java.util.Collection;
31+
import java.util.Set;
32+
33+
/** Internal implementation of CEL lists extensions. */
34+
final class CelListsExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
35+
36+
private static final TypeParamType LIST_PARAM_TYPE = TypeParamType.create("T");
37+
38+
@SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety.
39+
public enum Function {
40+
FLATTEN(
41+
CelFunctionDecl.newFunctionDeclaration(
42+
"flatten",
43+
CelOverloadDecl.newMemberOverload(
44+
"list_flatten",
45+
"Flattens a list by a single level",
46+
ListType.create(LIST_PARAM_TYPE),
47+
ListType.create(ListType.create(LIST_PARAM_TYPE)))),
48+
// TODO: add list_flatten_list_int
49+
CelRuntime.CelFunctionBinding.from(
50+
"list_flatten", Collection.class, list -> flatten(list, 1))),
51+
;
52+
53+
private final CelFunctionDecl functionDecl;
54+
private final ImmutableSet<CelFunctionBinding> functionBindings;
55+
56+
String getFunction() {
57+
return functionDecl.name();
58+
}
59+
60+
Function(CelFunctionDecl functionDecl, CelRuntime.CelFunctionBinding... functionBindings) {
61+
this.functionDecl = functionDecl;
62+
this.functionBindings = ImmutableSet.copyOf(functionBindings);
63+
}
64+
}
65+
66+
private final ImmutableSet<Function> functions;
67+
68+
CelListsExtensions() {
69+
this.functions = ImmutableSet.copyOf(Function.values());
70+
}
71+
72+
CelListsExtensions(Set<Function> functions) {
73+
this.functions = ImmutableSet.copyOf(functions);
74+
}
75+
76+
@Override
77+
public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
78+
functions.forEach(function -> checkerBuilder.addFunctionDeclarations(function.functionDecl));
79+
}
80+
81+
@Override
82+
public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
83+
functions.forEach(function -> runtimeBuilder.addFunctionBindings(function.functionBindings));
84+
}
85+
86+
@SuppressWarnings("unchecked")
87+
private static ImmutableList<Object> flatten(Collection<Object> list, int level) {
88+
Preconditions.checkArgument(level == 1, "recursive flatten is not supported yet.");
89+
ImmutableList.Builder<Object> builder = ImmutableList.builder();
90+
for (Object element : list) {
91+
if (element instanceof Collection) {
92+
Collection<Object> listItem = (Collection<Object>) element;
93+
builder.addAll(listItem);
94+
} else {
95+
builder.add(element);
96+
}
97+
}
98+
99+
return builder.build();
100+
}
101+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,33 @@ sets.intersects([1], [1, 2]) // true
406406
sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]]) // true
407407
```
408408

409+
## Lists
410+
411+
Extended functions for list manipulation. As a general note, all indices are
412+
zero-based.
413+
414+
### Flatten
415+
416+
Flattens a list by one level. Support for flattening to a specified level
417+
will be provided in the future.
418+
419+
Examples:
420+
421+
```
422+
[].flatten() // []
423+
[1,[2,3],[4]].flatten() // [1, 2, 3, 4]
424+
[1,[2,[3,4]]].flatten() // [1, 2, [3, 4]]
425+
[1,2,[],[],[3,4]].flatten() // [1, 2, 3, 4]
426+
```
427+
428+
Note that due to the current limitations of type-checker, a compilation error
429+
will occur if an already flat list is populated. For time being, you must wrap
430+
the list in dyn if you anticipate having to deal with a flat list:
431+
432+
```
433+
[1,2,3].flatten() // error
434+
dyn([1,2,3]).flatten() // [1,2,3]
435+
```
436+
437+
This will be addressed once we add the appropriate capabilities in the
438+
type-checker to handle type-reductions, or union types.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public void getAllFunctionNames() {
159159
"sets.equivalent",
160160
"sets.intersects",
161161
"base64.decode",
162-
"base64.encode");
162+
"base64.encode",
163+
"flatten");
163164
}
164165
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package dev.cel.extensions;
15+
16+
import static com.google.common.truth.Truth.assertThat;
17+
import static org.junit.Assert.assertThrows;
18+
19+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
20+
import com.google.testing.junit.testparameterinjector.TestParameters;
21+
import dev.cel.bundle.Cel;
22+
import dev.cel.bundle.CelFactory;
23+
import dev.cel.common.CelValidationException;
24+
import dev.cel.parser.CelStandardMacro;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
28+
@RunWith(TestParameterInjector.class)
29+
public class CelListsExtensionsTest {
30+
private static final Cel CEL =
31+
CelFactory.standardCelBuilder()
32+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
33+
.addCompilerLibraries(CelExtensions.lists())
34+
.addRuntimeLibraries(CelExtensions.lists())
35+
.build();
36+
37+
@Test
38+
@TestParameters("{expression: '[].flatten() == []'}")
39+
@TestParameters("{expression: '[[1, 2]].flatten().exists(i, i == 1)'}")
40+
@TestParameters("{expression: '[[], [[]], [[[]]]].flatten() == [[], [[]]]'}")
41+
@TestParameters("{expression: '[1,[2,[3,4]]].flatten() == [1,2,[3,4]]'}")
42+
@TestParameters("{expression: '[1,2,[],[],[3,4]].flatten() == [1,2,3,4]'}")
43+
@TestParameters("{expression: '[1,[2,3],[[4,5]], [[[6,7]]]].flatten() == [1,2,3,[4,5],[[6,7]]]'}")
44+
@TestParameters("{expression: 'dyn([1]).flatten() == [1]'}")
45+
@TestParameters("{expression: 'dyn([{1: 2}]).flatten() == [{1: 2}]'}")
46+
@TestParameters("{expression: 'dyn([1,2,3,4]).flatten() == [1,2,3,4]'}")
47+
public void flattenSingleLevel_success(String expression) throws Exception {
48+
boolean result = (boolean) CEL.createProgram(CEL.compile(expression).getAst()).eval();
49+
50+
assertThat(result).isTrue();
51+
}
52+
53+
@Test
54+
@TestParameters("{expression: '[1].flatten()'}")
55+
@TestParameters("{expression: '[{1: 2}].flatten()'}")
56+
@TestParameters("{expression: '[1,2,3,4].flatten()'}")
57+
public void flattenSingleLevel_listIsSingleLevel_throws(String expression) {
58+
// Note: Java lacks the capability of conditionally disabling type guards
59+
// due to the lack of full-fledged dynamic dispatch.
60+
assertThrows(CelValidationException.class, () -> CEL.compile(expression).getAst());
61+
}
62+
}

0 commit comments

Comments
 (0)