Skip to content

Commit 77d7853

Browse files
TristonianJonescopybara-github
authored andcommitted
Introduce ProtoUnsetFieldOptions to support unset handling like C++
PiperOrigin-RevId: 655712342
1 parent 882f631 commit 77d7853

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

common/src/main/java/dev/cel/common/CelOptions.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
@Immutable
3131
public abstract class CelOptions {
3232

33+
/**
34+
* ProtoUnsetFieldOptions describes how to handle Activation.fromProto() calls where proto message
35+
* fields may be unset and should either be handled perhaps as absent or as the default proto
36+
* value.
37+
*/
38+
public enum ProtoUnsetFieldOptions {
39+
// Do not bind a field if it is unset. Repeated fields are bound as empty list.
40+
SKIP,
41+
// Bind the (proto api) default value for a field.
42+
BIND_DEFAULT;
43+
}
44+
3345
public static final CelOptions DEFAULT = current().build();
3446

3547
public static final CelOptions LEGACY = newBuilder().disableCelStandardEquality(true).build();
@@ -95,6 +107,8 @@ public abstract class CelOptions {
95107

96108
public abstract boolean unwrapWellKnownTypesOnFunctionDispatch();
97109

110+
public abstract ProtoUnsetFieldOptions fromProtoUnsetFieldOption();
111+
98112
public abstract Builder toBuilder();
99113

100114
public ImmutableSet<ExprFeatures> toExprFeatures() {
@@ -185,7 +199,8 @@ public static Builder newBuilder() {
185199
.enableUnknownTracking(false)
186200
.enableCelValue(false)
187201
.comprehensionMaxIterations(-1)
188-
.unwrapWellKnownTypesOnFunctionDispatch(true);
202+
.unwrapWellKnownTypesOnFunctionDispatch(true)
203+
.fromProtoUnsetFieldOption(ProtoUnsetFieldOptions.BIND_DEFAULT);
189204
}
190205

191206
/**
@@ -479,6 +494,15 @@ public abstract static class Builder {
479494
@Deprecated
480495
public abstract Builder unwrapWellKnownTypesOnFunctionDispatch(boolean value);
481496

497+
/**
498+
* Configure how unset proto fields are handled when evaluating over a protobuf message where
499+
* fields are intended to be treated as top-level variables. Defaults to binding all fields to
500+
* their default value if unset.
501+
*
502+
* @see ProtoUnsetFieldOptions
503+
*/
504+
public abstract Builder fromProtoUnsetFieldOption(ProtoUnsetFieldOptions value);
505+
482506
public abstract CelOptions build();
483507
}
484508
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,15 @@ public static Activation fromProto(Message message, CelOptions celOptions) {
172172
new ProtoAdapter(
173173
DynamicProto.create(DefaultMessageFactory.INSTANCE), celOptions.enableUnsignedLongs());
174174

175+
boolean skipUnsetFields =
176+
celOptions.fromProtoUnsetFieldOption().equals(CelOptions.ProtoUnsetFieldOptions.SKIP);
177+
175178
for (FieldDescriptor field : message.getDescriptorForType().getFields()) {
179+
// If skipping unset fields and the field is not repeated, then continue.
180+
if (skipUnsetFields && !field.isRepeated() && !msgFieldValues.containsKey(field)) {
181+
continue;
182+
}
183+
176184
// Get the value of the field set on the message, if present, otherwise use reflection to
177185
// get the default value for the field using the FieldDescriptor.
178186
Object fieldValue = msgFieldValues.getOrDefault(field, message.getField(field));

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public final class ActivationTest {
3535
private static final CelOptions TEST_OPTIONS =
3636
CelOptions.current().enableTimestampEpoch(true).enableUnsignedLongs(true).build();
3737

38+
private static final CelOptions TEST_OPTIONS_SKIP_UNSET_FIELDS =
39+
CelOptions.current()
40+
.enableTimestampEpoch(true)
41+
.enableUnsignedLongs(true)
42+
.fromProtoUnsetFieldOption(CelOptions.ProtoUnsetFieldOptions.SKIP)
43+
.build();
44+
3845
@Test
3946
public void copyOf_success_withNullEntries() {
4047
Map<String, Object> map = new HashMap<>();
@@ -64,6 +71,13 @@ public void fromProto_unsetScalarField() {
6471
assertThat(activation.resolve("bb")).isEqualTo(0);
6572
}
6673

74+
@Test
75+
public void fromProto_unsetScalarField_skipUnsetFields() {
76+
Activation activation =
77+
Activation.fromProto(NestedMessage.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
78+
assertThat(activation.resolve("bb")).isNull();
79+
}
80+
6781
@Test
6882
public void fromProto_unsetAnyField() {
6983
// An unset Any field is the only field which cannot be accurately published into an Activation,
@@ -102,13 +116,32 @@ public void fromProto_unsetRepeatedField() {
102116
assertThat((List) activation.resolve("repeated_nested_message")).isEmpty();
103117
}
104118

119+
@Test
120+
public void fromProto_unsetRepeatedField_skipUnsetFields() {
121+
Activation activation =
122+
Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
123+
assertThat(activation.resolve("repeated_int64")).isInstanceOf(List.class);
124+
assertThat((List) activation.resolve("repeated_int64")).isEmpty();
125+
126+
assertThat(activation.resolve("repeated_nested_message")).isInstanceOf(List.class);
127+
assertThat((List) activation.resolve("repeated_nested_message")).isEmpty();
128+
}
129+
105130
@Test
106131
public void fromProto_unsetMapField() {
107132
Activation activation = Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS);
108133
assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class);
109134
assertThat((Map) activation.resolve("map_int32_int64")).isEmpty();
110135
}
111136

137+
@Test
138+
public void fromProto_unsetMapField_skipUnsetFields() {
139+
Activation activation =
140+
Activation.fromProto(TestAllTypes.getDefaultInstance(), TEST_OPTIONS_SKIP_UNSET_FIELDS);
141+
assertThat(activation.resolve("map_int32_int64")).isInstanceOf(Map.class);
142+
assertThat((Map) activation.resolve("map_int32_int64")).isEmpty();
143+
}
144+
112145
@Test
113146
public void fromProto_unsignedLongField_unsignedResult() {
114147
Activation activation =

0 commit comments

Comments
 (0)