diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
index 99e95ebd3..fffd35794 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
@@ -14,19 +14,21 @@
package dev.cel.common.values;
+import static com.google.common.base.Preconditions.checkNotNull;
+
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
-import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.MessageLite;
-import dev.cel.common.annotations.Internal;
import dev.cel.common.types.CelType;
import dev.cel.common.types.StructTypeReference;
import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields;
+import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor;
import java.io.IOException;
import java.util.Optional;
+import org.jspecify.annotations.Nullable;
/**
* ProtoMessageLiteValue is a struct value with protobuf support for {@link MessageLite}.
@@ -35,10 +37,23 @@
*
*
If the codebase has access to full protobuf messages with descriptors, use {@code
* ProtoMessageValue} instead.
+ *
+ *
Implements {@link OptimizedSelectable} so that select chains can address fields by number:
+ *
+ *
+ * Field renames: If a protobuf field is renamed in schema after an AST was compiled,
+ * resolving by {@link SelectField#fieldNumber()} maps the number to the runtime descriptor's
+ * current field name, preventing {@code CelAttributeNotFoundException}.
+ * Version skew / unknown fields: When evaluating payloads serialized by a newer binary
+ * containing fields absent from the local {@code CelLiteDescriptor}, the unknown wire bytes
+ * are preserved in {@link #unknownFields()} and decoded on demand using the compile-time wire
+ * type and default metadata in {@link SelectField}.
+ *
*/
@AutoValue
@Immutable
-public abstract class ProtoMessageLiteValue extends StructValue {
+public abstract class ProtoMessageLiteValue extends StructValue
+ implements OptimizedSelectable {
@Override
public abstract MessageLite value();
@@ -57,12 +72,11 @@ MessageFields messageFields() {
}
}
- @Internal
- public ImmutableMap fieldValues() {
+ ImmutableMap fieldValues() {
return messageFields().values();
}
- public ImmutableListMultimap unknownFields() {
+ ImmutableListMultimap unknownFields() {
return messageFields().unknowns();
}
@@ -84,11 +98,59 @@ public Optional find(String field) {
.map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue));
}
+ @Override
+ public Object selectByFieldNumber(SelectField field) {
+ FieldLiteDescriptor fd = findFieldDescriptor(field);
+ Object known = findKnownFieldValue(fd);
+ if (known != null) {
+ return protoLiteCelValueConverter().toRuntimeValue(known);
+ }
+ return RawProtoMessageLiteValue.selectWireOrDefault(
+ field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
+ }
+
+ @Override
+ public boolean hasFieldByNumber(SelectField field) {
+ FieldLiteDescriptor fd = findFieldDescriptor(field);
+ if (findKnownFieldValue(fd) != null) {
+ return true;
+ }
+ return RawProtoMessageLiteValue.isPresentInWire(
+ field, fd, unknownFields().get(field.fieldNumber()));
+ }
+
+ @Override
+ public Optional findByFieldNumber(SelectField field) {
+ FieldLiteDescriptor fd = findFieldDescriptor(field);
+ Object known = findKnownFieldValue(fd);
+ if (known != null) {
+ return Optional.of(protoLiteCelValueConverter().toRuntimeValue(known));
+ }
+ return RawProtoMessageLiteValue.navigateWire(
+ field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
+ }
+
+ private @Nullable FieldLiteDescriptor findFieldDescriptor(SelectField field) {
+ return protoLiteCelValueConverter()
+ .findFieldDescriptor(celType().name(), field.fieldNumber())
+ .orElse(null);
+ }
+
+ private @Nullable Object findKnownFieldValue(@Nullable FieldLiteDescriptor fieldDescriptor) {
+ if (fieldDescriptor == null) {
+ return null;
+ }
+ return fieldValues().get(fieldDescriptor.getFieldName());
+ }
+
public static ProtoMessageLiteValue create(
MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) {
- Preconditions.checkNotNull(value);
- Preconditions.checkNotNull(typeName);
+ checkNotNull(value);
+ checkNotNull(typeName);
+ checkNotNull(protoLiteCelValueConverter);
return new AutoValue_ProtoMessageLiteValue(
value, StructTypeReference.create(typeName), protoLiteCelValueConverter);
}
+
+ ProtoMessageLiteValue() {}
}
diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java
index 88799878e..ca370410d 100644
--- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java
+++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java
@@ -15,6 +15,7 @@
package dev.cel.common.values;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -37,6 +38,7 @@
import com.google.protobuf.UInt64Value;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
import dev.cel.common.internal.CelLiteDescriptorPool;
import dev.cel.common.internal.DefaultLiteDescriptorPool;
import dev.cel.expr.conformance.proto3.TestAllTypes;
@@ -46,11 +48,12 @@
import java.io.ByteArrayOutputStream;
import java.time.Duration;
import java.time.Instant;
+import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(TestParameterInjector.class)
-public class ProtoMessageLiteValueTest {
+public final class ProtoMessageLiteValueTest {
private static final CelLiteDescriptorPool DESCRIPTOR_POOL =
DefaultLiteDescriptorPool.newInstance(
ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor()));
@@ -280,4 +283,322 @@ public void unknownFields_retainsUnknownWireFields() throws Exception {
.valuesForKey(1000)
.containsExactly(ByteString.copyFromUtf8("hello unknown"));
}
+
+ @Test
+ public void selectByFieldNumber_knownField_returnsValue() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result = val.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default"));
+
+ assertThat(result).isEqualTo("foo");
+ }
+
+ @Test
+ public void selectByFieldNumber_unknownWireField_decoded() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeInt64(999, 42L);
+ cos.flush();
+ TestAllTypes proto =
+ TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry());
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result = val.selectByFieldNumber(SelectField.create(999L, "unknown_field", 3, 0L));
+
+ assertThat(result).isEqualTo(42L);
+ }
+
+ @Test
+ public void selectByFieldNumber_unknownRepeatedWireField_decoded() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeInt64(999, 10L);
+ cos.writeInt64(999, 20L);
+ cos.flush();
+ TestAllTypes proto =
+ TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry());
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result =
+ val.selectByFieldNumber(
+ SelectField.create(999L, "unknown_repeated", 3, ImmutableList.of()));
+
+ assertThat((Iterable>) result).containsExactly(10L, 20L).inOrder();
+ }
+
+ @Test
+ public void selectByFieldNumber_renamedField_resolvesByFieldNumber() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result =
+ val.selectByFieldNumber(SelectField.create(14L, "renamed_string", 9, "default"));
+
+ assertThat(result).isEqualTo("foo");
+ }
+
+ @Test
+ public void selectByFieldNumber_unknownFieldCollidesWithKnownFieldName_returnsUnknownFieldValue()
+ throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeInt64(999, 42L);
+ cos.flush();
+ TestAllTypes proto =
+ TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry())
+ .toBuilder()
+ .setSingleString("known_field_14")
+ .build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result = val.selectByFieldNumber(SelectField.create(999L, "single_string", 3, 0L));
+
+ assertThat(result).isEqualTo(42L);
+ }
+
+ @Test
+ public void selectByFieldNumber_renamedMapField_resolvesByFieldNumber() {
+ TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result =
+ val.selectByFieldNumber(SelectField.create(61L, "renamed_map", -1, ImmutableMap.of()));
+
+ assertThat(result).isEqualTo(ImmutableMap.of("k", "v"));
+ }
+
+ @Test
+ public void selectByFieldNumber_renamedRepeatedField_resolvesByFieldNumber() {
+ TestAllTypes proto =
+ TestAllTypes.newBuilder().addRepeatedInt64(10L).addRepeatedInt64(20L).build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Object result =
+ val.selectByFieldNumber(SelectField.create(32L, "renamed_repeated", 3, ImmutableList.of()));
+
+ assertThat(result).isEqualTo(ImmutableList.of(10L, 20L));
+ }
+
+ @Test
+ public void findByFieldNumber_intermediateUnknownSubmessage_returnsRawProtoMessage()
+ throws Exception {
+ ByteArrayOutputStream subBaos = new ByteArrayOutputStream();
+ CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos);
+ subCos.writeString(1, "inner");
+ subCos.flush();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeBytes(998, ByteString.copyFrom(subBaos.toByteArray()));
+ cos.flush();
+ TestAllTypes proto =
+ TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry());
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ Optional nav = val.findByFieldNumber(SelectField.create(998L, "unknown_submessage"));
+
+ assertThat(nav.map(v -> v instanceof RawProtoMessageLiteValue)).hasValue(true);
+ }
+
+ @Test
+ public void hasFieldByNumber_knownField_returnsTrue() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(14L, "single_string"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_unknownFieldPresent_returnsTrue() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeInt64(999, 42L);
+ cos.flush();
+ TestAllTypes proto =
+ TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry());
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(999L, "unknown_present"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_unknownFieldAbsent_returnsFalse() {
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(888L, "unknown_absent"))).isFalse();
+ }
+
+ @Test
+ public void hasFieldByNumber_renamedField_resolvesByFieldNumber() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(14L, "renamed_string"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_renamedMapField_resolvesByFieldNumber() {
+ TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(61L, "renamed_map"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_renamedRepeatedField_resolvesByFieldNumber() {
+ TestAllTypes proto = TestAllTypes.newBuilder().addRepeatedInt64(10L).build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(32L, "renamed_repeated"))).isTrue();
+ }
+
+ @Test
+ public void qualify_emptyList_returnsSameInstance() {
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(OptimizedSelectTraversal.qualify(val, ImmutableList.of())).isSameInstanceAs(val);
+ }
+
+ @Test
+ public void hasField_emptyList_returnsFalse() {
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(OptimizedSelectTraversal.hasField(val, ImmutableList.of())).isFalse();
+ }
+
+ @Test
+ public void qualify_mapField_returnsMap() {
+ TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(
+ 61L, "map_string_string", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of()));
+
+ Object result = OptimizedSelectTraversal.qualify(val, fields);
+
+ assertThat(result).isEqualTo(ImmutableMap.of("k", "v"));
+ }
+
+ @Test
+ public void qualify_nestedMessage_resolvesField() {
+ TestAllTypes proto =
+ TestAllTypes.newBuilder()
+ .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42))
+ .build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(21L, "single_nested_message", 11, null),
+ SelectField.create(1L, "bb", 5, 0));
+
+ Object result = OptimizedSelectTraversal.qualify(val, fields);
+
+ assertThat(result).isEqualTo(42L);
+ }
+
+ @Test
+ public void hasField_nestedMessage_resolvesPresence() {
+ TestAllTypes proto =
+ TestAllTypes.newBuilder()
+ .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42))
+ .build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(
+ OptimizedSelectTraversal.hasField(
+ val,
+ ImmutableList.of(
+ SelectField.create(21L, "single_nested_message"),
+ SelectField.create(1L, "bb"))))
+ .isTrue();
+ assertThat(
+ OptimizedSelectTraversal.hasField(
+ val,
+ ImmutableList.of(
+ SelectField.create(21L, "single_nested_message"),
+ SelectField.create(99L, "missing"))))
+ .isFalse();
+ }
+
+ @Test
+ public void qualify_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(2L, "single_int64", 3, 0L),
+ SelectField.create(3L, "leaf_field", 9, ""));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(val, fields));
+
+ assertThat(thrown).hasMessageThat().contains("leaf_field");
+ }
+
+ @Test
+ public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build();
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(2L, "single_int64"), SelectField.create(3L, "leaf_field"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.hasField(val, fields));
+
+ assertThat(thrown).hasMessageThat().contains("leaf_field");
+ }
}