Skip to content

Commit 511455c

Browse files
l46kokcopybara-github
authored andcommitted
Decouple Activation from full protobuf implementation
PiperOrigin-RevId: 731413856
1 parent 22ee2d8 commit 511455c

7 files changed

Lines changed: 167 additions & 69 deletions

File tree

runtime/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ java_library(
2222
],
2323
)
2424

25+
java_library(
26+
name = "activation",
27+
visibility = ["//:internal"],
28+
exports = [
29+
"//runtime/src/main/java/dev/cel/runtime:activation",
30+
],
31+
)
32+
33+
java_library(
34+
name = "proto_message_activation_factory",
35+
visibility = ["//:internal"],
36+
exports = [
37+
"//runtime/src/main/java/dev/cel/runtime:proto_message_activation_factory",
38+
],
39+
)
40+
2541
java_library(
2642
name = "function_binding",
2743
visibility = ["//:internal"],

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

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,8 @@
2020
import com.google.common.collect.ImmutableMap;
2121
import com.google.protobuf.ByteString;
2222
import com.google.protobuf.ByteString.ByteIterator;
23-
import com.google.protobuf.Descriptors.FieldDescriptor;
24-
import com.google.protobuf.Message;
25-
import dev.cel.common.CelOptions;
2623
import dev.cel.common.annotations.Internal;
27-
import dev.cel.common.internal.DefaultMessageFactory;
28-
import dev.cel.common.internal.DynamicProto;
29-
import dev.cel.common.internal.ProtoAdapter;
30-
import java.util.HashMap;
3124
import java.util.Map;
32-
import java.util.Optional;
3325
import org.jspecify.annotations.Nullable;
3426

3527
/**
@@ -136,50 +128,6 @@ public String toString() {
136128
};
137129
}
138130

139-
/**
140-
* Creates an {@code Activation} from a {@code Message} where each field in the message is exposed
141-
* as a top-level variable in the {@code Activation}.
142-
*
143-
* <p>Unset message fields are published with the default value for the field type. However, an
144-
* unset {@code google.protobuf.Any} value is not a valid CEL value, and will be published as an
145-
* {@code Exception} value on the {@code Activation} just as though an unset {@code Any} would if
146-
* it were accessed during a CEL evaluation.
147-
*/
148-
public static Activation fromProto(Message message, CelOptions celOptions) {
149-
Map<String, Object> variables = new HashMap<>();
150-
Map<FieldDescriptor, Object> msgFieldValues = message.getAllFields();
151-
152-
ProtoAdapter protoAdapter =
153-
new ProtoAdapter(
154-
DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions.enableUnsignedLongs());
155-
156-
boolean skipUnsetFields =
157-
celOptions.fromProtoUnsetFieldOption().equals(CelOptions.ProtoUnsetFieldOptions.SKIP);
158-
159-
for (FieldDescriptor field : message.getDescriptorForType().getFields()) {
160-
// If skipping unset fields and the field is not repeated, then continue.
161-
if (skipUnsetFields && !field.isRepeated() && !msgFieldValues.containsKey(field)) {
162-
continue;
163-
}
164-
165-
// Get the value of the field set on the message, if present, otherwise use reflection to
166-
// get the default value for the field using the FieldDescriptor.
167-
Object fieldValue = msgFieldValues.getOrDefault(field, message.getField(field));
168-
try {
169-
Optional<Object> adapted = protoAdapter.adaptFieldToValue(field, fieldValue);
170-
variables.put(field.getName(), adapted.orElse(null));
171-
} catch (IllegalArgumentException e) {
172-
variables.put(
173-
field.getName(),
174-
CelEvaluationExceptionBuilder.newBuilder(
175-
"illegal field value. field=%s, value=%s", field.getName(), fieldValue)
176-
.setCause(e)
177-
.build());
178-
}
179-
}
180-
return copyOf(variables);
181-
}
182-
183131
/**
184132
* Extends this binder by another binder. Names will be attempted to first resolve in the other
185133
* binder, then in this binder.

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@ BASE_SOURCES = [
1616

1717
# keep sorted
1818
INTERPRETER_SOURCES = [
19-
"Activation.java",
2019
"CallArgumentChecker.java",
2120
"DefaultDispatcher.java",
2221
"DefaultInterpreter.java",
2322
"DescriptorMessageProvider.java",
2423
"Dispatcher.java",
2524
"DynamicMessageFactory.java",
26-
"GlobalResolver.java",
27-
"Interpretable.java",
2825
"Interpreter.java",
2926
"InterpreterUtil.java",
3027
"MessageFactory.java",
@@ -33,6 +30,36 @@ INTERPRETER_SOURCES = [
3330
"UnknownTrackingInterpretable.java",
3431
]
3532

33+
java_library(
34+
name = "activation",
35+
srcs = ["Activation.java"],
36+
tags = [
37+
],
38+
deps = [
39+
":interpretable",
40+
":runtime_helpers",
41+
"//common/annotations",
42+
"@maven//:com_google_guava_guava",
43+
"@maven//:com_google_protobuf_protobuf_java",
44+
"@maven//:org_jspecify_jspecify",
45+
],
46+
)
47+
48+
java_library(
49+
name = "proto_message_activation_factory",
50+
srcs = ["ProtoMessageActivationFactory.java"],
51+
tags = [
52+
],
53+
deps = [
54+
":activation",
55+
":evaluation_exception_builder",
56+
"//common:options",
57+
"//common/internal:default_message_factory",
58+
"//common/internal:dynamic_proto",
59+
"@maven//:com_google_protobuf_protobuf_java",
60+
],
61+
)
62+
3663
java_library(
3764
name = "type_resolver",
3865
srcs = ["TypeResolver.java"],
@@ -89,6 +116,7 @@ java_library(
89116
":evaluation_exception_builder",
90117
":evaluation_listener",
91118
":function_overload_impl",
119+
":interpretable",
92120
":metadata",
93121
":runtime_helpers",
94122
":type_resolver",
@@ -246,6 +274,23 @@ java_library(
246274
],
247275
)
248276

277+
java_library(
278+
name = "interpretable",
279+
srcs = [
280+
"GlobalResolver.java",
281+
"Interpretable.java",
282+
],
283+
visibility = ["//visibility:private"],
284+
deps = [
285+
":evaluation_exception",
286+
":evaluation_listener",
287+
":function_overload_impl",
288+
"//common/annotations",
289+
"@maven//:com_google_errorprone_error_prone_annotations",
290+
"@maven//:org_jspecify_jspecify",
291+
],
292+
)
293+
249294
java_library(
250295
name = "standard_functions",
251296
srcs = ["CelStandardFunctions.java"],
@@ -322,13 +367,16 @@ java_library(
322367
tags = [
323368
],
324369
deps = [
370+
":activation",
325371
":descriptor_type_resolver",
326372
":evaluation_exception",
327373
":evaluation_listener",
328374
":function_binding",
329375
":function_overload",
330376
":function_overload_impl",
377+
":interpretable",
331378
":interpreter",
379+
":proto_message_activation_factory",
332380
":proto_message_runtime_equality",
333381
":runtime_equality",
334382
":runtime_type_provider_legacy",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public Object eval(Map<String, ?> mapValue) throws CelEvaluationException {
5656

5757
/** Evaluate the expression using {@code message} fields as the source of input variables. */
5858
public Object eval(Message message) throws CelEvaluationException {
59-
return evalInternal(Activation.fromProto(message, getOptions()));
59+
return evalInternal(ProtoMessageActivationFactory.fromProto(message, getOptions()));
6060
}
6161

6262
/** Evaluate a compiled program with a custom variable {@code resolver}. */
@@ -111,7 +111,7 @@ public Object trace(Map<String, ?> mapValue, CelEvaluationListener listener)
111111
*/
112112
public Object trace(Message message, CelEvaluationListener listener)
113113
throws CelEvaluationException {
114-
return evalInternal(Activation.fromProto(message, getOptions()), listener);
114+
return evalInternal(ProtoMessageActivationFactory.fromProto(message, getOptions()), listener);
115115
}
116116

117117
/**
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime;
16+
17+
import com.google.protobuf.Descriptors.FieldDescriptor;
18+
import com.google.protobuf.Message;
19+
import dev.cel.common.CelOptions;
20+
import dev.cel.common.internal.DefaultMessageFactory;
21+
import dev.cel.common.internal.DynamicProto;
22+
import dev.cel.common.internal.ProtoAdapter;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.Optional;
26+
27+
/** Package-private factory to facilitate binding a full protobuf message into an activation. */
28+
final class ProtoMessageActivationFactory {
29+
30+
/**
31+
* Creates an {@code Activation} from a {@code Message} where each field in the message is exposed
32+
* as a top-level variable in the {@code Activation}.
33+
*
34+
* <p>Unset message fields are published with the default value for the field type. However, an
35+
* unset {@code google.protobuf.Any} value is not a valid CEL value, and will be published as an
36+
* {@code Exception} value on the {@code Activation} just as though an unset {@code Any} would if
37+
* it were accessed during a CEL evaluation.
38+
*/
39+
public static Activation fromProto(Message message, CelOptions celOptions) {
40+
Map<String, Object> variables = new HashMap<>();
41+
Map<FieldDescriptor, Object> msgFieldValues = message.getAllFields();
42+
43+
ProtoAdapter protoAdapter =
44+
new ProtoAdapter(
45+
DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions.enableUnsignedLongs());
46+
47+
boolean skipUnsetFields =
48+
celOptions.fromProtoUnsetFieldOption().equals(CelOptions.ProtoUnsetFieldOptions.SKIP);
49+
50+
for (FieldDescriptor field : message.getDescriptorForType().getFields()) {
51+
// If skipping unset fields and the field is not repeated, then continue.
52+
if (skipUnsetFields && !field.isRepeated() && !msgFieldValues.containsKey(field)) {
53+
continue;
54+
}
55+
56+
// Get the value of the field set on the message, if present, otherwise use reflection to
57+
// get the default value for the field using the FieldDescriptor.
58+
Object fieldValue = msgFieldValues.getOrDefault(field, message.getField(field));
59+
try {
60+
Optional<Object> adapted = protoAdapter.adaptFieldToValue(field, fieldValue);
61+
variables.put(field.getName(), adapted.orElse(null));
62+
} catch (IllegalArgumentException e) {
63+
variables.put(
64+
field.getName(),
65+
CelEvaluationExceptionBuilder.newBuilder(
66+
"illegal field value. field=%s, value=%s", field.getName(), fieldValue)
67+
.setCause(e)
68+
.build());
69+
}
70+
}
71+
return Activation.copyOf(variables);
72+
}
73+
74+
private ProtoMessageActivationFactory() {}
75+
}

runtime/src/test/java/dev/cel/runtime/ActivationTest.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,27 @@ public void copyOf_success_withNullEntries() {
5656
@Test
5757
public void fromProto() {
5858
NestedMessage nestedMessage = NestedMessage.newBuilder().setBb(1).build();
59-
Activation activation = Activation.fromProto(nestedMessage, TEST_OPTIONS);
59+
Activation activation = ProtoMessageActivationFactory.fromProto(nestedMessage, TEST_OPTIONS);
6060
assertThat(activation.resolve("bb")).isEqualTo(1);
6161

6262
TestAllTypes testMessage =
6363
TestAllTypes.newBuilder().setSingleNestedMessage(nestedMessage).build();
64-
activation = Activation.fromProto(testMessage, TEST_OPTIONS);
64+
activation = ProtoMessageActivationFactory.fromProto(testMessage, TEST_OPTIONS);
6565
assertThat(activation.resolve("single_nested_message")).isEqualTo(nestedMessage);
6666
}
6767

6868
@Test
6969
public void fromProto_unsetScalarField() {
70-
Activation activation = Activation.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS);
70+
Activation activation =
71+
ProtoMessageActivationFactory.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS);
7172
assertThat(activation.resolve("bb")).isEqualTo(0);
7273
}
7374

