diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel
index 781566713..7c33e56b9 100644
--- a/common/internal/BUILD.bazel
+++ b/common/internal/BUILD.bazel
@@ -147,3 +147,8 @@ cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
+
+java_library(
+ name = "reflection_util",
+ exports = ["//common/src/main/java/dev/cel/common/internal:reflection_util"],
+)
diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
index 6b470d98c..58b15b103 100644
--- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
@@ -398,8 +398,11 @@ java_library(
java_library(
name = "reflection_util",
srcs = ["ReflectionUtil.java"],
+ tags = [
+ ],
deps = [
"//common/annotations",
+ "@maven//:com_google_guava_guava",
],
)
diff --git a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
index e513a446b..57c06e311 100644
--- a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
+++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
@@ -14,9 +14,14 @@
package dev.cel.common.internal;
+import com.google.common.reflect.TypeToken;
import dev.cel.common.annotations.Internal;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
/**
* Utility class for invoking Java reflection.
@@ -48,5 +53,35 @@ public static Object invoke(Method method, Object object, Object... params) {
}
}
+ /**
+ * Extracts the element type of a container type (List, Map, or Optional). Returns the type itself
+ * if it's not a container or if generic type info is missing.
+ */
+ public static Class> getElementType(Class> type, Type genericType) {
+ TypeToken> token = TypeToken.of(genericType);
+
+ if (List.class.isAssignableFrom(type)) {
+ return token.resolveType(List.class.getTypeParameters()[0]).getRawType();
+ }
+ if (Map.class.isAssignableFrom(type)) {
+ return token.resolveType(Map.class.getTypeParameters()[1]).getRawType();
+ }
+ // Optional is a final class, so reference equality is equivalent to isAssignableFrom
+ // but slightly more performant than tree traversal.
+ if (type == Optional.class) {
+ return token.resolveType(Optional.class.getTypeParameters()[0]).getRawType();
+ }
+
+ return type;
+ }
+
+ /**
+ * Extracts the raw Class from a Type. Handles Class, ParameterizedType, and WildcardType (returns
+ * upper bound). Returns Object.class as fallback.
+ */
+ public static Class> getRawType(Type type) {
+ return TypeToken.of(type).getRawType();
+ }
+
private ReflectionUtil() {}
}
diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel
index d572bb2bc..2c29a0602 100644
--- a/common/src/main/java/dev/cel/common/values/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel
@@ -60,7 +60,6 @@ java_library(
deps = [
"//common/values",
"@maven//:com_google_errorprone_error_prone_annotations",
- "@maven//:com_google_guava_guava",
],
)
@@ -72,7 +71,6 @@ cel_android_library(
deps = [
"//common/values:values_android",
"@maven//:com_google_errorprone_error_prone_annotations",
- "@maven_android//:com_google_guava_guava",
],
)
@@ -118,7 +116,6 @@ java_library(
deps = [
":values",
"//common/annotations",
- "@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
@@ -134,12 +131,31 @@ cel_android_library(
deps = [
":values_android",
"//common/annotations",
- "@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:org_jspecify_jspecify",
"@maven_android//:com_google_guava_guava",
],
)
+java_library(
+ name = "preadapted_list",
+ srcs = [
+ "CelPreAdaptedList.java",
+ ],
+ tags = [
+ ],
+ deps = ["//common/annotations"],
+)
+
+cel_android_library(
+ name = "preadapted_list_android",
+ srcs = [
+ "CelPreAdaptedList.java",
+ ],
+ tags = [
+ ],
+ deps = ["//common/annotations"],
+)
+
java_library(
name = "values",
srcs = CEL_VALUES_SOURCES,
@@ -148,6 +164,7 @@ java_library(
deps = [
":cel_byte_string",
":cel_value",
+ ":preadapted_list",
"//:auto_value",
"//common/annotations",
"//common/types",
@@ -198,6 +215,7 @@ cel_android_library(
deps = [
":cel_byte_string",
":cel_value_android",
+ ":preadapted_list_android",
"//:auto_value",
"//common/annotations",
"//common/types:type_providers_android",
@@ -226,7 +244,6 @@ java_library(
],
deps = [
":cel_byte_string",
- ":values",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/internal:well_known_proto",
@@ -234,6 +251,7 @@ java_library(
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
+ "@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
@@ -261,6 +279,7 @@ java_library(
],
deps = [
":base_proto_cel_value_converter",
+ ":preadapted_list",
":values",
"//:auto_value",
"//common:options",
@@ -273,7 +292,7 @@ java_library(
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
- "@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
@@ -316,8 +335,6 @@ java_library(
"//protobuf:cel_lite_descriptor",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
- "@maven//:com_google_protobuf_protobuf_java",
- "@maven//:org_jspecify_jspecify",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
@@ -343,7 +360,6 @@ cel_android_library(
"//protobuf:cel_lite_descriptor",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
- "@maven//:org_jspecify_jspecify",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
diff --git a/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java
new file mode 100644
index 000000000..c0ff25e45
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/CelPreAdaptedList.java
@@ -0,0 +1,49 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.values;
+
+import dev.cel.common.annotations.Internal;
+import java.util.AbstractList;
+import java.util.List;
+import java.util.RandomAccess;
+
+/**
+ * A zero-allocation view over a list we know is already adapted.
+ *
+ *
This class purely exists as an optimization scheme to avoid redundant collection traversals in
+ * {@link CelValueConverter}, and is not intended for general use.
+ */
+@Internal
+final class CelPreAdaptedList extends AbstractList implements RandomAccess {
+ private final List delegate;
+
+ private CelPreAdaptedList(List delegate) {
+ this.delegate = delegate;
+ }
+
+ static CelPreAdaptedList wrap(List safeList) {
+ return new CelPreAdaptedList<>(safeList);
+ }
+
+ @Override
+ public E get(int index) {
+ return delegate.get(index);
+ }
+
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+}
diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java
index 89f5ab100..20deef1d3 100644
--- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java
+++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java
@@ -20,8 +20,11 @@
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.annotations.Internal;
import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.RandomAccess;
import java.util.function.Function;
/**
@@ -53,16 +56,11 @@ public static CelValueConverter getDefaultInstance() {
* The value may be a {@link CelValue}, a {@link Collection} or a {@link Map}.
*/
public Object maybeUnwrap(Object value) {
- if (value instanceof CelValue) {
- return unwrap((CelValue) value);
+ if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
+ return value instanceof CelValue ? unwrap((CelValue) value) : value;
}
- Object mapped = mapContainer(value, maybeUnwrapFunction);
- if (mapped != value) {
- return mapped;
- }
-
- return value;
+ return mapContainer(value, maybeUnwrapFunction);
}
/**
@@ -70,6 +68,34 @@ public Object maybeUnwrap(Object value) {
* Returns the original value if it's not a supported container.
*/
protected Object mapContainer(Object value, Function mapper) {
+
+ // Zero allocation path for standard lists that support O(1) indexing
+ // Generally, protobuf lists (backed by arrays) fall into this category
+ if (value instanceof List && value instanceof RandomAccess) {
+ List list = (List) value;
+ for (int i = 0; i < list.size(); i++) {
+ Object element = list.get(i);
+ Object mapped = mapper.apply(element);
+
+ if (mapped != element) {
+ ImmutableList.Builder builder =
+ ImmutableList.builderWithExpectedSize(list.size());
+ for (int j = 0; j < i; j++) {
+ builder.add(list.get(j));
+ }
+ builder.add(mapped);
+ for (int j = i + 1; j < list.size(); j++) {
+ builder.add(mapper.apply(list.get(j)));
+ }
+ return builder.build();
+ }
+ }
+
+ // Zero allocations if unmodified
+ return value;
+ }
+
+ // Fallback for lists that are unordered
if (value instanceof Collection) {
Collection collection = (Collection) value;
ImmutableList.Builder builder =
@@ -82,12 +108,32 @@ protected Object mapContainer(Object value, Function mapper) {
if (value instanceof Map) {
Map map = (Map) value;
- ImmutableMap.Builder builder =
- ImmutableMap.builderWithExpectedSize(map.size());
- for (Map.Entry entry : map.entrySet()) {
- builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue()));
+ Iterator> iterator = map.entrySet().iterator();
+
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ Object mappedKey = mapper.apply(entry.getKey());
+ Object mappedValue = mapper.apply(entry.getValue());
+
+ if (mappedKey != entry.getKey() || mappedValue != entry.getValue()) {
+ ImmutableMap.Builder builder =
+ ImmutableMap.builderWithExpectedSize(map.size());
+
+ for (Map.Entry prevEntry : map.entrySet()) {
+ if (prevEntry.getKey() == entry.getKey()) {
+ break;
+ }
+ builder.put(mapper.apply(prevEntry.getKey()), mapper.apply(prevEntry.getValue()));
+ }
+ builder.put(mappedKey, mappedValue);
+ while (iterator.hasNext()) {
+ Map.Entry nextEntry = iterator.next();
+ builder.put(mapper.apply(nextEntry.getKey()), mapper.apply(nextEntry.getValue()));
+ }
+ return builder.buildOrThrow();
+ }
}
- return builder.buildOrThrow();
+ return value;
}
return value;
@@ -96,7 +142,7 @@ protected Object mapContainer(Object value, Function mapper) {
public Object toRuntimeValue(Object value) {
Preconditions.checkNotNull(value);
- if (value instanceof CelValue) {
+ if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
return value;
}
diff --git a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java
index 565c65438..948df759c 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoCelValueConverter.java
@@ -167,6 +167,18 @@ public Object fromProtoMessageFieldToCelValue(Message message, FieldDescriptor f
break;
}
+ if (fieldDescriptor.isRepeated()) {
+ switch (fieldDescriptor.getType()) {
+ case INT64:
+ case BOOL:
+ case STRING:
+ case DOUBLE:
+ return CelPreAdaptedList.wrap((List>) result);
+ default:
+ break;
+ }
+ }
+
return toRuntimeValue(result);
}
diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel
index c6a029106..dea4cd760 100644
--- a/extensions/BUILD.bazel
+++ b/extensions/BUILD.bazel
@@ -56,3 +56,8 @@ java_library(
name = "comprehensions",
exports = ["//extensions/src/main/java/dev/cel/extensions:comprehensions"],
)
+
+java_library(
+ name = "native",
+ exports = ["//extensions/src/main/java/dev/cel/extensions:native"],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
index f8e4bfc8c..73bab08c9 100644
--- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
+++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
@@ -34,6 +34,7 @@ java_library(
":encoders",
":lists",
":math",
+ ":native",
":optional_library",
":protos",
":regex",
@@ -185,6 +186,7 @@ java_library(
"//common/types",
"//common/values",
"//common/values:cel_byte_string",
+ "//common/values:cel_value",
"//compiler:compiler_builder",
"//extensions:extension_library",
"//parser:macro",
@@ -318,3 +320,26 @@ java_library(
"@maven//:com_google_guava_guava",
],
)
+
+java_library(
+ name = "native",
+ srcs = ["CelNativeTypesExtensions.java"],
+ tags = [
+ ],
+ deps = [
+ "//checker:checker_builder",
+ "//common/exceptions:attribute_not_found",
+ "//common/internal:reflection_util",
+ "//common/types",
+ "//common/types:type_providers",
+ "//common/values",
+ "//common/values:cel_byte_string",
+ "//common/values:cel_value",
+ "//common/values:cel_value_provider",
+ "//compiler:compiler_builder",
+ "//runtime",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
index 8f1770f3f..8adc39384 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
@@ -15,13 +15,13 @@
package dev.cel.extensions;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static java.util.Arrays.stream;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.errorprone.annotations.InlineMe;
import dev.cel.common.CelOptions;
import dev.cel.extensions.CelMathExtensions.Function;
+import java.util.EnumSet;
import java.util.Set;
/**
@@ -350,6 +350,18 @@ public static CelComprehensionsExtensions comprehensions() {
return COMPREHENSIONS_EXTENSIONS;
}
+ /**
+ * Extensions for supporting native Java types (POJOs) in CEL.
+ *
+ * Refer to README.md for details on property discovery, type mapping, and limitations.
+ *
+ *
Note: Passing classes with unsupported types or anonymous/local classes will result in an
+ * {@link IllegalArgumentException} when the runtime is built.
+ */
+ public static CelNativeTypesExtensions nativeTypes(Class>... classes) {
+ return CelNativeTypesExtensions.nativeTypes(classes);
+ }
+
/**
* Retrieves all function names used by every extension libraries.
*
@@ -359,18 +371,17 @@ public static CelComprehensionsExtensions comprehensions() {
*/
public static ImmutableSet getAllFunctionNames() {
return Streams.concat(
- stream(CelMathExtensions.Function.values())
- .map(CelMathExtensions.Function::getFunction),
- stream(CelStringExtensions.Function.values())
+ EnumSet.allOf(Function.class).stream().map(CelMathExtensions.Function::getFunction),
+ EnumSet.allOf(CelStringExtensions.Function.class).stream()
.map(CelStringExtensions.Function::getFunction),
- stream(SetsFunction.values()).map(SetsFunction::getFunction),
- stream(CelEncoderExtensions.Function.values())
+ EnumSet.allOf(SetsFunction.class).stream().map(SetsFunction::getFunction),
+ EnumSet.allOf(CelEncoderExtensions.Function.class).stream()
.map(CelEncoderExtensions.Function::getFunction),
- stream(CelListsExtensions.Function.values())
+ EnumSet.allOf(CelListsExtensions.Function.class).stream()
.map(CelListsExtensions.Function::getFunction),
- stream(CelRegexExtensions.Function.values())
+ EnumSet.allOf(CelRegexExtensions.Function.class).stream()
.map(CelRegexExtensions.Function::getFunction),
- stream(CelComprehensionsExtensions.Function.values())
+ EnumSet.allOf(CelComprehensionsExtensions.Function.class).stream()
.map(CelComprehensionsExtensions.Function::getFunction))
.collect(toImmutableSet());
}
diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java
new file mode 100644
index 000000000..8ea836064
--- /dev/null
+++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java
@@ -0,0 +1,1050 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.extensions;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Arrays.stream;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Primitives;
+import com.google.common.primitives.UnsignedLong;
+import com.google.common.reflect.TypeToken;
+import com.google.errorprone.annotations.Immutable;
+import dev.cel.checker.CelCheckerBuilder;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import dev.cel.common.internal.ReflectionUtil;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.CelTypeProvider;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.MapType;
+import dev.cel.common.types.OptionalType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.StructType;
+import dev.cel.common.types.StructTypeReference;
+import dev.cel.common.values.CelByteString;
+import dev.cel.common.values.CelValue;
+import dev.cel.common.values.CelValueConverter;
+import dev.cel.common.values.CelValueProvider;
+import dev.cel.common.values.StructValue;
+import dev.cel.compiler.CelCompilerLibrary;
+import dev.cel.runtime.CelRuntimeBuilder;
+import dev.cel.runtime.CelRuntimeLibrary;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Extension for supporting native Java types (POJOs) in CEL.
+ *
+ * This allows seamless plugin and evaluation of message creations and field selections without
+ * involving protobuf.
+ */
+@Immutable
+public final class CelNativeTypesExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
+
+ private final NativeTypeRegistry registry;
+
+ // Set of all standard java.lang.Object method names.
+ private static final ImmutableSet OBJECT_METHOD_NAMES =
+ stream(Object.class.getDeclaredMethods()).map(Method::getName).collect(toImmutableSet());
+
+ private static final ImmutableMap, CelType> JAVA_TO_CEL_TYPE_MAP =
+ ImmutableMap., CelType>builder()
+ .put(boolean.class, SimpleType.BOOL)
+ .put(Boolean.class, SimpleType.BOOL)
+ .put(String.class, SimpleType.STRING)
+ .put(int.class, SimpleType.INT)
+ .put(Integer.class, SimpleType.INT)
+ .put(long.class, SimpleType.INT)
+ .put(Long.class, SimpleType.INT)
+ .put(UnsignedLong.class, SimpleType.UINT)
+ .put(float.class, SimpleType.DOUBLE)
+ .put(Float.class, SimpleType.DOUBLE)
+ .put(double.class, SimpleType.DOUBLE)
+ .put(Double.class, SimpleType.DOUBLE)
+ .put(byte[].class, SimpleType.BYTES)
+ .put(CelByteString.class, SimpleType.BYTES)
+ .put(Duration.class, SimpleType.DURATION)
+ .put(Instant.class, SimpleType.TIMESTAMP)
+ .put(Object.class, SimpleType.DYN)
+ .buildOrThrow();
+
+ private static final ImmutableMap, Object> JAVA_TO_DEFAULT_VALUE_MAP =
+ ImmutableMap., Object>builder()
+ .put(boolean.class, false)
+ .put(Boolean.class, false)
+ .put(String.class, "")
+ .put(int.class, 0L)
+ .put(Integer.class, 0L)
+ .put(long.class, 0L)
+ .put(Long.class, 0L)
+ .put(UnsignedLong.class, UnsignedLong.ZERO)
+ .put(float.class, 0.0)
+ .put(Float.class, 0.0)
+ .put(double.class, 0.0)
+ .put(Double.class, 0.0)
+ .put(byte[].class, new byte[0])
+ .put(CelByteString.class, CelByteString.EMPTY)
+ .put(Duration.class, Duration.ZERO)
+ .put(Instant.class, Instant.EPOCH)
+ .put(Optional.class, Optional.empty())
+ .buildOrThrow();
+
+ /** Creates a new instance of {@link CelNativeTypesExtensions} for the given classes. */
+ static CelNativeTypesExtensions nativeTypes(Class>... classes) {
+ return new CelNativeTypesExtensions(new NativeTypeRegistry(NativeTypeScanner.scan(classes)));
+ }
+
+ @VisibleForTesting
+ NativeTypeRegistry getRegistry() {
+ return registry;
+ }
+
+ @Override
+ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
+ runtimeBuilder.setValueProvider(registry);
+ runtimeBuilder.setTypeProvider(registry);
+ }
+
+ @Override
+ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
+ checkerBuilder.setTypeProvider(registry);
+ }
+
+ /**
+ * NativeTypeScanner scans registered Java classes to extract properties and compile accessors.
+ */
+ @VisibleForTesting
+ static final class NativeTypeScanner {
+
+ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+
+ private NativeTypeScanner() {}
+
+ private static final class ScanResult {
+ private final ImmutableMap> classMap;
+ private final ImmutableMap typeMap;
+ private final ImmutableMap, StructType> classToTypeMap;
+ private final ImmutableMap, ImmutableMap> accessorMap;
+
+ ScanResult(
+ ImmutableMap> classMap,
+ ImmutableMap typeMap,
+ ImmutableMap, StructType> classToTypeMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ this.classMap = classMap;
+ this.typeMap = typeMap;
+ this.classToTypeMap = classToTypeMap;
+ this.accessorMap = accessorMap;
+ }
+ }
+
+ private static ScanResult scan(Class>... classes) {
+ ImmutableMap.Builder> classMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder typeMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder, StructType> classToTypeMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder, ImmutableMap> accessorMapBuilder =
+ ImmutableMap.builder();
+
+ Set> visited = new HashSet<>();
+ Queue> queue = new ArrayDeque<>(Arrays.asList(classes));
+
+ while (!queue.isEmpty()) {
+ Class> clazz = queue.poll();
+ if (shouldSkip(clazz, visited)) {
+ continue;
+ }
+ visited.add(clazz);
+
+ String typeName = getCelTypeName(clazz);
+ classMapBuilder.put(typeName, clazz);
+
+ ImmutableMap accessors = scanProperties(clazz, queue);
+ accessorMapBuilder.put(clazz, accessors);
+ }
+
+ ImmutableMap> classMap = classMapBuilder.buildOrThrow();
+ ImmutableMap, ImmutableMap> accessorMap =
+ accessorMapBuilder.buildOrThrow();
+
+ for (Map.Entry> entry : classMap.entrySet()) {
+ String typeName = entry.getKey();
+ Class> clazz = entry.getValue();
+
+ StructType structType = createStructType(clazz, classMap, accessorMap);
+ typeMapBuilder.put(typeName, structType);
+ classToTypeMapBuilder.put(clazz, structType);
+ }
+
+ ScanResult result =
+ new ScanResult(
+ classMap,
+ typeMapBuilder.buildOrThrow(),
+ classToTypeMapBuilder.buildOrThrow(),
+ accessorMap);
+
+ validateRegisteredClasses(result.classToTypeMap, result.classMap, result.accessorMap);
+
+ return result;
+ }
+
+ private static void validateRegisteredClasses(
+ ImmutableMap, StructType> classToTypeMap,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ for (Class> clazz : classToTypeMap.keySet()) {
+ for (String prop : getProperties(clazz)) {
+ try {
+ getPropertyType(clazz, prop, classMap, accessorMap);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unsupported type for property '" + prop + "' in class " + clazz.getName(), e);
+ }
+ }
+ }
+ }
+
+ private static boolean shouldSkip(Class> clazz, Set> visited) {
+ return clazz == null
+ || visited.contains(clazz)
+ || clazz.isInterface()
+ || isSupportedType(clazz);
+ }
+
+ private static boolean isSupportedType(Class> type) {
+ return JAVA_TO_CEL_TYPE_MAP.containsKey(type)
+ || type == Optional.class
+ || List.class.isAssignableFrom(type)
+ || Map.class.isAssignableFrom(type)
+ || type.isArray();
+ }
+
+ private static StructType createStructType(
+ Class> clazz,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ return StructType.create(
+ getCelTypeName(clazz),
+ getProperties(clazz),
+ fieldName -> Optional.of(getPropertyType(clazz, fieldName, classMap, accessorMap)));
+ }
+
+ private static CelType getPropertyType(
+ Class> clazz,
+ String propertyName,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ ImmutableMap accessors = accessorMap.get(clazz);
+ if (accessors != null) {
+ PropertyAccessor accessor = accessors.get(propertyName);
+ if (accessor != null) {
+ return mapJavaTypeToCelType(accessor.targetType, accessor.genericTargetType, classMap);
+ }
+ }
+ throw new IllegalArgumentException("No public field or getter for " + propertyName);
+ }
+
+ private static CelType mapJavaTypeToCelType(
+ Class> type, Type genericType, ImmutableMap> classMap) {
+
+ CelType celType = JAVA_TO_CEL_TYPE_MAP.get(type);
+ if (celType != null) {
+ return celType;
+ }
+
+ if (type.isInterface()
+ && !List.class.isAssignableFrom(type)
+ && !Map.class.isAssignableFrom(type)) {
+ throw new IllegalArgumentException("Unsupported interface type: " + type.getName());
+ }
+
+ TypeToken> token = TypeToken.of(genericType);
+
+ if (List.class.isAssignableFrom(type)) {
+ Type elementType = resolveGenericParameter(token, List.class, 0);
+ return ListType.create(
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(elementType), elementType, classMap));
+ }
+
+ if (Map.class.isAssignableFrom(type)) {
+ Type keyType = resolveGenericParameter(token, Map.class, 0);
+ Type valueType = resolveGenericParameter(token, Map.class, 1);
+
+ CelType celKeyType =
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(keyType), keyType, classMap);
+ if (celKeyType == SimpleType.DOUBLE) {
+ throw new IllegalArgumentException("Decimals are not allowed as map keys in CEL.");
+ }
+
+ return MapType.create(
+ celKeyType,
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(valueType), valueType, classMap));
+ }
+
+ // Optional is a final class, so reference equality is equivalent to isAssignableFrom
+ // but slightly more performant than tree traversal.
+ if (type == Optional.class) {
+ Type optionalType = resolveGenericParameter(token, Optional.class, 0);
+ return OptionalType.create(
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(optionalType), optionalType, classMap));
+ }
+
+ String typeName = getCelTypeName(type);
+ if (classMap.containsKey(typeName)) {
+ return StructTypeReference.create(typeName);
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported Java type for CEL mapping: " + type.getName());
+ }
+
+ private static ImmutableMap scanProperties(
+ Class> clazz, Queue> queue) {
+ ImmutableMap.Builder builtAccessors = ImmutableMap.builder();
+
+ for (String propName : getProperties(clazz)) {
+ buildPropertyAccessor(clazz, propName, queue)
+ .ifPresent(accessor -> builtAccessors.put(propName, accessor));
+ }
+
+ return builtAccessors.buildOrThrow();
+ }
+
+ private static Optional buildPropertyAccessor(
+ Class> clazz, String propName, Queue> queue) {
+ Method getter = findGetter(clazz, propName);
+ Field field = findField(clazz, propName);
+
+ Class> propType = null;
+ Type genericPropType = null;
+ Function compiledGetter = null;
+ BiConsumer compiledSetter = null;
+
+ if (getter != null) {
+ propType = getter.getReturnType();
+ genericPropType = getter.getGenericReturnType();
+ discoverCustomTypes(genericPropType, queue);
+ compiledGetter = compileGetter(getter);
+ } else if (field != null) {
+ propType = field.getType();
+ genericPropType = field.getGenericType();
+ discoverCustomTypes(genericPropType, queue);
+ compiledGetter = compileFieldGetter(field);
+ }
+
+ if (propType != null) {
+ Method setter = findSetter(clazz, propName, propType);
+ if (setter != null) {
+ compiledSetter = compileSetter(setter);
+ } else if (field != null
+ && !Modifier.isFinal(field.getModifiers())
+ && Primitives.wrap(field.getType()) == Primitives.wrap(propType)) {
+ compiledSetter = compileFieldSetter(field);
+ }
+ }
+
+ if (compiledGetter != null) {
+ return Optional.of(
+ new PropertyAccessor(compiledGetter, compiledSetter, propType, genericPropType));
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Recursively explores a {@link Type} and discovers any transitive, user-defined custom POJO
+ * classes nested inside multi-level generic collections, lists, maps, or optionals, pushing
+ * them into the scanning discovery queue.
+ *
+ * "Custom types" are any public non-primitive, non-built-in Java classes that require
+ * explicit properties reflective scanning and mapping to a CEL StructType schema (as opposed to
+ * standard built-in types like {@code String}, {@code List}, or {@code Map}).
+ *
+ * @param type The Java type token or parameterized collection type to recursively unpack.
+ * @param queue The central scanning queue where newly discovered custom classes are pushed for
+ * subsequent properties discovery.
+ */
+ private static void discoverCustomTypes(Type type, Queue> queue) {
+ Preconditions.checkNotNull(type, "Type to discover cannot be null.");
+ Preconditions.checkNotNull(queue, "Queue cannot be null.");
+ TypeToken> token = TypeToken.of(type);
+ Class> rawType = token.getRawType();
+
+ if (List.class.isAssignableFrom(rawType)) {
+ Type elementType = resolveGenericParameter(token, List.class, 0);
+ discoverCustomTypes(elementType, queue);
+ return;
+ }
+
+ if (Map.class.isAssignableFrom(rawType)) {
+ Type keyType = resolveGenericParameter(token, Map.class, 0);
+ Type valueType = resolveGenericParameter(token, Map.class, 1);
+ discoverCustomTypes(keyType, queue);
+ discoverCustomTypes(valueType, queue);
+ return;
+ }
+
+ if (rawType == Optional.class) {
+ Type optionalType = resolveGenericParameter(token, Optional.class, 0);
+ discoverCustomTypes(optionalType, queue);
+ return;
+ }
+
+ if (rawType.isArray()) {
+ Class> componentType = rawType.getComponentType();
+ discoverCustomTypes(componentType, queue);
+ return;
+ }
+
+ if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType)
+ && Modifier.isPublic(rawType.getModifiers())) {
+ queue.add(rawType);
+ }
+ }
+
+ private static Function compileGetter(Method getter) {
+ try {
+ getter.setAccessible(true);
+ MethodHandle mh = LOOKUP.unreflect(getter);
+ return instance -> {
+ try {
+ return mh.invoke(instance);
+ } catch (Throwable t) {
+ throw new IllegalStateException("Failed to invoke getter for " + getter, t);
+ }
+ };
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Failed to unreflect getter", e);
+ }
+ }
+
+ private static Function compileFieldGetter(Field field) {
+ try {
+ field.setAccessible(true);
+ MethodHandle mh = LOOKUP.unreflectGetter(field);
+ return instance -> {
+ try {
+ return mh.invoke(instance);
+ } catch (Throwable t) {
+ throw new IllegalStateException("Failed to get field " + field, t);
+ }
+ };
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Failed to access field " + field, e);
+ }
+ }
+
+ private static BiConsumer compileSetter(Method setter) {
+ try {
+ setter.setAccessible(true);
+ MethodHandle mh = LOOKUP.unreflect(setter);
+ return (instance, value) -> {
+ try {
+ mh.invoke(instance, value);
+ } catch (Throwable t) {
+ throw new IllegalStateException("Failed to invoke setter for " + setter, t);
+ }
+ };
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Failed to unreflect setter", e);
+ }
+ }
+
+ private static BiConsumer compileFieldSetter(Field field) {
+ try {
+ field.setAccessible(true);
+ MethodHandle mh = LOOKUP.unreflectSetter(field);
+ return (instance, value) -> {
+ try {
+ mh.invoke(instance, value);
+ } catch (Throwable t) {
+ throw new IllegalStateException("Failed to set field " + field, t);
+ }
+ };
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Failed to access field " + field, e);
+ }
+ }
+
+ private static @Nullable Method findGetter(Class> clazz, String propertyName) {
+ String getterName = buildMethodName("get", propertyName);
+ String isGetterName = buildMethodName("is", propertyName);
+
+ Method isGetter = null;
+ Method prefixLess = null;
+
+ for (Method method : clazz.getMethods()) {
+ if (method.isBridge() || method.isSynthetic()) {
+ // Ignore compiler-generated duplicates
+ continue;
+ }
+ if (method.getParameterCount() == 0) {
+ String name = method.getName();
+ if (name.equals(getterName)) {
+ return method;
+ }
+ if (name.equals(isGetterName)) {
+ isGetter = method;
+ }
+ if (name.equals(propertyName)) {
+ prefixLess = method;
+ }
+ }
+ }
+
+ if (isGetter != null) {
+ return isGetter;
+ }
+ return prefixLess;
+ }
+
+ private static @Nullable Field findField(Class> clazz, String propertyName) {
+ for (Field field : clazz.getFields()) {
+ if (field.getName().equals(propertyName)) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ private static @Nullable Method findSetter(
+ Class> clazz, String propertyName, Class> propertyType) {
+ String setterName = buildMethodName("set", propertyName);
+ return stream(clazz.getMethods())
+ .filter(m -> !m.isBridge() && !m.isSynthetic())
+ .filter(m -> m.getName().equals(setterName))
+ .filter(m -> m.getParameterCount() == 1)
+ .filter(m -> m.getParameterTypes()[0].equals(propertyType))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static Set getAllDeclaredFieldNames(Class> clazz) {
+ Set declaredFieldNames = new HashSet<>();
+ Class> currentClass = clazz;
+ while (currentClass != null) {
+ for (Field field : currentClass.getDeclaredFields()) {
+ declaredFieldNames.add(field.getName());
+ }
+ currentClass = currentClass.getSuperclass();
+ }
+ return declaredFieldNames;
+ }
+
+ @VisibleForTesting
+ static ImmutableSet getProperties(Class> clazz) {
+ ImmutableSet.Builder properties = ImmutableSet.builder();
+ Set declaredFieldNames = getAllDeclaredFieldNames(clazz);
+ for (Field field : clazz.getFields()) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+ properties.add(field.getName());
+ }
+ for (Method method : clazz.getMethods()) {
+ if (isGetter(method)) {
+ String propName = getPropertyName(method);
+ if (method.getName().startsWith("get") || method.getName().startsWith("is")) {
+ properties.add(propName);
+ } else if (declaredFieldNames.contains(propName)) {
+ properties.add(propName);
+ }
+ }
+ }
+ return properties.build();
+ }
+
+ private static boolean isGetter(Method method) {
+ if (Modifier.isStatic(method.getModifiers())) {
+ return false;
+ }
+ if (!Modifier.isPublic(method.getModifiers()) || method.getParameterCount() != 0) {
+ return false;
+ }
+ if (method.getReturnType() == void.class) {
+ return false;
+ }
+ String name = method.getName();
+ if (OBJECT_METHOD_NAMES.contains(name)) {
+ return false;
+ }
+ if (name.startsWith("get")) {
+ return name.length() > 3;
+ }
+ if (name.startsWith("is")) {
+ return name.length() > 2 && Primitives.wrap(method.getReturnType()) == Boolean.class;
+ }
+ return true;
+ }
+
+ private static String decapitalize(String name) {
+ Preconditions.checkArgument(name != null && !name.isEmpty());
+ if (name.length() > 1
+ && Character.isUpperCase(name.charAt(1))
+ && Character.isUpperCase(name.charAt(0))) {
+ return name;
+ }
+ char[] chars = name.toCharArray();
+ chars[0] = Character.toLowerCase(chars[0]);
+ return new String(chars);
+ }
+
+ private static String getPropertyName(Method method) {
+ String name = method.getName();
+ if (name.startsWith("get")) {
+ return decapitalize(name.substring(3));
+ }
+ if (name.startsWith("is")) {
+ return decapitalize(name.substring(2));
+ }
+ if (name.startsWith("set")) {
+ return decapitalize(name.substring(3));
+ }
+ return name;
+ }
+
+ private static String capitalize(String name) {
+ return Character.toUpperCase(name.charAt(0)) + name.substring(1);
+ }
+
+ private static String buildMethodName(String prefix, String propertyName) {
+ return prefix + capitalize(propertyName);
+ }
+ }
+
+ /**
+ * NativeTypeRegistry holds the state produced by NativeTypeScanner and acts as a CelValueProvider
+ * and CelTypeProvider for the CEL runtime.
+ */
+ @VisibleForTesting
+ @Immutable
+ static final class NativeTypeRegistry implements CelValueProvider, CelTypeProvider {
+
+ private final ImmutableMap> classMap;
+ private final ImmutableMap typeMap;
+ private final ImmutableMap, StructType> classToTypeMap;
+ private final ImmutableMap, ImmutableMap> accessorMap;
+ private final NativeValueConverter converter;
+
+ private NativeTypeRegistry(NativeTypeScanner.ScanResult scanResult) {
+ this.classMap = scanResult.classMap;
+ this.typeMap = scanResult.typeMap;
+ this.classToTypeMap = scanResult.classToTypeMap;
+ this.accessorMap = scanResult.accessorMap;
+ this.converter = new NativeValueConverter(this);
+ }
+
+ @Override
+ public ImmutableList types() {
+ return ImmutableList.copyOf(typeMap.values());
+ }
+
+ @Override
+ public Optional findType(String typeName) {
+ return Optional.ofNullable(typeMap.get(typeName));
+ }
+
+ @Override
+ public Optional newValue(String typeName, Map fields) {
+ Class> clazz = classMap.get(typeName);
+ if (clazz == null) {
+ return Optional.empty();
+ }
+
+ try {
+ Constructor> constructor = clazz.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Object instance = constructor.newInstance();
+ ImmutableMap accessors = accessorMap.get(clazz);
+
+ for (Map.Entry entry : fields.entrySet()) {
+ PropertyAccessor accessor = accessors.get(entry.getKey());
+ if (accessor == null) {
+ throw new IllegalArgumentException(
+ "Unknown field: " + entry.getKey() + " for type " + typeName);
+ }
+ Object value =
+ converter.toNative(entry.getValue(), accessor.targetType, accessor.genericTargetType);
+ accessor.setValue(instance, value);
+ }
+
+ StructType structType = typeMap.get(typeName);
+ return Optional.of(new PojoStructValue(instance, accessors, structType));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ "Failed to create instance of "
+ + typeName
+ + ": No public no-argument constructor found.",
+ e);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create instance of " + typeName, e);
+ }
+ }
+
+ @Override
+ public CelValueConverter celValueConverter() {
+ return this.converter;
+ }
+ }
+
+ /**
+ * PropertyAccessor holds the compiled getter and setter for a property, along with its type
+ * information.
+ */
+ @Immutable
+ @SuppressWarnings("Immutable")
+ private static final class PropertyAccessor {
+ private final Function getter;
+ private final @Nullable BiConsumer setter;
+ private final Class> targetType;
+ private final @Nullable Type genericTargetType;
+
+ private PropertyAccessor(
+ Function getter,
+ @Nullable BiConsumer setter,
+ Class> targetType,
+ @Nullable Type genericTargetType) {
+ this.getter = getter;
+ this.setter = setter;
+ this.targetType = targetType;
+ this.genericTargetType = genericTargetType;
+ }
+
+ Object getValue(Object instance) {
+ return getter.apply(instance);
+ }
+
+ Object getDefaultValue() {
+ return getDefaultValue(targetType);
+ }
+
+ private static Object getDefaultValue(Class> targetType) {
+ Object defaultValue = JAVA_TO_DEFAULT_VALUE_MAP.get(targetType);
+ if (defaultValue != null) {
+ return defaultValue;
+ }
+ if (List.class.isAssignableFrom(targetType)) {
+ return ImmutableList.of();
+ }
+ if (Map.class.isAssignableFrom(targetType)) {
+ return ImmutableMap.of();
+ }
+
+ try {
+ return targetType.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ String.format(
+ "Failed to instantiate default instance for uninitialized field of type [%s]. "
+ + "Please ensure the class has a public no-argument constructor or is"
+ + " initialized.",
+ targetType.getName()),
+ e);
+ }
+ }
+
+ void setValue(Object instance, Object value) {
+ if (setter != null) {
+ setter.accept(instance, value);
+ } else {
+ throw new IllegalStateException("No setter found for property");
+ }
+ }
+ }
+
+ /** NativeValueConverter handles conversion between Java objects and CEL values. */
+ @Immutable
+ private static final class NativeValueConverter extends CelValueConverter {
+
+ private final NativeTypeRegistry registry;
+
+ private NativeValueConverter(NativeTypeRegistry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public Object toRuntimeValue(Object value) {
+ if (value instanceof CelValue) {
+ return super.toRuntimeValue(value);
+ }
+
+ if (registry.classToTypeMap.containsKey(value.getClass())) {
+ return new PojoStructValue(
+ value,
+ registry.accessorMap.get(value.getClass()),
+ registry.classToTypeMap.get(value.getClass()));
+ }
+
+ return super.toRuntimeValue(value);
+ }
+
+ Object toNative(Object value, Class> targetType, Type genericType) {
+ if (value instanceof CelValue && !StructValue.class.isAssignableFrom(targetType)) {
+ value = super.maybeUnwrap(value);
+ }
+ if (targetType == Optional.class) {
+ if (value instanceof Optional) {
+ return value;
+ }
+ return Optional.ofNullable(value);
+ }
+ if (targetType == UnsignedLong.class) {
+ if (value instanceof UnsignedLong) {
+ return value;
+ }
+ }
+ if (targetType == byte[].class && value instanceof CelByteString) {
+ return ((CelByteString) value).toByteArray();
+ }
+
+ if (List.class.isAssignableFrom(targetType) && value instanceof List) {
+ return convertListToNative((List>) value, targetType, genericType);
+ }
+
+ if (Map.class.isAssignableFrom(targetType) && value instanceof Map) {
+ return convertMapToNative((Map, ?>) value, targetType, genericType);
+ }
+
+ return downcastPrimitives(value, targetType);
+ }
+
+ // Safe reflection collection cast.
+ @SuppressWarnings("unchecked")
+ private Object convertListToNative(List> list, Class> targetType, Type genericType) {
+ TypeToken> token = TypeToken.of(genericType);
+ Type elementType = resolveGenericParameter(token, List.class, 0);
+ Class> componentType = ReflectionUtil.getRawType(elementType);
+
+ boolean isConcreteClass =
+ !targetType.isInterface() && !Modifier.isAbstract(targetType.getModifiers());
+
+ // Instantiates concrete collection types to prevent ClassCastExceptions.
+ // For example, if a POJO field is declared as a concrete implementation like
+ // ArrayList, assigning a Guava ImmutableList will fail at runtime due to type
+ // mismatch.
+ if (isConcreteClass) {
+ List concreteList;
+ try {
+ concreteList = (List) targetType.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Failed to instantiate concrete collection class for field target type: "
+ + targetType.getName(),
+ e);
+ }
+
+ for (Object element : list) {
+ concreteList.add(toNative(element, componentType, elementType));
+ }
+ return concreteList;
+ }
+
+ ImmutableList.Builder builder = null;
+ for (int i = 0; i < list.size(); i++) {
+ Object element = list.get(i);
+ Object converted = toNative(element, componentType, elementType);
+ if (!Objects.equals(converted, element) && builder == null) {
+ builder = ImmutableList.builderWithExpectedSize(list.size());
+ for (int j = 0; j < i; j++) {
+ builder.add(list.get(j));
+ }
+ }
+ if (builder != null) {
+ builder.add(converted);
+ }
+ }
+
+ if (builder == null) {
+ return list;
+ }
+ return builder.build();
+ }
+
+ // Safe reflection collection cast.
+ @SuppressWarnings("unchecked")
+ private Object convertMapToNative(Map, ?> map, Class> targetType, Type genericType) {
+ TypeToken> token = TypeToken.of(genericType);
+ Type keyType = resolveGenericParameter(token, Map.class, 0);
+ Type valueType = resolveGenericParameter(token, Map.class, 1);
+ Class> rawKeyType = ReflectionUtil.getRawType(keyType);
+ Class> rawValueType = ReflectionUtil.getRawType(valueType);
+
+ boolean isConcreteClass =
+ !targetType.isInterface() && !Modifier.isAbstract(targetType.getModifiers());
+
+ // Instantiates concrete map types to prevent ClassCastExceptions.
+ // For example, if a POJO field is declared as a concrete implementation like HashMap,
+ // assigning a Guava ImmutableMap will fail at runtime due to type mismatch.
+ if (isConcreteClass) {
+ Map concreteMap;
+ try {
+ concreteMap = (Map) targetType.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Failed to instantiate concrete map class for field target type: "
+ + targetType.getName(),
+ e);
+ }
+
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ concreteMap.put(
+ toNative(entry.getKey(), rawKeyType, keyType),
+ toNative(entry.getValue(), rawValueType, valueType));
+ }
+ return concreteMap;
+ }
+
+ ImmutableMap.Builder builder = null;
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ Object key = entry.getKey();
+ Object val = entry.getValue();
+ Object convertedKey = toNative(key, rawKeyType, keyType);
+ Object convertedVal = toNative(val, rawValueType, valueType);
+
+ if ((!Objects.equals(convertedKey, key) || !Objects.equals(convertedVal, val))
+ && builder == null) {
+ builder = ImmutableMap.builderWithExpectedSize(map.size());
+ for (Map.Entry, ?> prevEntry : map.entrySet()) {
+ if (Objects.equals(prevEntry.getKey(), entry.getKey())) {
+ break;
+ }
+ builder.put(prevEntry.getKey(), prevEntry.getValue());
+ }
+ }
+
+ if (builder != null) {
+ builder.put(convertedKey, convertedVal);
+ }
+ }
+
+ if (builder == null) {
+ return map;
+ }
+ return builder.buildOrThrow();
+ }
+
+ private Object downcastPrimitives(Object value, Class> targetType) {
+ Class> wrappedTargetType = Primitives.wrap(targetType);
+ if (wrappedTargetType == Integer.class && value instanceof Long) {
+ return ((Long) value).intValue();
+ }
+ if (wrappedTargetType == Float.class && value instanceof Double) {
+ return ((Double) value).floatValue();
+ }
+
+ return value;
+ }
+ }
+
+ /** PojoStructValue represents a native Java object as a CEL struct value. */
+ @SuppressWarnings("Immutable")
+ private static final class PojoStructValue extends StructValue {
+ private final Object instance;
+ private final ImmutableMap accessors;
+ private final StructType celType;
+
+ private PojoStructValue(
+ Object instance, ImmutableMap accessors, StructType celType) {
+ this.instance = instance;
+ this.accessors = accessors;
+ this.celType = celType;
+ }
+
+ @Override
+ public Object value() {
+ return instance;
+ }
+
+ @Override
+ public boolean isZeroValue() {
+ throw new UnsupportedOperationException(
+ "isZeroValue is unsupported for ordinary Java POJOs. Please implement StructValue"
+ + " directly on the backing class if zero-value trait support is required.");
+ }
+
+ @Override
+ public CelType celType() {
+ return celType;
+ }
+
+ @Override
+ public Object select(String field) {
+ // Intentionally not proxying `find` here to avoid Optional wrapper allocations.
+ PropertyAccessor accessor = accessors.get(field);
+ if (accessor != null) {
+ Object value = accessor.getValue(instance);
+ if (value == null) {
+ return accessor.getDefaultValue();
+ }
+ return value;
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field);
+ }
+
+ @Override
+ public Optional find(String field) {
+ PropertyAccessor accessor = accessors.get(field);
+ if (accessor == null) {
+ return Optional.empty();
+ }
+ Object value = accessor.getValue(instance);
+ if (value == null) {
+ return Optional.empty();
+ }
+ return Optional.of(value);
+ }
+ }
+
+ private static String getCelTypeName(Class> clazz) {
+ String canonicalName = clazz.getCanonicalName();
+ if (canonicalName == null) {
+ throw new IllegalArgumentException(
+ "Cannot get canonical name for class: "
+ + clazz.getName()
+ + ". Anonymous or local classes are not supported.");
+ }
+ return canonicalName;
+ }
+
+ private static Type resolveGenericParameter(TypeToken> token, Class> baseClass, int index) {
+ return token.resolveType(baseClass.getTypeParameters()[index]).getType();
+ }
+
+ private CelNativeTypesExtensions(NativeTypeRegistry registry) {
+ this.registry = registry;
+ }
+}
diff --git a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java
index a3777c759..87a31341f 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelOptionalLibrary.java
@@ -53,6 +53,7 @@
import dev.cel.common.types.TypeParamType;
import dev.cel.common.types.TypeType;
import dev.cel.common.values.CelByteString;
+import dev.cel.common.values.CelValue;
import dev.cel.common.values.NullValue;
import dev.cel.compiler.CelCompilerLibrary;
import dev.cel.parser.CelMacro;
@@ -415,9 +416,6 @@ private static ImmutableList elideOptionalCollection(Collection variables) throws Exception {
+ CelAbstractSyntaxTree ast = isParseOnly ? CEL.parse(expr).getAst() : CEL.compile(expr).getAst();
+ return CEL.createProgram(ast).eval(variables);
+ }
+
+ @Test
+ public void nativeTypes_createStructAndSelect() throws Exception {
+ Object result =
+ eval(
+ "TestAllTypesPublicFieldsPojo{boolVal:"
+ + " true, stringVal: 'hello'}.stringVal == 'hello'");
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_createNestedStruct() throws Exception {
+ Object result =
+ eval(
+ "TestAllTypesPublicFieldsPojo{nestedVal:"
+ + " TestNestedType{value:"
+ + " 'nested'}}.nestedVal.value == 'nested'");
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_resolveVariableWithNestedField() throws Exception {
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addVar(
+ "pojo",
+ StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName()))
+ .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS)
+ .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS)
+ .build();
+ CelAbstractSyntaxTree ast =
+ isParseOnly
+ ? cel.parse("pojo.nestedVal.value == 'nested'").getAst()
+ : cel.compile("pojo.nestedVal.value == 'nested'").getAst();
+ CelRuntime.Program program = cel.createProgram(ast);
+ TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo();
+ TestNestedType nested = new TestNestedType();
+ nested.value = "nested";
+ pojo.nestedVal = nested;
+
+ Object result = program.eval(ImmutableMap.of("pojo", pojo));
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_createStructWithComplexTypes() throws Exception {
+ assertThat(
+ eval(
+ "TestAllTypesPublicFieldsPojo{"
+ + " durationVal: duration('5s'),"
+ + " listVal: ['a', 'b'],"
+ + " mapVal: {'key': 'value'}"
+ + "}.durationVal == duration('5s')"))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_createStructWithOptionalField() throws Exception {
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addCompilerLibraries(
+ CelExtensions.nativeTypes(TestRefValFieldType.class), CelExtensions.optional())
+ .addRuntimeLibraries(
+ CelExtensions.nativeTypes(TestRefValFieldType.class), CelExtensions.optional())
+ .build();
+ CelAbstractSyntaxTree ast =
+ cel.parse(
+ "TestRefValFieldType{optionalName: optional.of('my name')}.optionalName.orValue('')"
+ + " == 'my name'")
+ .getAst();
+ CelRuntime.Program program = cel.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_createComprehensiveStruct() throws Exception {
+ String expr =
+ "ComprehensiveTestAllTypes{\n"
+ + " nestedVal: ComprehensiveTestNestedType{nestedMapVal: {1: false}},\n"
+ + " boolVal: true,\n"
+ + " bytesVal: b'hello',\n"
+ + " durationVal: duration('5s'),\n"
+ + " doubleVal: 1.5,\n"
+ + " floatVal: 2.5,\n"
+ + " int32Val: 10,\n"
+ + " int64Val: 20,\n"
+ + " stringVal: 'hello world',\n"
+ + " timestampVal: timestamp('2011-08-06T01:23:45Z'),\n"
+ + " uint32Val: 100,\n"
+ + " uint64Val: 200,\n"
+ + " listVal: [\n"
+ + " ComprehensiveTestNestedType{\n"
+ + " nestedListVal:['goodbye', 'cruel', 'world'],\n"
+ + " nestedMapVal: {42: true},\n"
+ + " customName: 'name'\n"
+ + " }\n"
+ + " ],\n"
+ + " arrayVal: [\n"
+ + " ComprehensiveTestNestedType{\n"
+ + " nestedListVal:['goodbye', 'cruel', 'world'],\n"
+ + " nestedMapVal: {42: true},\n"
+ + " customName: 'name'\n"
+ + " }\n"
+ + " ],\n"
+ + " mapVal: {'map-key': ComprehensiveTestAllTypes{boolVal: true}},\n"
+ + " customSliceVal: [TestNestedSliceType{value: 'none'}],\n"
+ + " customMapVal: {'even': TestMapVal{value: 'more'}},\n"
+ + " customName: 'name'\n"
+ + "}";
+
+ CelAbstractSyntaxTree ast = CEL.parse(expr).getAst();
+ CelRuntime.Program program = CEL.createProgram(ast);
+ Object result = program.eval();
+
+ // Construct expected output
+ ComprehensiveTestAllTypes expected = new ComprehensiveTestAllTypes();
+ expected.boolVal = true;
+ expected.bytesVal = "hello".getBytes(UTF_8);
+ expected.durationVal = Duration.ofSeconds(5);
+ expected.doubleVal = 1.5;
+ expected.floatVal = 2.5f;
+ expected.int32Val = 10;
+ expected.int64Val = 20;
+ expected.stringVal = "hello world";
+ expected.timestampVal = Instant.parse("2011-08-06T01:23:45Z");
+ expected.uint32Val = 100;
+ expected.uint64Val = 200;
+ expected.customName = "name";
+
+ ComprehensiveTestNestedType nested1 = new ComprehensiveTestNestedType();
+ nested1.nestedMapVal = ImmutableMap.of(1L, false);
+ expected.nestedVal = nested1;
+
+ ComprehensiveTestNestedType nested2 = new ComprehensiveTestNestedType();
+ nested2.nestedListVal = ImmutableList.of("goodbye", "cruel", "world");
+ nested2.nestedMapVal = ImmutableMap.of(42L, true);
+ nested2.customName = "name";
+ expected.listVal = ImmutableList.of(nested2);
+ expected.arrayVal = ImmutableList.of(nested2);
+
+ ComprehensiveTestAllTypes mapValElement = new ComprehensiveTestAllTypes();
+ mapValElement.boolVal = true;
+ expected.mapVal = ImmutableMap.of("map-key", mapValElement);
+
+ TestNestedSliceType sliceElem = new TestNestedSliceType();
+ sliceElem.value = "none";
+ expected.customSliceVal = ImmutableList.of(sliceElem);
+
+ TestMapVal mapValElem = new TestMapVal();
+ mapValElem.value = "more";
+ expected.customMapVal = ImmutableMap.of("even", mapValElem);
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ public void nativeTypes_staticErrors() throws Exception {
+ // undeclared reference
+ CelValidationException e =
+ assertThrows(CelValidationException.class, () -> CEL.compile("UnknownType{}").getAst());
+ assertThat(e).hasMessageThat().contains("reference");
+
+ // undefined field
+ e =
+ assertThrows(
+ CelValidationException.class,
+ () -> CEL.compile("ComprehensiveTestAllTypes{undefinedField: true}").getAst());
+ assertThat(e).hasMessageThat().contains("undefined field");
+ }
+
+ @Test
+ public void nativeTypes_anonymousClass_throwsException() {
+ Object anon = new Object() {};
+
+ Class> clazz = anon.getClass();
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> CelExtensions.nativeTypes(clazz));
+ assertThat(exception).hasMessageThat().contains("Anonymous or local classes are not supported");
+ }
+
+ @Test
+ public void nativeTypes_createStruct_privateConstructor() throws Exception {
+ Object result = eval("TestPrivateConstructorPojo{value:" + " 'hello'}");
+
+ assertThat(result).isInstanceOf(TestPrivateConstructorPojo.class);
+ assertThat(((TestPrivateConstructorPojo) result).value).isEqualTo("hello");
+ }
+
+ @Test
+ public void nativeTypes_precedence_getterOverField() throws Exception {
+ assertThat(eval("TestPrecedencePojo{}.value")).isEqualTo("hello");
+ }
+
+ @Test
+ public void nativeTypes_protoPrecedence() throws Exception {
+ CelValueProvider customProvider =
+ (structType, fields) -> {
+ if (structType.equals("cel.expr.conformance.proto3.TestAllTypes")) {
+ return Optional.of("POJO_WINS");
+ }
+ return Optional.empty();
+ };
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .setValueProvider(customProvider)
+ .addMessageTypes(TestAllTypes.getDescriptor())
+ .build();
+ CelAbstractSyntaxTree ast = cel.compile("cel.expr.conformance.proto3.TestAllTypes{}").getAst();
+
+ Object result = cel.createProgram(ast).eval();
+
+ assertThat(result).isNotEqualTo("POJO_WINS");
+ assertThat(result).isInstanceOf(TestAllTypes.class);
+ }
+
+ @Test
+ public void nativeTypes_createWithSetterAndSelectWithGetter() throws Exception {
+ assertThat(eval("TestGetterSetterPojo{value: 'hello', active: true}.value == 'hello'"))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_missingNoArgConstructor_throws() throws Exception {
+ CelEvaluationException exception =
+ assertThrows(
+ CelEvaluationException.class,
+ () -> eval("TestMissingNoArgConstructorPojo{value: 'hello'}"));
+
+ assertThat(exception).hasMessageThat().contains("No public no-argument constructor found");
+ }
+
+ @Test
+ public void nativeTypes_createWithDeepConversion() throws Exception {
+ Object result = eval("TestDeepConversionPojo{ints: [1, 2], floats: {'a': 1.0, 'b': 2.0}}");
+
+ assertThat(result).isInstanceOf(TestDeepConversionPojo.class);
+ TestDeepConversionPojo pojo = (TestDeepConversionPojo) result;
+ assertThat(pojo.ints.get(0)).isEqualTo(1);
+ assertThat(pojo.floats).containsEntry("a", 1.0f);
+ }
+
+ @Test
+ public void nativeTypes_wildcardList_success() throws Exception {
+ assertThat(eval("TestWildcardPojo{values: ['hello']}.values[0] == 'hello'")).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_unsupportedTypeSet_throwsOnRegistration() throws Exception {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CelExtensions.nativeTypes(TestUnsupportedSetPojo.class));
+ assertThat(e).hasMessageThat().contains("Unsupported type for property 'strings'");
+ }
+
+ @Test
+ public void nativeTypes_arrayType_throwsOnRegistration() throws Exception {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class, () -> CelExtensions.nativeTypes(TestArrayPojo.class));
+ assertThat(e).hasMessageThat().contains("Unsupported type for property 'values'");
+ }
+
+ @Test
+ public void nativeTypes_packagePrivateClass_fieldAccess_success() throws Exception {
+ assertThat(eval("TestPackagePrivatePojo{value: 'hello'}.value == 'hello'")).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_packagePrivateClass_methodAccess_success() throws Exception {
+ assertThat(eval("TestPackagePrivateWithGetterPojo{value: 'hello'}.value == 'hello'"))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_privateField_notExposed() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestPrivateFieldPojo.class);
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+
+ CelValidationException e =
+ assertThrows(
+ CelValidationException.class,
+ () -> compiler.compile("TestPrivateFieldPojo{secret: 'hello'}").getAst());
+ assertThat(e).hasMessageThat().contains("undefined field");
+ }
+
+ @Test
+ public void nativeTypes_inheritance_success() throws Exception {
+ // Accessing child's prefix-less getter
+ assertThat(eval("TestChildPojo{}.childValue")).isEqualTo("child");
+ // Accessing parent's standard getter
+ assertThat(eval("TestChildPojo{}.standardValue")).isEqualTo("standard");
+ // Accessing parent's prefix-less getter
+ assertThat(eval("TestChildPojo{}.parentValue")).isEqualTo("parent");
+ }
+
+ @Test
+ public void nativeTypes_standardType_cannotBeConstructedAsStruct() throws Exception {
+ CelValidationException e =
+ assertThrows(
+ CelValidationException.class, () -> CEL.compile("java.lang.String{}").getAst());
+ assertThat(e).hasMessageThat().contains("undeclared reference");
+ }
+
+ @Test
+ public void nativeTypes_doubleMapKey_throwsOnRegistration() throws Exception {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CelExtensions.nativeTypes(TestDoubleMapKeyPojo.class));
+ assertThat(e).hasCauseThat().hasMessageThat().contains("Decimals are not allowed as map keys");
+ }
+
+ @Test
+ public void nativeTypes_optionalCustomStruct_registered() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestOptionalUrlPojo.class);
+ CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry();
+
+ Optional type = registry.findType(TestURLPojo.class.getCanonicalName());
+
+ assertThat(type).isPresent();
+ }
+
+ @Test
+ public void nativeTypes_abstractClass_throwsOnConstruction() throws Exception {
+ CelAbstractSyntaxTree ast = CEL.parse("TestAbstractPojo{}").getAst();
+ CelRuntime.Program program = CEL.createProgram(ast);
+
+ CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> program.eval());
+ assertThat(e).hasMessageThat().contains("Failed to create instance of");
+ assertThat(e).hasCauseThat().isInstanceOf(InstantiationException.class);
+ }
+
+ @Test
+ public void nativeTypes_nestedList_registered() throws Exception {
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class);
+ CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry();
+
+ Optional type =
+ registry.findType(TestAllTypesPublicFieldsPojo.class.getCanonicalName());
+
+ assertThat(type).isPresent();
+ StructType structType = (StructType) type.get();
+ assertThat(structType.findField("nestedListVal")).isPresent();
+ }
+
+ @Test
+ public void nativeTypes_invalidGetters_notRegistered() throws Exception {
+ ImmutableSet properties =
+ CelNativeTypesExtensions.NativeTypeScanner.getProperties(
+ TestAllTypesPublicFieldsPojo.class);
+
+ assertThat(properties).doesNotContain("invalidParam");
+ assertThat(properties).doesNotContain("invalidString");
+ }
+
+ @Test
+ public void nativeTypes_celByteString_success() throws Exception {
+ assertThat(eval("TestAllTypesPublicFieldsPojo{}.celBytesVal" + " == b'\\x01\\x02\\x03'"))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_celByteString_construction_success() throws Exception {
+ assertThat(
+ eval(
+ "dev.cel.extensions.CelNativeTypesExtensionsTest.TestAllTypesPublicFieldsPojo{celBytesVal:"
+ + " b'\\x01\\x02\\x03'}.celBytesVal == b'\\x01\\x02\\x03'"))
+ .isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_singleLetterGetter_success() throws Exception {
+ Object result = eval("TestAllTypesPublicFieldsPojo{}.a == 'a'");
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_getterNamedGet_rejected() throws Exception {
+ CelValidationException e =
+ assertThrows(
+ CelValidationException.class,
+ () -> CEL.compile("TestAllTypesPublicFieldsPojo{}.get").getAst());
+ assertThat(e).hasMessageThat().contains("undefined field 'get'");
+ }
+
+ @Test
+ public void nativeTypes_circularReference_success() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestCircularA.class);
+ CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry();
+
+ Optional typeA = registry.findType(TestCircularA.class.getCanonicalName());
+ Optional typeB = registry.findType(TestCircularB.class.getCanonicalName());
+
+ assertThat(typeA).isPresent();
+ assertThat(typeB).isPresent();
+ }
+
+ @Test
+ public void nativeTypes_specialDecapitalization_success() throws Exception {
+ Object result = eval("dev.cel.extensions.CelNativeTypesExtensionsTest.TestURLPojo{}.URL");
+
+ assertThat(result).isEqualTo("https://google.com");
+ }
+
+ @Test
+ public void nativeTypes_prefixLessGetter_success() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestPrefixLessGetterPojo.class);
+ CelRuntime celRuntime =
+ CelRuntimeFactory.plannerRuntimeBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelAbstractSyntaxTree ast =
+ celCompiler
+ .compile(
+ "dev.cel.extensions.CelNativeTypesExtensionsTest.TestPrefixLessGetterPojo{}.value")
+ .getAst();
+ CelRuntime.Program program = celRuntime.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo("hello");
+ }
+
+ @Test
+ public void nativeTypes_isGetter_success() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestGetterSetterPojo.class);
+ CelRuntime celRuntime =
+ CelRuntimeFactory.plannerRuntimeBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelAbstractSyntaxTree ast =
+ celCompiler
+ .compile(
+ "dev.cel.extensions.CelNativeTypesExtensionsTest.TestGetterSetterPojo{active:"
+ + " true}.active")
+ .getAst();
+ CelRuntime.Program program = celRuntime.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isEqualTo(true);
+ }
+
+ @Test
+ public void nativeTypes_selectUndefinedField_parsedOnly_throwsException() throws Exception {
+
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class);
+
+ CelRuntime celRuntime =
+ CelRuntimeFactory.plannerRuntimeBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+
+ CelAbstractSyntaxTree ast = celCompiler.parse("pojo.undefinedField").getAst();
+ CelRuntime.Program program = celRuntime.createProgram(ast);
+
+ TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo();
+
+ CelEvaluationException e =
+ assertThrows(
+ CelEvaluationException.class, () -> program.eval(ImmutableMap.of("pojo", pojo)));
+ assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class);
+ }
+
+ @Test
+ public void nativeTypes_createWithUint_fromUnsignedLong() throws Exception {
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class);
+ CelRuntime celRuntime =
+ CelRuntimeFactory.plannerRuntimeBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelCompiler celCompiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+ CelAbstractSyntaxTree ast =
+ celCompiler
+ .compile(
+ "dev.cel.extensions.CelNativeTypesExtensionsTest.TestAllTypesPublicFieldsPojo{uintVal:"
+ + " 42u}")
+ .getAst();
+ CelRuntime.Program program = celRuntime.createProgram(ast);
+
+ Object result = program.eval();
+
+ assertThat(result).isInstanceOf(TestAllTypesPublicFieldsPojo.class);
+ TestAllTypesPublicFieldsPojo pojo = (TestAllTypesPublicFieldsPojo) result;
+ assertThat(pojo.uintVal).isEqualTo(UnsignedLong.fromLongBits(42L));
+ }
+
+ @Test
+ public void nativeTypes_mapJavaTypeToCelType_allSupportedTypes() throws Exception {
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class);
+ CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry();
+
+ Optional type =
+ registry.findType(TestAllTypesPublicFieldsPojo.class.getCanonicalName());
+
+ assertThat(type).isPresent();
+ assertThat(type.get()).isInstanceOf(StructType.class);
+ StructType structType = (StructType) type.get();
+
+ assertThat(structType.findField("boolVal").map(StructType.Field::type))
+ .hasValue(SimpleType.BOOL);
+ assertThat(structType.findField("boolObjVal").map(StructType.Field::type))
+ .hasValue(SimpleType.BOOL);
+ assertThat(structType.findField("int32Val").map(StructType.Field::type))
+ .hasValue(SimpleType.INT);
+ assertThat(structType.findField("intObjVal").map(StructType.Field::type))
+ .hasValue(SimpleType.INT);
+ assertThat(structType.findField("int64Val").map(StructType.Field::type))
+ .hasValue(SimpleType.INT);
+ assertThat(structType.findField("longObjVal").map(StructType.Field::type))
+ .hasValue(SimpleType.INT);
+ assertThat(structType.findField("uintVal").map(StructType.Field::type))
+ .hasValue(SimpleType.UINT);
+ assertThat(structType.findField("floatVal").map(StructType.Field::type))
+ .hasValue(SimpleType.DOUBLE);
+ assertThat(structType.findField("floatObjVal").map(StructType.Field::type))
+ .hasValue(SimpleType.DOUBLE);
+ assertThat(structType.findField("doubleVal").map(StructType.Field::type))
+ .hasValue(SimpleType.DOUBLE);
+ assertThat(structType.findField("doubleObjVal").map(StructType.Field::type))
+ .hasValue(SimpleType.DOUBLE);
+ assertThat(structType.findField("stringVal").map(StructType.Field::type))
+ .hasValue(SimpleType.STRING);
+ assertThat(structType.findField("bytesVal").map(StructType.Field::type))
+ .hasValue(SimpleType.BYTES);
+ assertThat(structType.findField("durationVal").map(StructType.Field::type))
+ .hasValue(SimpleType.DURATION);
+ assertThat(structType.findField("timestampVal").map(StructType.Field::type))
+ .hasValue(SimpleType.TIMESTAMP);
+
+ assertThat(structType.findField("listVal").map(StructType.Field::type).get())
+ .isInstanceOf(ListType.class);
+ ListType listType =
+ (ListType) structType.findField("listVal").map(StructType.Field::type).get();
+ assertThat(listType.elemType()).isEqualTo(SimpleType.STRING);
+
+ assertThat(structType.findField("mapIntVal").map(StructType.Field::type).get())
+ .isInstanceOf(MapType.class);
+ MapType mapType = (MapType) structType.findField("mapIntVal").map(StructType.Field::type).get();
+ assertThat(mapType.keyType()).isEqualTo(SimpleType.STRING);
+ assertThat(mapType.valueType()).isEqualTo(SimpleType.INT);
+
+ assertThat(structType.findField("optionalVal").map(StructType.Field::type).get())
+ .isInstanceOf(OptionalType.class);
+ OptionalType optionalType =
+ (OptionalType) structType.findField("optionalVal").map(StructType.Field::type).get();
+ assertThat(optionalType.parameters().get(0)).isEqualTo(SimpleType.STRING);
+ }
+
+ @Test
+ public void nativeTypes_mapJavaTypeToCelType_customCollectionSubclasses() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestCustomCollectionPojo.class);
+ CelNativeTypesExtensions.NativeTypeRegistry registry = extensions.getRegistry();
+
+ Optional type = registry.findType(TestCustomCollectionPojo.class.getCanonicalName());
+ StructType structType = (StructType) type.get();
+
+ assertThat(structType.findField("customList").map(StructType.Field::type))
+ .hasValue(ListType.create(SimpleType.STRING));
+ assertThat(structType.findField("customMap").map(StructType.Field::type))
+ .hasValue(MapType.create(SimpleType.STRING, SimpleType.INT));
+ }
+
+ @Test
+ public void nativeTypes_objectMethods_notExposed() throws Exception {
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestAllTypesPublicFieldsPojo.class);
+ CelCompiler compiler =
+ CelCompilerFactory.standardCelCompilerBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addLibraries(extensions)
+ .build();
+
+ CelValidationException e =
+ assertThrows(
+ CelValidationException.class,
+ () -> compiler.compile("TestAllTypesPublicFieldsPojo{}.toString").getAst());
+ assertThat(e).hasMessageThat().contains("undefined field");
+ }
+
+ @Test
+ public void nativeTypes_nullSafeTraversal() throws Exception {
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS)
+ .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS)
+ .addVar(
+ "pojo",
+ StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName()))
+ .build();
+
+ TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo();
+ ImmutableMap vars = ImmutableMap.of("pojo", pojo);
+
+ assertThat(cel.createProgram(cel.compile("pojo.stringVal").getAst()).eval(vars)).isEqualTo("");
+ assertThat(cel.createProgram(cel.compile("pojo.int64Val").getAst()).eval(vars)).isEqualTo(0L);
+ assertThat(cel.createProgram(cel.compile("pojo.nestedVal.value").getAst()).eval(vars))
+ .isEqualTo("");
+ CelAbstractSyntaxTree abstractPojoAst = cel.compile("pojo.abstractPojo.value").getAst();
+ CelRuntime.Program abstractPojoProgram = cel.createProgram(abstractPojoAst);
+ CelEvaluationException e =
+ assertThrows(CelEvaluationException.class, () -> abstractPojoProgram.eval(vars));
+ assertThat(e).hasMessageThat().contains("Failed to instantiate default instance");
+ }
+
+ @Test
+ public void nativeTypes_presenceTest() throws Exception {
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addCompilerLibraries(NATIVE_TYPE_EXTENSIONS)
+ .addRuntimeLibraries(NATIVE_TYPE_EXTENSIONS)
+ .setStandardMacros(CelStandardMacro.STANDARD_MACROS)
+ .addVar(
+ "pojo",
+ StructTypeReference.create(TestAllTypesPublicFieldsPojo.class.getCanonicalName()))
+ .build();
+
+ TestAllTypesPublicFieldsPojo pojo = new TestAllTypesPublicFieldsPojo();
+ ImmutableMap nullVars = ImmutableMap.of("pojo", pojo);
+
+ TestAllTypesPublicFieldsPojo pojoWithValues = new TestAllTypesPublicFieldsPojo();
+ pojoWithValues.stringVal = "hello";
+ ImmutableMap valueVars = ImmutableMap.of("pojo", pojoWithValues);
+
+ boolean hasPopulatedString =
+ (boolean) cel.createProgram(cel.compile("has(pojo.stringVal)").getAst()).eval(valueVars);
+ assertThat(hasPopulatedString).isTrue();
+
+ boolean hasNullString =
+ (boolean) cel.createProgram(cel.compile("has(pojo.stringVal)").getAst()).eval(nullVars);
+ assertThat(hasNullString).isFalse();
+
+ assertThrows(
+ CelValidationException.class, () -> cel.compile("has(pojo.nonExistentField)").getAst());
+ }
+
+ @Test
+ public void nativeTypes_customStructValue_optionalOfNonZeroValue() throws Exception {
+ CelNativeTypesExtensions extensions =
+ CelExtensions.nativeTypes(TestCustomStructValuePojo.class);
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addCompilerLibraries(extensions, CelExtensions.optional())
+ .addRuntimeLibraries(extensions, CelExtensions.optional())
+ .addVar(
+ "pojo",
+ StructTypeReference.create(TestCustomStructValuePojo.class.getCanonicalName()))
+ .build();
+
+ TestCustomStructValuePojo emptyPojo =
+ new TestCustomStructValuePojo(ImmutableMap.of("value", ""));
+ ImmutableMap emptyVars = ImmutableMap.of("pojo", emptyPojo);
+ boolean isEmptyNone =
+ (boolean)
+ cel.createProgram(cel.compile("!optional.ofNonZeroValue(pojo).hasValue()").getAst())
+ .eval(emptyVars);
+ assertThat(isEmptyNone).isTrue();
+
+ TestCustomStructValuePojo populatedPojo =
+ new TestCustomStructValuePojo(ImmutableMap.of("value", "hello"));
+ ImmutableMap populatedVars = ImmutableMap.of("pojo", populatedPojo);
+ boolean isPopulatedPresent =
+ (boolean)
+ cel.createProgram(cel.compile("optional.ofNonZeroValue(pojo).hasValue()").getAst())
+ .eval(populatedVars);
+ assertThat(isPopulatedPresent).isTrue();
+ }
+
+ @Test
+ public void nativeTypes_staticMembers_skipped() throws Exception {
+ ImmutableSet properties =
+ CelNativeTypesExtensions.NativeTypeScanner.getProperties(TestStaticMembersPojo.class);
+
+ assertThat(properties).contains("instanceField");
+ assertThat(properties).doesNotContain("STATIC_FIELD");
+ assertThat(properties).doesNotContain("staticGetter");
+ assertThat(properties).doesNotContain("staticProperty");
+ }
+
+ @Test
+ public void nativeTypes_deeplyNestedGenerics_discovered() throws Exception {
+ CelNativeTypesExtensions extensions = CelExtensions.nativeTypes(TestNestedGenericsPojo.class);
+ Cel cel =
+ CelFactory.plannerCelBuilder()
+ .setContainer(CelContainer.ofName("dev.cel.extensions.CelNativeTypesExtensionsTest"))
+ .addCompilerLibraries(extensions)
+ .addRuntimeLibraries(extensions)
+ .addVar(
+ "pojo", StructTypeReference.create(TestNestedGenericsPojo.class.getCanonicalName()))
+ .build();
+
+ TestNestedSimplePojo simplePojo = new TestNestedSimplePojo();
+ TestNestedGenericsPojo pojo = new TestNestedGenericsPojo();
+ pojo.nestedList = ImmutableList.of(ImmutableList.of(simplePojo));
+
+ boolean result =
+ (boolean)
+ cel.createProgram(cel.compile("pojo.nestedList[0][0].value == 'nested'").getAst())
+ .eval(ImmutableMap.of("pojo", pojo));
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void nativeTypes_concreteCollectionInstantiation_success() throws Exception {
+ TestCustomCollectionPojo result =
+ (TestCustomCollectionPojo)
+ eval("TestCustomCollectionPojo{customList: ['a', 'b'], customMap: {'key': 1}}");
+
+ assertThat(result).isNotNull();
+ assertThat(result.customList).containsExactly("a", "b");
+ assertThat(result.customMap).containsEntry("key", 1L);
+ }
+
+ @Test
+ public void nativeTypes_getterFieldTypeMismatch_readOnly() throws Exception {
+ CelAbstractSyntaxTree ast =
+ CEL.compile("TestGetterFieldTypeMismatchPojo{mismatchField: 'hello'}").getAst();
+
+ CelRuntime.Program program = CEL.createProgram(ast);
+ CelEvaluationException exception =
+ assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of()));
+
+ assertThat(exception.getMessage()).contains("Failed to create instance");
+ }
+
+ public static class TestAllTypesPublicFieldsPojo {
+ public void doNothing() {}
+
+ public String getA() {
+ return "a";
+ }
+
+ public String get() {
+ return "get";
+ }
+
+ public boolean boolVal;
+ public String stringVal;
+ public long int64Val;
+ public int int32Val;
+ public double doubleVal;
+ public float floatVal;
+ public byte[] bytesVal;
+ public Duration durationVal;
+ public Instant timestampVal;
+ public TestNestedType nestedVal;
+ public List listVal;
+ public Map mapVal;
+
+ public Boolean boolObjVal;
+ public Integer intObjVal;
+ public Long longObjVal;
+ public UnsignedLong uintVal;
+ public Float floatObjVal;
+ public Double doubleObjVal;
+ public Optional optionalVal;
+ public Optional optionalNestedVal;
+ public Map mapIntVal;
+ public List> nestedListVal;
+ public CelByteString celBytesVal = CelByteString.of(new byte[] {1, 2, 3});
+ public TestAbstractPojo abstractPojo;
+
+ public String getInvalidParam(String param) {
+ return "invalid";
+ }
+
+ public String isInvalidString() {
+ return "invalid";
+ }
+ }
+
+ public static class TestNestedType {
+ public String value;
+ }
+
+ static class TestPackagePrivatePojo {
+ public String value;
+ }
+
+ static class TestPackagePrivateWithGetterPojo {
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+ }
+
+ public static class TestPrivateConstructorPojo {
+ public String value;
+
+ private TestPrivateConstructorPojo() {
+ this.value = "default";
+ }
+ }
+
+ public static class TestPrecedencePojo {
+ public int value = 1;
+
+ public String getValue() {
+ return "hello";
+ }
+ }
+
+ static final class TestGetterSetterPojo {
+ private String value;
+ private boolean active;
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+ }
+
+ public static final class TestUnsupportedSetPojo {
+ public Set strings;
+ }
+
+ public static final class TestDeepConversionPojo {
+ public List ints;
+ public Map floats;
+ }
+
+ public static final class TestMissingNoArgConstructorPojo {
+ public String value;
+
+ public TestMissingNoArgConstructorPojo(String value) {
+ this.value = value;
+ }
+ }
+
+ public static class TestRefValFieldType {
+ public Optional optionalName;
+ public int intVal;
+ public Instant time;
+ }
+
+ public static class ComprehensiveTestNestedType {
+ public List nestedListVal;
+ public Map nestedMapVal;
+ public String customName;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ComprehensiveTestNestedType)) {
+ return false;
+ }
+ ComprehensiveTestNestedType that = (ComprehensiveTestNestedType) o;
+ return Objects.equals(nestedListVal, that.nestedListVal)
+ && Objects.equals(nestedMapVal, that.nestedMapVal)
+ && Objects.equals(customName, that.customName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nestedListVal, nestedMapVal, customName);
+ }
+ }
+
+ public static class TestNestedSliceType {
+ public String value;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TestNestedSliceType)) {
+ return false;
+ }
+ TestNestedSliceType that = (TestNestedSliceType) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+ }
+
+ public static class TestMapVal {
+ public String value;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TestMapVal)) {
+ return false;
+ }
+ TestMapVal that = (TestMapVal) o;
+ return Objects.equals(value, that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+ }
+
+ public static class ComprehensiveTestAllTypes {
+ public ComprehensiveTestNestedType nestedVal;
+ public ComprehensiveTestNestedType nestedStructVal;
+ public boolean boolVal;
+ public byte[] bytesVal;
+ public Duration durationVal;
+ public double doubleVal;
+ public float floatVal;
+ public int int32Val;
+ public long int64Val;
+ public String stringVal;
+ public Instant timestampVal;
+ public long uint32Val;
+ public long uint64Val;
+ public List listVal;
+ public List arrayVal;
+ public byte[] bytesArrayVal;
+ public Map mapVal;
+ public List customSliceVal;
+ public Map customMapVal;
+ public String customName;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ComprehensiveTestAllTypes)) {
+ return false;
+ }
+ ComprehensiveTestAllTypes that = (ComprehensiveTestAllTypes) o;
+ return boolVal == that.boolVal
+ && doubleVal == that.doubleVal
+ && floatVal == that.floatVal
+ && int32Val == that.int32Val
+ && int64Val == that.int64Val
+ && uint32Val == that.uint32Val
+ && uint64Val == that.uint64Val
+ && Objects.equals(nestedVal, that.nestedVal)
+ && Objects.equals(nestedStructVal, that.nestedStructVal)
+ && Arrays.equals(bytesVal, that.bytesVal)
+ && Objects.equals(durationVal, that.durationVal)
+ && Objects.equals(stringVal, that.stringVal)
+ && Objects.equals(timestampVal, that.timestampVal)
+ && Objects.equals(listVal, that.listVal)
+ && Objects.equals(arrayVal, that.arrayVal)
+ && Arrays.equals(bytesArrayVal, that.bytesArrayVal)
+ && Objects.equals(mapVal, that.mapVal)
+ && Objects.equals(customSliceVal, that.customSliceVal)
+ && Objects.equals(customMapVal, that.customMapVal)
+ && Objects.equals(customName, that.customName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result =
+ Objects.hash(
+ nestedVal,
+ nestedStructVal,
+ boolVal,
+ durationVal,
+ doubleVal,
+ floatVal,
+ int32Val,
+ int64Val,
+ stringVal,
+ timestampVal,
+ uint32Val,
+ uint64Val,
+ listVal,
+ arrayVal,
+ mapVal,
+ customSliceVal,
+ customMapVal,
+ customName);
+ result = 31 * result + Arrays.hashCode(bytesVal);
+ result = 31 * result + Arrays.hashCode(bytesArrayVal);
+ return result;
+ }
+ }
+
+ public static final class TestPrivateFieldPojo {
+ // Intentionally unread to test private fields are not exposed
+ @SuppressWarnings("UnusedVariable")
+ private String secret;
+ }
+
+ public static class TestPrefixLessGetterPojo {
+ private String value = "hello";
+
+ public String value() {
+ return value;
+ }
+ }
+
+ public static class TestParentPojo {
+ private String parentValue = "parent";
+ private String standardValue = "standard";
+
+ public String parentValue() {
+ return parentValue;
+ }
+
+ public String getStandardValue() {
+ return standardValue;
+ }
+ }
+
+ public static class TestChildPojo extends TestParentPojo {
+ private String childValue = "child";
+
+ public String childValue() {
+ return childValue;
+ }
+ }
+
+ // Intentionally violating style guide to test special decapitalization.
+ @SuppressWarnings("IdentifierName")
+ public static class TestURLPojo {
+ public String getURL() {
+ return "https://google.com";
+ }
+ }
+
+ public static class TestDoubleMapKeyPojo {
+ public Map map;
+ }
+
+ public static class TestWildcardPojo {
+ public List extends String> values;
+ }
+
+ public static class TestArrayPojo {
+ public String[] values;
+ }
+
+ public static class TestOptionalUrlPojo {
+ public Optional optionalUrl;
+ }
+
+ public abstract static class TestAbstractPojo {
+ public String value;
+ }
+
+ public static class TestCircularA {
+ public TestCircularB b;
+ }
+
+ public static class TestCircularB {
+ public TestCircularA a;
+ }
+
+ public static class CustomListImplementation extends ArrayList {}
+
+ public static class CustomMapImplementation extends HashMap {}
+
+ public static class TestCustomCollectionPojo {
+ public CustomListImplementation customList;
+ public CustomMapImplementation customMap;
+ }
+
+ @SuppressWarnings("Immutable")
+ static final class TestCustomStructValuePojo extends StructValue {
+ private final ImmutableMap fields;
+
+ public TestCustomStructValuePojo(ImmutableMap fields) {
+ this.fields = fields;
+ }
+
+ @Override
+ public Object value() {
+ return this;
+ }
+
+ @Override
+ public boolean isZeroValue() {
+ for (Object val : fields.values()) {
+ if (val != null && !val.equals("") && !val.equals(0L)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public CelType celType() {
+ return StructTypeReference.create(TestCustomStructValuePojo.class.getCanonicalName());
+ }
+
+ @Override
+ public Optional find(String field) {
+ return Optional.ofNullable(fields.get(field));
+ }
+
+ @Override
+ public Object select(String field) {
+ Object val = fields.get(field);
+ if (val == null) {
+ throw new NoSuchElementException("Field not found: " + field);
+ }
+ return val;
+ }
+ }
+
+ public static class TestStaticMembersPojo {
+ public static final String STATIC_FIELD = "static_value";
+
+ public static String getStaticGetter() {
+ return "static_getter_value";
+ }
+
+ public static String staticProperty() {
+ return "static_property_value";
+ }
+
+ public String instanceField = "instance_value";
+ }
+
+ public static class TestNestedGenericsPojo {
+ public List> nestedList;
+ public Map> nestedMap;
+ }
+
+ public static class TestNestedSimplePojo {
+ public String value = "nested";
+ }
+
+ public static class TestGetterFieldTypeMismatchPojo {
+ public int mismatchField = 10;
+
+ public String getMismatchField() {
+ return "mismatch";
+ }
+ }
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java
index 0000ad764..561e25f7f 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java
+++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java
@@ -192,7 +192,9 @@ private static Object applyQualifiers(
// Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated
for (int i = 0; i < qualifiers.size(); i++) {
- obj = qualifiers.get(i).qualify(obj);
+ Qualifier element = qualifiers.get(i);
+ obj = element.qualify(obj);
+ obj = celValueConverter.toRuntimeValue(obj);
}
return celValueConverter.maybeUnwrap(obj);
diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java
index addbeb4d0..38f733c79 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java
+++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java
@@ -42,7 +42,9 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) {
// Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated
for (int i = 0; i < qualifiers.size(); i++) {
- obj = qualifiers.get(i).qualify(obj);
+ Qualifier element = qualifiers.get(i);
+ obj = element.qualify(obj);
+ obj = celValueConverter.toRuntimeValue(obj);
}
return celValueConverter.maybeUnwrap(obj);