Skip to content

Commit e24db4f

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

13 files changed

Lines changed: 291 additions & 32 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: 46 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. There are two primary modes of
27+
* 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,29 @@ 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(checkNotNull(value), OpaqueType.create(name));
65+
}
66+
67+
@AutoValue
68+
@AutoValue.CopyAnnotations
69+
@Immutable
70+
@SuppressWarnings("Immutable")
71+
abstract static class OpaqueValueWrapper extends OpaqueValue {
72+
@Override
73+
public abstract Object value();
74+
75+
@Override
76+
public abstract OpaqueType celType();
3677
}
3778
}

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");
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 CUSTOM_OPAQUE_VALUE_PROVIDER_USING_CREATE =
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("a", CUSTOM_OPAQUE_TYPE)
147+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
148+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
149+
.build();
150+
CelAbstractSyntaxTree ast = cel.compile("a").getAst();
151+
152+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
153+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", 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("a", CUSTOM_OPAQUE_TYPE)
164+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
165+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER)
166+
.build();
167+
CelAbstractSyntaxTree ast = cel.compile("type(a) == custom_opaque").getAst();
168+
169+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
170+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", rawValue));
171+
172+
assertThat(result).isEqualTo(true);
173+
}
174+
175+
@Test
176+
public void evaluate_typeOfCustomOpaqueValue_usingCreate() throws Exception {
177+
Cel cel =
178+
CelFactory.plannerCelBuilder()
179+
.addVar("a", CUSTOM_OPAQUE_TYPE)
180+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
181+
.setValueProvider(CUSTOM_OPAQUE_VALUE_PROVIDER_USING_CREATE)
182+
.build();
183+
CelAbstractSyntaxTree ast = cel.compile("type(a) == custom_opaque").getAst();
184+
185+
CustomOpaqueObject rawValue = new CustomOpaqueObject("hello");
186+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", 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("a", CUSTOM_OPAQUE_TYPE)
211+
.setTypeProvider(CUSTOM_OPAQUE_TYPE_PROVIDER)
212+
.build();
213+
CelAbstractSyntaxTree ast = cel.compile("type(a) == custom_opaque").getAst();
214+
215+
SelfReturningOpaqueObject rawValue = new SelfReturningOpaqueObject();
216+
Object result = cel.createProgram(ast).eval(ImmutableMap.of("a", 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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,10 @@ public CelRuntimeLegacyImpl build() {
346346

347347
DefaultInterpreter interpreter =
348348
new DefaultInterpreter(
349-
DescriptorTypeResolver.create(),
349+
DescriptorTypeResolver.create(
350+
celValueProvider != null
351+
? celValueProvider.celValueConverter()
352+
: dev.cel.common.values.CelValueConverter.getDefaultInstance()),
350353
runtimeTypeProvider,
351354
dispatcherBuilder.build(),
352355
options);

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

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import dev.cel.common.types.CelTypeProvider;
2424
import dev.cel.common.types.StructTypeReference;
2525
import dev.cel.common.types.TypeType;
26+
import dev.cel.common.values.CelValueConverter;
2627
import java.util.NoSuchElementException;
2728
import java.util.Optional;
2829
import org.jspecify.annotations.Nullable;
@@ -39,21 +40,13 @@ public final class DescriptorTypeResolver extends TypeResolver {
3940

4041
private final @Nullable CelTypeProvider typeProvider;
4142

42-
/**
43-
* Creates a {@code DescriptorTypeResolver}. All protobuf messages are resolved as a type of
44-
* {@link StructTypeReference}.
45-
*/
46-
public static DescriptorTypeResolver create() {
47-
return new DescriptorTypeResolver();
43+
public static DescriptorTypeResolver create(CelValueConverter celValueConverter) {
44+
return new DescriptorTypeResolver(null, celValueConverter);
4845
}
4946

50-
/**
51-
* Creates a {@code DescriptorTypeResolver}. If the protobuf message to be resolved can be found
52-
* in the provided {@link CelTypeProvider}, the message is resolved as a concrete {@code
53-
* ProtoMessageType} instead of a {@link StructTypeReference}.
54-
*/
55-
public static DescriptorTypeResolver create(CelTypeProvider typeProvider) {
56-
return new DescriptorTypeResolver(typeProvider);
47+
public static DescriptorTypeResolver create(
48+
CelTypeProvider typeProvider, CelValueConverter celValueConverter) {
49+
return new DescriptorTypeResolver(typeProvider, celValueConverter);
5750
}
5851

5952
@Override
@@ -81,11 +74,9 @@ public TypeType resolveObjectType(Object obj, CelType typeCheckedType) {
8174
return super.resolveObjectType(obj, typeCheckedType);
8275
}
8376

84-
private DescriptorTypeResolver() {
85-
this(null);
86-
}
87-
88-
private DescriptorTypeResolver(@Nullable CelTypeProvider typeProvider) {
77+
private DescriptorTypeResolver(
78+
@Nullable CelTypeProvider typeProvider, CelValueConverter celValueConverter) {
79+
super(celValueConverter);
8980
this.typeProvider = typeProvider;
9081
}
9182
}

0 commit comments

Comments
 (0)