From 69efe7793b50fef134050e8988422aebb24ace25 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Sun, 11 Oct 2026 17:50:48 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 997600155 --- .../java/dev/cel/common/values/BUILD.bazel | 2 + .../values/ProtoLiteCelValueConverter.java | 182 ++++++++++++-- .../common/values/ProtoMessageLiteValue.java | 106 ++++++--- .../ProtoLiteCelValueConverterTest.java | 125 +++++++++- .../values/ProtoMessageLiteValueTest.java | 222 ++++++++++++------ java_lite_proto_cel_library.bzl | 1 - .../main/java/dev/cel/protobuf/BUILD.bazel | 1 + .../dev/cel/protobuf/CelLiteDescriptor.java | 78 +++++- .../LiteDescriptorCodegenMetadata.java | 6 + .../protobuf/ProtoDescriptorCollector.java | 97 +++++++- .../cel_lite_descriptor_template.txt | 36 +++ .../test/java/dev/cel/protobuf/BUILD.bazel | 11 +- .../cel/protobuf/CelLiteDescriptorTest.java | 86 +++++++ .../ProtoDescriptorCollectorTest.java | 85 +++++++ .../dev/cel/runtime/CelLiteRuntimeTest.java | 16 +- testing/protos/BUILD.bazel | 15 ++ testing/src/test/resources/protos/BUILD.bazel | 21 ++ .../resources/protos/accessor_names.proto | 71 ++++++ 18 files changed, 1010 insertions(+), 151 deletions(-) create mode 100644 testing/src/test/resources/protos/accessor_names.proto 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 23821e278..1559f7ad8 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -329,6 +329,7 @@ java_library( deps = [ ":base_proto_cel_value_converter", ":optimized_selectable", + ":preadapted_list", ":select_field", ":values", "//:auto_value", @@ -360,6 +361,7 @@ cel_android_library( deps = [ ":base_proto_cel_value_converter_android", ":optimized_selectable_android", + ":preadapted_list_android", ":select_field_android", ":values_android", "//:auto_value", 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 c4b9dacaa..18e2675ce 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -25,12 +25,14 @@ import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; +import com.google.protobuf.Internal.EnumLite; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelLiteDescriptorPool; import dev.cel.common.internal.WellKnownProto; +import dev.cel.protobuf.CelLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; @@ -118,7 +120,10 @@ private Object readLengthDelimitedField( return CelByteString.of(inputStream.readByteArray()); case MESSAGE: return mergeOrReadMessageField( - inputStream.readBytes(), fieldDescriptor.getFieldProtoTypeName(), existingValue); + inputStream.readBytes(), + fieldDescriptor.getFieldProtoTypeName(), + existingValue, + /* isView= */ true); case STRING: return inputStream.readStringRequireUtf8(); default: @@ -126,26 +131,28 @@ private Object readLengthDelimitedField( } } + // Reads bytes of their own, not a view of another message's. private Object readMessageField(ByteString bytes, String fieldProtoTypeName) { - return mergeOrReadMessageField(bytes, fieldProtoTypeName, /* existingValue= */ null); + return mergeOrReadMessageField( + bytes, fieldProtoTypeName, /* existingValue= */ null, /* isView= */ false); } private Object mergeOrReadMessageField( - ByteString bytes, String fieldProtoTypeName, @Nullable Object existingValue) { + ByteString bytes, String fieldProtoTypeName, @Nullable Object existingValue, boolean isView) { MessageLiteDescriptor descriptor = descriptorPool.findDescriptor(fieldProtoTypeName).orElse(null); if (descriptor == null) { if (existingValue instanceof RawProtoMessageLiteValue) { bytes = ((RawProtoMessageLiteValue) existingValue).toByteString().concat(bytes); } - return RawProtoMessageLiteValue.create(bytes, fieldProtoTypeName, this, /* isView= */ true); + return RawProtoMessageLiteValue.create(bytes, fieldProtoTypeName, this, isView); } WellKnownProto wellKnownProto = WellKnownProto.getByTypeName(fieldProtoTypeName).orElse(null); if (isStructLike(wellKnownProto)) { if (existingValue instanceof ProtoMessageLiteValue) { bytes = ((ProtoMessageLiteValue) existingValue).toByteString().concat(bytes); } - return ProtoMessageLiteValue.create(bytes, fieldProtoTypeName, this); + return ProtoMessageLiteValue.create(bytes, descriptor, this); } if (existingValue instanceof MessageLite) { return mergeMessageLite(((MessageLite) existingValue).toBuilder(), bytes, fieldProtoTypeName); @@ -169,18 +176,7 @@ private Optional findFieldDescriptor(String protoTypeName, .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); } - Optional findFieldDescriptor(String protoTypeName, String fieldName) { - return descriptorPool - .findDescriptor(protoTypeName) - .flatMap(desc -> desc.findByFieldName(fieldName)); - } - - MessageLite parseMessageLite(ByteString bytes, String protoTypeName) { - MessageLiteDescriptor descriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); - return parseMessageLite(bytes, descriptor); - } - - private static MessageLite parseMessageLite(ByteString bytes, MessageLiteDescriptor descriptor) { + static MessageLite parseMessageLite(ByteString bytes, MessageLiteDescriptor descriptor) { if (bytes.isEmpty()) { return descriptor.newMessageBuilder().getDefaultInstanceForType(); } @@ -216,7 +212,7 @@ private Object toRuntimeValue(MessageLite msg, MessageLiteDescriptor descriptor) WellKnownProto wellKnownProto = WellKnownProto.getByTypeName(descriptor.getProtoTypeName()).orElse(null); if (isStructLike(wellKnownProto)) { - return ProtoMessageLiteValue.create(msg, descriptor.getProtoTypeName(), this); + return ProtoMessageLiteValue.create(msg, descriptor, this); } return fromWellKnownProto(msg, checkNotNull(wellKnownProto)); @@ -464,6 +460,145 @@ boolean hasFieldByNumber(ByteString bytes, String protoTypeName, SelectField fie return false; } + /** + * Reads {@code fieldDescriptor} of {@code message} with its generated getter rather than from + * wire bytes: its CEL value, null if it's absent, or {@link CelLiteDescriptor#NO_GETTER} if it + * can't be read that way. + */ + @SuppressWarnings("ReferenceEquality") // NO_GETTER is a sentinel instance. + @Nullable Object readFieldByGetter( + ProtoMessageLiteValue message, FieldLiteDescriptor fieldDescriptor) { + MessageLite msg = message.rawValue(); + if (msg == null) { + return CelLiteDescriptor.NO_GETTER; + } + Object value = message.descriptor().getFieldValue(msg, fieldDescriptor.getFieldNumber()); + if (value == null || value == CelLiteDescriptor.NO_GETTER) { + return value; + } + switch (fieldDescriptor.getEncodingType()) { + case LIST: + List list = (List) value; + if (list.isEmpty()) { + return null; + } + switch (fieldDescriptor.getProtoFieldType()) { + case INT64: + case SINT64: + case SFIXED64: + case DOUBLE: + case BOOL: + case STRING: + // These elements are their own CEL values, so the immutable list needn't be copied. + return CelPreAdaptedList.wrap(list); + default: + break; + } + ImmutableList.Builder listBuilder = + ImmutableList.builderWithExpectedSize(list.size()); + for (int i = 0; i < list.size(); i++) { + listBuilder.add(fromGetterValue(list.get(i), fieldDescriptor)); + } + return listBuilder.build(); + case MAP: + Map map = (Map) value; + if (map.isEmpty()) { + return null; + } + MessageLiteDescriptor entryDescriptor = + descriptorPool.getDescriptorOrThrow(fieldDescriptor.getFieldProtoTypeName()); + FieldLiteDescriptor keyDescriptor = + entryDescriptor.getByFieldNumberOrThrow(MAP_KEY_FIELD_NUMBER); + FieldLiteDescriptor valueDescriptor = + entryDescriptor.getByFieldNumberOrThrow(MAP_VALUE_FIELD_NUMBER); + ImmutableMap.Builder mapBuilder = + ImmutableMap.builderWithExpectedSize(map.size()); + for (Map.Entry entry : map.entrySet()) { + mapBuilder.put( + fromGetterValue(entry.getKey(), keyDescriptor), + fromGetterValue(entry.getValue(), valueDescriptor)); + } + return mapBuilder.buildOrThrow(); + case SINGULAR: + return fieldDescriptor.getJavaType() == JavaType.MESSAGE + ? readSubmessage(message, (MessageLite) value, fieldDescriptor) + : fromGetterValue(value, fieldDescriptor); + } + throw new IllegalStateException( + "Unexpected encoding type: " + fieldDescriptor.getEncodingType()); + } + + /** Like {@link #fromGetterValue}, reusing {@code parent}'s last submessage value. */ + @SuppressWarnings("ReferenceEquality") // Checks whether the getter returned the same instance. + private Object readSubmessage( + ProtoMessageLiteValue parent, MessageLite submessage, FieldLiteDescriptor fieldDescriptor) { + Object last = parent.lastSubmessage(); + if (last instanceof ProtoMessageLiteValue + && ((ProtoMessageLiteValue) last).rawValue() == submessage) { + return last; + } + if (last instanceof SerializedSubmessage + && ((SerializedSubmessage) last).submessage == submessage) { + return ((SerializedSubmessage) last).value; + } + Object value = fromGetterValue(submessage, fieldDescriptor); + if (value instanceof ProtoMessageLiteValue) { + parent.setLastSubmessage(value); + } else if (value instanceof RawProtoMessageLiteValue) { + // Serialized, for lack of a descriptor for its class. + parent.setLastSubmessage(new SerializedSubmessage(submessage, value)); + } + return value; + } + + /** Like {@link #hasFieldByNumber}, through the field's generated getter if it has one. */ + @SuppressWarnings("ReferenceEquality") // NO_GETTER is a sentinel instance. + Object hasByGetter(ProtoMessageLiteValue message, SelectField field) { + MessageLite msg = message.rawValue(); + if (msg == null) { + return CelLiteDescriptor.NO_GETTER; + } + Object value = message.descriptor().getFieldValue(msg, field.fieldNumber()); + if (value == CelLiteDescriptor.NO_GETTER) { + return value; + } + if (value instanceof List) { + return !((List) value).isEmpty(); + } + if (value instanceof Map) { + return !((Map) value).isEmpty(); + } + return value != null; + } + + /** Converts a field's (or element's) Java value as returned by its getter to its CEL value. */ + private Object fromGetterValue(Object value, FieldLiteDescriptor fieldDescriptor) { + switch (fieldDescriptor.getProtoFieldType()) { + case UINT32: + case FIXED32: + return UnsignedLong.fromLongBits(Integer.toUnsignedLong((Integer) value)); + case UINT64: + case FIXED64: + return UnsignedLong.fromLongBits((Long) value); + case ENUM: + // Open enums are read as their numbers; closed ones as enum constants. + return value instanceof Integer + ? ((Integer) value).longValue() + : (long) ((EnumLite) value).getNumber(); + case MESSAGE: + MessageLite message = (MessageLite) value; + MessageLiteDescriptor descriptor = descriptorPool.findDescriptor(message).orElse(null); + // A submessage whose class has no descriptor is read from its own bytes, not its root's: + // by its type's descriptor if the pool has one for another class (such as an older + // version of it), or else descriptorless. + return descriptor != null + ? toRuntimeValue(message, descriptor) + : readMessageField(message.toByteString(), fieldDescriptor.getFieldProtoTypeName()); + default: + return super.toRuntimeValue(value); + } + } + /** Describes {@code field} by the type information it carries, built once and kept on it. */ private static FieldLiteDescriptor newFieldDescriptor(SelectField field) { Object cached = field.liteFieldDescriptor(); @@ -648,6 +783,17 @@ private static IllegalArgumentException newDecodeException(String protoTypeName, "Failed to decode proto message of type: " + protoTypeName, e); } + /** A submessage serialized for lack of a descriptor for its class, and its value. */ + private static final class SerializedSubmessage { + private final MessageLite submessage; + private final Object value; + + private SerializedSubmessage(MessageLite submessage, Object value) { + this.submessage = submessage; + this.value = value; + } + } + private ProtoLiteCelValueConverter(CelLiteDescriptorPool celLiteDescriptorPool) { this.descriptorPool = checkNotNull(celLiteDescriptorPool); } 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 d8c23c8e0..a2c5744c1 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -24,7 +24,9 @@ import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; +import dev.cel.protobuf.CelLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; import java.util.Objects; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -66,11 +68,32 @@ abstract class ProtoMessageLiteValue extends StructValue // re-serializing intermediate hops; lazily parsed into a MessageLite only if value() is called. abstract @Nullable ByteString wireBytes(); - @Override - public abstract CelType celType(); + abstract MessageLiteDescriptor descriptor(); abstract ProtoLiteCelValueConverter protoLiteCelValueConverter(); + // The last submessage read through a getter, so that select chains sharing this message don't + // convert (or, without a descriptor, serialize) it again: a ProtoMessageLiteValue wrapping it, or + // a SerializedSubmessage. Like this wrapper, it lasts one evaluation, as values leave it through + // value(). Entries are reached through final fields and checked by identity, so a race at most + // repeats a conversion. + @SuppressWarnings("Immutable") + private @Nullable Object lastSubmessage; + + @Nullable Object lastSubmessage() { + return lastSubmessage; + } + + void setLastSubmessage(Object lastSubmessage) { + this.lastSubmessage = lastSubmessage; + } + + @Override + public CelType celType() { + // Built on demand, since one of these values is created on every hop but rarely asked its type. + return StructTypeReference.create(descriptor().getProtoTypeName()); + } + @Memoized @Override public MessageLite value() { @@ -78,8 +101,7 @@ public MessageLite value() { if (msg != null) { return msg; } - return protoLiteCelValueConverter() - .parseMessageLite(checkNotNull(wireBytes()), celType().name()); + return ProtoLiteCelValueConverter.parseMessageLite(checkNotNull(wireBytes()), descriptor()); } @Memoized @@ -110,20 +132,23 @@ public final boolean equals(Object other) { return false; } ProtoMessageLiteValue that = (ProtoMessageLiteValue) other; - return this.celType().equals(that.celType()) && this.value().equals(that.value()); + return this.descriptor().getProtoTypeName().equals(that.descriptor().getProtoTypeName()) + && this.value().equals(that.value()); } @Override public final int hashCode() { - return Objects.hash(value(), celType()); + return Objects.hash(value(), descriptor().getProtoTypeName()); } @Override public Object select(String field) { - FieldLiteDescriptor fd = findFieldDescriptor(field).orElse(null); + FieldLiteDescriptor fd = descriptor().findByFieldName(field).orElse(null); if (fd == null) { throw CelAttributeNotFoundException.of( - String.format("field '%s' is not declared in message '%s'", field, celType().name())); + String.format( + "field '%s' is not declared in message '%s'", + field, descriptor().getProtoTypeName())); } Object fieldValue = readField(fd); return fieldValue != null ? fieldValue : protoLiteCelValueConverter().getDefaultCelValue(fd); @@ -131,59 +156,78 @@ public Object select(String field) { @Override public Optional find(String field) { - FieldLiteDescriptor fd = findFieldDescriptor(field).orElse(null); // Per SelectableValue#find, a field that doesn't exist is reported as absent. - return Optional.ofNullable(fd).map(value -> readField(value)); + return descriptor().findByFieldName(field).map(this::readField); } + // An in-memory message is read through its descriptor's generated getters, falling back to its + // wire bytes (which serializes it) only for fields without one, such as those unknown to its + // descriptor. Other messages are read from their wire bytes. + @Override public Object selectByFieldNumber(SelectField field) { - return protoLiteCelValueConverter() - .selectByFieldNumber(toByteString(), celType().name(), field); + FieldLiteDescriptor fd = descriptor().findByFieldNumber(field.fieldNumber()).orElse(null); + if (fd == null) { + return protoLiteCelValueConverter() + .selectByFieldNumber(toByteString(), descriptor().getProtoTypeName(), field); + } + Object fieldValue = readField(fd); + if (fieldValue != null) { + return fieldValue; + } + return field.defaultValue() != null + ? field.defaultValue() + : protoLiteCelValueConverter().getDefaultCelValue(fd); } @Override + @SuppressWarnings("ReferenceEquality") // NO_GETTER is a sentinel instance. public boolean hasFieldByNumber(SelectField field) { - return protoLiteCelValueConverter().hasFieldByNumber(toByteString(), celType().name(), field); + Object present = protoLiteCelValueConverter().hasByGetter(this, field); + return present != CelLiteDescriptor.NO_GETTER + ? (Boolean) present + : protoLiteCelValueConverter() + .hasFieldByNumber(toByteString(), descriptor().getProtoTypeName(), field); } @Override public Optional findByFieldNumber(SelectField field) { - return protoLiteCelValueConverter().findByFieldNumber(toByteString(), celType().name(), field); + FieldLiteDescriptor fd = descriptor().findByFieldNumber(field.fieldNumber()).orElse(null); + return fd != null + ? Optional.ofNullable(readField(fd)) + : protoLiteCelValueConverter() + .findByFieldNumber(toByteString(), descriptor().getProtoTypeName(), field); } + @SuppressWarnings("ReferenceEquality") // NO_GETTER is a sentinel instance. private @Nullable Object readField(FieldLiteDescriptor fd) { - return protoLiteCelValueConverter().readField(toByteString(), celType().name(), fd); - } - - private Optional findFieldDescriptor(String fieldName) { - return protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldName); + Object value = protoLiteCelValueConverter().readFieldByGetter(this, fd); + return value != CelLiteDescriptor.NO_GETTER + ? value + : protoLiteCelValueConverter() + .readField(toByteString(), descriptor().getProtoTypeName(), fd); } static ProtoMessageLiteValue create( - MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) { + MessageLite value, + MessageLiteDescriptor descriptor, + ProtoLiteCelValueConverter protoLiteCelValueConverter) { checkNotNull(value); - checkNotNull(typeName); + checkNotNull(descriptor); checkNotNull(protoLiteCelValueConverter); return new AutoValue_ProtoMessageLiteValue( - value, - /* wireBytes= */ null, - StructTypeReference.create(typeName), - protoLiteCelValueConverter); + value, /* wireBytes= */ null, descriptor, protoLiteCelValueConverter); } static ProtoMessageLiteValue create( ByteString wireBytes, - String typeName, + MessageLiteDescriptor descriptor, ProtoLiteCelValueConverter protoLiteCelValueConverter) { checkNotNull(wireBytes); - checkNotNull(typeName); + checkNotNull(descriptor); checkNotNull(protoLiteCelValueConverter); return new AutoValue_ProtoMessageLiteValue( - /* rawValue= */ null, - wireBytes, - StructTypeReference.create(typeName), - protoLiteCelValueConverter); + /* rawValue= */ null, wireBytes, descriptor, protoLiteCelValueConverter); } ProtoMessageLiteValue() {} diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index 22da58d55..f50317485 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -15,6 +15,7 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; @@ -42,7 +43,9 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.internal.CelLiteDescriptorPool; import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.expr.conformance.proto2.TestAllTypes.NestedEnum; 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.NestedMessage; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; @@ -53,6 +56,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.function.Supplier; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,6 +91,16 @@ public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { private static final ProtoLiteCelValueConverter PROTO_LITE_CEL_VALUE_CONVERTER = ProtoLiteCelValueConverter.newInstance(DESCRIPTOR_POOL); + private static final ProtoLiteCelValueConverter ALL_TYPES_CONVERTER = + ProtoLiteCelValueConverter.newInstance( + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of( + TestAllTypesCelDescriptor.getDescriptor(), + NestedTestAllTypesCelDescriptor.getDescriptor(), + dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(), + dev.cel.expr.conformance.proto2.NestedTestAllTypesCelDescriptor + .getDescriptor()))); + @Test public void fromProtoMessageToCelValue_withTestMessage_convertsToProtoMessageLiteValueFromProtoMessage() { @@ -420,14 +434,6 @@ public void readField_splitSingularSubmessages_mergesAllOccurrences() throws Exc .build()); } - @Test - public void parseMessageLite_emptyBytes_returnsDefaultInstanceSingleton() { - MessageLite parsed = - PROTO_LITE_CEL_VALUE_CONVERTER.parseMessageLite(ByteString.EMPTY, PROTO3_TEST_ALL_TYPES); - - assertThat(parsed).isSameInstanceAs(TestAllTypes.getDefaultInstance()); - } - @Test public void readField_emptyBytes_returnsNull() { FieldLiteDescriptor fd = @@ -616,6 +622,109 @@ public void readField_groupInSkippedField_throwsUnsupportedOperationException( assertThat(e).hasMessageThat().isEqualTo("Groups are not supported"); } + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum GetterParityTestCase { + PROTO3_DEFAULT(TestAllTypes.getDefaultInstance()), + PROTO3_POPULATED( + TestAllTypes.newBuilder() + .setSingleInt32(Integer.MIN_VALUE) + .setSingleInt64(Long.MIN_VALUE) + .setSingleUint32(-1) + .setSingleUint64(-1L) + .setSingleSint32(-5) + .setSingleFixed32(-1) + .setSingleSfixed64(-10L) + .setSingleFloat(-0.0f) + .setSingleDouble(Double.NaN) + .setSingleBool(true) + .setSingleString("s") + .setSingleBytes(ByteString.copyFromUtf8("b")) + .setOptionalBool(false) + .setSingleNestedEnumValue(42) + .setStandaloneMessage(NestedMessage.newBuilder().setBb(1)) + .setSingleDuration(Duration.newBuilder().setSeconds(3)) + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(4)) + .setSingleInt64Wrapper(Int64Value.of(7)) + .setSingleStringWrapper(StringValue.of("")) + .setFieldMask(FieldMask.newBuilder().addPaths("p")) + .addRepeatedInt32(-1) + .addRepeatedInt64(2L) + .addRepeatedUint64(-1L) + .addRepeatedSint64(-6L) + .addRepeatedSfixed64(-10L) + .addRepeatedFloat(1.5f) + .addRepeatedDouble(-0.0) + .addRepeatedString("r") + .addRepeatedBytes(ByteString.copyFromUtf8("rb")) + .addRepeatedNestedMessage(NestedMessage.newBuilder().setBb(2)) + .addRepeatedNestedEnumValue(43) + .putMapBoolBool(true, false) + .putMapInt32Uint64(-1, -1L) + .putMapInt32EnumValue(1, 99) + .putMapStringMessage("m", NestedMessage.newBuilder().setBb(3).build()) + .putMapInt64NestedType(5L, NestedTestAllTypes.getDefaultInstance()) + .setOneofBool(false) + .build()), + PROTO2_POPULATED( + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32(-32) + .setSingleBool(false) + .setSingleString("") + .setStandaloneEnum(NestedEnum.BAZ) + .addRepeatedNestedEnum(NestedEnum.BAR) + .putMapInt32Enum(1, NestedEnum.BAZ) + .setOneofType(dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDefaultInstance()) + .build()); + + private final MessageLite message; + + GetterParityTestCase(MessageLite message) { + this.message = message; + } + } + + @Test + public void selectAndFind_throughGetters_matchWireBytes( + @TestParameter GetterParityTestCase testCase) { + ProtoMessageLiteValue throughGetters = + (ProtoMessageLiteValue) ALL_TYPES_CONVERTER.toRuntimeValue(testCase.message); + ProtoMessageLiteValue fromWire = + ProtoMessageLiteValue.create( + testCase.message.toByteString(), throughGetters.descriptor(), ALL_TYPES_CONVERTER); + + for (FieldLiteDescriptor field : throughGetters.descriptor().getFieldDescriptors()) { + if (field.getProtoFieldType() == FieldLiteDescriptor.Type.GROUP) { + continue; + } + String name = field.getFieldName(); + SelectField selectField = SelectField.create(field.getFieldNumber(), name); + assertWithMessage(name) + .that(outcome(() -> throughGetters.find(name))) + .isEqualTo(outcome(() -> fromWire.find(name))); + assertWithMessage(name) + .that(outcome(() -> throughGetters.select(name))) + .isEqualTo(outcome(() -> fromWire.select(name))); + assertWithMessage(name) + .that(outcome(() -> throughGetters.findByFieldNumber(selectField))) + .isEqualTo(outcome(() -> fromWire.findByFieldNumber(selectField))); + assertWithMessage(name) + .that(outcome(() -> throughGetters.selectByFieldNumber(selectField))) + .isEqualTo(outcome(() -> fromWire.selectByFieldNumber(selectField))); + assertWithMessage(name) + .that(outcome(() -> throughGetters.hasFieldByNumber(selectField))) + .isEqualTo(outcome(() -> fromWire.hasFieldByNumber(selectField))); + } + } + + // The value read, or the class of the exception thrown reading it. + private static Object outcome(Supplier read) { + try { + return read.get(); + } catch (RuntimeException e) { + return e.getClass(); + } + } + private static FieldLiteDescriptor fieldDescriptor(String messageName, int fieldNumber) { return DESCRIPTOR_POOL.getDescriptorOrThrow(messageName).getByFieldNumberOrThrow(fieldNumber); } 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 bb93a19ce..6e4cf752d 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -43,10 +43,13 @@ 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; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Duration; @@ -59,17 +62,33 @@ 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); + private static final MessageLiteDescriptor TEST_ALL_TYPES_DESCRIPTOR = + DESCRIPTOR_POOL.getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes"); + + private static final MessageLiteDescriptor NESTED_MESSAGE_DESCRIPTOR = + 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()))); + @Test public void create_withEmptyMessage() { ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.value()).isEqualTo(TestAllTypes.getDefaultInstance()); @@ -81,7 +100,7 @@ public void create_withPopulatedMessage() { ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( TestAllTypes.newBuilder().setSingleInt64(1L).build(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.value()) @@ -93,9 +112,7 @@ public void create_withPopulatedMessage() { public void create_withEmptyByteString() { ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - ByteString.EMPTY, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + ByteString.EMPTY, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.isZeroValue()).isTrue(); assertThat(messageLiteValue.toByteString()).isEqualTo(ByteString.EMPTY); @@ -103,10 +120,13 @@ public void create_withEmptyByteString() { } @Test - public void isZeroValue_emptyWireBytesWithoutDescriptor_returnsTrueWithoutParsing() { + public void isZeroValue_emptyWireBytes_returnsTrueWithoutParsing() { + // The descriptor's null builder makes any parse throw. ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - ByteString.EMPTY, "unregistered.Message", PROTO_LITE_CEL_VALUE_CONVERTER); + ByteString.EMPTY, + new MessageLiteDescriptor("unregistered.Message", ImmutableList.of(), () -> null), + PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.isZeroValue()).isTrue(); } @@ -117,9 +137,7 @@ public void isZeroValue_nonEmptyWireBytesForDefaultMessage_returnsTrue() { ByteString explicitZeroFieldBytes = ByteString.copyFrom(new byte[] {0x08, 0x00}); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - explicitZeroFieldBytes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + explicitZeroFieldBytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.isZeroValue()).isTrue(); } @@ -131,9 +149,7 @@ public void create_withPopulatedByteString_selectsAndLazilyMaterializesValue() { ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - expected.toByteString(), - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + expected.toByteString(), TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(messageLiteValue.select("single_int64")).isEqualTo(42L); assertThat(messageLiteValue.select("single_string")).isEqualTo("hello"); @@ -153,37 +169,30 @@ public void equals_byteStringBackedAndMessageBacked_areEqual() { .addEqualityGroup( ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER), ProtoMessageLiteValue.create( - ByteString.EMPTY, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER)) + ByteString.EMPTY, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER)) .addEqualityGroup( ProtoMessageLiteValue.create( - populated, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER), + populated, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER), ProtoMessageLiteValue.create( populated.toByteString(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER), - ProtoMessageLiteValue.create( - populated, "cel.expr.conformance.proto3.TestAllTypes", distinctConverter)) + ProtoMessageLiteValue.create(populated, TEST_ALL_TYPES_DESCRIPTOR, distinctConverter)) .addEqualityGroup( ProtoMessageLiteValue.create( - different, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER)) + different, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER)) .addEqualityGroup( ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "different.TypeName", + new MessageLiteDescriptor("different.TypeName", ImmutableList.of(), () -> null), PROTO_LITE_CEL_VALUE_CONVERTER)) .addEqualityGroup( ProtoMessageLiteValue.create( NestedMessage.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + NESTED_MESSAGE_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER)) .testEquals(); } @@ -193,9 +202,7 @@ public void create_withCorruptByteString_throwsOnValueMaterialization() { ByteString corruptBytes = ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte) 0xFF}); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - corruptBytes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + corruptBytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, messageLiteValue::value); @@ -212,9 +219,7 @@ public void create_withCorruptByteString_throwsOnSelect() { ByteString corruptBytes = ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte) 0xFF}); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - corruptBytes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + corruptBytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> messageLiteValue.select("single_int64")); @@ -231,9 +236,7 @@ public void create_withCorruptByteString_throwsOnSelectByFieldNumber() { ByteString corruptBytes = ByteString.copyFrom(new byte[] {0x10, (byte) 0x80}); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - corruptBytes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + corruptBytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); SelectField selectField = SelectField.create(2L, "single_int64", 3, 0L); IllegalArgumentException thrown = @@ -253,9 +256,7 @@ public void create_withCorruptByteString_throwsOnHasFieldByNumber() { ByteString corruptBytes = ByteString.copyFrom(new byte[] {0x10, (byte) 0x80}); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - corruptBytes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + corruptBytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); SelectField selectField = SelectField.create(2L, "single_int64"); IllegalArgumentException thrown = @@ -303,7 +304,7 @@ public void create_withMismatchedWireType_throwsOnSelect( ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( ByteString.copyFrom(testCase.bytes), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); IllegalArgumentException thrown = @@ -327,7 +328,7 @@ public void select_otherFieldHasMismatchedWireType_decodesOnlySelectedField() { .concat(TestAllTypes.newBuilder().setSingleInt64(42L).build().toByteString()); ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( - bytes, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + bytes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = messageLiteValue.select("single_int64"); @@ -339,7 +340,7 @@ public void select_undeclaredField_throwsCelAttributeNotFoundException() { ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); CelAttributeNotFoundException thrown = @@ -373,7 +374,7 @@ public void find_returnsValueOnlyIfFieldIsPresent(@TestParameter FindFieldTestCa ProtoMessageLiteValue messageLiteValue = ProtoMessageLiteValue.create( TestAllTypes.newBuilder().setSingleString("foo").build(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Optional result = messageLiteValue.find(testCase.fieldName); @@ -425,7 +426,7 @@ private enum SelectFieldTestCase { ImmutableList.of( ProtoMessageLiteValue.create( NestedMessage.newBuilder().setBb(10).build(), - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + NESTED_MESSAGE_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER))), REPEATED_INT64_WRAPPER("repeated_int64_wrapper", ImmutableList.of(11L, 12L)), @@ -516,9 +517,7 @@ public void selectField_success(@TestParameter SelectFieldTestCase testCase) { .build(); ProtoMessageLiteValue protoMessageValue = ProtoMessageLiteValue.create( - testAllTypes, - "cel.expr.conformance.proto3.TestAllTypes", - PROTO_LITE_CEL_VALUE_CONVERTER); + testAllTypes, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object selectedValue = protoMessageValue.select(testCase.fieldName); @@ -550,8 +549,12 @@ public void selectField_containerField_returnsImmutableContainer( @TestParameter ContainerFieldTestCase testCase) { ProtoMessageLiteValue protoMessageValue = ProtoMessageLiteValue.create( - TestAllTypes.newBuilder().addRepeatedInt64(1L).putMapInt64Int64(1L, 2L).build(), - "cel.expr.conformance.proto3.TestAllTypes", + TestAllTypes.newBuilder() + .addRepeatedInt64(1L) + .putMapInt64Int64(1L, 2L) + .build() + .toByteString(), + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object selectedValue = protoMessageValue.select(testCase.selectField.fieldName()); @@ -564,8 +567,12 @@ public void selectByFieldNumber_containerField_returnsImmutableContainer( @TestParameter ContainerFieldTestCase testCase) { ProtoMessageLiteValue protoMessageValue = ProtoMessageLiteValue.create( - TestAllTypes.newBuilder().addRepeatedInt64(1L).putMapInt64Int64(1L, 2L).build(), - "cel.expr.conformance.proto3.TestAllTypes", + TestAllTypes.newBuilder() + .addRepeatedInt64(1L) + .putMapInt64Int64(1L, 2L) + .build() + .toByteString(), + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object selectedValue = protoMessageValue.selectByFieldNumber(testCase.selectField); @@ -607,7 +614,7 @@ private enum DefaultValueTestCase { "single_nested_message", ProtoMessageLiteValue.create( NestedMessage.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + NESTED_MESSAGE_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER)); private final String fieldName; @@ -624,7 +631,7 @@ public void selectField_defaultValue(@TestParameter DefaultValueTestCase testCas ProtoMessageLiteValue protoMessageValue = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object selectedValue = protoMessageValue.select(testCase.fieldName); @@ -637,7 +644,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default")); @@ -649,7 +656,7 @@ public void selectByFieldNumber_absentKnownMessageField_returnsDefaultMessage() ProtoMessageLiteValue val = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(21L, "single_nested_message", 11)); @@ -658,7 +665,7 @@ public void selectByFieldNumber_absentKnownMessageField_returnsDefaultMessage() .isEqualTo( ProtoMessageLiteValue.create( NestedMessage.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes.NestedMessage", + NESTED_MESSAGE_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER)); } @@ -672,7 +679,7 @@ public void selectByFieldNumber_unknownWireField_decoded() throws Exception { TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(999L, "unknown_field", 3, 0L)); @@ -690,7 +697,7 @@ public void selectByFieldNumber_unknownRepeatedWireField_decoded() throws Except TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber( @@ -704,7 +711,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(14L, "renamed_string", 9, "default")); @@ -726,7 +733,7 @@ public void selectByFieldNumber_unknownFieldCollidesWithKnownFieldName_returnsUn .build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(999L, "single_string", 3, 0L)); @@ -738,7 +745,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber( @@ -753,7 +760,7 @@ public void selectByFieldNumber_renamedRepeatedField_resolvesByFieldNumber() { TestAllTypes.newBuilder().addRepeatedInt64(10L).addRepeatedInt64(20L).build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Object result = val.selectByFieldNumber(SelectField.create(32L, "renamed_repeated", 3, ImmutableList.of())); @@ -776,7 +783,7 @@ public void findByFieldNumber_intermediateUnknownSubmessage_returnsRawProtoMessa TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); Optional nav = val.findByFieldNumber(SelectField.create(998L, "unknown_submessage")); @@ -788,7 +795,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(14L, "single_string"))).isTrue(); } @@ -803,7 +810,7 @@ public void hasFieldByNumber_unknownFieldPresent_returnsTrue() throws Exception TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(999L, "unknown_present"))).isTrue(); } @@ -813,7 +820,7 @@ public void hasFieldByNumber_unknownFieldAbsent_returnsFalse() { ProtoMessageLiteValue val = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(888L, "unknown_absent"))).isFalse(); @@ -824,7 +831,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(14L, "renamed_string"))).isTrue(); } @@ -834,7 +841,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(61L, "renamed_map"))).isTrue(); } @@ -844,7 +851,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(val.hasFieldByNumber(SelectField.create(32L, "renamed_repeated"))).isTrue(); } @@ -854,7 +861,7 @@ public void qualify_emptyList_returnsSameInstance() { ProtoMessageLiteValue val = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(OptimizedSelectTraversal.qualify(val, ImmutableList.of())).isSameInstanceAs(val); @@ -865,7 +872,7 @@ public void hasField_emptyList_returnsFalse() { ProtoMessageLiteValue val = ProtoMessageLiteValue.create( TestAllTypes.getDefaultInstance(), - "cel.expr.conformance.proto3.TestAllTypes", + TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat(OptimizedSelectTraversal.hasField(val, ImmutableList.of())).isFalse(); @@ -876,7 +883,7 @@ 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); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( SelectField.createMap(61L, "map_string_string", SelectField.MapEntrySpec.create(9, 9))); @@ -894,7 +901,7 @@ public void qualify_nestedMessage_resolvesField() { .build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( SelectField.create(21L, "single_nested_message", 11), @@ -913,7 +920,7 @@ public void hasField_nestedMessage_resolvesPresence() { .build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); assertThat( OptimizedSelectTraversal.hasField( @@ -936,7 +943,7 @@ public void qualify_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldN TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( SelectField.create(2L, "single_int64", 3, 0L), @@ -955,7 +962,7 @@ public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildField TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); ProtoMessageLiteValue val = ProtoMessageLiteValue.create( - proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + proto, TEST_ALL_TYPES_DESCRIPTOR, PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( SelectField.create(2L, "single_int64"), SelectField.create(3L, "leaf_field")); @@ -967,4 +974,63 @@ public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildField assertThat(thrown).hasMessageThat().contains("leaf_field"); } + + private enum SubmessageMemoTestCase { + DESCRIBED(PROTO_LITE_CEL_VALUE_CONVERTER), + DESCRIPTORLESS(PARTIAL_CONVERTER); + + private final ProtoLiteCelValueConverter converter; + + SubmessageMemoTestCase(ProtoLiteCelValueConverter converter) { + this.converter = converter; + } + } + + @Test + public void selectByFieldNumber_sameSubmessageTwice_reusesValue( + @TestParameter SubmessageMemoTestCase testCase) { + ProtoMessageLiteValue root = + (ProtoMessageLiteValue) + testCase.converter.toRuntimeValue( + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(2L)) + .build()); + SelectField payload = SelectField.create(2L, "payload", 11); + Object first = root.selectByFieldNumber(payload); + + Object second = root.selectByFieldNumber(payload); + + assertThat(second).isSameInstanceAs(first); + } + + @Test + public void qualify_otherSubmessageInBetween_readsEachOne( + @TestParameter SubmessageMemoTestCase testCase) { + ProtoMessageLiteValue root = + (ProtoMessageLiteValue) + testCase.converter.toRuntimeValue( + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(1L))) + .setPayload(TestAllTypes.newBuilder().setSingleInt64(2L)) + .build()); + SelectField child = SelectField.create(1L, "child", 11); + SelectField payload = SelectField.create(2L, "payload", 11); + SelectField singleInt64 = SelectField.create(2L, "single_int64", 3, 0L); + + ImmutableList values = + ImmutableList.of( + OptimizedSelectTraversal.qualify(root, ImmutableList.of(payload, singleInt64)), + OptimizedSelectTraversal.qualify(root, ImmutableList.of(child, payload, singleInt64)), + OptimizedSelectTraversal.qualify(root, ImmutableList.of(payload, singleInt64))); + Object firstPayload = root.selectByFieldNumber(payload); + Object middleChild = root.selectByFieldNumber(child); + Object secondPayload = root.selectByFieldNumber(payload); + + assertThat(values).containsExactly(2L, 1L, 2L).inOrder(); + assertThat(middleChild).isNotSameInstanceAs(firstPayload); + assertThat(secondPayload).isNotSameInstanceAs(firstPayload); + assertThat(secondPayload).isNotSameInstanceAs(middleChild); + } } diff --git a/java_lite_proto_cel_library.bzl b/java_lite_proto_cel_library.bzl index efed0bedc..b56fd7b66 100644 --- a/java_lite_proto_cel_library.bzl +++ b/java_lite_proto_cel_library.bzl @@ -27,7 +27,6 @@ def java_lite_proto_cel_library( Args: name: name of this target. deps: The list of proto_library rules to generate Java code for. - proto_src: Name of the proto_library target. java_descriptor_class_suffix (optional): Suffix for the Java class name of the generated CEL lite descriptor. Default is "CelLiteDescriptor". debug: (optional) If true, prints additional information during codegen for debugging purposes. diff --git a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel index b2dac98e7..1aa8003aa 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel +++ b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel @@ -36,6 +36,7 @@ java_library( deps = [ "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java index 14973505c..25f59a399 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java @@ -17,6 +17,7 @@ import static java.lang.Math.ceil; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; @@ -28,6 +29,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; /** * Base class for code generated CEL lite descriptors to extend from. @@ -37,6 +39,9 @@ @Internal @Immutable public abstract class CelLiteDescriptor { + /** Returned by {@link MessageLiteDescriptor#getFieldValue} for a field without a getter. */ + public static final Object NO_GETTER = new Object(); + @SuppressWarnings("Immutable") // Copied to unmodifiable map private final Map protoFqnToDescriptors; @@ -73,10 +78,22 @@ public static final class MessageLiteDescriptor { @SuppressWarnings("Immutable") // Does not alter the descriptor content private final Supplier messageBuilderSupplier; + // The generated descriptor declaring this message, and this message's position in it. + @SuppressWarnings("Immutable") // Set once, before the descriptor is published + private @Nullable CelLiteDescriptor owner; + + @SuppressWarnings("Immutable") // Set once, before the descriptor is published + private int index; + public String getProtoTypeName() { return fullyQualifiedProtoTypeName; } + @Override + public String toString() { + return fullyQualifiedProtoTypeName; + } + public List getFieldDescriptors() { return fieldLiteDescriptors; } @@ -104,6 +121,21 @@ public MessageLite.Builder newMessageBuilder() { return messageBuilderSupplier.get(); } + /** Whether {@link #getFieldValue} reads this message's fields with their generated getters. */ + public boolean hasGetters() { + return owner != null; + } + + /** + * Reads field {@code fieldNumber} of {@code message}, an instance of this message type, with + * its generated getter: the Java value (an open enum's number; a repeated or map field's list + * or map, possibly empty), null if a singular field is unset, or {@link #NO_GETTER} if this + * descriptor has no getter for it. + */ + public @Nullable Object getFieldValue(MessageLite message, int fieldNumber) { + return owner == null ? NO_GETTER : owner.getFieldValue(index, message, fieldNumber); + } + /** * CEL Library Internals. Do not use. * @@ -353,10 +385,54 @@ public FieldLiteDescriptor( } } + /** + * Overridden by generated descriptors to read field {@code fieldNumber} of {@code message}, an + * instance of their {@code messageIndex}th message type, with its generated getter. See {@link + * MessageLiteDescriptor#getFieldValue}. + */ + protected @Nullable Object getFieldValue(int messageIndex, Object message, int fieldNumber) { + return NO_GETTER; + } + + // Used by generated getters: a field without presence is unset when it holds its default value. + + protected static @Nullable Object nonDefault(int value) { + return value != 0 ? value : null; + } + + protected static @Nullable Object nonDefault(long value) { + return value != 0 ? value : null; + } + + protected static @Nullable Object nonDefault(float value) { + return Float.floatToRawIntBits(value) != 0 ? value : null; + } + + protected static @Nullable Object nonDefault(double value) { + return Double.doubleToRawLongBits(value) != 0 ? value : null; + } + + protected static @Nullable Object nonDefault(boolean value) { + return value ? Boolean.TRUE : null; + } + + protected static @Nullable Object nonDefault(String value) { + return value.isEmpty() ? null : value; + } + + protected static @Nullable Object nonDefault(ByteString value) { + return value.isEmpty() ? null : value; + } + protected CelLiteDescriptor(String version, List messageInfoList) { Map protoFqnMap = new HashMap<>(getMapInitialCapacity(messageInfoList.size())); - for (MessageLiteDescriptor msgInfo : messageInfoList) { + for (int i = 0; i < messageInfoList.size(); i++) { + MessageLiteDescriptor msgInfo = messageInfoList.get(i); + if (msgInfo.owner == null) { + msgInfo.owner = this; + msgInfo.index = i; + } protoFqnMap.put(msgInfo.getProtoTypeName(), msgInfo); } diff --git a/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java b/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java index 19372b9b4..db67c7b3e 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java +++ b/protobuf/src/main/java/dev/cel/protobuf/LiteDescriptorCodegenMetadata.java @@ -103,6 +103,10 @@ public String getProtoFieldTypeEnumName() { public abstract String getFieldProtoTypeName(); + // Java expression reading this field from a message named m with its generated getter, or + // empty if the field has no getter. + public abstract String getGetterExpression(); + abstract FieldLiteDescriptor.JavaType getJavaType(); abstract FieldLiteDescriptor.Type getProtoFieldType(); @@ -131,6 +135,8 @@ abstract static class Builder { abstract Builder setFieldProtoTypeName(String fieldProtoTypeName); + abstract Builder setGetterExpression(String getterExpression); + abstract FieldLiteDescriptorMetadata build(); } diff --git a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java index c2fe20557..301638cd4 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java +++ b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java @@ -18,8 +18,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.GeneratorNames; @@ -35,6 +35,18 @@ */ final class ProtoDescriptorCollector { + private static final ImmutableSet FORBIDDEN_ACCESSOR_NAMES = + ImmutableSet.of( + "Class", + "DefaultInstanceForType", + "ParserForType", + "SerializedSize", + "AllFields", + "DescriptorForType", + "InitializationErrorString", + "UnknownFields", + "CachedSize"); + private final DebugPrinter debugPrinter; ImmutableList collectCodegenMetadata(Descriptor descriptor) { @@ -49,14 +61,15 @@ ImmutableList collectCodegenMetadata(Descriptor d for (Descriptor messageDescriptor : descriptorList) { LiteDescriptorCodegenMetadata.Builder descriptorCodegenBuilder = LiteDescriptorCodegenMetadata.newBuilder(); - for (Descriptors.FieldDescriptor fieldDescriptor : messageDescriptor.getFields()) { + for (FieldDescriptor fieldDescriptor : messageDescriptor.getFields()) { FieldLiteDescriptorMetadata.Builder fieldDescriptorCodegenBuilder = FieldLiteDescriptorMetadata.newBuilder() .setFieldNumber(fieldDescriptor.getNumber()) .setFieldName(fieldDescriptor.getName()) .setIsPacked(fieldDescriptor.isPacked()) .setJavaType(adaptJavaType(fieldDescriptor.getJavaType())) - .setProtoFieldType(adaptFieldProtoType(fieldDescriptor.getType())); + .setProtoFieldType(adaptFieldProtoType(fieldDescriptor.getType())) + .setGetterExpression(getterExpression(fieldDescriptor)); switch (fieldDescriptor.getJavaType()) { case ENUM: @@ -107,8 +120,82 @@ static ProtoDescriptorCollector newInstance(DebugPrinter debugPrinter) { return new ProtoDescriptorCollector(debugPrinter); } - private static FieldLiteDescriptor.Type adaptFieldProtoType( - Descriptors.FieldDescriptor.Type type) { + /** + * Returns a Java expression reading {@code field} from a message named {@code m} with its + * generated getter: null if the field is unset, and open enums as their numbers. Empty for + * groups, which CEL doesn't support. + */ + private static String getterExpression(FieldDescriptor field) { + if (field.getType() == FieldDescriptor.Type.GROUP) { + return ""; + } + String name = accessorName(field); + if (field.isMapField()) { + FieldDescriptor value = field.getMessageType().findFieldByNumber(2); + return "m.get" + name + (isOpenEnum(value) ? "Value" : "") + "Map()"; + } + String suffix = isOpenEnum(field) ? "Value" : ""; + if (field.isRepeated()) { + return "m.get" + name + suffix + "List()"; + } + String getter = "m.get" + name + suffix + "()"; + return field.hasPresence() + ? "m.has" + name + "() ? " + getter + " : null" + : "nonDefault(" + getter + ")"; + } + + private static boolean isOpenEnum(FieldDescriptor field) { + return field.getJavaType() == JavaType.ENUM && !field.legacyEnumFieldTreatedAsClosed(); + } + + // Mirrors protobuf's Java generator (compiler/java/context.cc): fields whose accessors would + // clash with another field's get their numbers appended. + private static String accessorName(FieldDescriptor field) { + String name = capitalizedName(field); + for (FieldDescriptor other : field.getContainingType().getFields()) { + if (other != field) { + String otherName = capitalizedName(other); + if (clashesOneWay(field, name, other, otherName) + || clashesOneWay(other, otherName, field, name)) { + return name + field.getNumber(); + } + } + } + return name; + } + + // Whether other's getter is named like field's; or, if only field is repeated, like field's + // getXCount() or getXList(); or, if field is an open enum, like its getXValue(). + private static boolean clashesOneWay( + FieldDescriptor field, String name, FieldDescriptor other, String otherName) { + return otherName.equals(name) + || (field.isRepeated() + && !other.isRepeated() + && (otherName.equals(name + "Count") || otherName.equals(name + "List"))) + || (isOpenEnum(field) && otherName.equals(name + "Value")); + } + + // Mirrors protobuf's Java generator (compiler/java/names.cc): underscores and digits capitalize + // the next letter, and names clashing with inherited methods get a trailing underscore. + private static String capitalizedName(FieldDescriptor field) { + String fieldName = field.getName(); + StringBuilder name = new StringBuilder(fieldName.length() + 1); + boolean capitalizeNext = true; + for (char c : fieldName.toCharArray()) { + if (c == '_') { + capitalizeNext = true; + } else { + name.append(capitalizeNext ? Character.toUpperCase(c) : c); + capitalizeNext = Character.isDigit(c); + } + } + if (FORBIDDEN_ACCESSOR_NAMES.contains(name.toString())) { + name.append('_'); + } + return name.toString(); + } + + private static FieldLiteDescriptor.Type adaptFieldProtoType(FieldDescriptor.Type type) { switch (type) { case DOUBLE: return FieldLiteDescriptor.Type.DOUBLE; diff --git a/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt index 2d8515146..af2670154 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt +++ b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt @@ -70,6 +70,42 @@ public final class ${descriptor_class_name} extends CelLiteDescriptor { return Collections.unmodifiableList(descriptors); } + @java.lang.Override + protected java.lang.Object getFieldValue( + int messageIndex, java.lang.Object message, int fieldNumber) { + switch (messageIndex) { + <#list descriptor_metadata_list as descriptor_metadata> + <#if descriptor_metadata.javaClassName??> + case ${descriptor_metadata?index}: + return getFieldValue${descriptor_metadata?index}( + (${descriptor_metadata.javaClassName}) message, fieldNumber); + + + default: + return NO_GETTER; + } + } +<#-- A method per message bounds each by one message's fields: HotSpot won't JIT over 8000 B. --> +<#-- java.lang names are qualified: the proto's package may declare its own Object or Override. --> +<#list descriptor_metadata_list as descriptor_metadata> +<#if descriptor_metadata.javaClassName??> + + private static java.lang.Object getFieldValue${descriptor_metadata?index}( + ${descriptor_metadata.javaClassName} m, int fieldNumber) { + switch (fieldNumber) { + <#list descriptor_metadata.fieldDescriptors as field_descriptor> + <#if field_descriptor.getterExpression?has_content> + case ${field_descriptor.fieldNumber}: + return ${field_descriptor.getterExpression}; + + + default: + return NO_GETTER; + } + } + + + private ${descriptor_class_name}() { super("${version}", newDescriptors()); } diff --git a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel index 635379aab..e14ddeb04 100644 --- a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel +++ b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel @@ -12,10 +12,13 @@ java_test( deps = [ "//:java_truth", "//protobuf:cel_lite_descriptor", + "//testing/protos:accessor_names_cel_java_proto_lite", + "//testing/protos:accessor_names_java_proto_lite", "//testing/protos:test_all_types_cel_java_proto3_lite", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto_lite", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -24,15 +27,21 @@ java_test( name = "proto_descriptor_collector_test", srcs = ["ProtoDescriptorCollectorTest.java"], test_class = "dev.cel.protobuf.ProtoDescriptorCollectorTest", - runtime_deps = ["@maven//:com_google_protobuf_protobuf_java"], + runtime_deps = [ + # Prevent Classloader from picking protolite. We need full version to access descriptors to codegen CelLiteDescriptor. + "@maven//:com_google_protobuf_protobuf_java", + ], deps = [ "//:java_truth", "//protobuf:debug_printer", "//protobuf:lite_descriptor_codegen_metadata", "//protobuf:proto_descriptor_collector", + "//testing/protos:accessor_names_java_proto", "//testing/protos:multi_file_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", ], diff --git a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java index 90969a17d..1ec0e136a 100644 --- a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java +++ b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java @@ -17,14 +17,19 @@ 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; import com.google.protobuf.WireFormat; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypesCelLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import dev.cel.testing.testdata.AccessorNames; +import dev.cel.testing.testdata.AccessorNamesCelLiteDescriptor; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -55,6 +60,9 @@ public void testAllTypesMessageLiteDescriptor_fullyQualifiedNames() { assertThat(testAllTypesDescriptor.getProtoTypeName()) .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(testAllTypesDescriptor.toString()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(testAllTypesDescriptor.hasGetters()).isTrue(); } @Test @@ -215,6 +223,84 @@ public void protoFieldType_numbersAndWireTypes(@TestParameter ProtoFieldTypeTest assertThat(testCase.type.toWireFormatFieldType()).isEqualTo(testCase.expectedWireType); } + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum AccessorNameTestCase { + REPEATED_CLASHING_WITH_COUNT(AccessorNames.FOO_FIELD_NUMBER, ImmutableList.of(1)), + CLASHING_WITH_REPEATED_COUNT(AccessorNames.FOO_COUNT_FIELD_NUMBER, 2L), + REPEATED_CLASHING_WITH_LIST(AccessorNames.BAR_FIELD_NUMBER, ImmutableList.of("b")), + CLASHING_WITH_REPEATED_LIST(AccessorNames.BAR_LIST_FIELD_NUMBER, "c"), + OPEN_ENUM_CLASHING_WITH_VALUE(AccessorNames.KIND_FIELD_NUMBER, 1), + CLASHING_WITH_OPEN_ENUM_VALUE(AccessorNames.KIND_VALUE_FIELD_NUMBER, 3), + MAP_CLASHING_WITH_COUNT(AccessorNames.BAZ_FIELD_NUMBER, ImmutableMap.of("k", 5)), + CLASHING_WITH_MAP_COUNT(AccessorNames.BAZ_COUNT_FIELD_NUMBER, 6), + FORBIDDEN_NAME(AccessorNames.CLASS_FIELD_NUMBER, 4), + SAME_CAPITALIZED_NAME(AccessorNames.FIELD1NAME_FIELD_NUMBER, "d"), + OTHER_SAME_CAPITALIZED_NAME(AccessorNames.FIELD1_NAME_FIELD_NUMBER, "e"); + + private final int fieldNumber; + private final Object expectedValue; + + AccessorNameTestCase(int fieldNumber, Object expectedValue) { + this.fieldNumber = fieldNumber; + this.expectedValue = expectedValue; + } + } + + @Test + public void getFieldValue_protobufAccessorName_readsField( + @TestParameter AccessorNameTestCase testCase) { + AccessorNames message = + AccessorNames.newBuilder() + .addFoo1(1) + .setFooCount2(2L) + .addBar3("b") + .setBarList4("c") + .setKind5(AccessorNames.Kind.KIND_ON) + .setKindValue6(3) + .putBaz7("k", 5) + .setBazCount8(6) + .setClass_(4) + .setField1Name10("d") + .setField1Name11("e") + .build(); + + Object value = + accessorNamesDescriptor("AccessorNames").getFieldValue(message, testCase.fieldNumber); + + assertThat(value).isEqualTo(testCase.expectedValue); + } + + @Test + public void getFieldValue_messageAfterRoot_readsField() { + AccessorNames.Nested message = AccessorNames.Nested.newBuilder().setX(7).build(); + + Object value = + accessorNamesDescriptor("AccessorNames.Nested") + .getFieldValue(message, AccessorNames.Nested.X_FIELD_NUMBER); + + assertThat(value).isEqualTo(7); + } + + @Test + public void getFieldValue_unownedDescriptor_returnsNoGetter() { + MessageLiteDescriptor descriptor = + new MessageLiteDescriptor("unowned.Message", ImmutableList.of(), TestAllTypes::newBuilder); + + Object value = + descriptor.getFieldValue( + TestAllTypes.newBuilder().setSingleInt64(1L).build(), + TestAllTypes.SINGLE_INT64_FIELD_NUMBER); + + assertThat(descriptor.hasGetters()).isFalse(); + assertThat(value).isSameInstanceAs(CelLiteDescriptor.NO_GETTER); + } + + private static MessageLiteDescriptor accessorNamesDescriptor(String name) { + return AccessorNamesCelLiteDescriptor.getDescriptor() + .getProtoTypeNamesToDescriptors() + .get("dev.cel.testing.testdata." + name); + } + @Test public void protoFieldType_forNumber_roundTripAllTypes( @TestParameter FieldLiteDescriptor.Type type) { diff --git a/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java b/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java index 9e1e30005..cfdda72a3 100644 --- a/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java +++ b/protobuf/src/test/java/dev/cel/protobuf/ProtoDescriptorCollectorTest.java @@ -14,12 +14,18 @@ package dev.cel.protobuf; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.expr.conformance.proto3.NestedTestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.protobuf.LiteDescriptorCodegenMetadata.FieldLiteDescriptorMetadata; +import dev.cel.testing.testdata.AccessorNames; import dev.cel.testing.testdata.MultiFile; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +48,85 @@ public void collectCodegenMetadata_containsAllDescriptors() { assertThat(nestedTestAllTypesDescriptors).hasSize(1); } + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum GetterExpressionTestCase { + REPEATED_CLASHING_WITH_COUNT(AccessorNames.getDescriptor(), "foo", "m.getFoo1List()"), + CLASHING_WITH_REPEATED_COUNT( + AccessorNames.getDescriptor(), "foo_count", "nonDefault(m.getFooCount2())"), + REPEATED_CLASHING_WITH_LIST(AccessorNames.getDescriptor(), "bar", "m.getBar3List()"), + CLASHING_WITH_REPEATED_LIST( + AccessorNames.getDescriptor(), "bar_list", "nonDefault(m.getBarList4())"), + OPEN_ENUM_CLASHING_WITH_VALUE( + AccessorNames.getDescriptor(), "kind", "nonDefault(m.getKind5Value())"), + CLASHING_WITH_OPEN_ENUM_VALUE( + AccessorNames.getDescriptor(), "kind_value", "nonDefault(m.getKindValue6())"), + MAP_CLASHING_WITH_COUNT(AccessorNames.getDescriptor(), "baz", "m.getBaz7Map()"), + CLASHING_WITH_MAP_COUNT( + AccessorNames.getDescriptor(), "baz_count", "nonDefault(m.getBazCount8())"), + FORBIDDEN_NAME(AccessorNames.getDescriptor(), "class", "nonDefault(m.getClass_())"), + SAME_CAPITALIZED_NAME( + AccessorNames.getDescriptor(), "field1name", "nonDefault(m.getField1Name10())"), + OTHER_SAME_CAPITALIZED_NAME( + AccessorNames.getDescriptor(), "field1_name", "nonDefault(m.getField1Name11())"), + SINGULAR_NEAR_MISS(AccessorNames.getDescriptor(), "qux", "nonDefault(m.getQux())"), + SINGULAR_COUNT_NEAR_MISS( + AccessorNames.getDescriptor(), "qux_count", "nonDefault(m.getQuxCount())"), + NON_ENUM_VALUE_NEAR_MISS( + AccessorNames.getDescriptor(), "qux_value", "nonDefault(m.getQuxValue())"), + REPEATED_NEAR_MISS(AccessorNames.getDescriptor(), "quux", "m.getQuuxList()"), + REPEATED_COUNT_NEAR_MISS(AccessorNames.getDescriptor(), "quux_count", "m.getQuuxCountList()"), + FORBIDDEN_CASE_NEAR_MISS( + AccessorNames.getDescriptor(), "serializedsize", "nonDefault(m.getSerializedsize())"), + MESSAGE_WITH_PRESENCE( + AccessorNames.getDescriptor(), "nested", "m.hasNested() ? m.getNested() : null"), + OPEN_ENUM_REPEATED( + TestAllTypes.getDescriptor(), "repeated_nested_enum", "m.getRepeatedNestedEnumValueList()"), + OPEN_ENUM_MAP_VALUE( + TestAllTypes.getDescriptor(), "map_int32_enum", "m.getMapInt32EnumValueMap()"), + CLOSED_ENUM_SINGULAR( + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor(), + "standalone_enum", + "m.hasStandaloneEnum() ? m.getStandaloneEnum() : null"), + CLOSED_ENUM_REPEATED( + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor(), + "repeated_nested_enum", + "m.getRepeatedNestedEnumList()"), + CLOSED_ENUM_MAP_VALUE( + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor(), + "map_int32_enum", + "m.getMapInt32EnumMap()"), + GROUP(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor(), "nestedgroup", ""); + + private final Descriptor descriptor; + private final String fieldName; + private final String expectedExpression; + + GetterExpressionTestCase(Descriptor descriptor, String fieldName, String expectedExpression) { + this.descriptor = descriptor; + this.fieldName = fieldName; + this.expectedExpression = expectedExpression; + } + } + + @Test + public void collectCodegenMetadata_getterExpression( + @TestParameter GetterExpressionTestCase testCase) { + ProtoDescriptorCollector collector = + ProtoDescriptorCollector.newInstance(DebugPrinter.newInstance(false)); + + ImmutableList descriptors = + collector.collectCodegenMetadata(testCase.descriptor); + + ImmutableMap expressionsByFieldName = + descriptors.get(0).getFieldDescriptors().stream() + .collect( + toImmutableMap( + FieldLiteDescriptorMetadata::getFieldName, + FieldLiteDescriptorMetadata::getGetterExpression)); + assertThat(expressionsByFieldName) + .containsEntry(testCase.fieldName, testCase.expectedExpression); + } + @Test public void collectCodegenMetadata_withProtoDependencies_containsAllDescriptors() { ProtoDescriptorCollector collector = diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java index 79de0b70d..4833b8cc4 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeTest.java @@ -117,9 +117,9 @@ public void messageCreation_emptyMessage() throws Exception { @Test public void messageCreation_fieldsPopulated() throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.compile("TestAllTypes{single_int32: 4}").getAst(); + Program program = CEL_RUNTIME.createProgram(ast); - CelEvaluationException e = - assertThrows(CelEvaluationException.class, () -> CEL_RUNTIME.createProgram(ast).eval()); + CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); assertThat(e) .hasMessageThat() @@ -193,7 +193,7 @@ public void fieldSelection_unsigned(String expression) throws Exception { @Test @TestParameters("{expression: 'msg.repeated_int32'}") @TestParameters("{expression: 'msg.repeated_int64'}") - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_packedRepeatedInts(String expression) throws Exception { // Note: non-LEN delimited primitives such as ints are packed by default in proto3 CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); @@ -212,7 +212,7 @@ public void fieldSelection_packedRepeatedInts(String expression) throws Exceptio } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_repeatedStrings() throws Exception { // Note: len-delimited fields, such as string and messages are not packed. CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.repeated_string").getAst(); @@ -226,7 +226,7 @@ public void fieldSelection_repeatedStrings() throws Exception { } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_repeatedBoolWrappers() throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.compile("msg.repeated_bool_wrapper").getAst(); TestAllTypes msg = @@ -245,7 +245,7 @@ public void fieldSelection_repeatedBoolWrappers() throws Exception { @Test @TestParameters("{expression: 'msg.map_string_int32'}") @TestParameters("{expression: 'msg.map_string_int64'}") - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_map(String expression) throws Exception { CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); TestAllTypes msg = @@ -340,7 +340,7 @@ public void fieldSelection_timestamp() throws Exception { } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_jsonStruct() throws Exception { String expression = "msg.single_struct"; CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); @@ -370,7 +370,7 @@ public void fieldSelection_jsonValue() throws Exception { } @Test - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // Safe cast in test public void fieldSelection_jsonListValue() throws Exception { String expression = "msg.list_value"; CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); diff --git a/testing/protos/BUILD.bazel b/testing/protos/BUILD.bazel index 17706ca45..ac0c691b0 100644 --- a/testing/protos/BUILD.bazel +++ b/testing/protos/BUILD.bazel @@ -29,6 +29,21 @@ alias( actual = "//testing/src/test/resources/protos:message_with_enum_java_proto", ) +alias( + name = "accessor_names_java_proto", + actual = "//testing/src/test/resources/protos:accessor_names_java_proto", +) + +alias( + name = "accessor_names_java_proto_lite", + actual = "//testing/src/test/resources/protos:accessor_names_java_proto_lite", +) + +alias( + name = "accessor_names_cel_java_proto_lite", + actual = "//testing/src/test/resources/protos:accessor_names_cel_java_proto_lite", +) + alias( name = "multi_file_cel_java_proto_lite", actual = "//testing/src/test/resources/protos:multi_file_cel_java_proto_lite", diff --git a/testing/src/test/resources/protos/BUILD.bazel b/testing/src/test/resources/protos/BUILD.bazel index 1fac2e1f0..2717e70ff 100644 --- a/testing/src/test/resources/protos/BUILD.bazel +++ b/testing/src/test/resources/protos/BUILD.bazel @@ -1,3 +1,4 @@ +load("@com_google_protobuf//bazel:java_lite_proto_library.bzl", "java_lite_proto_library") load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("//:java_lite_proto_cel_library.bzl", "java_lite_proto_cel_library") @@ -67,6 +68,26 @@ java_proto_library( ], ) +proto_library( + name = "accessor_names_proto", + srcs = ["accessor_names.proto"], +) + +java_proto_library( + name = "accessor_names_java_proto", + deps = [":accessor_names_proto"], +) + +java_lite_proto_library( + name = "accessor_names_java_proto_lite", + deps = [":accessor_names_proto"], +) + +java_lite_proto_cel_library( + name = "accessor_names_cel_java_proto_lite", + deps = [":accessor_names_proto"], +) + java_lite_proto_cel_library( name = "multi_file_cel_java_proto_lite", deps = [ diff --git a/testing/src/test/resources/protos/accessor_names.proto b/testing/src/test/resources/protos/accessor_names.proto new file mode 100644 index 000000000..e62a45cef --- /dev/null +++ b/testing/src/test/resources/protos/accessor_names.proto @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package dev.cel.testing.testdata; + +option java_multiple_files = true; +option java_outer_classname = "AccessorNamesProto"; +option java_package = "dev.cel.testing.testdata"; + +// Fields whose Java accessor names protobuf doesn't derive by plain +// camel-casing, and near misses that it doesn't rename, for testing generated +// getters, which fail to compile if misnamed. +message AccessorNames { + enum Kind { + KIND_UNSPECIFIED = 0; + KIND_ON = 1; + } + + message Nested { + int32 x = 1; + } + + // Clashes with the getFooCount() of the repeated foo. + repeated int32 foo = 1; + int64 foo_count = 2; + + // Clashes with the getBarList() of the repeated bar. + repeated string bar = 3; + string bar_list = 4; + + // Clashes with the getKindValue() of the open enum kind. + Kind kind = 5; + int32 kind_value = 6; + + // Clashes with the getBazCount() of the map baz. + map baz = 7; + int32 baz_count = 8; + + // Clashes with Object.getClass(), so it gets a trailing underscore. + int32 class = 9; + + // A digit capitalizes the next letter, so both are Field1Name and get their + // numbers. + string field1name = 10; + string field1_name = 11; + + // Near misses: qux is neither repeated nor an enum, quux_count is repeated + // too, and only exact names of inherited methods get a trailing underscore. + int32 qux = 12; + int32 qux_count = 13; + int32 qux_value = 14; + repeated int32 quux = 15; + repeated int32 quux_count = 16; + int32 serializedsize = 17; + + // A message type after the root one, read through its own generated method. + Nested nested = 18; +}