7475
@Test
7576
public void fromProto_unsetScalarField_skipUnsetFields() {
7677
Activation activation =
77-
Activation.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
78+
ProtoMessageActivationFactory.fromProto(
79+
NestedMessage.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
7880
assertThat(activation.resolve("bb")).isNull();
7981
}
8082

@@ -83,7 +85,8 @@ public void fromProto_unsetAnyField() {
8385
// An unset Any field is the only field which cannot be accurately published into an Activation,
8486
// and is instead published as an error value which should fit nicely with the CEL evaluation
8587
// semantics.
86-
Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
88+
Activation activation =
89+
ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
8790
assertThat(activation.resolve("single_any")).isInstanceOf(Throwable.class);
8891
assertThat((Throwable) activation.resolve("single_any"))
8992
.hasMessageThat()
@@ -95,20 +98,23 @@ public void fromProto_unsetAnyField() {
9598

9699
@Test
97100
public void fromProto_unsetValueField() {
98-
Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
101+
Activation activation =
102+
ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
99103
assertThat(activation.resolve("single_value")).isEqualTo(NullValue.NULL_VALUE);
100104
}
101105

102106
@Test
103107
public void fromProto_unsetMessageField() {
104108
Activation activation =
105-
Activation.fromProto(NestedTestAllTypes.getDefaultInstance(), TEST_OPTIONS);
109+
ProtoMessageActivationFactory.fromProto(
110+
NestedTestAllTypes.getDefaultInstance(), TEST_OPTIONS);
106111
assertThat(activation.resolve("payload")).isEqualTo(TestAllTypes.getDefaultInstance());
107112
}
108113

109114
@Test
110115
public void fromProto_unsetRepeatedField() {
111-
Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
116+
Activation activation =
117+
ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
112118
assertThat(activation.resolve("repeated_int64")).isInstanceOf(List.class);
113119
assertThat((List) activation.resolve("repeated_int64")).isEmpty();
114120

@@ -119,7 +125,8 @@ public void fromProto_unsetRepeatedField() {
119125
@Test
120126
public void fromProto_unsetRepeatedField_skipUnsetFields() {
121127
Activation activation =
122-
Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
128+
ProtoMessageActivationFactory.fromProto(
129+
TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
123130
assertThat(activation.resolve("repeated_int64")).isInstanceOf(List.class);
124131
assertThat((List) activation.resolve("repeated_int64")).isEmpty();
125132

@@ -129,23 +136,25 @@ public void fromProto_unsetRepeatedField_skipUnsetFields() {
129136

130137
@Test
131138
public void fromProto_unsetMapField() {
132-
Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
139+
Activation activation =
140+
ProtoMessageActivationFactory.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
133141
assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class);
134142
assertThat((Map) activation.resolve("map_int32_int64")).isEmpty();
135143
}
136144

137145
@Test
138146
public void fromProto_unsetMapField_skipUnsetFields() {
139147
Activation activation =
140-
Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
148+
ProtoMessageActivationFactory.fromProto(
149+
TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
141150
assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class);
142151
assertThat((Map) activation.resolve("map_int32_int64")).isEmpty();
143152
}
144153

145154
@Test
146155
public void fromProto_unsignedLongField_unsignedResult() {
147156
Activation activation =
148-
Activation.fromProto(
157+
ProtoMessageActivationFactory.fromProto(
149158
TestAllTypes.newBuilder()
150159
.setSingleUint32(1)
151160
.setSingleUint64(UnsignedLong.MAX_VALUE.longValue())

0 commit comments

Comments
 (0)