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 0d1d5431f..d572bb2bc 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -78,10 +78,14 @@ cel_android_library( 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 +94,52 @@ 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_errorprone_error_prone_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//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "values", srcs = CEL_VALUES_SOURCES, 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 70d04acc8..89f5ab100 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -21,8 +21,8 @@ import dev.cel.common.annotations.Internal; import java.util.Collection; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; +import java.util.function.Function; /** * {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link @@ -37,6 +37,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; } @@ -51,14 +57,26 @@ public Object maybeUnwrap(Object value) { return unwrap((CelValue) value); } + Object mapped = mapContainer(value, maybeUnwrapFunction); + if (mapped != value) { + return mapped; + } + + return value; + } + + /** + * 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) { 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(); } @@ -67,19 +85,14 @@ public Object maybeUnwrap(Object value) { ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(map.size()); for (Map.Entry entry : map.entrySet()) { - builder.put(maybeUnwrap(entry.getKey()), maybeUnwrap(entry.getValue())); + builder.put(mapper.apply(entry.getKey()), mapper.apply(entry.getValue())); } - return builder.buildOrThrow(); } return value; } - /** - * Canonicalizes an inbound {@code value} into a suitable Java object representation for - * evaluation. - */ public Object toRuntimeValue(Object value) { Preconditions.checkNotNull(value); @@ -87,14 +100,15 @@ public Object toRuntimeValue(Object value) { 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); } @@ -136,31 +150,8 @@ private 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/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/values/BUILD.bazel b/common/values/BUILD.bazel index 74bfa9e0f..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"], diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index dcdf3be52..adfba967b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -487,12 +487,6 @@ public CelRuntime build() { DynamicProto dynamicProto = DynamicProto.create(defaultMessageFactory); CelValueProvider protoMessageValueProvider = ProtoMessageValueProvider.newInstance(options(), dynamicProto); - CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); - if (valueProvider() != null) { - protoMessageValueProvider = - CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); - } - RuntimeEquality runtimeEquality = ProtoMessageRuntimeEquality.create(dynamicProto, options()); ImmutableSet runtimeLibraries = runtimeLibrariesBuilder().build(); // Add libraries, such as extensions @@ -505,6 +499,12 @@ public CelRuntime build() { } } + if (valueProvider() != null) { + protoMessageValueProvider = + CombinedCelValueProvider.combine(protoMessageValueProvider, valueProvider()); + } + CelValueConverter celValueConverter = protoMessageValueProvider.celValueConverter(); + CelTypeProvider messageTypeProvider = ProtoMessageTypeProvider.newBuilder() .setCelDescriptors(celDescriptors)