Skip to content

Commit a5817b4

Browse files
l46kokcopybara-github
authored andcommitted
Make OpaqueValue subclassable and resolve type in TypeResolver
PiperOrigin-RevId: 932735464
1 parent 6e267b4 commit a5817b4

14 files changed

Lines changed: 303 additions & 23 deletions

File tree

bundle/src/main/java/dev/cel/bundle/CelImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ public Builder setTypeProvider(TypeProvider typeProvider) {
327327
@Override
328328
public CelBuilder setTypeProvider(CelTypeProvider celTypeProvider) {
329329
compilerBuilder.setTypeProvider(celTypeProvider);
330+
if (runtimeBuilder instanceof CelRuntimeImpl.Builder) {
331+
runtimeBuilder.setTypeProvider(celTypeProvider);
332+
}
330333
return this;
331334
}
332335

common/src/main/java/dev/cel/common/values/OpaqueValue.java

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,32 @@
1414

1515
package dev.cel.common.values;
1616

17+
import static com.google.common.base.Preconditions.checkNotNull;
18+
1719
import com.google.auto.value.AutoValue;
20+
import com.google.errorprone.annotations.Immutable;
1821
import dev.cel.common.types.OpaqueType;
1922

20-
/** OpaqueValue is the value representation of OpaqueType. */
21-
@AutoValue
22-
@AutoValue.CopyAnnotations
23-
@SuppressWarnings("Immutable") // Java Object is mutable.
23+
/**
24+
* OpaqueValue is the value representation of an {@link OpaqueType}.
25+
*
26+
* <p>Users may provide a custom opaque type that CEL can understand. Note that this is only
27+
* supported for the Planner runtime. There are two primary modes of extending this class:
28+
*
29+
* <ul>
30+
* <li><b>Direct Extension (Recommended):</b> A domain object directly extends {@code OpaqueValue}
31+
* and returns {@code this} for its {@link #value()} method. This approach allows the CEL
32+
* engine to evaluate the object natively without stripping its type information, eliminating
33+
* the need to register a custom {@link CelValueConverter}.
34+
* <li><b>Wrapping:</b> A domain object is wrapped into an {@code OpaqueValue} via the {@link
35+
* #create(String, Object)} factory method. This is required when users cannot modify their
36+
* existing POJOs to extend {@code OpaqueValue}. However, because the CEL runtime aggressively
37+
* unwraps objects during evaluation, this mode necessitates implementing and registering a
38+
* custom {@code CelValueConverter} that maps the unwrapped native Java object back into its
39+
* corresponding {@code OpaqueValue}.
40+
* </ul>
41+
*/
42+
@Immutable
2443
public abstract class OpaqueValue extends CelValue {
2544

2645
@Override
@@ -31,7 +50,30 @@ public boolean isZeroValue() {
3150
@Override
3251
public abstract OpaqueType celType();
3352

53+
/**
54+
* Creates an {@code OpaqueValue} by wrapping a domain object.
55+
*
56+
* <p>This method should only be used for the "Wrapping" extension mode (see class Javadoc) when
57+
* users cannot modify their POJOs to directly extend {@code OpaqueValue}. Using this method
58+
* necessitates implementing and registering a custom {@link CelValueConverter}.
59+
*
60+
* @param name The name of the opaque type.
61+
* @param value The raw Java object to wrap.
62+
*/
3463
public static OpaqueValue create(String name, Object value) {
35-
return new AutoValue_OpaqueValue(value, OpaqueType.create(name));
64+
return new AutoValue_OpaqueValue_OpaqueValueWrapper(
65+
checkNotNull(value), OpaqueType.create(name));
66+
}
67+
68+
@AutoValue
69+
@AutoValue.CopyAnnotations
70+
@Immutable
71+
@SuppressWarnings("Immutable")
72+
abstract static class OpaqueValueWrapper extends OpaqueValue {
73+
@Override
74+
public abstract Object value();
75+
76+
@Override
77+
public abstract OpaqueType celType();
3678
}
3779
}

common/src/test/java/dev/cel/common/values/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ java_library(
3535
"//testing/protos:test_all_types_cel_java_proto3",
3636
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",
3737
"@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
38+
"@maven//:com_google_errorprone_error_prone_annotations",
3839
"@maven//:com_google_guava_guava",
3940
"@maven//:com_google_guava_guava_testlib",
4041
"@maven//:com_google_protobuf_protobuf_java",

common/src/test/java/dev/cel/common/values/OpaqueValueTest.java

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@
1717
import static com.google.common.truth.Truth.assertThat;
1818
import static org.junit.Assert.assertThrows;
1919

20+
import com.google.common.collect.ImmutableList;
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.errorprone.annotations.Immutable;
23+
import dev.cel.bundle.Cel;
24+
import dev.cel.bundle.CelFactory;
25+
import dev.cel.common.CelAbstractSyntaxTree;
26+
import dev.cel.common.types.CelType;
27+
import dev.cel.common.types.CelTypeProvider;
2028
import dev.cel.common.types.OpaqueType;
29+
import java.util.Map;
30+
import java.util.Optional;
2131
import org.junit.Test;
2232
import org.junit.runner.RunWith;
2333
import org.junit.runners.JUnit4;
@@ -37,4 +47,177 @@ public void opaqueValue_construct() {
3747
public void create_nullValue_throws() {
3848
assertThrows(NullPointerException.class, () -> OpaqueValue.create("opaque_type_name", null));
3949
}
50+
51+
private static final OpaqueType CUSTOM_OPAQUE_TYPE = OpaqueType.create("custom_opaque_type");
52+
53+
private static final CelTypeProvider CUSTOM_OPAQUE_TYPE_PROVIDER =
54+
new CelTypeProvider() {
55+
@Override
56+
public ImmutableList<CelType> types() {
57+
return ImmutableList.of(CUSTOM_OPAQUE_TYPE);
58+
}
59+
60+
@Override
61+
public Optional<CelType> findType(String typeName) {
62+
return typeName.equals(CUSTOM_OPAQUE_TYPE.name())
63+
? Optional.of(CUSTOM_OPAQUE_TYPE)
64+
: Optional.empty();
65+
}
66+
};
67+
68+
private static final CelValueProvider CUSTOM_OPAQUE_VALUE_PROVIDER =
69+
new CelValueProvider() {
70+
@Override
71+
public Optional<Object> newValue(String structType, Map<String, Object> fields) {
72+
return Optional.empty();
73+
}
74+
75+
@Override
76+
public CelValueConverter celValueConverter() {
77+
return new CelValueConverter() {
78+
@Override
79+
public Object toRuntimeValue(Object value) {
80+
if (value instanceof CustomOpaqueObject) {
81+
CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value;
82+
return new CelCustomOpaqueValue(customOpaqueObject);
83+
}
84+
return super.toRuntimeValue(value);
85+
}
86+
};
87+
}
88+
};
89+
90+
private static final CelValueProvider WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER =
91+
new CelValueProvider() {
92+
@Override
93+
public Optional<Object> newValue(String structType, Map<String, Object> fields) {
94+
return Optional.empty();
95+
}
96+
97+
@Override
98+
public CelValueConverter celValueConverter() {
99+
return new CelValueConverter() {
100+
@Override
101+
public Object toRuntimeValue(Object value) {
102+
if (value instanceof CustomOpaqueObject) {
103+
CustomOpaqueObject customOpaqueObject = (CustomOpaqueObject) value;
104+
return OpaqueValue.create(CUSTOM_OPAQUE_TYPE.name(), customOpaqueObject);
105+
}
106+
return super.toRuntimeValue(value);
107+
}
108+
};
109+
}
110+
};
111+
112+
@Immutable
113+
private static class CustomOpaqueObject {
114+
private final String value;
115+
116+
CustomOpaqueObject(String value) {
117+
this.value = value;
118+
}
119+
120+
String getValue() {
121+
return value;
122+
}
123+
}
124+
125+
@Immutable
126+
private static class CelCustomOpaqueValue extends OpaqueValue {
127+
private final CustomOpaqueObject obj;
128+
129+
CelCustomOpaqueValue(CustomOpaqueObject obj) {
130+
this.obj = obj;
131+
}
132+
133+
@Override
134+
public CustomOpaqueObject value() {
135+
return obj;
136+
}
137+
138+
@Override
139+
public OpaqueType celType() {
140+
return CUSTOM_OPAQUE_TYPE;
141+
}
142+
}
143+
144+
@Test
145+
public void evaluate_customOpaqueValue_asVariable() throws Exception {
146+
Cel cel =
147+
CelFactory.plannerCelBuilder()
148+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
149+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
150+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
151+
.build();
152+
CelAbstractSyntaxTree ast = cel.compile("opaque_var").getAst();
153+
154+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
155+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
156+
157+
assertThat(result).isInstanceOf(CustomOpaqueObject.class);
158+
assertThat(((CustomOpaqueObject) result).getValue()).isEqualTo("hello");
159+
}
160+
161+
@Test
162+
public void evaluate_typeOfCustomOpaqueValue() throws Exception {
163+
Cel cel =
164+
CelFactory.plannerCelBuilder()
165+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
166+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
167+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
168+
.build();
169+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
170+
171+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
172+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
173+
174+
assertThat(result).isEqualTo(true);
175+
}
176+
177+
@Test
178+
public void evaluate_typeOfCustomOpaqueValue_wrapped() throws Exception {
179+
Cel cel =
180+
CelFactory.plannerCelBuilder()
181+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
182+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
183+
.setValueProvider(WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER)
184+
.build();
185+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
186+
187+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
188+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
189+
190+
assertThat(result).isEqualTo(true);
191+
}
192+
193+
@Immutable
194+
private static class SelfReturningOpaqueObject extends OpaqueValue {
195+
SelfReturningOpaqueObject() {}
196+
197+
@Override
198+
public Object value() {
199+
return this;
200+
}
201+
202+
@Override
203+
public OpaqueType celType() {
204+
return CUSTOM_OPAQUE_TYPE;
205+
}
206+
}
207+
208+
@Test
209+
public void evaluate_selfReturningOpaqueValue_noConverter() throws Exception {
210+
Cel cel =
211+
CelFactory.plannerCelBuilder()
212+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
213+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
214+
.build();
215+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
216+
217+
SelfReturningOpaqueObject rawValue = new SelfReturningOpaqueObject();
218+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
219+
220+
assertThat(result).isEqualTo(true);
221+
}
40222
}
223+

common/src/test/java/dev/cel/common/values/StructValueTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ public void celTypeTest() {
128128
assertThat(value.celType()).isEqualTo(CUSTOM_STRUCT_TYPE);
129129
}
130130

131+
@Test
132+
public void evaluate_typeOfCustomStruct() throws Exception {
133+
Cel cel =
134+
CelFactory.plannerCelBuilder()
135+
.setOptions(CelOptions.current().enableHeterogeneousNumericComparisons(true).build())
136+
.addVar("a", CUSTOM_STRUCT_TYPE)
137+
.setTypeProvider(CUSTOM_STRUCT_TYPE_PROVIDER)
138+
.setValueProvider(CUSTOM_STRUCT_VALUE_PROVIDER)
139+
.build();
140+
CelAbstractSyntaxTree ast = cel.compile("type(a) == custom_struct").getAst();
141+
142+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", new CelCustomStructValue(20)));
143+
144+
assertThat(result).isEqualTo(true);
145+
}
146+
131147
@Test
132148
public void evaluate_usingCustomClass_createNewStruct() throws Exception {
133149
Cel cel =

runtime/src/main/java/dev/cel/runtime/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ java_library(
213213
"//common/types:type_providers",
214214
"//common/values",
215215
"//common/values:cel_byte_string",
216+
"//common/values:cel_value",
216217
"@maven//:com_google_errorprone_error_prone_annotations",
217218
"@maven//:com_google_guava_guava",
218219
"@maven//:com_google_protobuf_protobuf_java",
@@ -229,6 +230,7 @@ cel_android_library(
229230
"//common/types:type_providers_android",
230231
"//common/types:types_android",
231232
"//common/values:cel_byte_string",
233+
"//common/values:cel_value_android",
232234
"//common/values:values_android",
233235
"@maven//:com_google_errorprone_error_prone_annotations",
234236
"@maven//:com_google_guava_guava",
@@ -247,6 +249,7 @@ java_library(
247249
"//common/annotations",
248250
"//common/types",
249251
"//common/types:type_providers",
252+
"//common/values",
250253
"@maven//:com_google_errorprone_error_prone_annotations",
251254
"@maven//:com_google_guava_guava",
252255
"@maven//:com_google_protobuf_protobuf_java",
@@ -896,6 +899,7 @@ java_library(
896899
"//common/internal:proto_message_factory",
897900
"//common/types:cel_types",
898901
"//common/types:type_providers",
902+
"//common/values",
899903
"//common/values:cel_value_provider",
900904
"//common/values:proto_message_value_provider",
901905
"//runtime/standard:int",

runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ public CelRuntime build() {
521521
}
522522

523523
DescriptorTypeResolver descriptorTypeResolver =
524-
DescriptorTypeResolver.create(combinedTypeProvider);
524+
DescriptorTypeResolver.create(combinedTypeProvider, celValueConverter);
525525
TypeFunction typeFunction = TypeFunction.create(descriptorTypeResolver);
526526

527527
mutableFunctionBindings.putAll(functionBindings());

runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import dev.cel.common.internal.ProtoMessageFactory;
4343
import dev.cel.common.types.CelTypeProvider;
4444
import dev.cel.common.types.CelTypes;
45+
import dev.cel.common.values.CelValueConverter;
4546
import dev.cel.common.values.CelValueProvider;
4647
import dev.cel.common.values.ProtoMessageValueProvider;
4748
import dev.cel.runtime.standard.IntFunction.IntOverload;
@@ -330,6 +331,7 @@ public CelRuntimeLegacyImpl build() {
330331
customBinding.getDefinition());
331332
}
332333

334+
CelValueConverter celValueConverter = CelValueConverter.getDefaultInstance();
333335
RuntimeTypeProvider runtimeTypeProvider;
334336

335337
if (options.enableCelValue()) {
@@ -340,13 +342,17 @@ public CelRuntimeLegacyImpl build() {
340342
}
341343

342344
runtimeTypeProvider = CelValueRuntimeTypeProvider.newInstance(messageValueProvider);
345+
celValueConverter = messageValueProvider.celValueConverter();
343346
} else {
344347
runtimeTypeProvider = new DescriptorMessageProvider(runtimeTypeFactory, options);
348+
if (celValueProvider != null) {
349+
celValueConverter = celValueProvider.celValueConverter();
350+
}
345351
}
346352

347353
DefaultInterpreter interpreter =
348354
new DefaultInterpreter(
349-
DescriptorTypeResolver.create(),
355+
DescriptorTypeResolver.create(celValueConverter),
350356
runtimeTypeProvider,
351357
dispatcherBuilder.build(),
352358
options);

0 commit comments

Comments
 (0)