Skip to content

Commit 462b16a

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

13 files changed

Lines changed: 300 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/OpaqueValueTest.java

Lines changed: 181 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,175 @@ 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 customOpaqueObject) {
81+
return new CelCustomOpaqueValue(customOpaqueObject);
82+
}
83+
return super.toRuntimeValue(value);
84+
}
85+
};
86+
}
87+
};
88+
89+
private static final CelValueProvider WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER =
90+
new CelValueProvider() {
91+
@Override
92+
public Optional<Object> newValue(String structType, Map<String, Object> fields) {
93+
return Optional.empty();
94+
}
95+
96+
@Override
97+
public CelValueConverter celValueConverter() {
98+
return new CelValueConverter() {
99+
@Override
100+
public Object toRuntimeValue(Object value) {
101+
if (value instanceof CustomOpaqueObject customOpaqueObject) {
102+
return OpaqueValue.create(CUSTOM_OPAQUE_TYPE.name(), customOpaqueObject);
103+
}
104+
return super.toRuntimeValue(value);
105+
}
106+
};
107+
}
108+
};
109+
110+
@Immutable
111+
private static class CustomOpaqueObject {
112+
private final String value;
113+
114+
CustomOpaqueObject(String value) {
115+
this.value = value;
116+
}
117+
118+
String getValue() {
119+
return value;
120+
}
121+
}
122+
123+
@Immutable
124+
private static class CelCustomOpaqueValue extends OpaqueValue {
125+
private final CustomOpaqueObject obj;
126+
127+
CelCustomOpaqueValue(CustomOpaqueObject obj) {
128+
this.obj = obj;
129+
}
130+
131+
@Override
132+
public CustomOpaqueObject value() {
133+
return obj;
134+
}
135+
136+
@Override
137+
public OpaqueType celType() {
138+
return CUSTOM_OPAQUE_TYPE;
139+
}
140+
}
141+
142+
@Test
143+
public void evaluate_customOpaqueValue_asVariable() throws Exception {
144+
Cel cel =
145+
CelFactory.plannerCelBuilder()
146+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
147+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
148+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
149+
.build();
150+
CelAbstractSyntaxTree ast = cel.compile("opaque_var").getAst();
151+
152+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
153+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
154+
155+
assertThat(result).isInstanceOf(CustomOpaqueObject.class);
156+
assertThat(((CustomOpaqueObject) result).getValue()).isEqualTo("hello");
157+
}
158+
159+
@Test
160+
public void evaluate_typeOfCustomOpaqueValue() throws Exception {
161+
Cel cel =
162+
CelFactory.plannerCelBuilder()
163+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
164+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
165+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
166+
.build();
167+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
168+
169+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
170+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
171+
172+
assertThat(result).isEqualTo(true);
173+
}
174+
175+
@Test
176+
public void evaluate_typeOfCustomOpaqueValue_wrapped() throws Exception {
177+
Cel cel =
178+
CelFactory.plannerCelBuilder()
179+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
180+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
181+
.setValueProvider(WRAPPED_CUSTOM_OPAQUE_VALUE_PROVIDER)
182+
.build();
183+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
184+
185+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
186+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
187+
188+
assertThat(result).isEqualTo(true);
189+
}
190+
191+
@Immutable
192+
private static class SelfReturningOpaqueObject extends OpaqueValue {
193+
SelfReturningOpaqueObject() {}
194+
195+
@Override
196+
public Object value() {
197+
return this;
198+
}
199+
200+
@Override
201+
public OpaqueType celType() {
202+
return CUSTOM_OPAQUE_TYPE;
203+
}
204+
}
205+
206+
@Test
207+
public void evaluate_selfReturningOpaqueValue_noConverter() throws Exception {
208+
Cel cel =
209+
CelFactory.plannerCelBuilder()
210+
.addVar("opaque_var", CUSTOM_OPAQUE_TYPE)
211+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
212+
.build();
213+
CelAbstractSyntaxTree ast = cel.compile("type(opaque_var) == custom_opaque_type").getAst();
214+
215+
SelfReturningOpaqueObject rawValue = new SelfReturningOpaqueObject();
216+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("opaque_var", rawValue));
217+
218+
assertThat(result).isEqualTo(true);
219+
}
40220
}
221+

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)