Skip to content

Commit e30cf84

Browse files
l46kokcopybara-github
authored andcommitted
Expand support to arrays to Native type extensions
Closes #1059 PiperOrigin-RevId: 922995119
1 parent 98ce0bf commit e30cf84

5 files changed

Lines changed: 172 additions & 23 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ java_library(
329329
deps = [
330330
"//checker:checker_builder",
331331
"//common/exceptions:attribute_not_found",
332+
"//common/exceptions:invalid_argument",
332333
"//common/internal:reflection_util",
333334
"//common/types",
334335
"//common/types:type_providers",

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.errorprone.annotations.Immutable;
2929
import dev.cel.checker.CelCheckerBuilder;
3030
import dev.cel.common.exceptions.CelAttributeNotFoundException;
31+
import dev.cel.common.exceptions.CelInvalidArgumentException;
3132
import dev.cel.common.internal.ReflectionUtil;
3233
import dev.cel.common.types.CelType;
3334
import dev.cel.common.types.CelTypeProvider;
@@ -47,6 +48,7 @@
4748
import dev.cel.runtime.CelRuntimeLibrary;
4849
import java.lang.invoke.MethodHandle;
4950
import java.lang.invoke.MethodHandles;
51+
import java.lang.reflect.Array;
5052
import java.lang.reflect.Constructor;
5153
import java.lang.reflect.Field;
5254
import java.lang.reflect.Method;
@@ -285,6 +287,15 @@ private static CelType mapJavaTypeToCelType(
285287
return celType;
286288
}
287289

290+
if (type.isArray()) {
291+
TypeToken<?> token = TypeToken.of(genericType);
292+
TypeToken<?> componentToken =
293+
Preconditions.checkNotNull(
294+
token.getComponentType(), "Array component type cannot be null");
295+
return ListType.create(
296+
mapJavaTypeToCelType(componentToken.getRawType(), componentToken.getType(), classMap));
297+
}
298+
288299
if (type.isInterface()
289300
&& !List.class.isAssignableFrom(type)
290301
&& !Map.class.isAssignableFrom(type)) {
@@ -412,6 +423,14 @@ private void discover(Type type) {
412423
TypeToken<?> token = TypeToken.of(type);
413424
Class<?> rawType = token.getRawType();
414425

426+
if (rawType.isArray()) {
427+
TypeToken<?> componentToken =
428+
Preconditions.checkNotNull(
429+
token.getComponentType(), "Array component type cannot be null");
430+
discover(componentToken.getType());
431+
return;
432+
}
433+
415434
if (List.class.isAssignableFrom(rawType)) {
416435
discover(ReflectionUtil.resolveGenericParameter(token, List.class, 0));
417436
return;
@@ -767,6 +786,9 @@ private static Object getDefaultValue(Class<?> targetType) {
767786
if (Map.class.isAssignableFrom(targetType)) {
768787
return ImmutableMap.of();
769788
}
789+
if (targetType.isArray()) {
790+
return Array.newInstance(targetType.getComponentType(), 0);
791+
}
770792

771793
try {
772794
Constructor<?> constructor = targetType.getDeclaredConstructor();
@@ -814,6 +836,10 @@ public Object toRuntimeValue(Object value) {
814836
return new PojoStructValue(value, accessors, registry.classToTypeMap.get(clazz));
815837
}
816838

839+
if (clazz.isArray() && clazz != byte[].class) {
840+
return convertArrayToList(value);
841+
}
842+
817843
return super.toRuntimeValue(value);
818844
}
819845

@@ -836,8 +862,14 @@ Object toNative(Object value, Class<?> targetType, Type genericType) {
836862
return ((CelByteString) value).toByteArray();
837863
}
838864

839-
if (List.class.isAssignableFrom(targetType) && value instanceof List) {
840-
return convertListToNative((List<?>) value, targetType, genericType);
865+
if (value instanceof List) {
866+
List<?> listValue = (List<?>) value;
867+
if (List.class.isAssignableFrom(targetType)) {
868+
return convertListToNative(listValue, targetType, genericType);
869+
}
870+
if (targetType.isArray()) {
871+
return convertListToArray(listValue, targetType, genericType);
872+
}
841873
}
842874

843875
if (Map.class.isAssignableFrom(targetType) && value instanceof Map) {
@@ -849,7 +881,7 @@ Object toNative(Object value, Class<?> targetType, Type genericType) {
849881

850882
// Safe reflection collection cast.
851883
@SuppressWarnings("unchecked")
852-
private Object convertListToNative(List<?> list, Class<?> targetType, Type genericType) {
884+
private List<?> convertListToNative(List<?> list, Class<?> targetType, Type genericType) {
853885
TypeToken<?> token = TypeToken.of(genericType);
854886
Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0);
855887
Class<?> componentType = ReflectionUtil.getRawType(elementType);
@@ -901,7 +933,7 @@ private Object convertListToNative(List<?> list, Class<?> targetType, Type gener
901933

902934
// Safe reflection collection cast.
903935
@SuppressWarnings("unchecked")
904-
private Object convertMapToNative(Map<?, ?> map, Class<?> targetType, Type genericType) {
936+
private Map<?, ?> convertMapToNative(Map<?, ?> map, Class<?> targetType, Type genericType) {
905937
TypeToken<?> token = TypeToken.of(genericType);
906938
Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0);
907939
Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1);
@@ -962,6 +994,36 @@ private Object convertMapToNative(Map<?, ?> map, Class<?> targetType, Type gener
962994
return builder.buildOrThrow();
963995
}
964996

997+
private Object convertListToArray(List<?> list, Class<?> targetType, Type genericType) {
998+
Class<?> componentType = targetType.getComponentType();
999+
Object array = Array.newInstance(componentType, list.size());
1000+
TypeToken<?> token = TypeToken.of(genericType);
1001+
TypeToken<?> componentToken =
1002+
Preconditions.checkNotNull(
1003+
token.getComponentType(), "Array component type cannot be null");
1004+
Type componentGenericType = componentToken.getType();
1005+
1006+
for (int i = 0; i < list.size(); i++) {
1007+
Object element = list.get(i);
1008+
Object converted = toNative(element, componentType, componentGenericType);
1009+
Array.set(array, i, converted);
1010+
}
1011+
return array;
1012+
}
1013+
1014+
private ImmutableList<Object> convertArrayToList(Object array) {
1015+
int length = Array.getLength(array);
1016+
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(length);
1017+
for (int i = 0; i < length; i++) {
1018+
Object element = Array.get(array, i);
1019+
if (element == null) {
1020+
throw new CelInvalidArgumentException(String.format("Element at index %d is null.", i));
1021+
}
1022+
builder.add(toRuntimeValue(element));
1023+
}
1024+
return builder.build();
1025+
}
1026+
9651027
private Object downcastPrimitives(Object value, Class<?> targetType) {
9661028
Class<?> wrappedTargetType = Primitives.wrap(targetType);
9671029
if (wrappedTargetType == Integer.class && value instanceof Long) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,14 +1114,14 @@ The type-mapping between Java and CEL is as follows:
11141114
| `String` | `string` |
11151115
| `java.time.Duration` | `duration` |
11161116
| `java.time.Instant` | `timestamp` |
1117-
| `java.util.List` | `list` |
1117+
| `java.util.List`, `T[]` (except `byte[]`) | `list` |
11181118
| `java.util.Map` | `map` |
11191119
| `java.util.Optional` | `optional_type` |
11201120

11211121
### Notes
11221122

11231123
* This is only supported for the planner runtime (e.g., `CelRuntimeFactory.plannerRuntimeBuilder()`).
1124-
* Native Java arrays (except `byte[]`) are not supported. Use `java.util.List` instead.
1124+
* Native Java arrays are supported. `byte[]` maps to `bytes`, while other arrays map to `list`.
11251125
* If there is a name collision with a Protobuf type, the protobuf type will take precedence.
11261126
* Instantiating new struct values (e.g., `Account{id: 1234}`) requires the class to have a no-argument constructor (public, protected, package-private, or private).
11271127
* Final fields are supported only in a **read-only** capacity; they cannot be populated when instantiating new struct values.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ java_library(
2020
"//common/exceptions:attribute_not_found",
2121
"//common/exceptions:divide_by_zero",
2222
"//common/exceptions:index_out_of_bounds",
23+
"//common/exceptions:invalid_argument",
2324
"//common/types",
2425
"//common/types:type_providers",
2526
"//common/values",

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

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import dev.cel.common.CelContainer;
3131
import dev.cel.common.CelValidationException;
3232
import dev.cel.common.exceptions.CelAttributeNotFoundException;
33+
import dev.cel.common.exceptions.CelInvalidArgumentException;
3334
import dev.cel.common.types.CelType;
3435
import dev.cel.common.types.ListType;
3536
import dev.cel.common.types.MapType;
@@ -89,7 +90,8 @@ public final class CelNativeTypesExtensionsTest {
8990
TestNestedSimplePojo.class,
9091
TestGetterFieldTypeMismatchPojo.class,
9192
TestAbstractPojo.class,
92-
TestURLPojo.class);
93+
TestURLPojo.class,
94+
TestArrayPojo.class);
9395

9496
private static final Cel CEL =
9597
CelFactory.plannerCelBuilder()
@@ -322,10 +324,10 @@ public void nativeTypes_anonymousClass_throwsException() {
322324

323325
@Test
324326
public void nativeTypes_createStruct_privateConstructor() throws Exception {
325-
Object result = eval("TestPrivateConstructorPojo{value:" + " 'hello'}");
327+
TestPrivateConstructorPojo result =
328+
(TestPrivateConstructorPojo) eval("TestPrivateConstructorPojo{value:" + " 'hello'}");
326329

327-
assertThat(result).isInstanceOf(TestPrivateConstructorPojo.class);
328-
assertThat(((TestPrivateConstructorPojo) result).value).isEqualTo("hello");
330+
assertThat(result.value).isEqualTo("hello");
329331
}
330332

331333
@Test
@@ -374,10 +376,9 @@ public void nativeTypes_missingNoArgConstructor_throws() throws Exception {
374376

375377
@Test
376378
public void nativeTypes_createWithDeepConversion() throws Exception {
377-
Object result = eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}");
378-
379-
assertThat(result).isInstanceOf(TestDeepConversionPojo.class);
380-
TestDeepConversionPojo pojo = (TestDeepConversionPojo) result;
379+
TestDeepConversionPojo pojo =
380+
(TestDeepConversionPojo)
381+
eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}");
381382
assertThat(pojo.ints.get(0)).isEqualTo(1);
382383
assertThat(pojo.floats).containsEntry("a", 1.0f);
383384
}
@@ -397,11 +398,93 @@ public void nativeTypes_unsupportedTypeSet_throwsOnRegistration() throws Excepti
397398
}
398399

399400
@Test
400-
public void nativeTypes_arrayType_throwsOnRegistration() throws Exception {
401-
IllegalArgumentException e =
401+
public void nativeTypes_arrayType_construction() throws Exception {
402+
String expr =
403+
"TestArrayPojo{"
404+
+ " strings: ['a', 'b'],"
405+
+ " ints: [1, 2],"
406+
+ " nesteds: [TestNestedType{value: 'nested'}],"
407+
+ " matrix: [[1, 2], [3, 4]],"
408+
+ " nestedMatrix: [[TestNestedType{value: 'm1'}], [TestNestedType{value: 'm2'}]],"
409+
+ " byteArrays: [b'foo', b'bar']"
410+
+ "}";
411+
412+
TestArrayPojo pojo = (TestArrayPojo) eval(expr);
413+
414+
assertThat(pojo.strings).isEqualTo(new String[] {"a", "b"});
415+
assertThat(pojo.ints).isEqualTo(new int[] {1, 2});
416+
assertThat(pojo.nesteds).hasLength(1);
417+
assertThat(pojo.nesteds[0].value).isEqualTo("nested");
418+
assertThat(pojo.matrix).hasLength(2);
419+
assertThat(pojo.matrix[0]).isEqualTo(new int[] {1, 2});
420+
assertThat(pojo.matrix[1]).isEqualTo(new int[] {3, 4});
421+
assertThat(pojo.nestedMatrix).hasLength(2);
422+
assertThat(pojo.nestedMatrix[0][0].value).isEqualTo("m1");
423+
assertThat(pojo.nestedMatrix[1][0].value).isEqualTo("m2");
424+
assertThat(pojo.byteArrays).hasLength(2);
425+
assertThat(pojo.byteArrays[0]).isEqualTo("foo".getBytes(UTF_8));
426+
assertThat(pojo.byteArrays[1]).isEqualTo("bar".getBytes(UTF_8));
427+
}
428+
429+
@Test
430+
public void nativeTypes_arrayType_selection() throws Exception {
431+
CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestArrayPojo.class);
432+
Cel cel =
433+
CelFactory.plannerCelBuilder()
434+
.setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
435+
.addCompilerLibraries(extensions)
436+
.addRuntimeLibraries(extensions)
437+
.addVar("pojo", StructTypeReference.create(TestArrayPojo.class.getCanonicalName()))
438+
.build();
439+
String expr =
440+
"pojo.strings[1] == 'b'"
441+
+ " && pojo.ints[0] == 1"
442+
+ " && pojo.nesteds[0].value == 'nested'"
443+
+ " && pojo.matrix[1][0] == 3"
444+
+ " && pojo.nestedMatrix[1][0].value == 'm2'"
445+
+ " && pojo.byteArrays[1] == b'bar'";
446+
CelAbstractSyntaxTree ast = cel.compile(expr).getAst();
447+
CelRuntime.Program program = cel.createProgram(ast);
448+
449+
TestArrayPojo input = new TestArrayPojo();
450+
input.strings = new String[] {"a", "b"};
451+
input.ints = new int[] {1, 2};
452+
TestNestedType nested = new TestNestedType();
453+
nested.value = "nested";
454+
input.nesteds = new TestNestedType[] {nested};
455+
input.matrix = new int[][] {{1, 2}, {3, 4}};
456+
TestNestedType m1 = new TestNestedType();
457+
m1.value = "m1";
458+
TestNestedType m2 = new TestNestedType();
459+
m2.value = "m2";
460+
input.nestedMatrix = new TestNestedType[][] {{m1}, {m2}};
461+
input.byteArrays = new byte[][] {"foo".getBytes(UTF_8), "bar".getBytes(UTF_8)};
462+
463+
assertThat(program.eval(ImmutableMap.of("pojo", input))).isEqualTo(true);
464+
}
465+
466+
@Test
467+
public void nativeTypes_arrayWithNullElement_throws() throws Exception {
468+
CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestArrayPojo.class);
469+
Cel cel =
470+
CelFactory.plannerCelBuilder()
471+
.setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
472+
.addCompilerLibraries(extensions)
473+
.addRuntimeLibraries(extensions)
474+
.addVar("pojo", StructTypeReference.create(TestArrayPojo.class.getCanonicalName()))
475+
.build();
476+
477+
CelAbstractSyntaxTree ast = cel.compile("pojo.strings").getAst();
478+
CelRuntime.Program program = cel.createProgram(ast);
479+
480+
TestArrayPojo input = new TestArrayPojo();
481+
input.strings = new String[] {"a", null, "c"};
482+
483+
CelEvaluationException e =
402484
assertThrows(
403-
IllegalArgumentException.class, () -> CelExtensions.nativeTypes(TestArrayPojo.class));
404-
assertThat(e).hasMessageThat().contains("Unsupported type for property 'values'");
485+
CelEvaluationException.class, () -> program.eval(ImmutableMap.of("pojo", input)));
486+
assertThat(e).hasCauseThat().isInstanceOf(CelInvalidArgumentException.class);
487+
assertThat(e).hasCauseThat().hasMessageThat().contains("Element at index 1 is null.");
405488
}
406489

407490
@Test
@@ -653,10 +736,7 @@ public void nativeTypes_createWithUint_fromUnsignedLong() throws Exception {
653736
.getAst();
654737
CelRuntime.Program program = celRuntime.createProgram(ast);
655738

656-
Object result = program.eval();
657-
658-
assertThat(result).isInstanceOf(TestAllTypesPublicFieldsPojo.class);
659-
TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) result;
739+
TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) program.eval();
660740
assertThat(pojo.uintVal).isEqualTo(UnsignedLong.fromLongBits(42L));
661741
}
662742

@@ -1245,7 +1325,12 @@ public static class TestWildcardPojo {
12451325
}
12461326

12471327
public static class TestArrayPojo {
1248-
public String[] values;
1328+
public String[] strings;
1329+
public int[] ints;
1330+
public TestNestedType[] nesteds;
1331+
public int[][] matrix;
1332+
public TestNestedType[][] nestedMatrix;
1333+
public byte[][] byteArrays;
12491334
}
12501335

12511336
public static class TestOptionalUrlPojo {

0 commit comments

Comments
 (0)