diff --git a/bundle/src/main/java/dev/cel/bundle/CelImpl.java b/bundle/src/main/java/dev/cel/bundle/CelImpl.java index f6b985065..f0db128c1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelImpl.java +++ b/bundle/src/main/java/dev/cel/bundle/CelImpl.java @@ -327,6 +327,9 @@ public Builder setTypeProvider(TypeProvider typeProvider) { @Override public CelBuilder setTypeProvider(CelTypeProvider celTypeProvider) { compilerBuilder.setTypeProvider(celTypeProvider); + if (runtimeBuilder instanceof CelRuntimeImpl.Builder) { + runtimeBuilder.setTypeProvider(celTypeProvider); + } return this; } diff --git a/common/src/main/java/dev/cel/common/values/OpaqueValue.java b/common/src/main/java/dev/cel/common/values/OpaqueValue.java index 3350d05d4..8b3ac4574 100644 --- a/common/src/main/java/dev/cel/common/values/OpaqueValue.java +++ b/common/src/main/java/dev/cel/common/values/OpaqueValue.java @@ -14,13 +14,32 @@ package dev.cel.common.values; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; import dev.cel.common.types.OpaqueType; -/** OpaqueValue is the value representation of OpaqueType. */ -@AutoValue -@AutoValue.CopyAnnotations -@SuppressWarnings("Immutable") // Java Object is mutable. +/** + * OpaqueValue is the value representation of an {@link OpaqueType}. + * + *

Users may provide a custom opaque type that CEL can understand. Note that this is only + * supported for the Planner runtime. There are two primary modes of extending this class: + * + *

+ */ +@Immutable public abstract class OpaqueValue extends CelValue { @Override @@ -31,7 +50,30 @@ public boolean isZeroValue() { @Override public abstract OpaqueType celType(); + /** + * Creates an {@code OpaqueValue} by wrapping a domain object. + * + *

This method should only be used for the "Wrapping" extension mode (see class Javadoc) when + * users cannot modify their POJOs to directly extend {@code OpaqueValue}. Using this method + * necessitates implementing and registering a custom {@link CelValueConverter}. + * + * @param name The name of the opaque type. + * @param value The raw Java object to wrap. + */ public static OpaqueValue create(String name, Object value) { - return new AutoValue_OpaqueValue(value, OpaqueType.create(name)); + return new AutoValue_OpaqueValue_OpaqueValueWrapper( + checkNotNull(value), OpaqueType.create(name)); + } + + @AutoValue + @AutoValue.CopyAnnotations + @Immutable + @SuppressWarnings("Immutable") + abstract static class OpaqueValueWrapper extends OpaqueValue { + @Override + public abstract Object value(); + + @Override + public abstract OpaqueType celType(); } } 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 cd7c24a63..76c761567 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -35,6 +35,7 @@ java_library( "//testing/protos:test_all_types_cel_java_proto3", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_guava_guava_testlib", "@maven//:com_google_protobuf_protobuf_java", diff --git a/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java b/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java index d97bcd28a..326572842 100644 --- a/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java +++ b/common/src/test/java/dev/cel/common/values/OpaqueValueTest.java @@ -17,7 +17,17 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.OpaqueType; +import java.util.Map; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,4 +47,177 @@ public void opaqueValue_construct() { public void create_nullValue_throws() { assertThrows(NullPointerException.class, () -> OpaqueValue.create("opaque_type_name", null)); } + + private static final OpaqueType CUSTOM_OPAQUE_TYPE = OpaqueType.create("custom_opaque_type"); + + private static final CelTypeProvider CUSTOM_OPAQUE_TYPE_PROVIDER = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(CUSTOM_OPAQUE_TYPE); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals(CUSTOM_OPAQUE_TYPE.name()) + ? Optional.of(CUSTOM_OPAQUE_TYPE) + : Optional.empty(); + } + }; + + private static final CelValueProvider CUSTOM_OPAQUE_VALUE_PROVIDER = + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return new CelValueConverter() { + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CustomOpaqueObject) { + CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value; + return new CelCustomOpaqueValue(customOpaqueObject); + } + return super.toRuntimeValue(value); + } + }; + } + }; + + private static final CelValueProvider WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER = + new CelValueProvider() { + @Override + public Optional newValue(String structType, Map fields) { + return Optional.empty(); + } + + @Override + public CelValueConverter celValueConverter() { + return new CelValueConverter() { + @Override + public Object toRuntimeValue(Object value) { + if (value instanceof CustomOpaqueObject) { + CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value; + return OpaqueValue.create(CUSTOM_OPAQUE_TYPE.name(), customOpaqueObject); + } + return super.toRuntimeValue(value); + } + }; + } + }; + + @Immutable + private static class CustomOpaqueObject { + private final String value; + + CustomOpaqueObject(String value) { + this.value = value; + } + + String getValue() { + return value; + } + } + + @Immutable + private static class CelCustomOpaqueValue extends OpaqueValue { + private final CustomOpaqueObject obj; + + CelCustomOpaqueValue(CustomOpaqueObject obj) { + this.obj = obj; + } + + @Override + public CustomOpaqueObject value() { + return obj; + } + + @Override + public OpaqueType celType() { + return CUSTOM_OPAQUE_TYPE; + } + } + + @Test + public void evaluate_customOpaqueValue_asVariable() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("opaque_var").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isInstanceOf(CustomOpaqueObject.class); + assertThat(((CustomOpaqueObject) result).getValue()).isEqualTo("hello"); + } + + @Test + public void evaluate_typeOfCustomOpaqueValue() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } + + @Test + public void evaluate_typeOfCustomOpaqueValue_wrapped() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .setValueProvider(WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + CustomOpaqueObject rawValue = new CustomOpaqueObject("hello"); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } + + @Immutable + private static class SelfReturningOpaqueObject extends OpaqueValue { + SelfReturningOpaqueObject() {} + + @Override + public Object value() { + return this; + } + + @Override + public OpaqueType celType() { + return CUSTOM_OPAQUE_TYPE; + } + } + + @Test + public void evaluate_selfReturningOpaqueValue_noConverter() throws Exception { + Cel cel = + CelFactory.plannerCelBuilder() + .addVar("opaque_var", CUSTOM_OPAQUE_TYPE) + .setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER) + .build(); + CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst(); + + SelfReturningOpaqueObject rawValue = new SelfReturningOpaqueObject(); + Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue)); + + assertThat(result).isEqualTo(true); + } } + 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 f25db8e87..978222869 100644 --- a/common/src/test/java/dev/cel/common/values/StructValueTest.java +++ b/common/src/test/java/dev/cel/common/values/StructValueTest.java @@ -128,6 +128,22 @@ public void celTypeTest() { assertThat(value.celType()).isEqualTo(CUSTOM_STRUCT_TYPE); } + @Test + public void evaluate_typeOfCustomStruct() throws Exception { + Cel cel = + 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("type(a) == custom_struct").getAst(); + + Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", new CelCustomStructValue(20))); + + assertThat(result).isEqualTo(true); + } + @Test public void evaluate_usingCustomClass_createNewStruct() throws Exception { Cel cel = diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 5178ae27c..17160e346 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -213,6 +213,7 @@ java_library( "//common/types:type_providers", "//common/values", "//common/values:cel_byte_string", + "//common/values:cel_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -229,6 +230,7 @@ cel_android_library( "//common/types:type_providers_android", "//common/types:types_android", "//common/values:cel_byte_string", + "//common/values:cel_value_android", "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -247,6 +249,7 @@ java_library( "//common/annotations", "//common/types", "//common/types:type_providers", + "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", @@ -896,6 +899,7 @@ java_library( "//common/internal:proto_message_factory", "//common/types:cel_types", "//common/types:type_providers", + "//common/values", "//common/values:cel_value_provider", "//common/values:proto_message_value_provider", "//runtime/standard:int", diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java index adfba967b..b02f64b61 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java @@ -521,7 +521,7 @@ public CelRuntime build() { } DescriptorTypeResolver descriptorTypeResolver = - DescriptorTypeResolver.create(combinedTypeProvider); + DescriptorTypeResolver.create(combinedTypeProvider, celValueConverter); TypeFunction typeFunction = TypeFunction.create(descriptorTypeResolver); mutableFunctionBindings.putAll(functionBindings()); diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java index 33702b2c6..b9ce022cf 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java @@ -42,6 +42,7 @@ import dev.cel.common.internal.ProtoMessageFactory; import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.CelTypes; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.ProtoMessageValueProvider; import dev.cel.runtime.standard.IntFunction.IntOverload; @@ -330,6 +331,7 @@ public CelRuntimeLegacyImpl build() { customBinding.getDefinition()); } + CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance(); RuntimeTypeProvider runtimeTypeProvider; if (options.enableCelValue()) { @@ -340,13 +342,17 @@ public CelRuntimeLegacyImpl build() { } runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider); + celValueConverter = messageValueProvider.celValueConverter(); } else { runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options); + if (celValueProvider != null) { + celValueConverter = celValueProvider.celValueConverter(); + } } DefaultInterpreter interpreter = new DefaultInterpreter( - DescriptorTypeResolver.create(), + DescriptorTypeResolver.create(celValueConverter), runtimeTypeProvider, dispatcherBuilder.build(), options); diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java index 63fcb87b6..3d5208e2e 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorTypeResolver.java @@ -23,6 +23,7 @@ import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; import java.util.NoSuchElementException; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -42,9 +43,13 @@ public final class DescriptorTypeResolver extends TypeResolver { /** * Creates a {@code DescriptorTypeResolver}. All protobuf messages are resolved as a type of * {@link StructTypeReference}. + * + * @deprecated This only exists to maintain support for the legacy runtime. Use {@link + * #create(CelTypeProvider, CelValueConverter)} instead. */ - public static DescriptorTypeResolver create() { - return new DescriptorTypeResolver(); + @Deprecated + static DescriptorTypeResolver create(CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(null, celValueConverter); } /** @@ -52,8 +57,9 @@ public static DescriptorTypeResolver create() { * in the provided {@link CelTypeProvider}, the message is resolved as a concrete {@code * ProtoMessageType} instead of a {@link StructTypeReference}. */ - public static DescriptorTypeResolver create(CelTypeProvider typeProvider) { - return new DescriptorTypeResolver(typeProvider); + public static DescriptorTypeResolver create( + CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + return new DescriptorTypeResolver(typeProvider, celValueConverter); } @Override @@ -81,11 +87,9 @@ public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { return super.resolveObjectType(obj, typeCheckedType); } - private DescriptorTypeResolver() { - this(null); - } - - private DescriptorTypeResolver(@Nullable CelTypeProvider typeProvider) { + private DescriptorTypeResolver( + @Nullable CelTypeProvider typeProvider, CelValueConverter celValueConverter) { + super(celValueConverter); this.typeProvider = typeProvider; } } diff --git a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java index d58eb3be4..8ce2d7733 100644 --- a/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/LiteRuntimeImpl.java @@ -177,7 +177,7 @@ public CelLiteRuntime build() { Interpreter interpreter = new DefaultInterpreter( - TypeResolver.create(), + TypeResolver.create(celValueProvider.celValueConverter()), CelValueRuntimeTypeProvider.newInstance(celValueProvider), dispatcherBuilder.build(), celOptions); diff --git a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java index c2ebf521c..691810837 100644 --- a/runtime/src/main/java/dev/cel/runtime/TypeResolver.java +++ b/runtime/src/main/java/dev/cel/runtime/TypeResolver.java @@ -36,6 +36,8 @@ import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValue; +import dev.cel.common.values.CelValueConverter; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -53,8 +55,10 @@ @Internal public class TypeResolver { - static TypeResolver create() { - return new TypeResolver(); + private final CelValueConverter celValueConverter; + + static TypeResolver create(CelValueConverter celValueConverter) { + return new TypeResolver(celValueConverter); } // Sentinel runtime value representing the special "type" ident. This ensures following to be @@ -147,6 +151,13 @@ public TypeType resolveObjectType(Object obj, CelType typeCheckedType) { return wellKnownTypeType.get(); } + if (celValueConverter != null) { + Object celVal = celValueConverter.toRuntimeValue(obj); + if (celVal instanceof CelValue) { + return TypeType.create(((CelValue) celVal).celType()); + } + } + if (obj instanceof MessageLiteOrBuilder) { // TODO: Replace with CelLiteDescriptor throw new UnsupportedOperationException("Not implemented yet"); @@ -193,5 +204,7 @@ private static CelType adaptStructType(StructType typeOfType) { return newTypeOfType; } - protected TypeResolver() {} + protected TypeResolver(CelValueConverter celValueConverter) { + this.celValueConverter = checkNotNull(celValueConverter); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java index bd0e96856..a23e3b2fb 100644 --- a/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DefaultInterpreterTest.java @@ -24,6 +24,7 @@ import dev.cel.common.CelOptions; import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.SimpleType; +import dev.cel.common.values.CelValueConverter; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; import dev.cel.parser.CelStandardMacro; @@ -98,7 +99,10 @@ public Object adapt(String messageName, Object message) { notStrictlyFalseBinding.getDefinition()); DefaultInterpreter defaultInterpreter = new DefaultInterpreter( - new TypeResolver(), emptyProvider, dispatcherBuilder.build(), CelOptions.DEFAULT); + TypeResolver.create(CelValueConverter.getDefaultInstance()), + emptyProvider, + dispatcherBuilder.build(), + CelOptions.DEFAULT); DefaultInterpretable interpretable = (DefaultInterpretable) defaultInterpreter.createInterpretable(ast); diff --git a/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java index c5a11b680..0437a31e5 100644 --- a/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java +++ b/runtime/src/test/java/dev/cel/runtime/TypeResolverTest.java @@ -28,6 +28,7 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeType; +import dev.cel.common.values.CelValueConverter; import java.util.ArrayList; import java.util.HashMap; import java.util.Optional; @@ -36,7 +37,8 @@ @RunWith(TestParameterInjector.class) public class TypeResolverTest { - private static final TypeResolver TYPE_RESOLVER = TypeResolver.create(); + private static final TypeResolver TYPE_RESOLVER = + TypeResolver.create(CelValueConverter.getDefaultInstance()); @Test public void resolveWellKnownObjectType_sentinelRuntimeType() { diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index c58ae782b..c749028ff 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -180,7 +180,9 @@ private static DefaultDispatcher newDispatcher() { addBindingsToDispatcher( builder, stdFunctions.newFunctionBindings(RUNTIME_EQUALITY, CEL_OPTIONS)); - TypeFunction typeFunction = TypeFunction.create(DescriptorTypeResolver.create(TYPE_PROVIDER)); + TypeFunction typeFunction = + TypeFunction.create( + DescriptorTypeResolver.create(TYPE_PROVIDER, CelValueConverter.getDefaultInstance())); addBindingsToDispatcher( builder, typeFunction.newFunctionBindings(CEL_OPTIONS, RUNTIME_EQUALITY));