Skip to content

Commit 7fc97d7

Browse files
l46kokcopybara-github
authored andcommitted
Expand support to arrays to Native type extensions
Closes #1059 PiperOrigin-RevId: 922995119
1 parent 02052f3 commit 7fc97d7

5 files changed

Lines changed: 174 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;
@@ -289,6 +291,15 @@ private static CelType mapJavaTypeToCelType(
289291
return celType;
290292
}
291293

294+
if (type.isArray()) {
295+
TypeToken<?> token = TypeToken.of(genericType);
296+
TypeToken<?> componentToken =
297+
Preconditions.checkNotNull(
298+
token.getComponentType(), "Array component type cannot be null");
299+
return ListType.create(
300+
mapJavaTypeToCelType(componentToken.getRawType(), componentToken.getType(), classMap));
301+
}
302+
292303
if (type.isInterface()
293304
&& !List.class.isAssignableFrom(type)
294305
&& !Map.class.isAssignableFrom(type)) {
@@ -416,6 +427,14 @@ private void discover(Type type) {
416427
TypeToken<?> token = TypeToken.of(type);
417428
Class<?> rawType = token.getRawType();
418429

430+
if (rawType.isArray()) {
431+
TypeToken<?> componentToken =
432+
Preconditions.checkNotNull(
433+
token.getComponentType(), "Array component type cannot be null");
434+
discover(componentToken.getType());
435+
return;
436+
}
437+
419438
if (List.class.isAssignableFrom(rawType)) {
420439
discover(ReflectionUtil.resolveGenericParameter(token, List.class, 0));
421440
return;
@@ -775,6 +794,9 @@ private static Object getDefaultValue(Class<?> targetType) {
775794
if (Map.class.isAssignableFrom(targetType)) {
776795
return ImmutableMap.of();
777796
}
797+
if (targetType.isArray()) {
798+
return Array.newInstance(targetType.getComponentType(), 0);
799+
}
778800

779801
try {
780802
Constructor<?> constructor = targetType.getDeclaredConstructor();
@@ -822,6 +844,10 @@ public Object toRuntimeValue(Object value) {
822844
return new PojoStructValue(value, accessors, registry.classToTypeMap.get(clazz));
823845
}
824846

847+
if (clazz.isArray() && clazz != byte[].class) {
848+
return convertArrayToList(value);
849+
}
850+
825851
return super.toRuntimeValue(value);
826852
}
827853

@@ -844,8 +870,14 @@ Object toNative(Object value, Class<?> targetType, Type genericType) {
844870
return ((CelByteString) value).toByteArray();
845871
}
846872

847-
if (List.class.isAssignableFrom(targetType) && value instanceof List) {
848-
return convertListToNative((List<?>) value, targetType, genericType);
873+
if (value instanceof List) {
874+
List<?> listValue = (List<?>) value;
875+
if (List.class.isAssignableFrom(targetType)) {
876+
return convertListToNative(listValue, targetType, genericType);
877+
}
878+
if (targetType.isArray()) {
879+
return convertListToArray(listValue, targetType, genericType);
880+
}
849881
}
850882

851883
if (Map.class.isAssignableFrom(targetType) && value instanceof Map) {
@@ -857,7 +889,7 @@ Object toNative(Object value, Class<?> targetType, Type genericType) {
857889

858890
// Safe reflection collection cast.
859891
@SuppressWarnings("unchecked")
860-
private Object convertListToNative(List<?> list, Class<?> targetType, Type genericType) {
892+
private List<?> convertListToNative(List<?> list, Class<?> targetType, Type genericType) {
861893
TypeToken<?> token = TypeToken.of(genericType);
862894
Type elementType = ReflectionUtil.resolveGenericParameter(token, List.class, 0);
863895
Class<?> componentType = ReflectionUtil.getRawType(elementType);
@@ -909,7 +941,7 @@ private Object convertListToNative(List<?> list, Class<?> targetType, Type gener
909941

910942
// Safe reflection collection cast.
911943
@SuppressWarnings("unchecked")
912-
private Object convertMapToNative(Map<?, ?> map, Class<?> targetType, Type genericType) {
944+
private Map<?, ?> convertMapToNative(Map<?, ?> map, Class<?> targetType, Type genericType) {
913945
TypeToken<?> token = TypeToken.of(genericType);
914946
Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0);
915947
Type valueType = ReflectionUtil.resolveGenericParameter(token, Map.class, 1);
@@ -970,6 +1002,36 @@ private Object convertMapToNative(Map<?, ?> map, Class<?> targetType, Type gener
9701002
return builder.buildOrThrow();
9711003
}
9721004

1005+
private Object convertListToArray(List<?> list, Class<?> targetType, Type genericType) {
1006+
Class<?> componentType = targetType.getComponentType();
1007+
Object array = Array.newInstance(componentType, list.size());
1008+
TypeToken<?> token = TypeToken.of(genericType);
1009+
TypeToken<?> componentToken =
1010+
Preconditions.checkNotNull(
1011+
token.getComponentType(), "Array component type cannot be null");
1012+
Type componentGenericType = componentToken.getType();
1013+
1014+
for (int i = 0; i < list.size(); i++) {
1015+
Object element = list.get(i);
1016+
Object converted = toNative(element, componentType, componentGenericType);
1017+
Array.set(array, i, converted);
1018+
}
1019+
return array;
1020+
}
1021+
1022+
private ImmutableList<Object> convertArrayToList(Object array) {
1023+
int length = Array.getLength(array);
1024+
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(length);
1025+
for (int i = 0; i < length; i++) {
1026+
Object element = Array.get(array, i);
1027+
if (element == null) {
1028+
throw new CelInvalidArgumentException(String.format("Element at index %d is null.", i));
1029+
}
1030+
builder.add(toRuntimeValue(element));
1031+
}
1032+
return builder.build();
1033+
}
1034+
9731035
private Object downcastPrimitives(Object value, Class<?> targetType) {
9741036
Class<?> wrappedTargetType = Primitives.wrap(targetType);
9751037
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
* Java `enum` properties are not currently supported and will be safely ignored during scanning.
11261126
* If there is a name collision with a Protobuf type, the protobuf type will take precedence.
11271127
* Instantiating new struct values (e.g., `Account{id: 1234}`) requires the class to have a no-argument constructor (public, protected, package-private, or private).

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: 104 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;
@@ -90,7 +91,8 @@ public final class CelNativeTypesExtensionsTest {
9091
TestGetterFieldTypeMismatchPojo.class,
9192
TestAbstractPojo.class,
9293
TestURLPojo.class,
93-
PojoWithEnum.class);
94+
PojoWithEnum.class,
95+
TestArrayPojo.class);
9496

9597
private static final Cel CEL =
9698
CelFactory.plannerCelBuilder()
@@ -323,10 +325,10 @@ public void nativeTypes_anonymousClass_throwsException() {
323325

324326
@Test
325327
public void nativeTypes_createStruct_privateConstructor() throws Exception {
326-
Object result = eval("TestPrivateConstructorPojo{value:" + " 'hello'}");
328+
TestPrivateConstructorPojo result =
329+
(TestPrivateConstructorPojo) eval("TestPrivateConstructorPojo{value:" + " 'hello'}");
327330

328-
assertThat(result).isInstanceOf(TestPrivateConstructorPojo.class);
329-
assertThat(((TestPrivateConstructorPojo) result).value).isEqualTo("hello");
331+
assertThat(result.value).isEqualTo("hello");
330332
}
331333

332334
@Test
@@ -375,10 +377,9 @@ public void nativeTypes_missingNoArgConstructor_throws() throws Exception {
375377

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

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

408490
@Test
@@ -662,10 +744,7 @@ public void nativeTypes_createWithUint_fromUnsignedLong() throws Exception {
662744
.getAst();
663745
CelRuntime.Program program = celRuntime.createProgram(ast);
664746

665-
Object result = program.eval();
666-
667-
assertThat(result).isInstanceOf(TestAllTypesPublicFieldsPojo.class);
668-
TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) result;
747+
TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) program.eval();
669748
assertThat(pojo.uintVal).isEqualTo(UnsignedLong.fromLongBits(42L));
670749
}
671750

@@ -782,6 +861,8 @@ public void nativeTypes_nullSafeTraversal() throws Exception {
782861
assertThat(cel.createProgram(cel.compile("pojo.int64Val").getAst()).eval(vars)).isEqualTo(0L);
783862
assertThat(cel.createProgram(cel.compile("pojo.nestedVal.value").getAst()).eval(vars))
784863
.isEqualTo("");
864+
assertThat(cel.createProgram(cel.compile("size(pojo.arrayVal) == 0").getAst()).eval(vars))
865+
.isEqualTo(true);
785866
CelAbstractSyntaxTree abstractPojoAst = cel.compile("pojo.abstractPojo.value").getAst();
786867
CelRuntime.Program abstractPojoProgram = cel.createProgram(abstractPojoAst);
787868
CelEvaluationException e =
@@ -942,6 +1023,7 @@ public String get() {
9421023
public double doubleVal;
9431024
public float floatVal;
9441025
public byte[] bytesVal;
1026+
public String[] arrayVal;
9451027
public Duration durationVal;
9461028
public Instant timestampVal;
9471029
public TestNestedType nestedVal;
@@ -1259,7 +1341,12 @@ public static class TestWildcardPojo {
12591341
}
12601342

12611343
public static class TestArrayPojo {
1262-
public String[] values;
1344+
public String[] strings;
1345+
public int[] ints;
1346+
public TestNestedType[] nesteds;
1347+
public int[][] matrix;
1348+
public TestNestedType[][] nestedMatrix;
1349+
public byte[][] byteArrays;
12631350
}
12641351

12651352
public static class TestOptionalUrlPojo {

0 commit comments

Comments
 (0)