diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 1559f7ad8..70e2d79da 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -280,6 +280,7 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":optimized_select_traversal", ":optimized_selectable", ":preadapted_list", ":select_field", @@ -328,6 +329,7 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":optimized_select_traversal", ":optimized_selectable", ":preadapted_list", ":select_field", @@ -360,6 +362,7 @@ cel_android_library( ], deps = [ ":base_proto_cel_value_converter_android", + ":optimized_select_traversal_android", ":optimized_selectable_android", ":preadapted_list_android", ":select_field_android", @@ -483,6 +486,7 @@ java_library( ":select_field", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", ], ) @@ -495,6 +499,7 @@ cel_android_library( ":select_field_android", "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven_android//:com_google_guava_guava", ], ) diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java index 4de724fef..ce35643dd 100644 --- a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java @@ -35,8 +35,17 @@ public final class OptimizedSelectTraversal { * Qualifies {@code target} through every field in {@code fields} and returns the terminal value. */ public static Object qualify(Object target, ImmutableList fields) { + return qualify(target, fields, 0); + } + + /** Qualifies {@code target} through the fields of {@code fields} from index {@code from} on. */ + static Object qualify(Object target, ImmutableList fields, int from) { Object current = target; - for (int i = 0; i < fields.size(); i++) { + for (int i = from; i < fields.size(); i++) { + // With only the last field left, there's nothing to walk. + if (i < fields.size() - 1 && current instanceof OptimizedSelectable) { + return ((OptimizedSelectable) current).selectPath(fields, i); + } current = qualifyField(current, fields.get(i)); } return current; diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java index 828d15227..ff2fbe583 100644 --- a/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java @@ -14,6 +14,7 @@ package dev.cel.common.values; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; import java.util.Optional; @@ -23,7 +24,8 @@ * *

Implementations resolve individual field selections against themselves by protobuf field * number. Walking the chain across multiple fields and heterogeneous values belongs to {@link - * OptimizedSelectTraversal}. + * OptimizedSelectTraversal}, though an implementation may walk the part of it that it can read + * itself through {@link #selectPath}. * *

CEL Library Internals. Do Not Use. */ @@ -42,4 +44,11 @@ public interface OptimizedSelectable { * step of a presence test, or empty if absent. */ Optional findByFieldNumber(SelectField field); + + /** + * Selects {@code fields} from index {@code from} on, starting at this value, and returns the last + * one's value. Implementations may walk the nested values they can read without materializing + * each one, and hand the rest back to {@link OptimizedSelectTraversal}. + */ + Object selectPath(ImmutableList fields, int from); } diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index fd441b0fa..52658ea0a 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -506,6 +506,46 @@ boolean hasFieldByNumber( "Unexpected encoding type: " + fieldDescriptor.getEncodingType()); } + /** + * Like {@link OptimizedSelectable#selectPath}: walks {@code message}'s in-memory submessages + * through their getters without wrapping them, then selects the rest from the last one reached. + */ + Object selectPath(ProtoMessageLiteValue message, ImmutableList fields, int from) { + MessageLite current = message.rawValue(); + MessageLiteDescriptor descriptor = message.descriptor(); + if (current == null || !descriptor.hasGetters()) { + // Only in-memory messages read through generated getters can be walked. + return OptimizedSelectTraversal.qualify( + message.selectByFieldNumber(fields.get(from)), fields, from + 1); + } + int i = from; + // Walks submessages up to the one holding the last field, stopping at one that's unset or has + // no descriptor with getters, such as well-known types. + while (i < fields.size() - 1) { + Object submessage = descriptor.getFieldValue(current, fields.get(i).fieldNumber()); + if (!(submessage instanceof MessageLite)) { + break; + } + Optional submessageDescriptor = + descriptorPool + .findDescriptor((MessageLite) submessage) + .filter(MessageLiteDescriptor::hasGetters); + if (!submessageDescriptor.isPresent()) { + break; + } + current = (MessageLite) submessage; + descriptor = submessageDescriptor.get(); + i++; + } + // Selects the next field from the last message reached, or from this one if none was. + ProtoMessageLiteValue reached = message; + if (i > from) { + reached = ProtoMessageLiteValue.create(current, descriptor, this); + } + return OptimizedSelectTraversal.qualify( + reached.selectByFieldNumber(fields.get(i)), fields, i + 1); + } + /** Like {@link #hasFieldByNumber}, through the field's generated getter. */ boolean hasByGetter(ProtoMessageLiteValue message, SelectField field) { return readGetter(message, field.fieldNumber()) != null; 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 27129b1fe..f864d355c 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; @@ -147,6 +148,11 @@ public Optional find(String field) { return Optional.ofNullable(descriptor().findByFieldName(field)).map(this::readField); } + @Override + public Object selectPath(ImmutableList fields, int from) { + return protoLiteCelValueConverter().selectPath(this, fields, from); + } + @Override public Object selectByFieldNumber(SelectField field) { FieldLiteDescriptor fd = descriptor().findByFieldNumber(field.fieldNumber()); diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java index fa3864ad2..5525898d4 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValue.java @@ -17,11 +17,14 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.Message; import dev.cel.common.internal.CelDescriptorPool; +import dev.cel.common.internal.WellKnownProto; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; import java.util.Optional; @@ -76,9 +79,7 @@ public Optional find(String field) { @Override public Object selectByFieldNumber(SelectField field) { - FieldDescriptor fieldDescriptor = findFieldByNumber(value().getDescriptorForType(), field); - - return protoCelValueConverter().fromProtoMessageFieldToCelValue(value(), fieldDescriptor); + return selectField(value(), field); } @Override @@ -95,6 +96,42 @@ public Optional findByFieldNumber(SelectField field) { return findFieldValue(fieldDescriptor); } + @Override + public Object selectPath(ImmutableList fields, int from) { + Message message = value(); + if (message instanceof DynamicMessage) { + // Each hop adapts a dynamic message into its generated class, if it has one. + return OptimizedSelectTraversal.qualify( + selectByFieldNumber(fields.get(from)), fields, from + 1); + } + int i = from; + // Walks nested messages without wrapping them, up to the one holding the last field. + while (i < fields.size() - 1) { + FieldDescriptor fieldDescriptor = + findFieldByNumber(message.getDescriptorForType(), fields.get(i)); + if (!isWalkable(fieldDescriptor)) { + break; + } + message = (Message) message.getField(fieldDescriptor); + i++; + } + return OptimizedSelectTraversal.qualify(selectField(message, fields.get(i)), fields, i + 1); + } + + // A singular message field that isn't of a well-known type, which the converter would adapt. + private static boolean isWalkable(FieldDescriptor fieldDescriptor) { + return fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE + && !fieldDescriptor.isRepeated() + && !WellKnownProto.getByTypeName(fieldDescriptor.getMessageType().getFullName()) + .isPresent(); + } + + private Object selectField(Message message, SelectField field) { + FieldDescriptor fieldDescriptor = findFieldByNumber(message.getDescriptorForType(), field); + + return protoCelValueConverter().fromProtoMessageFieldToCelValue(message, fieldDescriptor); + } + public static ProtoMessageValue create( Message value, CelDescriptorPool celDescriptorPool, diff --git a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java index 03847d576..81643d46c 100644 --- a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; @@ -136,6 +137,19 @@ public Optional findByFieldNumber(SelectField field) { return protoLiteCelValueConverter().findByFieldNumber(toByteString(), protoTypeName(), field); } + @Override + public Object selectPath(ImmutableList fields, int from) { + // Wire bytes have no submessages in memory to walk. Consecutive hops through wire bytes are + // selected here, rather than each going back through the traversal. + Object current = this; + int i = from; + while (i < fields.size() - 1 && current instanceof RawProtoMessageLiteValue) { + current = ((RawProtoMessageLiteValue) current).selectByFieldNumber(fields.get(i)); + i++; + } + return OptimizedSelectTraversal.qualify(current, fields, i); + } + static RawProtoMessageLiteValue create( ByteString rawWireBytes, ProtoLiteCelValueConverter protoLiteCelValueConverter) { return create(rawWireBytes, "", protoLiteCelValueConverter); diff --git a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java index 575ed260d..af3cdbe18 100644 --- a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java +++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java @@ -237,6 +237,27 @@ public void hasField_intermediateUnsupportedTarget_throwsException() { assertThat(thrown).hasMessageThat().contains("child_key"); } + @Test + public void qualify_valueWalkingRestOfChain_returnsItsResult() { + FakeOptimizedSelectable walking = + new FakeOptimizedSelectable(ImmutableMap.of()) { + @Override + public Object selectPath(ImmutableList fields, int from) { + return "walked from " + fields.get(from).fieldName(); + } + }; + FakeOptimizedSelectable root = new FakeOptimizedSelectable(ImmutableMap.of("outer", walking)); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, "outer", 11), + SelectField.create(2L, "middle", 11), + SelectField.create(3L, "leaf", 9, "")); + + Object result = OptimizedSelectTraversal.qualify(root, fields); + + assertThat(result).isEqualTo("walked from middle"); + } + @Test public void hasField_unsupportedTarget_throwsException() { ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field")); @@ -272,7 +293,7 @@ public void hasField_errorValue_returnsFalse() { } @SuppressWarnings("Immutable") - private static final class FakeOptimizedSelectable implements OptimizedSelectable { + private static class FakeOptimizedSelectable implements OptimizedSelectable { private final ImmutableMap values; @Override @@ -297,6 +318,12 @@ public Optional findByFieldNumber(SelectField field) { return Optional.ofNullable(values.get(field.fieldName())); } + @Override + public Object selectPath(ImmutableList fields, int from) { + return OptimizedSelectTraversal.qualify( + selectByFieldNumber(fields.get(from)), fields, from + 1); + } + FakeOptimizedSelectable(Map values) { this.values = ImmutableMap.copyOf(values); } 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 537745bd2..df5fd1df9 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -43,6 +43,8 @@ import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.CelLiteDescriptorPool; import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; +import dev.cel.expr.conformance.proto3.NestedTestAllTypesCelDescriptor; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; @@ -63,7 +65,9 @@ public final class ProtoMessageLiteValueTest { private static final CelLiteDescriptorPool DESCRIPTOR_POOL = DefaultLiteDescriptorPool.newInstance( - ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); + ImmutableSet.of( + TestAllTypesCelDescriptor.getDescriptor(), + NestedTestAllTypesCelDescriptor.getDescriptor())); private static final ProtoLiteCelValueConverter PROTO_LITE_CEL_VALUE_CONVERTER = ProtoLiteCelValueConverter.newInstance(DESCRIPTOR_POOL); @@ -75,6 +79,24 @@ public final class ProtoMessageLiteValueTest { DESCRIPTOR_POOL.getDescriptorOrThrow( "cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + // Like PROTO_LITE_CEL_VALUE_CONVERTER, but NestedTestAllTypes's TestAllTypes payload has no + // descriptor. + private static final ProtoLiteCelValueConverter PARTIAL_CONVERTER = + ProtoLiteCelValueConverter.newInstance( + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(NestedTestAllTypesCelDescriptor.getDescriptor()))); + + private static final SelectField CHILD = SelectField.create(1L, "child", 11); + private static final SelectField PAYLOAD = SelectField.create(2L, "payload", 11); + private static final SelectField SINGLE_INT64 = SelectField.create(2L, "single_int64", 3, 0L); + private static final ImmutableList STANDALONE_BB = + ImmutableList.of( + SelectField.create(23L, "standalone_message", 11), SelectField.create(1L, "bb", 5, 0L)); + private static final ImmutableList TIMESTAMP_SECONDS = + ImmutableList.of( + SelectField.create(102L, "single_timestamp", 11), + SelectField.create(1L, "seconds", 3, 0L)); + @Test public void create_withEmptyMessage() { ProtoMessageLiteValue messageLiteValue = @@ -959,4 +981,143 @@ public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildField assertThat(thrown).hasMessageThat().contains("leaf_field"); } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum WalkableChainTestCase { + ALL_DESCRIBED(PROTO_LITE_CEL_VALUE_CONVERTER, nestedPayloadInt64(42L), 42L), + DESCRIPTORLESS_PAYLOAD(PARTIAL_CONVERTER, nestedPayloadInt64(42L), 42L), + UNSET_AFTER_ONE_HOP( + PROTO_LITE_CEL_VALUE_CONVERTER, + NestedTestAllTypes.newBuilder().setChild(NestedTestAllTypes.getDefaultInstance()).build(), + 0L); + + private final ProtoLiteCelValueConverter converter; + private final NestedTestAllTypes message; + private final long expected; + + WalkableChainTestCase( + ProtoLiteCelValueConverter converter, NestedTestAllTypes message, long expected) { + this.converter = converter; + this.message = message; + this.expected = expected; + } + } + + @Test + public void selectPath_nestedChainWithGetters_returnsLeafValue( + @TestParameter WalkableChainTestCase testCase) { + ProtoMessageLiteValue root = + (ProtoMessageLiteValue) testCase.converter.toRuntimeValue(testCase.message); + + Object result = root.selectPath(ImmutableList.of(CHILD, CHILD, PAYLOAD, SINGLE_INT64), 0); + + assertThat(result).isEqualTo(testCase.expected); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum UnwalkableChainTestCase { + UNSET_FIRST_HOP(TEST_ALL_TYPES_DESCRIPTOR, TestAllTypes.getDefaultInstance(), 0L), + ROOT_WITHOUT_GETTERS( + new MessageLiteDescriptor("unowned.TestAllTypes", ImmutableList.of(), () -> null), + standaloneBb(7), + 7L); + + private final MessageLiteDescriptor descriptor; + private final TestAllTypes message; + private final long expected; + + UnwalkableChainTestCase(MessageLiteDescriptor descriptor, TestAllTypes message, long expected) { + this.descriptor = descriptor; + this.message = message; + this.expected = expected; + } + } + + @Test + public void selectPath_unwalkableChain_selectsOneHopAtATime( + @TestParameter UnwalkableChainTestCase testCase) { + ProtoMessageLiteValue root = + ProtoMessageLiteValue.create( + testCase.message, testCase.descriptor, PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = root.selectPath(STANDALONE_BB, 0); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void selectPath_wireBackedRoot_readsWireBytes() { + ProtoMessageLiteValue root = + ProtoMessageLiteValue.create( + standaloneBb(7).toByteString(), + TEST_ALL_TYPES_DESCRIPTOR, + PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = root.selectPath(STANDALONE_BB, 0); + + assertThat(result).isEqualTo(7L); + } + + @Test + public void qualify_wellKnownTypeMidChain_throwsLikeEachHop() { + ProtoMessageLiteValue root = + (ProtoMessageLiteValue) + PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue( + TestAllTypes.newBuilder() + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(1)) + .build()); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(root, TIMESTAMP_SECONDS)); + + assertThat(thrown).hasMessageThat().contains("seconds"); + } + + @Test + public void qualify_unknownFieldMidChain_readsWireBytes() throws Exception { + ByteArrayOutputStream inner = new ByteArrayOutputStream(); + CodedOutputStream innerOut = CodedOutputStream.newInstance(inner); + innerOut.writeString(1, "inner"); + innerOut.flush(); + ByteArrayOutputStream child = new ByteArrayOutputStream(); + CodedOutputStream childOut = CodedOutputStream.newInstance(child); + childOut.writeBytes(998, ByteString.copyFrom(inner.toByteArray())); + childOut.flush(); + ProtoMessageLiteValue root = + (ProtoMessageLiteValue) + PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue( + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.parseFrom( + child.toByteArray(), ExtensionRegistryLite.getEmptyRegistry())) + .build()); + + Object result = + OptimizedSelectTraversal.qualify( + root, + ImmutableList.of( + CHILD, + SelectField.create(998L, "unknown", 11), + SelectField.create(1L, "s", 9, ""))); + + assertThat(result).isEqualTo("inner"); + } + + private static NestedTestAllTypes nestedPayloadInt64(long value) { + return NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(value)))) + .build(); + } + + private static TestAllTypes standaloneBb(int bb) { + return TestAllTypes.newBuilder() + .setStandaloneMessage(NestedMessage.newBuilder().setBb(bb)) + .build(); + } } diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java index 5524e363e..03dec6045 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueTest.java @@ -31,6 +31,7 @@ import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; import com.google.protobuf.ListValue; +import com.google.protobuf.Message; import com.google.protobuf.StringValue; import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; @@ -42,11 +43,13 @@ import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; import dev.cel.common.internal.DynamicProto; import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto2.NestedTestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto2.TestAllTypes.NestedMessage; @@ -66,6 +69,13 @@ public final class ProtoMessageValueTest { DynamicProto.create(DefaultMessageFactory.INSTANCE), CelOptions.DEFAULT); + private static final ImmutableList CHILD_CHILD_PAYLOAD_INT64 = + ImmutableList.of( + SelectField.create(1L, "child", 11), + SelectField.create(1L, "child", 11), + SelectField.create(2L, "payload", 11), + SelectField.create(2L, "single_int64", 3, -64L)); + @Test public void emptyProtoMessage() { ProtoMessageValue protoMessageValue = @@ -325,7 +335,7 @@ public void selectField_fixed64_returnsUnsignedLong() { @SuppressWarnings("ImmutableEnumChecker") // Test only private enum SelectFieldJsonValueTestCase { - NULL(Value.newBuilder().build(), NullValue.NULL_VALUE), + NULL(Value.getDefaultInstance(), NullValue.NULL_VALUE), BOOL(Value.newBuilder().setBoolValue(true).build(), true), DOUBLE(Value.newBuilder().setNumberValue(4.5d).build(), 4.5d), STRING(Value.newBuilder().setStringValue("test").build(), "test"), @@ -666,4 +676,102 @@ private static TestAllTypes createPopulatedTestAllTypes() { .setStandaloneEnum(NestedEnum.BAR) .build(); } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum WalkableChainTestCase { + SET( + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(42)))) + .build(), + 42L), + // Reads single_int64's declared default from the unset submessages' default instances. + UNSET_INTERMEDIATE(NestedTestAllTypes.getDefaultInstance(), -64L); + + private final NestedTestAllTypes message; + private final long expected; + + WalkableChainTestCase(NestedTestAllTypes message, long expected) { + this.message = message; + this.expected = expected; + } + } + + @Test + public void selectPath_nestedChain_returnsLeafValue( + @TestParameter WalkableChainTestCase testCase) { + ProtoMessageValue root = newProtoMessageValue(testCase.message); + + Object result = root.selectPath(CHILD_CHILD_PAYLOAD_INT64, 0); + + assertThat(result).isEqualTo(testCase.expected); + } + + @Test + public void selectPath_dynamicRoot_selectsOneHopAtATime() { + ProtoMessageValue root = + newProtoMessageValue(DynamicMessage.getDefaultInstance(NestedTestAllTypes.getDescriptor())); + + Object result = root.selectPath(CHILD_CHILD_PAYLOAD_INT64, 0); + + assertThat(result).isEqualTo(-64L); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum UnwalkableChainTestCase { + UNKNOWN_FIRST_HOP( + TestAllTypes.getDefaultInstance(), + ImmutableList.of( + SelectField.create(99999L, "unknown", 11), SelectField.create(1L, "bb", 5, 0L)), + IllegalArgumentException.class), + SCALAR_FIRST_HOP( + TestAllTypes.newBuilder().setSingleInt64(1L).build(), + ImmutableList.of( + SelectField.create(2L, "single_int64", 3, 0L), SelectField.create(1L, "bb", 5, 0L)), + CelAttributeNotFoundException.class), + REPEATED_FIRST_HOP( + TestAllTypes.newBuilder() + .addRepeatedNestedMessage(NestedMessage.getDefaultInstance()) + .build(), + ImmutableList.of( + SelectField.create(51L, "repeated_nested_message", 11), + SelectField.create(1L, "bb", 5, 0L)), + CelAttributeNotFoundException.class), + WELL_KNOWN_FIRST_HOP( + TestAllTypes.newBuilder().setSingleTimestamp(Timestamp.newBuilder().setSeconds(1)).build(), + ImmutableList.of( + SelectField.create(102L, "single_timestamp", 11), + SelectField.create(1L, "seconds", 3, 0L)), + CelAttributeNotFoundException.class); + + private final Message message; + private final ImmutableList fields; + private final Class thrown; + + UnwalkableChainTestCase( + Message message, ImmutableList fields, Class thrown) { + this.message = message; + this.fields = fields; + this.thrown = thrown; + } + } + + @Test + public void selectPath_unwalkableChain_throwsLikeEachHop( + @TestParameter UnwalkableChainTestCase testCase) { + ProtoMessageValue root = newProtoMessageValue(testCase.message); + + assertThrows(testCase.thrown, () -> root.selectPath(testCase.fields, 0)); + } + + private static ProtoMessageValue newProtoMessageValue(Message message) { + return ProtoMessageValue.create( + message, + DefaultDescriptorPool.INSTANCE, + PROTO_CEL_VALUE_CONVERTER, + /* enableJsonFieldNames= */ false); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java index 149c0afe8..dbd1554f2 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java @@ -2591,9 +2591,14 @@ private static CelLiteDescriptor newClientCelLiteDescriptor( return new CelLiteDescriptor(version, descriptors) { @Override protected @Nullable Object getFieldValue(int messageIndex, Object message, int fieldNumber) { + MessageLiteDescriptor clientDesc = descriptors.get(messageIndex); + // Like the client's generated code, which has no getter for a field its schema lacks. + if (clientDesc.findByFieldNumber(fieldNumber) == null) { + return null; + } return fullDescriptor .getProtoTypeNamesToDescriptors() - .get(descriptors.get(messageIndex).getProtoTypeName()) + .get(clientDesc.getProtoTypeName()) .getFieldValue((MessageLite) message, fieldNumber); } };