typeAlia
}
};
}
+
+ private static Cel setupEnv(CelBuilder celBuilder) {
+ ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
+ SingleFileExtensionsProto.registerAllExtensions(extensionRegistry);
+ return celBuilder
+ .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
+ .addMessageTypes(SingleFile.getDescriptor())
+ .addFileTypes(SingleFileExtensionsProto.getDescriptor())
+ .addCompilerLibraries(CelExtensions.protos())
+ .setExtensionRegistry(extensionRegistry)
+ .setOptions(
+ CelOptions.current()
+ .enableJsonFieldNames(true)
+ .enableHeterogeneousNumericComparisons(true)
+ .enableQuotedIdentifierSyntax(true)
+ .build())
+ .build();
+ }
}
diff --git a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
index c0c54381d..92a70c2d6 100644
--- a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
+++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java
@@ -63,7 +63,7 @@ public void toCheckerBuilder_isImmutable() {
public void toCheckerBuilder_singularFields_copied() {
CelStandardDeclarations subsetDecls =
CelStandardDeclarations.newBuilder().includeFunctions(StandardFunction.BOOL).build();
- CelOptions celOptions = CelOptions.current().enableTimestampEpoch(true).build();
+ CelOptions celOptions = CelOptions.current().build();
CelContainer celContainer = CelContainer.ofName("foo");
CelType expectedResultType = SimpleType.BOOL;
CelTypeProvider customTypeProvider =
diff --git a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
index d5d5d9a3a..846201d32 100644
--- a/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
+++ b/checker/src/test/java/dev/cel/checker/ExprCheckerTest.java
@@ -517,6 +517,71 @@ public void jsonType() throws Exception {
runTest();
}
+ @Test
+ public void jsonTypeNullConstruction() throws Exception {
+ // Ok
+ source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+
+ // Error
+ source = "google.protobuf.Value{null_value: null}";
+ runTest();
+
+ // Ok
+ source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}";
+ runTest();
+
+ // Ok but not expected (int coerced to double/json number 0.0)
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{single_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+
+ // Error
+ source = "cel.expr.conformance.proto3.TestAllTypes{null_value: null}";
+ runTest();
+
+ // Ok
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}";
+ runTest();
+ }
+
+ @Test
+ public void jsonTypeNullAccess() throws Exception {
+ source = "google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null";
+ runTest();
+
+ source = "cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null";
+ runTest();
+
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{single_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.single_value == null";
+ runTest();
+
+ // Error
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.null_value == null";
+ runTest();
+
+ // Ok
+ source =
+ "cel.expr.conformance.proto3.TestAllTypes{null_value:"
+ + " google.protobuf.NullValue.NULL_VALUE}.null_value == 0";
+ runTest();
+
+ // Error
+ source = "google.protobuf.NullValue.NULL_VALUE == null";
+ runTest();
+
+ // Ok
+ source = "google.protobuf.NullValue.NULL_VALUE == 0";
+ runTest();
+ }
+
// Call Style and User Functions
// =============================
diff --git a/checker/src/test/resources/jsonTypeNullAccess.baseline b/checker/src/test/resources/jsonTypeNullAccess.baseline
new file mode 100644
index 000000000..834b8fde8
--- /dev/null
+++ b/checker/src/test/resources/jsonTypeNullAccess.baseline
@@ -0,0 +1,54 @@
+Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE} == null
+=====>
+_==_(
+ google.protobuf.Value{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~dyn^google.protobuf.Value,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null}.single_value == null
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ single_value:null~null
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE}.single_value == null
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.single_value~dyn,
+ null~null
+)~bool^equals
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null
+=====>
+ERROR: test_location:1:103: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0))
+ | cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == null
+ | ......................................................................................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}.null_value == 0
+=====>
+_==_(
+ cel.expr.conformance.proto3.TestAllTypes{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+ }~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes.null_value~int,
+ 0~int
+)~bool^equals
+
+Source: google.protobuf.NullValue.NULL_VALUE == null
+=====>
+ERROR: test_location:1:38: found no matching overload for '_==_' applied to '(int, null)' (candidates: (%A0, %A0))
+ | google.protobuf.NullValue.NULL_VALUE == null
+ | .....................................^
+
+Source: google.protobuf.NullValue.NULL_VALUE == 0
+=====>
+_==_(
+ google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE,
+ 0~int
+)~bool^equals
\ No newline at end of file
diff --git a/checker/src/test/resources/jsonTypeNullConstruction.baseline b/checker/src/test/resources/jsonTypeNullConstruction.baseline
new file mode 100644
index 000000000..5b9b211a8
--- /dev/null
+++ b/checker/src/test/resources/jsonTypeNullConstruction.baseline
@@ -0,0 +1,35 @@
+Source: google.protobuf.Value{null_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+google.protobuf.Value{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~dyn^google.protobuf.Value
+
+Source: google.protobuf.Value{null_value: null}
+=====>
+ERROR: test_location:1:33: expected type of field 'null_value' is 'int' but provided type is 'null'
+ | google.protobuf.Value{null_value: null}
+ | ................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: null}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ single_value:null~null
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
+
+Source: cel.expr.conformance.proto3.TestAllTypes{single_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ single_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: null}
+=====>
+ERROR: test_location:1:52: expected type of field 'null_value' is 'int' but provided type is 'null'
+ | cel.expr.conformance.proto3.TestAllTypes{null_value: null}
+ | ...................................................^
+
+Source: cel.expr.conformance.proto3.TestAllTypes{null_value: google.protobuf.NullValue.NULL_VALUE}
+=====>
+cel.expr.conformance.proto3.TestAllTypes{
+ null_value:google.protobuf.NullValue.NULL_VALUE~int^google.protobuf.NullValue.NULL_VALUE
+}~cel.expr.conformance.proto3.TestAllTypes^cel.expr.conformance.proto3.TestAllTypes
\ No newline at end of file
diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel
index 0a07e0d63..7c33e56b9 100644
--- a/common/internal/BUILD.bazel
+++ b/common/internal/BUILD.bazel
@@ -128,11 +128,6 @@ cel_android_library(
exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"],
)
-java_library(
- name = "proto_java_qualified_names",
- exports = ["//common/src/main/java/dev/cel/common/internal:proto_java_qualified_names"],
-)
-
java_library(
name = "proto_time_utils",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils"],
@@ -152,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/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java
index 9cf9a9caa..c39e0fea8 100644
--- a/common/src/main/java/dev/cel/common/CelOptions.java
+++ b/common/src/main/java/dev/cel/common/CelOptions.java
@@ -139,7 +139,7 @@ public static Builder newBuilder() {
.retainRepeatedUnaryOperators(false)
.retainUnbalancedLogicalExpressions(false)
.enableHiddenAccumulatorVar(true)
- .enableQuotedIdentifierSyntax(false)
+ .enableQuotedIdentifierSyntax(true)
// Type-Checker options
.enableCompileTimeOverloadResolution(false)
.enableHomogeneousLiterals(false)
@@ -177,6 +177,7 @@ public static Builder current() {
.enableUnsignedComparisonAndArithmeticIsUnsigned(true)
.enableUnsignedLongs(true)
.enableRegexPartialMatch(true)
+ .enableTimestampEpoch(true)
.errorOnDuplicateMapKeys(true)
.evaluateCanonicalTypesToNativeValues(true)
.errorOnIntWrap(true)
@@ -292,14 +293,20 @@ public abstract static class Builder {
public abstract Builder enableHomogeneousLiterals(boolean value);
/**
- * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Uxix epoch
+ * Enable the {@code int64_to_timestamp} overload which creates a timestamp from Unix epoch
* seconds.
*
- * This option will be automatically enabled after a sufficient period of time has elapsed to
- * ensure that all runtimes support the implementation.
+ *
Historically used to opt-in to this feature, this option is now enabled by default across
+ * all runtimes.
*
*
TODO: Remove this feature once it has been auto-enabled.
+ *
+ * @deprecated This option is now enabled by default. If you are passing {@code true}, simply
+ * remove this method call. If you are passing {@code false} to disable this feature, subset
+ * the environment instead using {@code dev.cel.checker.CelStandardDeclarations} and {@code
+ * dev.cel.runtime.CelStandardFunctions}.
*/
+ @Deprecated
public abstract Builder enableTimestampEpoch(boolean value);
/**
diff --git a/common/src/main/java/dev/cel/common/formats/ParserContext.java b/common/src/main/java/dev/cel/common/formats/ParserContext.java
index 0bdfdb299..17eff473f 100644
--- a/common/src/main/java/dev/cel/common/formats/ParserContext.java
+++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java
@@ -42,6 +42,32 @@ public interface ParserContext {
Map getIdToOffsetMap();
- /** NewString creates a new ValueString from the YAML node. */
- ValueString newValueString(T node);
+ /**
+ * @deprecated Use {@link #newSourceString} instead.
+ */
+ @Deprecated
+ default ValueString newValueString(T node) {
+ return newSourceString(node);
+ }
+
+ /**
+ * NewYamlString creates a new ValueString from the YAML node, evaluated according to standard
+ * YAML parsing rules.
+ *
+ * This respects the whitespace folding semantics defined by the node's scalar style (e.g.,
+ * folded string {@code >} versus literal string {@code |}). Use this method for general string
+ * fields such as {@code description}, {@code name}, or {@code id}.
+ */
+ ValueString newYamlString(T node);
+
+ /**
+ * NewRawString creates a new ValueString from the YAML node, preserving formatting for accurate
+ * source mapping.
+ *
+ *
This extracts the verbatim text directly from the source file, preserving raw block
+ * indentation and unmodified newlines. Use this method when the string represents code or a CEL
+ * expression where precise character-level offsets must be maintained for accurate diagnostic
+ * error reporting.
+ */
+ ValueString newSourceString(T node);
}
diff --git a/common/src/main/java/dev/cel/common/formats/YamlHelper.java b/common/src/main/java/dev/cel/common/formats/YamlHelper.java
index e0780b01f..c16126f95 100644
--- a/common/src/main/java/dev/cel/common/formats/YamlHelper.java
+++ b/common/src/main/java/dev/cel/common/formats/YamlHelper.java
@@ -136,7 +136,7 @@ public static boolean newBoolean(ParserContext ctx, Node node) {
}
public static String newString(ParserContext ctx, Node node) {
- return ctx.newValueString(node).value();
+ return ctx.newYamlString(node).value();
}
private YamlHelper() {}
diff --git a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
index 456872803..9f6077562 100644
--- a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
+++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java
@@ -62,7 +62,18 @@ public Map getIdToOffsetMap() {
}
@Override
- public ValueString newValueString(Node node) {
+ public ValueString newYamlString(Node node) {
+ long id = collectMetadata(node);
+ if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
+ return ValueString.of(id, ERROR);
+ }
+
+ ScalarNode scalarNode = (ScalarNode) node;
+ return ValueString.of(id, scalarNode.getValue());
+ }
+
+ @Override
+ public ValueString newSourceString(Node node) {
long id = collectMetadata(node);
if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
return ValueString.of(id, ERROR);
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 912b4de4b..58b15b103 100644
--- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
@@ -153,7 +153,6 @@ java_library(
tags = [
],
deps = [
- ":proto_java_qualified_names",
":reflection_util",
"//common/annotations",
"@maven//:com_google_guava_guava",
@@ -397,22 +396,13 @@ java_library(
)
java_library(
- name = "proto_java_qualified_names",
- srcs = ["ProtoJavaQualifiedNames.java"],
+ name = "reflection_util",
+ srcs = ["ReflectionUtil.java"],
tags = [
],
deps = [
"//common/annotations",
"@maven//:com_google_guava_guava",
- "@maven//:com_google_protobuf_protobuf_java",
- ],
-)
-
-java_library(
- name = "reflection_util",
- srcs = ["ReflectionUtil.java"],
- deps = [
- "//common/annotations",
],
)
diff --git a/common/src/main/java/dev/cel/common/internal/Constants.java b/common/src/main/java/dev/cel/common/internal/Constants.java
index d2c0719ec..49bca7489 100644
--- a/common/src/main/java/dev/cel/common/internal/Constants.java
+++ b/common/src/main/java/dev/cel/common/internal/Constants.java
@@ -207,6 +207,9 @@ private static void decodeString(
continue;
}
skipNewline = false;
+ if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) {
+ throw new ParseException("Invalid unicode code point", seqOffset);
+ }
buffer.appendCodePoint(codePoint);
} else {
// Normalize '\r' and '\r\n' to '\n'.
@@ -231,6 +234,9 @@ private static void decodeString(
// For raw literals, all escapes are valid and those characters come through literally in
// the string.
buffer.appendCodePoint('\\');
+ if (codePoint >= MIN_SURROGATE && codePoint <= MAX_SURROGATE) {
+ throw new ParseException("Invalid unicode code point", seqOffset);
+ }
buffer.appendCodePoint(codePoint);
continue;
}
diff --git a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
index fcb0e7056..163d0273e 100644
--- a/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
+++ b/common/src/main/java/dev/cel/common/internal/DefaultInstanceMessageFactory.java
@@ -15,6 +15,7 @@
package dev.cel.common.internal;
import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.GeneratorNames;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLite;
import dev.cel.common.annotations.Internal;
@@ -45,9 +46,7 @@ public static DefaultInstanceMessageFactory getInstance() {
public Optional getPrototype(Descriptor descriptor) {
MessageLite defaultInstance =
DefaultInstanceMessageLiteFactory.getInstance()
- .getPrototype(
- descriptor.getFullName(),
- ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(descriptor))
+ .getPrototype(descriptor.getFullName(), GeneratorNames.getBytecodeClassName(descriptor))
.orElse(null);
if (defaultInstance == null) {
return Optional.empty();
diff --git a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
index 4a021cd90..68d05e127 100644
--- a/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
+++ b/common/src/main/java/dev/cel/common/internal/DefaultMessageFactory.java
@@ -52,7 +52,7 @@ public Optional newBuilder(String messageName) {
DefaultInstanceMessageFactory.getInstance().getPrototype(descriptor.get());
if (message.isPresent()) {
- return message.map(Message::toBuilder);
+ return message.map(Message::newBuilderForType);
}
return Optional.of(DynamicMessage.newBuilder(descriptor.get()));
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
index b6648a5b8..7e3910433 100644
--- a/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
+++ b/common/src/main/java/dev/cel/common/internal/ProtoAdapter.java
@@ -222,9 +222,7 @@ public Optional adaptValueToFieldType(
throw new IllegalArgumentException("Unsupported field type");
}
- String typeFullName = fieldDescriptor.getMessageType().getFullName();
- if (!WellKnownProto.ANY_VALUE.typeName().equals(typeFullName)
- && !WellKnownProto.JSON_VALUE.typeName().equals(typeFullName)) {
+ if (!isFieldAnyOrJson(fieldDescriptor)) {
return Optional.empty();
}
}
@@ -242,7 +240,11 @@ public Optional adaptValueToFieldType(
getDefaultValueForMaybeMessage(keyDescriptor),
valueDescriptor.getLiteType(),
getDefaultValueForMaybeMessage(valueDescriptor));
+ boolean isValueAnyOrJson = isFieldAnyOrJson(valueDescriptor);
for (Map.Entry entry : ((Map, ?>) fieldValue).entrySet()) {
+ if (!isValueAnyOrJson && entry.getValue() instanceof NullValue) {
+ continue;
+ }
mapEntries.add(
protoMapEntry.toBuilder()
.setKey(keyConverter.backwardConverter().convert(entry.getKey()))
@@ -252,15 +254,54 @@ public Optional adaptValueToFieldType(
return Optional.of(mapEntries);
}
if (fieldDescriptor.isRepeated()) {
+ List> listValue = (List>) fieldValue;
+
+ if (!isFieldAnyOrJson(fieldDescriptor)) {
+ listValue = filterOutNullValues(listValue);
+ }
+
return Optional.of(
- AdaptingTypes.adaptingList(
- (List>) fieldValue, fieldToValueConverter(fieldDescriptor).reverse()));
+ AdaptingTypes.adaptingList(listValue, fieldToValueConverter(fieldDescriptor).reverse()));
}
return Optional.of(
fieldToValueConverter(fieldDescriptor).backwardConverter().convert(fieldValue));
}
+ private static List> filterOutNullValues(List> originalList) {
+ List filteredList = null;
+
+ for (int i = 0; i < originalList.size(); i++) {
+ Object elem = originalList.get(i);
+
+ if (elem instanceof NullValue) {
+ if (filteredList == null) {
+ filteredList = new ArrayList<>(originalList.size() - 1);
+ if (i > 0) {
+ filteredList.addAll(originalList.subList(0, i));
+ }
+ }
+ } else if (filteredList != null) {
+ filteredList.add(elem);
+ }
+ }
+
+ // Return the original list if no nulls were found to avoid unnecessary allocations
+ return filteredList != null ? filteredList : originalList;
+ }
+
+ private static boolean isFieldAnyOrJson(FieldDescriptor fieldDescriptor) {
+ if (!fieldDescriptor.getType().equals(FieldDescriptor.Type.MESSAGE)) {
+ return false;
+ }
+
+ String typeFullName = fieldDescriptor.getMessageType().getFullName();
+
+ return WellKnownProto.getByTypeName(typeFullName)
+ .map(wkp -> wkp.equals(WellKnownProto.ANY_VALUE) || wkp.equals(WellKnownProto.JSON_VALUE))
+ .orElse(false);
+ }
+
@SuppressWarnings("rawtypes")
private BidiConverter fieldToValueConverter(FieldDescriptor fieldDescriptor) {
switch (fieldDescriptor.getType()) {
diff --git a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java
deleted file mode 100644
index f27181a50..000000000
--- a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2025 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.internal;
-
-import com.google.protobuf.Descriptors.Descriptor;
-import com.google.protobuf.Descriptors.FileDescriptor;
-import com.google.protobuf.GeneratorNames;
-import dev.cel.common.annotations.Internal;
-
-/**
- * Helper class for constructing a fully qualified Java class name from a protobuf descriptor.
- *
- * CEL Library Internals. Do Not Use.
- */
-@Internal
-public final class ProtoJavaQualifiedNames {
- /**
- * Retrieves the full Java class name from the given descriptor
- *
- * @return fully qualified class name.
- *
Example 1: dev.cel.expr.Value
- *
Example 2: com.google.rpc.context.AttributeContext$Resource (Nested classes)
- *
Example 3: com.google.api.expr.cel.internal.testdata$SingleFileProto$SingleFile$Path
- * (Nested class with java multiple files disabled)
- */
- public static String getFullyQualifiedJavaClassName(Descriptor descriptor) {
- return GeneratorNames.getBytecodeClassName(descriptor);
- }
-
- /**
- * Gets the java package name from the descriptor. See
- * https://developers.google.com/protocol-buffers/docs/reference/java-generated#package for rules
- * on package name generation
- */
- public static String getJavaPackageName(FileDescriptor fileDescriptor) {
- return GeneratorNames.getFileJavaPackage(fileDescriptor.toProto());
- }
-
- private ProtoJavaQualifiedNames() {}
-}
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..97bed650f 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,11 @@
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;
/**
* Utility class for invoking Java reflection.
@@ -48,5 +50,18 @@ public static Object invoke(Method method, Object object, Object... params) {
}
}
+ /** Resolves a generic parameter of a base class from a type token. */
+ public static Type resolveGenericParameter(TypeToken> token, Class> baseClass, int index) {
+ return token.resolveType(baseClass.getTypeParameters()[index]).getType();
+ }
+
+ /**
+ * 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 53ffdda3d..5ccc498fd 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,16 +71,19 @@ cel_android_library(
deps = [
"//common/values:values_android",
"@maven//:com_google_errorprone_error_prone_annotations",
- "@maven_android//:com_google_guava_guava",
],
)
java_library(
name = "combined_cel_value_provider",
- srcs = ["CombinedCelValueProvider.java"],
+ srcs = [
+ "CombinedCelValueProvider.java",
+ ],
tags = [
],
deps = [
+ ":combined_cel_value_converter",
+ ":values",
"//common/values:cel_value_provider",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
@@ -90,16 +92,70 @@ java_library(
cel_android_library(
name = "combined_cel_value_provider_android",
- srcs = ["CombinedCelValueProvider.java"],
+ srcs = [
+ "CombinedCelValueProvider.java",
+ ],
tags = [
],
deps = [
+ ":combined_cel_value_converter_android",
+ ":values_android",
"//common/values:cel_value_provider_android",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven_android//:com_google_guava_guava",
],
)
+java_library(
+ name = "combined_cel_value_converter",
+ srcs = [
+ "CombinedCelValueConverter.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":values",
+ "//common/annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
+cel_android_library(
+ name = "combined_cel_value_converter_android",
+ srcs = [
+ "CombinedCelValueConverter.java",
+ ],
+ tags = [
+ ],
+ deps = [
+ ":values_android",
+ "//common/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,
@@ -108,6 +164,7 @@ java_library(
deps = [
":cel_byte_string",
":cel_value",
+ ":preadapted_list",
"//:auto_value",
"//common/annotations",
"//common/types",
@@ -118,6 +175,38 @@ java_library(
],
)
+java_library(
+ name = "mutable_map_value",
+ srcs = ["MutableMapValue.java"],
+ tags = [
+ ],
+ deps = [
+ "//common/annotations",
+ "//common/exceptions:attribute_not_found",
+ "//common/types",
+ "//common/types:type_providers",
+ "//common/values",
+ "//common/values:cel_value",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
+cel_android_library(
+ name = "mutable_map_value_android",
+ srcs = ["MutableMapValue.java"],
+ tags = [
+ ],
+ deps = [
+ ":cel_value_android",
+ "//common/annotations",
+ "//common/exceptions:attribute_not_found",
+ "//common/types:type_providers_android",
+ "//common/types:types_android",
+ "//common/values:values_android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ ],
+)
+
cel_android_library(
name = "values_android",
srcs = CEL_VALUES_SOURCES,
@@ -126,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",
@@ -154,7 +244,6 @@ java_library(
],
deps = [
":cel_byte_string",
- ":values",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/internal:well_known_proto",
@@ -189,6 +278,7 @@ java_library(
],
deps = [
":base_proto_cel_value_converter",
+ ":preadapted_list",
":values",
"//:auto_value",
"//common:options",
@@ -201,7 +291,6 @@ java_library(
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
- "@maven//:org_jspecify_jspecify",
],
)
@@ -244,8 +333,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",
],
)
@@ -271,7 +358,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 2af0a76cb..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,9 +20,12 @@
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.Map.Entry;
import java.util.Optional;
+import java.util.RandomAccess;
+import java.util.function.Function;
/**
* {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link
@@ -37,6 +40,12 @@ public class CelValueConverter {
private static final CelValueConverter DEFAULT_INSTANCE = new CelValueConverter();
+ @SuppressWarnings("Immutable") // Method reference is immutable
+ private final Function maybeUnwrapFunction;
+
+ @SuppressWarnings("Immutable") // Method reference is immutable
+ private final Function toRuntimeValueFunction;
+
public static CelValueConverter getDefaultInstance() {
return DEFAULT_INSTANCE;
}
@@ -47,54 +56,105 @@ 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;
+ }
+
+ return mapContainer(value, maybeUnwrapFunction);
+ }
+
+ /**
+ * Maps a container (Collection or Map) by applying the provided mapper function to its elements.
+ * 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 =
ImmutableList.builderWithExpectedSize(collection.size());
for (Object element : collection) {
- builder.add(maybeUnwrap(element));
+ builder.add(mapper.apply(element));
}
-
return builder.build();
}
if (value instanceof Map) {
Map map = (Map) value;
- ImmutableMap.Builder builder =
- ImmutableMap.builderWithExpectedSize(map.size());
- for (Map.Entry entry : map.entrySet()) {
- builder.put(maybeUnwrap(entry.getKey()), maybeUnwrap(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;
}
- /**
- * Canonicalizes an inbound {@code value} into a suitable Java object representation for
- * evaluation.
- */
public Object toRuntimeValue(Object value) {
Preconditions.checkNotNull(value);
- if (value instanceof CelValue) {
+ if (value instanceof CelValue || value instanceof CelPreAdaptedList) {
return value;
}
- if (value instanceof Collection) {
- return toListValue((Collection) value);
- } else if (value instanceof Map) {
- return toMapValue((Map) value);
- } else if (value instanceof Optional) {
+ Object mapped = mapContainer(value, toRuntimeValueFunction);
+ if (mapped != value) {
+ return mapped;
+ }
+
+ if (value instanceof Optional) {
Optional optionalValue = (Optional) value;
return optionalValue
- .map(this::toRuntimeValue)
+ .map(toRuntimeValueFunction)
.map(OptionalValue::create)
.orElse(OptionalValue.EMPTY);
}
@@ -117,7 +177,7 @@ protected Object normalizePrimitive(Object value) {
}
/** Adapts a {@link CelValue} to a plain old Java Object. */
- private static Object unwrap(CelValue celValue) {
+ private Object unwrap(CelValue celValue) {
Preconditions.checkNotNull(celValue);
if (celValue instanceof OptionalValue) {
@@ -126,7 +186,7 @@ private static Object unwrap(CelValue celValue) {
return Optional.empty();
}
- return Optional.of(optionalValue.value());
+ return Optional.of(maybeUnwrap(optionalValue.value()));
}
if (celValue instanceof ErrorValue) {
@@ -136,31 +196,8 @@ private static Object unwrap(CelValue celValue) {
return celValue.value();
}
- private ImmutableList toListValue(Collection iterable) {
- Preconditions.checkNotNull(iterable);
-
- ImmutableList.Builder listBuilder =
- ImmutableList.builderWithExpectedSize(iterable.size());
- for (Object entry : iterable) {
- listBuilder.add(toRuntimeValue(entry));
- }
-
- return listBuilder.build();
- }
-
- private ImmutableMap toMapValue(Map map) {
- Preconditions.checkNotNull(map);
-
- ImmutableMap.Builder mapBuilder =
- ImmutableMap.builderWithExpectedSize(map.size());
- for (Entry entry : map.entrySet()) {
- Object mapKey = toRuntimeValue(entry.getKey());
- Object mapValue = toRuntimeValue(entry.getValue());
- mapBuilder.put(mapKey, mapValue);
- }
-
- return mapBuilder.buildOrThrow();
+ protected CelValueConverter() {
+ this.maybeUnwrapFunction = this::maybeUnwrap;
+ this.toRuntimeValueFunction = this::toRuntimeValue;
}
-
- protected CelValueConverter() {}
}
diff --git a/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java b/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java
new file mode 100644
index 000000000..46e5fc3f1
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueConverter.java
@@ -0,0 +1,84 @@
+// 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import dev.cel.common.annotations.Internal;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@code CombinedCelValueConverter} delegates value conversion to a list of underlying {@link
+ * CelValueConverter}s.
+ */
+@Internal
+public final class CombinedCelValueConverter extends CelValueConverter {
+ private final ImmutableList converters;
+
+ public static CombinedCelValueConverter combine(ImmutableList converters) {
+ return new CombinedCelValueConverter(converters);
+ }
+
+ private CombinedCelValueConverter(ImmutableList converters) {
+ this.converters = checkNotNull(converters);
+ }
+
+ @Override
+ public @Nullable Object toRuntimeValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ // Let the base class handle CelValues, Optionals, Collections, Maps, and primitives.
+ Object baseResult = super.toRuntimeValue(value);
+ if (baseResult != value) {
+ return baseResult;
+ }
+
+ // If the base class left the object unchanged (e.g. a raw POJO), try the delegates.
+ for (CelValueConverter converter : converters) {
+ Object result = converter.toRuntimeValue(value);
+ if (result != value) {
+ return result;
+ }
+ }
+
+ return value;
+ }
+
+ @Override
+ public @Nullable Object maybeUnwrap(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ // Let the base class handle standard unwrapping and container unrolling.
+ Object baseResult = super.maybeUnwrap(value);
+ if (baseResult != value) {
+ return baseResult;
+ }
+
+ // Try delegates for specialized unwrapping.
+ for (CelValueConverter converter : converters) {
+ Object result = converter.maybeUnwrap(value);
+ if (result != value) {
+ return result;
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java
index 8fe62cb7b..d51c3afce 100644
--- a/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java
+++ b/common/src/main/java/dev/cel/common/values/CombinedCelValueProvider.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
@@ -49,6 +50,14 @@ public Optional newValue(String structType, Map fields)
return Optional.empty();
}
+ @Override
+ public CelValueConverter celValueConverter() {
+ return CombinedCelValueConverter.combine(
+ celValueProviders.stream()
+ .map(CelValueProvider::celValueConverter)
+ .collect(toImmutableList()));
+ }
+
/** Returns the underlying {@link CelValueProvider}s in order. */
public ImmutableList valueProviders() {
return celValueProviders;
diff --git a/common/src/main/java/dev/cel/common/values/MutableMapValue.java b/common/src/main/java/dev/cel/common/values/MutableMapValue.java
new file mode 100644
index 000000000..706436b2e
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/MutableMapValue.java
@@ -0,0 +1,146 @@
+// 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 com.google.errorprone.annotations.Immutable;
+import dev.cel.common.annotations.Internal;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.MapType;
+import dev.cel.common.types.SimpleType;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A custom CelValue implementation that allows O(1) insertions for maps during comprehension.
+ *
+ * CEL Library Internals. Do Not Use.
+ */
+@Internal
+@Immutable
+@SuppressWarnings("Immutable") // Intentionally mutable for performance reasons
+public final class MutableMapValue extends CelValue
+ implements SelectableValue, Map {
+ private final Map internalMap;
+ private final CelType celType;
+
+ public static MutableMapValue create(Map, ?> map) {
+ return new MutableMapValue(map);
+ }
+
+ @Override
+ public int size() {
+ return internalMap.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return internalMap.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return internalMap.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return internalMap.containsValue(value);
+ }
+
+ @Override
+ public Object get(Object key) {
+ return internalMap.get(key);
+ }
+
+ @Override
+ public Object put(Object key, Object value) {
+ return internalMap.put(key, value);
+ }
+
+ @Override
+ public Object remove(Object key) {
+ return internalMap.remove(key);
+ }
+
+ @Override
+ public void putAll(Map, ?> m) {
+ internalMap.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ internalMap.clear();
+ }
+
+ @Override
+ public Set keySet() {
+ return internalMap.keySet();
+ }
+
+ @Override
+ public Collection values() {
+ return internalMap.values();
+ }
+
+ @Override
+ public Set> entrySet() {
+ return internalMap.entrySet();
+ }
+
+ @Override
+ public Object select(Object field) {
+ Object val = internalMap.get(field);
+ if (val != null) {
+ return val;
+ }
+ if (!internalMap.containsKey(field)) {
+ throw CelAttributeNotFoundException.forMissingMapKey(field.toString());
+ }
+ throw CelAttributeNotFoundException.of(
+ String.format("Map value cannot be null for key: %s", field));
+ }
+
+ @Override
+ public Optional> find(Object field) {
+ if (internalMap.containsKey(field)) {
+ return Optional.ofNullable(internalMap.get(field));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Object value() {
+ return this;
+ }
+
+ @Override
+ public boolean isZeroValue() {
+ return internalMap.isEmpty();
+ }
+
+ @Override
+ public CelType celType() {
+ return celType;
+ }
+
+ private MutableMapValue(Map, ?> map) {
+ this.internalMap = new LinkedHashMap<>(map);
+ this.celType = MapType.create(SimpleType.DYN, SimpleType.DYN);
+ }
+}
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/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
index 52f0f1594..2e4d980c7 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
@@ -35,7 +35,7 @@
*/
@AutoValue
@Immutable
-public abstract class ProtoMessageLiteValue extends StructValue {
+public abstract class ProtoMessageLiteValue extends StructValue {
@Override
public abstract MessageLite value();
diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java
index e402bb429..627bd2c1d 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java
@@ -28,7 +28,7 @@
/** ProtoMessageValue is a struct value with protobuf support. */
@AutoValue
@Immutable
-public abstract class ProtoMessageValue extends StructValue {
+public abstract class ProtoMessageValue extends StructValue {
@Override
public abstract Message value();
@@ -92,11 +92,6 @@ public static ProtoMessageValue create(
private FieldDescriptor findField(
CelDescriptorPool celDescriptorPool, Descriptor descriptor, String fieldName) {
- FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName);
- if (fieldDescriptor != null) {
- return fieldDescriptor;
- }
-
if (enableJsonFieldNames()) {
for (FieldDescriptor fd : descriptor.getFields()) {
if (fd.getJsonName().equals(fieldName)) {
@@ -105,6 +100,11 @@ private FieldDescriptor findField(
}
}
+ FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName);
+ if (fieldDescriptor != null) {
+ return fieldDescriptor;
+ }
+
return celDescriptorPool
.findExtensionDescriptor(descriptor, fieldName)
.orElseThrow(
diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java
index b7895d845..7beb40c61 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java
@@ -68,11 +68,6 @@ public Optional newValue(String structType, Map fields)
}
private FieldDescriptor findField(Descriptor descriptor, String fieldName) {
- FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName);
- if (fieldDescriptor != null) {
- return fieldDescriptor;
- }
-
if (celOptions.enableJsonFieldNames()) {
for (FieldDescriptor fd : descriptor.getFields()) {
if (fd.getJsonName().equals(fieldName)) {
@@ -81,6 +76,11 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) {
}
}
+ FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName);
+ if (fieldDescriptor != null) {
+ return fieldDescriptor;
+ }
+
return protoMessageFactory
.getDescriptorPool()
.findExtensionDescriptor(descriptor, fieldName)
diff --git a/common/src/main/java/dev/cel/common/values/StructValue.java b/common/src/main/java/dev/cel/common/values/StructValue.java
index 8775ef5c4..aa44ec420 100644
--- a/common/src/main/java/dev/cel/common/values/StructValue.java
+++ b/common/src/main/java/dev/cel/common/values/StructValue.java
@@ -19,13 +19,21 @@
/**
* StructValue is a representation of a structured object with typed properties.
*
- * Users may extend from this class to provide a custom struct that CEL can understand (ex:
- * POJOs). Custom struct implementations must provide all functionalities denoted in the CEL
- * specification, such as field selection, presence testing and new object creation.
+ *
Users may extend from this class to provide a custom struct that CEL can understand by
+ * wrapping a native Java object (e.g., a POJO or a Map). Custom struct implementations must provide
+ * all functionalities denoted in the CEL specification, such as field selection, presence testing
+ * and new object creation.
*
*
For an expression `e` selecting a field `f`, `e.f` must throw an exception if `f` does not
* exist in the struct (i.e: hasField returns false). If the field exists but is not set, the
* implementation should return an appropriate default value based on the struct's semantics.
+ *
+ * @param The type of the field identifier. Only {@code String} is supported for now, but we may
+ * extend support to other types in the future.
+ * @param The type of the wrapped native object.
*/
@Immutable
-public abstract class StructValue extends CelValue implements SelectableValue {}
+public abstract class StructValue extends CelValue implements SelectableValue {
+ @Override
+ public abstract V value();
+}
diff --git a/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java b/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java
index cc5ba5632..7be994391 100644
--- a/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java
+++ b/common/src/test/java/dev/cel/common/internal/DynamicProtoTest.java
@@ -37,7 +37,7 @@
import dev.cel.common.CelDescriptorUtil;
import dev.cel.common.CelDescriptors;
import dev.cel.testing.testdata.MultiFile;
-import dev.cel.testing.testdata.SingleFileProto.SingleFile;
+import dev.cel.testing.testdata.SingleFile;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java
index 16797b714..c9f9d9e21 100644
--- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java
+++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java
@@ -23,7 +23,7 @@
import dev.cel.common.types.StructType.Field;
import dev.cel.expr.conformance.proto2.TestAllTypes;
import dev.cel.expr.conformance.proto2.TestAllTypesExtensions;
-import dev.cel.testing.testdata.SingleFileProto.SingleFile;
+import dev.cel.testing.testdata.SingleFile;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -269,8 +269,8 @@ public void findField_withJsonNameOption() {
(ProtoMessageType) typeProvider.findType(SingleFile.getDescriptor().getFullName()).get();
// Note that these are the same fields, with json_name option set
- Optional snakeCasedField = msgType.findField("snake_cased");
- Optional jsonNameField = msgType.findField("camelCased");
+ Optional snakeCasedField = msgType.findField("int64_camel_case_json_name");
+ Optional jsonNameField = msgType.findField("int64CamelCaseJsonName");
assertThat(snakeCasedField).isEmpty();
assertThat(jsonNameField).isPresent();
diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel
index ab7eae8dd..bf151fcb7 100644
--- a/common/src/test/java/dev/cel/common/values/BUILD.bazel
+++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel
@@ -24,6 +24,7 @@ java_library(
"//common/values",
"//common/values:cel_byte_string",
"//common/values:cel_value_provider",
+ "//common/values:combined_cel_value_converter",
"//common/values:combined_cel_value_provider",
"//common/values:proto_message_lite_value",
"//common/values:proto_message_lite_value_provider",
diff --git a/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java
new file mode 100644
index 000000000..8574587bc
--- /dev/null
+++ b/common/src/test/java/dev/cel/common/values/CombinedCelValueConverterTest.java
@@ -0,0 +1,112 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class CombinedCelValueConverterTest {
+
+ @Test
+ public void toRuntimeValue_delegatesToUnderlyingConverters() {
+ CustomConverter converter1 = new CustomConverter("target1", "replacement1");
+ CustomConverter converter2 = new CustomConverter("target2", "replacement2");
+ CelValueConverter combined =
+ CombinedCelValueConverter.combine(ImmutableList.of(converter1, converter2));
+
+ assertThat(combined.toRuntimeValue("target1")).isEqualTo("replacement1");
+ assertThat(combined.toRuntimeValue("target2")).isEqualTo("replacement2");
+ assertThat(combined.toRuntimeValue("unhandled")).isEqualTo("unhandled");
+ }
+
+ @Test
+ public void maybeUnwrap_delegatesToUnderlyingConverters() {
+ CustomConverter converter1 = new CustomConverter("target1", "replacement1");
+ CustomConverter converter2 = new CustomConverter("target2", "replacement2");
+ CelValueConverter combined =
+ CombinedCelValueConverter.combine(ImmutableList.of(converter1, converter2));
+
+ assertThat(combined.maybeUnwrap("replacement1")).isEqualTo("target1");
+ assertThat(combined.maybeUnwrap("replacement2")).isEqualTo("target2");
+ assertThat(combined.maybeUnwrap("unhandled")).isEqualTo("unhandled");
+ }
+
+ @Test
+ public void combinedCelValueProvider_returnsCombinedConverter() {
+ CustomConverter converter1 = new CustomConverter("target1", "replacement1");
+ CustomConverter converter2 = new CustomConverter("target2", "replacement2");
+ CustomProvider provider1 = new CustomProvider(converter1);
+ CustomProvider provider2 = new CustomProvider(converter2);
+
+ CombinedCelValueProvider combinedProvider =
+ CombinedCelValueProvider.combine(provider1, provider2);
+ CelValueConverter combinedConverter = combinedProvider.celValueConverter();
+
+ assertThat(combinedConverter).isInstanceOf(CombinedCelValueConverter.class);
+ assertThat(combinedConverter.toRuntimeValue("target1")).isEqualTo("replacement1");
+ assertThat(combinedConverter.toRuntimeValue("target2")).isEqualTo("replacement2");
+ }
+
+ private static class CustomConverter extends CelValueConverter {
+ private final String target;
+ private final String replacement;
+
+ private CustomConverter(String target, String replacement) {
+ this.target = target;
+ this.replacement = replacement;
+ }
+
+ @Override
+ public Object toRuntimeValue(Object value) {
+ if (value.equals(target)) {
+ return replacement;
+ }
+ return value;
+ }
+
+ @Override
+ public Object maybeUnwrap(Object value) {
+ if (value.equals(replacement)) {
+ return target;
+ }
+ return value;
+ }
+ }
+
+ private static class CustomProvider implements CelValueProvider {
+ private final CelValueConverter converter;
+
+ private CustomProvider(CelValueConverter converter) {
+ this.converter = converter;
+ }
+
+ @Override
+ public Optional newValue(String structType, Map fields) {
+ return Optional.empty();
+ }
+
+ @Override
+ public CelValueConverter celValueConverter() {
+ return converter;
+ }
+ }
+}
diff --git a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java
index 24b3ea30b..f00954e3d 100644
--- a/common/src/test/java/dev/cel/common/values/OptionalValueTest.java
+++ b/common/src/test/java/dev/cel/common/values/OptionalValueTest.java
@@ -141,7 +141,7 @@ public void celTypeTest() {
}
@SuppressWarnings("Immutable") // Test only
- private static class CelCustomStruct extends StructValue {
+ private static class CelCustomStruct extends StructValue {
private final long data;
@Override
diff --git a/common/src/test/java/dev/cel/common/values/StructValueTest.java b/common/src/test/java/dev/cel/common/values/StructValueTest.java
index b8d6371a8..f25db8e87 100644
--- a/common/src/test/java/dev/cel/common/values/StructValueTest.java
+++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java
@@ -59,18 +59,34 @@ public Optional findType(String typeName) {
};
private static final CelValueProvider CUSTOM_STRUCT_VALUE_PROVIDER =
- (structType, fields) -> {
- if (structType.equals(CUSTOM_STRUCT_TYPE.name())) {
- return Optional.of(new CelCustomStructValue(fields));
+ new CelValueProvider() {
+ @Override
+ public Optional newValue(String structType, Map fields) {
+ if (structType.equals(CUSTOM_STRUCT_TYPE.name())) {
+ return Optional.of(new CelCustomStructValue(fields));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public CelValueConverter celValueConverter() {
+ return new CelValueConverter() {
+ @Override
+ public Object toRuntimeValue(Object value) {
+ if (value instanceof CustomPojo) {
+ return new CelCustomStructValue((CustomPojo) value);
+ }
+ return super.toRuntimeValue(value);
+ }
+ };
}
- return Optional.empty();
};
@Test
public void emptyStruct() {
CelCustomStructValue celCustomStruct = new CelCustomStructValue(0);
- assertThat(celCustomStruct.value()).isEqualTo(celCustomStruct);
+ assertThat(celCustomStruct.value().getData()).isEqualTo(0L);
assertThat(celCustomStruct.isZeroValue()).isTrue();
}
@@ -78,7 +94,7 @@ public void emptyStruct() {
public void constructStruct() {
CelCustomStructValue celCustomStruct = new CelCustomStructValue(5);
- assertThat(celCustomStruct.value()).isEqualTo(celCustomStruct);
+ assertThat(celCustomStruct.value().getData()).isEqualTo(5L);
assertThat(celCustomStruct.isZeroValue()).isFalse();
}
@@ -115,41 +131,41 @@ public void celTypeTest() {
@Test
public void evaluate_usingCustomClass_createNewStruct() throws Exception {
Cel cel =
- CelFactory.standardCelBuilder()
- .setOptions(CelOptions.current().enableCelValue(true).build())
+ CelFactory.plannerCelBuilder()
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
.setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER)
.build();
CelAbstractSyntaxTree ast = cel.compile("custom_struct{data: 50}").getAst();
- CelCustomStructValue result = (CelCustomStructValue) cel.createProgram(ast).eval();
+ CustomPojo result = (CustomPojo) cel.createProgram(ast).eval();
- assertThat(result.data).isEqualTo(50);
+ assertThat(result.getData()).isEqualTo(50);
}
@Test
public void evaluate_usingCustomClass_asVariable() throws Exception {
Cel cel =
- CelFactory.standardCelBuilder()
- .setOptions(CelOptions.current().enableCelValue(true).build())
+ CelFactory.plannerCelBuilder()
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
.addVar("a", CUSTOM_STRUCT_TYPE)
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
.setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER)
.build();
CelAbstractSyntaxTree ast = cel.compile("a").getAst();
- CelCustomStructValue result =
- (CelCustomStructValue)
+ CustomPojo result =
+ (CustomPojo)
cel.createProgram(ast).eval(ImmutableMap.of("a", new CelCustomStructValue(10)));
- assertThat(result.data).isEqualTo(10);
+ assertThat(result.getData()).isEqualTo(10);
}
@Test
public void evaluate_usingCustomClass_asVariableSelectField() throws Exception {
Cel cel =
- CelFactory.standardCelBuilder()
- .setOptions(CelOptions.current().enableCelValue(true).build())
+ CelFactory.plannerCelBuilder()
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
.addVar("a", CUSTOM_STRUCT_TYPE)
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
.setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER)
@@ -163,8 +179,8 @@ public void evaluate_usingCustomClass_asVariableSelectField() throws Exception {
@Test
public void evaluate_usingCustomClass_selectField() throws Exception {
Cel cel =
- CelFactory.standardCelBuilder()
- .setOptions(CelOptions.current().enableCelValue(true).build())
+ CelFactory.plannerCelBuilder()
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
.setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER)
.build();
@@ -178,8 +194,8 @@ public void evaluate_usingCustomClass_selectField() throws Exception {
@Test
public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws Exception {
Cel cel =
- CelFactory.standardCelBuilder()
- .setOptions(CelOptions.current().enableCelValue(true).build())
+ CelFactory.plannerCelBuilder()
+ .setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
.setValueProvider(
CombinedCelValueProvider.combine(
@@ -197,19 +213,31 @@ public void evaluate_usingMultipleProviders_selectFieldFromCustomClass() throws
// TODO: Bring back evaluate_usingMultipleProviders_selectFieldFromProtobufMessage
// once planner is exposed from factory
+ private static class CustomPojo {
+ private final long data;
+
+ CustomPojo(long data) {
+ this.data = data;
+ }
+
+ long getData() {
+ return data;
+ }
+ }
+
@SuppressWarnings("Immutable") // Test only
- private static class CelCustomStructValue extends StructValue {
+ private static class CelCustomStructValue extends StructValue {
- private final long data;
+ private final CustomPojo pojo;
@Override
- public CelCustomStructValue value() {
- return this;
+ public CustomPojo value() {
+ return pojo;
}
@Override
public boolean isZeroValue() {
- return data == 0;
+ return pojo.getData() == 0;
}
@Override
@@ -226,7 +254,7 @@ public Object select(String field) {
@Override
public Optional find(String field) {
if (field.equals("data")) {
- return Optional.of(value().data);
+ return Optional.of(pojo.getData());
}
return Optional.empty();
@@ -237,7 +265,11 @@ private CelCustomStructValue(Map fields) {
}
private CelCustomStructValue(long data) {
- this.data = data;
+ this.pojo = new CustomPojo(data);
+ }
+
+ private CelCustomStructValue(CustomPojo pojo) {
+ this.pojo = pojo;
}
}
}
diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel
index f1fa107b6..9853289a9 100644
--- a/common/values/BUILD.bazel
+++ b/common/values/BUILD.bazel
@@ -37,6 +37,18 @@ cel_android_library(
exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_provider_android"],
)
+java_library(
+ name = "combined_cel_value_converter",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter"],
+)
+
+cel_android_library(
+ name = "combined_cel_value_converter_android",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/values:combined_cel_value_converter_android"],
+)
+
java_library(
name = "values",
exports = ["//common/src/main/java/dev/cel/common/values"],
@@ -47,6 +59,18 @@ cel_android_library(
exports = ["//common/src/main/java/dev/cel/common/values:values_android"],
)
+java_library(
+ name = "mutable_map_value",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value"],
+)
+
+cel_android_library(
+ name = "mutable_map_value_android",
+ visibility = ["//:internal"],
+ exports = ["//common/src/main/java/dev/cel/common/values:mutable_map_value_android"],
+)
+
java_library(
name = "base_proto_cel_value_converter",
exports = ["//common/src/main/java/dev/cel/common/values:base_proto_cel_value_converter"],
diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel
index fb2b1a159..e9ed58642 100644
--- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel
+++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel
@@ -119,8 +119,7 @@ _TESTS_TO_SKIP_LEGACY = [
# Skip until fixed.
"fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous",
- # TODO: Add strings.format and strings.quote.
- "string_ext/quote",
+ # TODO: Add strings.format.quote.
"string_ext/format",
"string_ext/format_errors",
@@ -148,8 +147,7 @@ _TESTS_TO_SKIP_LEGACY = [
]
_TESTS_TO_SKIP_PLANNER = [
- # TODO: Add strings.format and strings.quote.
- "string_ext/quote",
+ # TODO: Add strings.format.
"string_ext/format",
"string_ext/format_errors",
diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java
index 5a25fb9d9..db57ccb79 100644
--- a/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java
+++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTest.java
@@ -16,8 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_EXTENSION_REGISTRY;
-import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_REGISTRY;
import static dev.cel.testing.utils.ExprValueUtils.fromValue;
import static dev.cel.testing.utils.ExprValueUtils.toExprValue;
@@ -29,6 +27,8 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.TypeRegistry;
import dev.cel.checker.CelChecker;
import dev.cel.common.CelContainer;
import dev.cel.common.CelOptions;
@@ -58,7 +58,6 @@ public final class ConformanceTest extends Statement {
private static final CelOptions OPTIONS =
CelOptions.current()
- .enableTimestampEpoch(true)
.enableHeterogeneousNumericComparisons(true)
.enableProtoDifferencerEquality(true)
.enableOptionalSyntax(true)
@@ -85,6 +84,21 @@ public final class ConformanceTest extends Statement {
CelExtensions.strings(),
CelOptionalLibrary.INSTANCE);
+ static final TypeRegistry CONFORMANCE_TYPE_REGISTRY =
+ TypeRegistry.newBuilder()
+ .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor())
+ .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor())
+ .build();
+
+ static final ExtensionRegistry CONFORMANCE_EXTENSION_REGISTRY =
+ createConformanceExtensionRegistry();
+
+ private static ExtensionRegistry createConformanceExtensionRegistry() {
+ ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
+ dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry);
+ return extensionRegistry;
+ }
+
private static final CelParser PARSER_WITH_MACROS =
CelParserFactory.standardCelParserBuilder()
.setOptions(OPTIONS)
@@ -107,7 +121,7 @@ private static CelChecker getChecker(SimpleTest test) throws Exception {
ImmutableList.Builder decls =
ImmutableList.builderWithExpectedSize(test.getTypeEnvCount());
for (dev.cel.expr.Decl decl : test.getTypeEnvList()) {
- decls.add(Decl.parseFrom(decl.toByteArray(), DEFAULT_EXTENSION_REGISTRY));
+ decls.add(Decl.parseFrom(decl.toByteArray(), CONFORMANCE_EXTENSION_REGISTRY));
}
return CelCompilerFactory.standardCelCheckerBuilder()
.setOptions(OPTIONS)
@@ -128,7 +142,7 @@ private static CelRuntime getRuntime(SimpleTest test, boolean usePlanner) {
// CEL-Internal-2
.setOptions(OPTIONS)
.addLibraries(CANONICAL_RUNTIME_EXTENSIONS)
- .setExtensionRegistry(DEFAULT_EXTENSION_REGISTRY)
+ .setExtensionRegistry(CONFORMANCE_EXTENSION_REGISTRY)
.addMessageTypes(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor())
.addMessageTypes(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor())
.addFileTypes(dev.cel.expr.conformance.proto2.TestAllTypesExtensions.getDescriptor());
@@ -152,7 +166,8 @@ private static ImmutableMap getBindings(SimpleTest test) throws
private static Object fromExprValue(ExprValue value) throws Exception {
switch (value.getKindCase()) {
case VALUE:
- return fromValue(value.getValue());
+ return fromValue(
+ value.getValue(), CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY);
default:
throw new IllegalArgumentException(
String.format("Unexpected binding value kind: %s", value.getKindCase()));
@@ -225,7 +240,7 @@ public void evaluate() throws Throwable {
assertThat(result)
.ignoringRepeatedFieldOrderOfFieldDescriptors(
MapValue.getDescriptor().findFieldByName("entries"))
- .unpackingAnyUsing(DEFAULT_TYPE_REGISTRY, DEFAULT_EXTENSION_REGISTRY)
+ .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY)
.isEqualTo(ExprValue.newBuilder().setValue(test.getValue()).build());
break;
case EVAL_ERROR:
@@ -238,7 +253,7 @@ public void evaluate() throws Throwable {
assertThat(result)
.ignoringRepeatedFieldOrderOfFieldDescriptors(
MapValue.getDescriptor().findFieldByName("entries"))
- .unpackingAnyUsing(DEFAULT_TYPE_REGISTRY, DEFAULT_EXTENSION_REGISTRY)
+ .unpackingAnyUsing(CONFORMANCE_TYPE_REGISTRY, CONFORMANCE_EXTENSION_REGISTRY)
.isEqualTo(ExprValue.newBuilder().setValue(test.getTypedResult().getResult()).build());
assertThat(resultType).isEqualTo(test.getTypedResult().getDeducedType());
break;
diff --git a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java
index dc3d5021e..4c3631d31 100644
--- a/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java
+++ b/conformance/src/test/java/dev/cel/conformance/ConformanceTestRunner.java
@@ -14,8 +14,7 @@
package dev.cel.conformance;
-import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_EXTENSION_REGISTRY;
-import static dev.cel.testing.utils.ExprValueUtils.DEFAULT_TYPE_REGISTRY;
+import static dev.cel.conformance.ConformanceTest.CONFORMANCE_EXTENSION_REGISTRY;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
@@ -50,14 +49,16 @@ private static ImmutableSortedMap loadTestFiles() {
SPLITTER.splitToList(System.getProperty("dev.cel.conformance.ConformanceTests.tests"));
try {
TextFormat.Parser parser =
- TextFormat.Parser.newBuilder().setTypeRegistry(DEFAULT_TYPE_REGISTRY).build();
+ TextFormat.Parser.newBuilder()
+ .setTypeRegistry(ConformanceTest.CONFORMANCE_TYPE_REGISTRY)
+ .build();
ImmutableSortedMap.Builder testFiles =
ImmutableSortedMap.naturalOrder();
for (String testPath : testPaths) {
SimpleTestFile.Builder fileBuilder = SimpleTestFile.newBuilder();
try (BufferedReader input =
Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8)) {
- parser.merge(input, DEFAULT_EXTENSION_REGISTRY, fileBuilder);
+ parser.merge(input, CONFORMANCE_EXTENSION_REGISTRY, fileBuilder);
}
SimpleTestFile testFile = fileBuilder.build();
testFiles.put(testFile.getName(), testFile);
diff --git a/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel
new file mode 100644
index 000000000..e4d80eccf
--- /dev/null
+++ b/conformance/src/test/java/dev/cel/conformance/policy/BUILD.bazel
@@ -0,0 +1,36 @@
+load("@rules_java//java:defs.bzl", "java_library")
+load(":cel_policy_conformance_test.bzl", "cel_policy_conformance_test_java")
+
+package(
+ default_applicable_licenses = ["//:license"],
+ default_testonly = True,
+)
+
+java_library(
+ name = "run",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//:auto_value",
+ "//:java_truth",
+ "//bundle:cel",
+ "//policy:parser_factory",
+ "//policy:validation_exception",
+ "//policy/testing:k8s_test_tag_handler",
+ "//runtime:function_binding",
+ "//testing/testrunner:cel_expression_source",
+ "//testing/testrunner:cel_test_context",
+ "//testing/testrunner:cel_test_suite",
+ "//testing/testrunner:cel_test_suite_text_proto_parser",
+ "//testing/testrunner:cel_test_suite_yaml_parser",
+ "//testing/testrunner:test_runner_library",
+ "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_java",
+ "@maven//:junit_junit",
+ ],
+)
+
+cel_policy_conformance_test_java(
+ name = "policy_conformance_tests",
+ testdata = "@cel_policy//conformance:testdata",
+)
diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java
new file mode 100644
index 000000000..d7851bb72
--- /dev/null
+++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTest.java
@@ -0,0 +1,114 @@
+// 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.conformance.policy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.Struct;
+import dev.cel.bundle.Cel;
+import dev.cel.bundle.CelFactory;
+import dev.cel.expr.conformance.proto3.TestAllTypes;
+import dev.cel.policy.CelPolicyParserFactory;
+import dev.cel.policy.CelPolicyValidationException;
+import dev.cel.policy.testing.K8sTagHandler;
+import dev.cel.runtime.CelFunctionBinding;
+import dev.cel.testing.testrunner.CelExpressionSource;
+import dev.cel.testing.testrunner.CelTestContext;
+import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase;
+import dev.cel.testing.testrunner.TestRunnerLibrary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+import org.junit.runners.model.Statement;
+
+/** Statement representing a single CEL policy conformance test case. */
+public final class PolicyConformanceTest extends Statement {
+
+ private static final Cel CEL =
+ CelFactory.standardCelBuilder()
+ .addFunctionBindings(
+ CelFunctionBinding.fromOverloads(
+ "locationCode",
+ CelFunctionBinding.from(
+ "locationCode_string",
+ String.class,
+ (ip) -> {
+ switch (ip) {
+ case "10.0.0.1":
+ return "us";
+ case "10.0.0.2":
+ return "de";
+ default:
+ return "ir";
+ }
+ })))
+ .build();
+
+ private final String name;
+ private final CelTestCase testCase;
+ private final String dirPath;
+
+ public PolicyConformanceTest(String name, CelTestCase testCase, String dirPath) {
+ this.name = name;
+ this.testCase = testCase;
+ this.dirPath = dirPath;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void evaluate() throws Throwable {
+ String policyFile = Paths.get(dirPath, "policy.yaml").toString();
+
+ CelTestContext.Builder contextBuilder =
+ CelTestContext.newBuilder()
+ .setCelExpression(CelExpressionSource.fromSource(policyFile))
+ .setCel(CEL)
+ .addFileTypes(
+ TestAllTypes.getDescriptor().getFile(),
+ Struct.getDescriptor().getFile());
+
+ // Scopes the custom Kubernetes tag visitor exclusively to k8s tests to prevent non-standard
+ // grammar leakage.
+ if (name.startsWith("k8s/")) {
+ contextBuilder.setCelPolicyParser(
+ CelPolicyParserFactory.newYamlParserBuilder().addTagVisitor(new K8sTagHandler()).build());
+ }
+
+ Path yamlConfigPath = Paths.get(dirPath, "config.yaml");
+ Path textprotoConfigPath = Paths.get(dirPath, "config.textproto");
+
+ if (Files.exists(yamlConfigPath)) {
+ contextBuilder.setConfigFile(yamlConfigPath.toString());
+ } else if (Files.exists(textprotoConfigPath)) {
+ contextBuilder.setConfigFile(textprotoConfigPath.toString());
+ }
+
+ try {
+ TestRunnerLibrary.runTest(testCase, contextBuilder.build());
+ } catch (CelPolicyValidationException e) {
+ if (testCase.output().kind() == CelTestCase.Output.Kind.EVAL_ERROR) {
+ String expectedError = testCase.output().evalError().get(0).toString();
+ assertThat(e.getMessage().toLowerCase(Locale.US))
+ .contains(expectedError.toLowerCase(Locale.US));
+ } else {
+ throw e;
+ }
+ }
+ }
+}
diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java
new file mode 100644
index 000000000..62812b124
--- /dev/null
+++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTestRunner.java
@@ -0,0 +1,219 @@
+// 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.conformance.policy;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Files;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.TypeRegistry;
+import com.google.protobuf.Value;
+import dev.cel.testing.testrunner.CelTestSuite;
+import dev.cel.testing.testrunner.CelTestSuite.CelTestSection;
+import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase;
+import dev.cel.testing.testrunner.CelTestSuiteTextProtoParser;
+import dev.cel.testing.testrunner.CelTestSuiteYamlParser;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.ParentRunner;
+import org.junit.runners.model.InitializationError;
+
+/** Custom JUnit runner for CEL policy conformance tests. */
+public final class PolicyConformanceTestRunner extends ParentRunner {
+
+ private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings();
+ private static final String TESTS_YAML_FILE_NAME = "tests.yaml";
+ private static final String TESTS_TEXTPROTO_FILE_NAME = "tests.textproto";
+ private static final String POLICY_YAML_FILE_NAME = "policy.yaml";
+ private static final TypeRegistry TYPE_REGISTRY =
+ TypeRegistry.newBuilder()
+ .add(Struct.getDescriptor())
+ .add(Value.getDescriptor())
+ .add(ListValue.getDescriptor())
+ .build();
+
+ private static final String TEST_DIRS_PROP =
+ System.getProperty("dev.cel.policy.conformance.tests");
+ private static final String TESTDATA_DIR =
+ System.getProperty("dev.cel.policy.conformance.testdata_dir", "testdata");
+ private static final String SKIP_TESTS_PROP =
+ System.getProperty("dev.cel.policy.conformance.skip_tests");
+
+ private static final ImmutableList TESTS_TO_SKIP =
+ Strings.isNullOrEmpty(SKIP_TESTS_PROP)
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(SPLITTER.splitToList(SKIP_TESTS_PROP));
+
+ private static final ImmutableList TEST_DIRS =
+ Strings.isNullOrEmpty(TEST_DIRS_PROP)
+ ? discoverTestDirs(TESTDATA_DIR)
+ : ImmutableList.copyOf(SPLITTER.splitToList(TEST_DIRS_PROP));
+
+ private static ImmutableList discoverTestDirs(String testdataDir) {
+ File dir = new File(testdataDir);
+ if (!dir.exists() || !dir.isDirectory()) {
+ return ImmutableList.of();
+ }
+ File[] topLevelDirs = dir.listFiles(File::isDirectory);
+ if (topLevelDirs == null) {
+ return ImmutableList.of();
+ }
+
+ ImmutableList.Builder testDirsBuilder = ImmutableList.builder();
+ Arrays.sort(topLevelDirs);
+ for (File topLevelDir : topLevelDirs) {
+ if (hasTestSuite(topLevelDir)) {
+ testDirsBuilder.add(topLevelDir.getName());
+ continue;
+ }
+
+ // Check one level deeper to support nested tests like compile_errors/unreachable
+ File[] subDirs = topLevelDir.listFiles(File::isDirectory);
+ if (subDirs == null) {
+ continue;
+ }
+
+ Arrays.sort(subDirs);
+ for (File subDir : subDirs) {
+ if (hasTestSuite(subDir)) {
+ testDirsBuilder.add(topLevelDir.getName() + "/" + subDir.getName());
+ }
+ }
+ }
+
+ return testDirsBuilder.build();
+ }
+
+ private static boolean hasTestSuite(File dir) {
+ return (new File(dir, TESTS_YAML_FILE_NAME).exists()
+ || new File(dir, TESTS_TEXTPROTO_FILE_NAME).exists())
+ && new File(dir, POLICY_YAML_FILE_NAME).exists();
+ }
+
+ private final ImmutableList tests;
+
+ private ImmutableList loadTests() {
+ if (TEST_DIRS.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ ImmutableList.Builder testsBuilder = ImmutableList.builder();
+
+ for (String dir : TEST_DIRS) {
+ String fullDirPath = TESTDATA_DIR + "/" + dir;
+ try {
+ ImmutableList suites = readTestSuites(fullDirPath);
+ for (CelTestSuiteContext namedSuite : suites) {
+ for (CelTestSection section : namedSuite.testSuite().sections()) {
+ for (CelTestCase testCase : section.tests()) {
+ String baseName = String.format("%s/%s/%s", dir, section.name(), testCase.name());
+ String displayName = baseName + namedSuite.formatSuffix();
+ if (!shouldSkipTest(baseName, TESTS_TO_SKIP)) {
+ testsBuilder.add(new PolicyConformanceTest(displayName, testCase, fullDirPath));
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load test suite in " + fullDirPath, e);
+ }
+ }
+ return testsBuilder.build();
+ }
+
+ private static boolean shouldSkipTest(String name, List testsToSkip) {
+ for (String testToSkip : testsToSkip) {
+ if (name.startsWith(testToSkip)) {
+ String consumedName = name.substring(testToSkip.length());
+ if (consumedName.isEmpty() || consumedName.startsWith("/")) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static ImmutableList readTestSuites(String dirPath)
+ throws Exception {
+ File dir = new File(dirPath);
+ File yamlFile = new File(dir, TESTS_YAML_FILE_NAME);
+ File textprotoFile = new File(dir, TESTS_TEXTPROTO_FILE_NAME);
+
+ boolean bothExist = yamlFile.exists() && textprotoFile.exists();
+ ImmutableList.Builder suitesBuilder = ImmutableList.builder();
+
+ if (yamlFile.exists()) {
+ suitesBuilder.add(
+ CelTestSuiteContext.create(
+ CelTestSuiteYamlParser.newInstance()
+ .parse(Files.asCharSource(yamlFile, UTF_8).read()),
+ bothExist ? " (yaml)" : ""));
+ }
+ if (textprotoFile.exists()) {
+ suitesBuilder.add(
+ CelTestSuiteContext.create(
+ CelTestSuiteTextProtoParser.newInstance()
+ .parse(Files.asCharSource(textprotoFile, UTF_8).read(), TYPE_REGISTRY),
+ bothExist ? " (textproto)" : ""));
+ }
+
+ ImmutableList suites = suitesBuilder.build();
+ if (suites.isEmpty()) {
+ throw new IllegalArgumentException(
+ String.format(
+ "No %s or %s found in %s", TESTS_YAML_FILE_NAME, TESTS_TEXTPROTO_FILE_NAME, dirPath));
+ }
+ return suites;
+ }
+
+ @Override
+ protected ImmutableList getChildren() {
+ return tests;
+ }
+
+ @Override
+ protected Description describeChild(PolicyConformanceTest child) {
+ return Description.createTestDescription(getTestClass().getJavaClass(), child.getName());
+ }
+
+ @Override
+ protected void runChild(PolicyConformanceTest child, RunNotifier notifier) {
+ runLeaf(child, describeChild(child), notifier);
+ }
+
+ public PolicyConformanceTestRunner(Class> clazz) throws InitializationError {
+ super(clazz);
+ this.tests = loadTests();
+ }
+
+ @AutoValue
+ abstract static class CelTestSuiteContext {
+ abstract CelTestSuite testSuite();
+
+ abstract String formatSuffix();
+
+ static CelTestSuiteContext create(CelTestSuite testSuite, String formatSuffix) {
+ return new AutoValue_PolicyConformanceTestRunner_CelTestSuiteContext(testSuite, formatSuffix);
+ }
+ }
+}
diff --git a/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java
new file mode 100644
index 000000000..46596763e
--- /dev/null
+++ b/conformance/src/test/java/dev/cel/conformance/policy/PolicyConformanceTests.java
@@ -0,0 +1,21 @@
+// 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.conformance.policy;
+
+import org.junit.runner.RunWith;
+
+/** Main test class for CEL policy conformance tests. */
+@RunWith(PolicyConformanceTestRunner.class)
+public class PolicyConformanceTests {}
diff --git a/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl
new file mode 100644
index 000000000..b53d982bb
--- /dev/null
+++ b/conformance/src/test/java/dev/cel/conformance/policy/cel_policy_conformance_test.bzl
@@ -0,0 +1,54 @@
+# 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.
+
+"""Macro to run CEL policy conformance tests."""
+
+load("@rules_java//java:defs.bzl", "java_test")
+
+def cel_policy_conformance_test_java(
+ name,
+ testdata,
+ test_cases = [],
+ skip_tests = [],
+ **kwargs):
+ """Macro to run CEL policy conformance tests for Java.
+
+ Args:
+ name: The name of the test target.
+ testdata: Testdata filegroup target.
+ test_cases: (optional) List of test case names (directory names) to run.
+ skip_tests: (optional) List of test case names (directory names) to skip.
+ **kwargs: Other standard Bazel target attributes.
+ """
+
+ lbl = native.package_relative_label(testdata)
+
+ # Under Bzlmod, external repository runfiles are located in sibling directories
+ # named after their canonical repository name.
+ repo_prefix = "../" + lbl.workspace_name + "/" if lbl.workspace_name else ""
+ testdata_dir = repo_prefix + lbl.package + "/" + lbl.name
+
+ java_test(
+ name = name,
+ jvm_flags = [
+ "-Ddev.cel.policy.conformance.tests=" + ",".join(test_cases),
+ "-Ddev.cel.policy.conformance.testdata_dir=" + testdata_dir,
+ "-Ddev.cel.policy.conformance.skip_tests=" + ",".join(skip_tests),
+ ],
+ data = [testdata],
+ size = "small",
+ test_class = "dev.cel.conformance.policy.PolicyConformanceTests",
+ runtime_deps = [Label(":run")],
+ **kwargs
+ )
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 ed2d19d6f..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",
@@ -42,6 +43,7 @@ java_library(
":strings",
"//common:options",
"//extensions:extension_library",
+ "@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
],
)
@@ -121,7 +123,6 @@ java_library(
":extension_library",
"//checker:checker_builder",
"//common:compiler_common",
- "//common:options",
"//common/ast",
"//common/exceptions:numeric_overflow",
"//common/internal:comparison_functions",
@@ -142,6 +143,7 @@ java_library(
deps = [
"//common:compiler_common",
"//common/ast",
+ "//common/types",
"//compiler:compiler_builder",
"//extensions:extension_library",
"//parser:macro",
@@ -184,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",
@@ -306,6 +309,7 @@ java_library(
"//common:options",
"//common/ast",
"//common/types",
+ "//common/values:mutable_map_value",
"//compiler:compiler_builder",
"//extensions:extension_library",
"//parser:macro",
@@ -316,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/CelBindingsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java
index 5eb2c2e8c..0e6537334 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelBindingsExtensions.java
@@ -22,7 +22,11 @@
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.CelFunctionDecl;
import dev.cel.common.CelIssue;
+import dev.cel.common.CelOverloadDecl;
import dev.cel.common.ast.CelExpr;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.TypeParamType;
import dev.cel.compiler.CelCompilerLibrary;
import dev.cel.parser.CelMacro;
import dev.cel.parser.CelMacroExprFactory;
@@ -62,7 +66,15 @@ public int version() {
@Override
public ImmutableSet functions() {
- return ImmutableSet.of();
+ // TODO: Add bindings for block once decorator support is available.
+ return ImmutableSet.of(
+ CelFunctionDecl.newFunctionDeclaration(
+ "cel.@block",
+ CelOverloadDecl.newGlobalOverload(
+ "cel_block_list",
+ TypeParamType.create("T"),
+ ListType.create(SimpleType.DYN),
+ TypeParamType.create("T"))));
}
@Override
diff --git a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java
index 23663f02e..3bf47c4a6 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelComprehensionsExtensions.java
@@ -29,6 +29,7 @@
import dev.cel.common.ast.CelExpr;
import dev.cel.common.types.MapType;
import dev.cel.common.types.TypeParamType;
+import dev.cel.common.values.MutableMapValue;
import dev.cel.compiler.CelCompilerLibrary;
import dev.cel.parser.CelMacro;
import dev.cel.parser.CelMacroExprFactory;
@@ -118,29 +119,18 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
@Override
public void setRuntimeOptions(
CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) {
- for (Function function : functions) {
- for (CelOverloadDecl overload : function.functionDecl.overloads()) {
- switch (overload.overloadId()) {
- case MAP_INSERT_OVERLOAD_MAP_MAP:
- runtimeBuilder.addFunctionBindings(
- CelFunctionBinding.from(
- MAP_INSERT_OVERLOAD_MAP_MAP,
- Map.class,
- Map.class,
- (map1, map2) -> mapInsertMap(map1, map2, runtimeEquality)));
- break;
- case MAP_INSERT_OVERLOAD_KEY_VALUE:
- runtimeBuilder.addFunctionBindings(
- CelFunctionBinding.from(
- MAP_INSERT_OVERLOAD_KEY_VALUE,
- ImmutableList.of(Map.class, Object.class, Object.class),
- args -> mapInsertKeyValue(args, runtimeEquality)));
- break;
- default:
- // Nothing to add.
- }
- }
- }
+ runtimeBuilder.addFunctionBindings(
+ CelFunctionBinding.fromOverloads(
+ MAP_INSERT_FUNCTION,
+ CelFunctionBinding.from(
+ MAP_INSERT_OVERLOAD_MAP_MAP,
+ Map.class,
+ Map.class,
+ (map1, map2) -> mapInsertMap(map1, map2, runtimeEquality)),
+ CelFunctionBinding.from(
+ MAP_INSERT_OVERLOAD_KEY_VALUE,
+ ImmutableList.of(Map.class, Object.class, Object.class),
+ args -> mapInsertKeyValue(args, runtimeEquality))));
}
@Override
@@ -182,38 +172,46 @@ public void setParserOptions(CelParserBuilder parserBuilder) {
parserBuilder.addMacros(macros());
}
- // TODO: Implement a more efficient map insertion based on mutability once mutable
- // maps are supported in Java stack.
- private static ImmutableMap mapInsertMap(
+ private static Map mapInsertMap(
Map, ?> targetMap, Map, ?> mapToMerge, RuntimeEquality equality) {
- ImmutableMap.Builder resultBuilder =
- ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size());
-
- for (Map.Entry, ?> entry : mapToMerge.entrySet()) {
- if (equality.findInMap(targetMap, entry.getKey()).isPresent()) {
+ for (Object key : mapToMerge.keySet()) {
+ if (equality.findInMap(targetMap, key).isPresent()) {
throw new IllegalArgumentException(
- String.format("insert failed: key '%s' already exists", entry.getKey()));
- } else {
- resultBuilder.put(entry.getKey(), entry.getValue());
+ String.format("insert failed: key '%s' already exists", key));
}
}
- return resultBuilder.putAll(targetMap).buildOrThrow();
+
+ if (targetMap instanceof MutableMapValue) {
+ MutableMapValue wrapper = (MutableMapValue) targetMap;
+ wrapper.putAll(mapToMerge);
+ return wrapper;
+ }
+
+ return ImmutableMap.builderWithExpectedSize(targetMap.size() + mapToMerge.size())
+ .putAll(targetMap)
+ .putAll(mapToMerge)
+ .buildOrThrow();
}
- private static ImmutableMap mapInsertKeyValue(
- Object[] args, RuntimeEquality equality) {
- Map, ?> map = (Map, ?>) args[0];
+ private static Map mapInsertKeyValue(Object[] args, RuntimeEquality equality) {
+ Map, ?> mapArg = (Map, ?>) args[0];
Object key = args[1];
Object value = args[2];
- if (equality.findInMap(map, key).isPresent()) {
+ if (equality.findInMap(mapArg, key).isPresent()) {
throw new IllegalArgumentException(
String.format("insert failed: key '%s' already exists", key));
}
+ if (mapArg instanceof MutableMapValue) {
+ MutableMapValue mutableMap = (MutableMapValue) mapArg;
+ mutableMap.put(key, value);
+ return mutableMap;
+ }
+
ImmutableMap.Builder builder =
- ImmutableMap.builderWithExpectedSize(map.size() + 1);
- return builder.put(key, value).putAll(map).buildOrThrow();
+ ImmutableMap.builderWithExpectedSize(mapArg.size() + 1);
+ return builder.put(key, value).putAll(mapArg).buildOrThrow();
}
private static Optional expandAllMacro(
diff --git a/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java
index a98f9db41..498b8555e 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelEncoderExtensions.java
@@ -135,9 +135,13 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
functions.forEach(
function -> {
if (celOptions.evaluateCanonicalTypesToNativeValues()) {
- runtimeBuilder.addFunctionBindings(function.nativeBytesFunctionBinding);
+ runtimeBuilder.addFunctionBindings(
+ CelFunctionBinding.fromOverloads(
+ function.getFunction(), function.nativeBytesFunctionBinding));
} else {
- runtimeBuilder.addFunctionBindings(function.protoBytesFunctionBinding);
+ runtimeBuilder.addFunctionBindings(
+ CelFunctionBinding.fromOverloads(
+ function.getFunction(), function.protoBytesFunctionBinding));
}
});
}
diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
index 2d14ed118..8adc39384 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
@@ -15,11 +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;
/**
@@ -121,12 +123,9 @@ public static CelProtoExtensions protos() {
* This will include all functions denoted in {@link CelMathExtensions.Function}, including any
* future additions. To expose only a subset of these, use {@link #math(CelOptions,
* CelMathExtensions.Function...)} or {@link #math(CelOptions,int)} instead.
- *
- * @param celOptions CelOptions to configure CelMathExtension with. This should be the same
- * options object used to configure the compilation/runtime environments.
*/
- public static CelMathExtensions math(CelOptions celOptions) {
- return CelMathExtensions.library(celOptions).latest();
+ public static CelMathExtensions math() {
+ return CelMathExtensions.library().latest();
}
/**
@@ -134,8 +133,8 @@ public static CelMathExtensions math(CelOptions celOptions) {
*
*
Refer to README.md for functions available in each version.
*/
- public static CelMathExtensions math(CelOptions celOptions, int version) {
- return CelMathExtensions.library(celOptions).version(version);
+ public static CelMathExtensions math(int version) {
+ return CelMathExtensions.library().version(version);
}
/**
@@ -150,13 +149,9 @@ public static CelMathExtensions math(CelOptions celOptions, int version) {
* collision.
*
*
This will include only the specific functions denoted by {@link CelMathExtensions.Function}.
- *
- * @param celOptions CelOptions to configure CelMathExtension with. This should be the same
- * options object used to configure the compilation/runtime environments.
*/
- public static CelMathExtensions math(
- CelOptions celOptions, CelMathExtensions.Function... functions) {
- return math(celOptions, ImmutableSet.copyOf(functions));
+ public static CelMathExtensions math(CelMathExtensions.Function... functions) {
+ return math(ImmutableSet.copyOf(functions));
}
/**
@@ -171,13 +166,49 @@ public static CelMathExtensions math(
* collision.
*
*
This will include only the specific functions denoted by {@link CelMathExtensions.Function}.
- *
- * @param celOptions CelOptions to configure CelMathExtension with. This should be the same
- * options object used to configure the compilation/runtime environments.
*/
+ public static CelMathExtensions math(Set functions) {
+ return new CelMathExtensions(functions);
+ }
+
+ /**
+ * @deprecated Use {@link #math()} instead.
+ */
+ @Deprecated
+ @InlineMe(replacement = "CelExtensions.math()", imports = "dev.cel.extensions.CelExtensions")
+ public static CelMathExtensions math(CelOptions unused) {
+ return math();
+ }
+
+ /**
+ * @deprecated Use {@link #math(int)} instead.
+ */
+ @Deprecated
+ @InlineMe(
+ replacement = "CelExtensions.math(version)",
+ imports = "dev.cel.extensions.CelExtensions")
+ public static CelMathExtensions math(CelOptions unused, int version) {
+ return math(version);
+ }
+
+ /**
+ * @deprecated Use {@link #math(Function...)} instead.
+ */
+ @Deprecated
+ public static CelMathExtensions math(CelOptions unused, CelMathExtensions.Function... functions) {
+ return math(ImmutableSet.copyOf(functions));
+ }
+
+ /**
+ * @deprecated Use {@link #math(Set)} instead.
+ */
+ @Deprecated
+ @InlineMe(
+ replacement = "CelExtensions.math(functions)",
+ imports = "dev.cel.extensions.CelExtensions")
public static CelMathExtensions math(
- CelOptions celOptions, Set functions) {
- return new CelMathExtensions(celOptions, functions);
+ CelOptions unused, Set functions) {
+ return math(functions);
}
/**
@@ -319,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.
*
@@ -328,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());
}
@@ -354,7 +396,7 @@ public static CelExtensionLibrary extends CelExtensionLibrary.FeatureSet> getE
case "lists":
return CelListsExtensions.library();
case "math":
- return CelMathExtensions.library(options);
+ return CelMathExtensions.library();
case "optional":
return CelOptionalLibrary.library();
case "protos":
diff --git a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java
index a91edd822..79539b008 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java
@@ -128,7 +128,8 @@ public enum Function {
"list_sort",
"Sorts a list with comparable elements.",
ListType.create(TypeParamType.create("T")),
- ListType.create(TypeParamType.create("T"))))),
+ ListType.create(TypeParamType.create("T")))),
+ CelFunctionBinding.from("list_sort", Collection.class, CelListsExtensions::sort)),
SORT_BY(
CelFunctionDecl.newFunctionDeclaration(
"lists.@sortByAssociatedKeys",
@@ -136,7 +137,11 @@ public enum Function {
"list_sortByAssociatedKeys",
"Sorts a list by a key value. Used by the 'sortBy' macro",
ListType.create(TypeParamType.create("T")),
- ListType.create(TypeParamType.create("T")))));
+ ListType.create(TypeParamType.create("T")))),
+ CelFunctionBinding.from(
+ "list_sortByAssociatedKeys",
+ Collection.class,
+ CelListsExtensions::sortByAssociatedKeys));
private final CelFunctionDecl functionDecl;
private final ImmutableSet functionBindings;
@@ -147,7 +152,10 @@ String getFunction() {
Function(CelFunctionDecl functionDecl, CelFunctionBinding... functionBindings) {
this.functionDecl = functionDecl;
- this.functionBindings = ImmutableSet.copyOf(functionBindings);
+ this.functionBindings =
+ functionBindings.length > 0
+ ? CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings)
+ : ImmutableSet.of();
}
}
@@ -240,32 +248,13 @@ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
@Override
public void setRuntimeOptions(
CelRuntimeBuilder runtimeBuilder, RuntimeEquality runtimeEquality, CelOptions celOptions) {
- for (Function function : functions) {
- runtimeBuilder.addFunctionBindings(function.functionBindings);
- for (CelOverloadDecl overload : function.functionDecl.overloads()) {
- switch (overload.overloadId()) {
- case "list_distinct":
- runtimeBuilder.addFunctionBindings(
- CelFunctionBinding.from(
- "list_distinct", Collection.class, (list) -> distinct(list, runtimeEquality)));
- break;
- case "list_sort":
- runtimeBuilder.addFunctionBindings(
- CelFunctionBinding.from(
- "list_sort", Collection.class, (list) -> sort(list, celOptions)));
- break;
- case "list_sortByAssociatedKeys":
- runtimeBuilder.addFunctionBindings(
- CelFunctionBinding.from(
- "list_sortByAssociatedKeys",
- Collection.class,
- (list) -> sortByAssociatedKeys(list, celOptions)));
- break;
- default:
- // Nothing to add
- }
- }
- }
+ functions.forEach(function -> runtimeBuilder.addFunctionBindings(function.functionBindings));
+
+ runtimeBuilder.addFunctionBindings(
+ CelFunctionBinding.fromOverloads(
+ "distinct",
+ CelFunctionBinding.from(
+ "list_distinct", Collection.class, (list) -> distinct(list, runtimeEquality))));
}
private static ImmutableList slice(Collection list, long from, long to) {
@@ -369,22 +358,18 @@ private static List reverse(Collection list) {
}
}
- private static ImmutableList sort(Collection objects, CelOptions options) {
- return ImmutableList.sortedCopyOf(
- new CelObjectComparator(options.enableHeterogeneousNumericComparisons()), objects);
+ private static ImmutableList sort(Collection objects) {
+ return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects);
}
private static class CelObjectComparator implements Comparator {
- private final boolean enableHeterogeneousNumericComparisons;
- CelObjectComparator(boolean enableHeterogeneousNumericComparisons) {
- this.enableHeterogeneousNumericComparisons = enableHeterogeneousNumericComparisons;
- }
+ CelObjectComparator() {}
@SuppressWarnings({"unchecked"})
@Override
public int compare(Object o1, Object o2) {
- if (o1 instanceof Number && o2 instanceof Number && enableHeterogeneousNumericComparisons) {
+ if (o1 instanceof Number && o2 instanceof Number) {
return ComparisonFunctions.numericCompare((Number) o1, (Number) o2);
}
@@ -444,12 +429,9 @@ private static Optional sortByMacro(
@SuppressWarnings({"unchecked", "rawtypes"})
private static ImmutableList sortByAssociatedKeys(
- Collection> keyValuePairs, CelOptions options) {
+ Collection> keyValuePairs) {
List[] array = keyValuePairs.toArray(new List[0]);
- Arrays.sort(
- array,
- new CelObjectByKeyComparator(
- new CelObjectComparator(options.enableHeterogeneousNumericComparisons())));
+ Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator()));
ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(array.length);
for (List pair : array) {
builder.add(pair.get(1));
diff --git a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java
index 22336eb22..63108aa0c 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelMathExtensions.java
@@ -27,7 +27,6 @@
import dev.cel.checker.CelCheckerBuilder;
import dev.cel.common.CelFunctionDecl;
import dev.cel.common.CelIssue;
-import dev.cel.common.CelOptions;
import dev.cel.common.CelOverloadDecl;
import dev.cel.common.ast.CelConstant;
import dev.cel.common.ast.CelExpr;
@@ -136,7 +135,8 @@ public final class CelMathExtensions
return builder.buildOrThrow();
}
- enum Function {
+ /** Enumeration of functions for Math extension. */
+ public enum Function {
MAX(
CelFunctionDecl.newFunctionDeclaration(
MATH_MAX_FUNCTION,
@@ -206,51 +206,59 @@ enum Function {
MATH_MAX_OVERLOAD_DOC,
SimpleType.DYN,
ListType.create(SimpleType.DYN))),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@max_double", Double.class, x -> x),
- CelFunctionBinding.from("math_@max_int", Long.class, x -> x),
- CelFunctionBinding.from(
- "math_@max_double_double", Double.class, Double.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from("math_@max_list_dyn", List.class, CelMathExtensions::maxList)),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@max_uint", Long.class, x -> x),
- CelFunctionBinding.from(
- "math_@max_uint_uint", Long.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_double_uint", Double.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_uint_int", Long.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_uint_double", Long.class, Double.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_int_uint", Long.class, Long.class, CelMathExtensions::maxPair)),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x),
- CelFunctionBinding.from(
- "math_@max_uint_uint",
- UnsignedLong.class,
- UnsignedLong.class,
- CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_double_uint",
- Double.class,
- UnsignedLong.class,
- CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_uint_double",
- UnsignedLong.class,
- Double.class,
- CelMathExtensions::maxPair),
- CelFunctionBinding.from(
- "math_@max_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::maxPair))),
+ ImmutableSet.builder()
+ .add(CelFunctionBinding.from("math_@max_double", Double.class, x -> x))
+ .add(CelFunctionBinding.from("math_@max_int", Long.class, x -> x))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_double_double",
+ Double.class,
+ Double.class,
+ CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_int_int", Long.class, Long.class, CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_int_double", Long.class, Double.class, CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_double_int", Double.class, Long.class, CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_list_dyn", List.class, CelMathExtensions::maxList))
+ .add(CelFunctionBinding.from("math_@max_uint", UnsignedLong.class, x -> x))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_uint_uint",
+ UnsignedLong.class,
+ UnsignedLong.class,
+ CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_double_uint",
+ Double.class,
+ UnsignedLong.class,
+ CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_uint_int",
+ UnsignedLong.class,
+ Long.class,
+ CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_uint_double",
+ UnsignedLong.class,
+ Double.class,
+ CelMathExtensions::maxPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@max_int_uint",
+ Long.class,
+ UnsignedLong.class,
+ CelMathExtensions::maxPair))
+ .build()),
MIN(
CelFunctionDecl.newFunctionDeclaration(
MATH_MIN_FUNCTION,
@@ -320,51 +328,59 @@ enum Function {
MATH_MIN_OVERLOAD_DOC,
SimpleType.DYN,
ListType.create(SimpleType.DYN))),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@min_double", Double.class, x -> x),
- CelFunctionBinding.from("math_@min_int", Long.class, x -> x),
- CelFunctionBinding.from(
- "math_@min_double_double", Double.class, Double.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_double_int", Double.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from("math_@min_list_dyn", List.class, CelMathExtensions::minList)),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@min_uint", Long.class, x -> x),
- CelFunctionBinding.from(
- "math_@min_uint_uint", Long.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_double_uint", Double.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_uint_int", Long.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_uint_double", Long.class, Double.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_int_uint", Long.class, Long.class, CelMathExtensions::minPair)),
- ImmutableSet.of(
- CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x),
- CelFunctionBinding.from(
- "math_@min_uint_uint",
- UnsignedLong.class,
- UnsignedLong.class,
- CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_double_uint",
- Double.class,
- UnsignedLong.class,
- CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_uint_int", UnsignedLong.class, Long.class, CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_uint_double",
- UnsignedLong.class,
- Double.class,
- CelMathExtensions::minPair),
- CelFunctionBinding.from(
- "math_@min_int_uint", Long.class, UnsignedLong.class, CelMathExtensions::minPair))),
+ ImmutableSet.builder()
+ .add(CelFunctionBinding.from("math_@min_double", Double.class, x -> x))
+ .add(CelFunctionBinding.from("math_@min_int", Long.class, x -> x))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_double_double",
+ Double.class,
+ Double.class,
+ CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_int_int", Long.class, Long.class, CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_int_double", Long.class, Double.class, CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_double_int", Double.class, Long.class, CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_list_dyn", List.class, CelMathExtensions::minList))
+ .add(CelFunctionBinding.from("math_@min_uint", UnsignedLong.class, x -> x))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_uint_uint",
+ UnsignedLong.class,
+ UnsignedLong.class,
+ CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_double_uint",
+ Double.class,
+ UnsignedLong.class,
+ CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_uint_int",
+ UnsignedLong.class,
+ Long.class,
+ CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_uint_double",
+ UnsignedLong.class,
+ Double.class,
+ CelMathExtensions::minPair))
+ .add(
+ CelFunctionBinding.from(
+ "math_@min_int_uint",
+ Long.class,
+ UnsignedLong.class,
+ CelMathExtensions::minPair))
+ .build()),
CEIL(
CelFunctionDecl.newFunctionDeclaration(
MATH_CEIL_FUNCTION,
@@ -646,36 +662,14 @@ enum Function {
private final CelFunctionDecl functionDecl;
private final ImmutableSet functionBindings;
- private final ImmutableSet functionBindingsULongSigned;
- private final ImmutableSet functionBindingsULongUnsigned;
String getFunction() {
return functionDecl.name();
}
Function(CelFunctionDecl functionDecl, ImmutableSet bindings) {
- this(functionDecl, bindings, ImmutableSet.of(), ImmutableSet.of());
- }
-
- Function(
- CelFunctionDecl functionDecl,
- ImmutableSet functionBindings,
- ImmutableSet functionBindingsULongSigned,
- ImmutableSet functionBindingsULongUnsigned) {
this.functionDecl = functionDecl;
- this.functionBindings =
- functionBindings.isEmpty()
- ? ImmutableSet.of()
- : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindings);
- this.functionBindingsULongSigned =
- functionBindingsULongSigned.isEmpty()
- ? ImmutableSet.of()
- : CelFunctionBinding.fromOverloads(functionDecl.name(), functionBindingsULongSigned);
- this.functionBindingsULongUnsigned =
- functionBindingsULongUnsigned.isEmpty()
- ? ImmutableSet.of()
- : CelFunctionBinding.fromOverloads(
- functionDecl.name(), functionBindingsULongUnsigned);
+ this.functionBindings = bindings;
}
}
@@ -684,10 +678,8 @@ private static final class Library implements CelExtensionLibrarybuilder()
.addAll(version1.functions)
.add(Function.SQRT)
- .build(),
- enableUnsignedLongs);
+ .build());
}
@Override
@@ -734,25 +724,20 @@ public ImmutableSet versions() {
}
}
- private static final Library LIBRARY_UNSIGNED_LONGS_ENABLED = new Library(true);
- private static final Library LIBRARY_UNSIGNED_LONGS_DISABLED = new Library(false);
+ private static final Library LIBRARY = new Library();
- static CelExtensionLibrary library(CelOptions celOptions) {
- return celOptions.enableUnsignedLongs()
- ? LIBRARY_UNSIGNED_LONGS_ENABLED
- : LIBRARY_UNSIGNED_LONGS_DISABLED;
+ static CelExtensionLibrary library() {
+ return LIBRARY;
}
- private final boolean enableUnsignedLongs;
private final ImmutableSet functions;
private final int version;
- CelMathExtensions(CelOptions celOptions, Set functions) {
- this(-1, functions, celOptions.enableUnsignedLongs());
+ CelMathExtensions(Set functions) {
+ this(-1, functions);
}
- private CelMathExtensions(int version, Set functions, boolean enableUnsignedLongs) {
- this.enableUnsignedLongs = enableUnsignedLongs;
+ private CelMathExtensions(int version, Set functions) {
this.version = version;
this.functions = ImmutableSet.copyOf(functions);
}
@@ -788,11 +773,11 @@ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
functions.forEach(
function -> {
- runtimeBuilder.addFunctionBindings(function.functionBindings);
- runtimeBuilder.addFunctionBindings(
- enableUnsignedLongs
- ? function.functionBindingsULongUnsigned
- : function.functionBindingsULongSigned);
+ ImmutableSet combined = function.functionBindings;
+ if (!combined.isEmpty()) {
+ runtimeBuilder.addFunctionBindings(
+ CelFunctionBinding.fromOverloads(function.functionDecl.name(), combined));
+ }
});
}
@@ -889,7 +874,7 @@ private static double round(double x) {
if (isNaN(x) || isInfinite(x)) {
return x;
}
- return DoubleMath.roundToLong(x, RoundingMode.HALF_EVEN);
+ return DoubleMath.roundToLong(x, RoundingMode.HALF_UP);
}
private static Number sign(Number x) {
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..ae9483f7c
--- /dev/null
+++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java
@@ -0,0 +1,1048 @@
+// 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 = ReflectionUtil.resolveGenericParameter(token, List.class, 0);
+ return ListType.create(
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(elementType), elementType, classMap));
+ }
+
+ if (Map.class.isAssignableFrom(type)) {
+ Type keyType = ReflectionUtil.resolveGenericParameter(token, Map.class, 0);
+ Type valueType = ReflectionUtil.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 = ReflectionUtil.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();
+ queue.addAll(TypeReferenceCollector.collect(genericPropType));
+ compiledGetter = compileGetter(getter);
+ } else if (field != null) {
+ propType = field.getType();
+ genericPropType = field.getGenericType();
+ queue.addAll(TypeReferenceCollector.collect(genericPropType));
+ 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, collecting
+ * them for subsequent properties discovery.
+ *
+ * "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}).
+ */
+ private static final class TypeReferenceCollector {
+ private final Set> collectedTypes = new HashSet<>();
+
+ /**
+ * Traverses the given type and returns an immutable set of all custom POJO classes found.
+ *
+ * @param type The Java type token or parameterized collection type to recursively unpack.
+ */
+ private static ImmutableSet> collect(Type type) {
+ TypeReferenceCollector collector = new TypeReferenceCollector();
+ collector.discover(type);
+ return ImmutableSet.copyOf(collector.collectedTypes);
+ }
+
+ private void discover(Type type) {
+ Preconditions.checkNotNull(type, "Type to discover cannot be null.");
+ TypeToken> token = TypeToken.of(type);
+ Class> rawType = token.getRawType();
+
+ if (List.class.isAssignableFrom(rawType)) {
+ discover(ReflectionUtil.resolveGenericParameter(token, List.class, 0));
+ return;
+ }
+
+ if (Map.class.isAssignableFrom(rawType)) {
+ discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 0));
+ discover(ReflectionUtil.resolveGenericParameter(token, Map.class, 1));
+ return;
+ }
+
+ if (rawType == Optional.class) {
+ discover(ReflectionUtil.resolveGenericParameter(token, Optional.class, 0));
+ return;
+ }
+
+ // Custom types are non-builtin, public classes
+ if (!JAVA_TO_DEFAULT_VALUE_MAP.containsKey(rawType)
+ && Modifier.isPublic(rawType.getModifiers())) {
+ collectedTypes.add(rawType);
+ }
+ }
+ }
+
+ private static Function