Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bundle/src/main/java/dev/cel/bundle/CelImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
52 changes: 47 additions & 5 deletions common/src/main/java/dev/cel/common/values/OpaqueValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p>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:
*
* <ul>
* <li><b>Direct Extension (Recommended):</b> A domain object directly extends {@code OpaqueValue}
* and returns {@code this} for its {@link #value()} method. This approach allows the CEL
* engine to evaluate the object natively without stripping its type information, eliminating
* the need to register a custom {@link CelValueConverter}.
* <li><b>Wrapping:</b> A domain object is wrapped into an {@code OpaqueValue} via the {@link
* #create(String, Object)} factory method. This is required when users cannot modify their
* existing POJOs to extend {@code OpaqueValue}. However, because the CEL runtime aggressively
* unwraps objects during evaluation, this mode necessitates implementing and registering a
* custom {@code CelValueConverter} that maps the unwrapped native Java object back into its
* corresponding {@code OpaqueValue}.
* </ul>
*/
@Immutable
public abstract class OpaqueValue extends CelValue {

@Override
Expand All @@ -31,7 +50,30 @@ public boolean isZeroValue() {
@Override
public abstract OpaqueType celType();

/**
* Creates an {@code OpaqueValue} by wrapping a domain object.
*
* <p>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();
}
}
1 change: 1 addition & 0 deletions common/src/test/java/dev/cel/common/values/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
183 changes: 183 additions & 0 deletions common/src/test/java/dev/cel/common/values/OpaqueValueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<CelType> types() {
return ImmutableList.of(CUSTOM_OPAQUE_TYPE);
}

@Override
public Optional<CelType> 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<Object> newValue(String structType, Map<String, Object> 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<Object> newValue(String structType, Map<String, Object> 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);
}
}

16 changes: 16 additions & 0 deletions common/src/test/java/dev/cel/common/values/StructValueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
4 changes: 4 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ public CelRuntime build() {
}

DescriptorTypeResolver descriptorTypeResolver =
DescriptorTypeResolver.create(combinedTypeProvider);
DescriptorTypeResolver.create(combinedTypeProvider, celValueConverter);
TypeFunction typeFunction = TypeFunction.create(descriptorTypeResolver);

mutableFunctionBindings.putAll(functionBindings());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -330,6 +331,7 @@ public CelRuntimeLegacyImpl build() {
customBinding.getDefinition());
}

CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance();
RuntimeTypeProvider runtimeTypeProvider;

if (options.enableCelValue()) {
Expand All @@ -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);
Expand Down
Loading
Loading