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 ae417383e..bc8e50b05 100644 --- a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; @@ -35,8 +36,10 @@ import dev.cel.common.types.StructTypeReference; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import java.io.IOException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import org.jspecify.annotations.Nullable; @@ -59,6 +62,8 @@ public abstract class RawProtoMessageLiteValue extends StructValue unknowns, ProtoLiteCelValueConverter converter) { + SelectField.MapEntrySpec mapEntrySpec = field.mapEntrySpec(); + if (mapEntrySpec != null) { + return decodeMapEntries( + unknowns, mapEntrySpec, field.protoTypeName(), field.fieldName(), converter); + } + int typeCode = field.typeCode(); if (typeCode == SelectField.CEL_MAP_TYPE_CODE) { throw new UnsupportedOperationException( @@ -240,6 +251,100 @@ static Optional navigateWire( return Optional.of(lastEntry); } + /** + * Decodes map entries from wire bytes without descriptors (version skew), using the + * optimizer-provided {@link SelectField.MapEntrySpec}. Per protobuf semantics, repeated key or + * value tags within an entry are last-one-wins for scalars and merged for messages, and absent + * keys or values take their type's default. + */ + private static ImmutableMap decodeMapEntries( + ImmutableList unknowns, + SelectField.MapEntrySpec spec, + String valueProtoTypeName, + String fieldName, + ProtoLiteCelValueConverter converter) { + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + try { + for (Object raw : unknowns) { + ByteString bytes = requireType(raw, ByteString.class, WireFormat.FieldType.MESSAGE); + mapBuilder.put( + decodeSingleMapEntry(bytes.newCodedInput(), spec, valueProtoTypeName, converter)); + } + } catch (IOException e) { + throw new IllegalArgumentException("Failed to decode map entry for field: " + fieldName, e); + } + return mapBuilder.buildKeepingLast(); + } + + private static Map.Entry decodeSingleMapEntry( + CodedInputStream in, + SelectField.MapEntrySpec spec, + String valueProtoTypeName, + ProtoLiteCelValueConverter converter) + throws IOException { + WireFormat.FieldType keyWireType = + FieldLiteDescriptor.Type.forNumber(spec.keyTypeCode()).toWireFormatFieldType(); + WireFormat.FieldType valWireType = + FieldLiteDescriptor.Type.forNumber(spec.valueTypeCode()).toWireFormatFieldType(); + boolean isMessageValue = valWireType == WireFormat.FieldType.MESSAGE; + Object key = resolveDefaultMapScalarValue(spec.keyTypeCode()); + Object value = + isMessageValue ? ByteString.EMPTY : resolveDefaultMapScalarValue(spec.valueTypeCode()); + for (int tag = in.readTag(); tag != 0; tag = in.readTag()) { + int tagWireType = WireFormat.getTagWireType(tag); + int fieldNumber = WireFormat.getTagFieldNumber(tag); + Object parsedValue = ProtoLiteCelValueConverter.readUnknownField(tagWireType, in); + switch (fieldNumber) { + case MAP_KEY_FIELD_NUMBER: + key = decodeWireValue(parsedValue, keyWireType, /* protoTypeName= */ "", converter); + break; + case MAP_VALUE_FIELD_NUMBER: + value = + isMessageValue + ? ((ByteString) value) + .concat(requireType(parsedValue, ByteString.class, valWireType)) + : decodeWireValue(parsedValue, valWireType, /* protoTypeName= */ "", converter); + break; + default: + throw new IllegalStateException("Unexpected field number in map entry: " + fieldNumber); + } + } + if (isMessageValue) { + value = decodeWireValue(value, valWireType, valueProtoTypeName, converter); + } + return new AbstractMap.SimpleImmutableEntry<>(key, value); + } + + private static Object resolveDefaultMapScalarValue(int typeCode) { + FieldLiteDescriptor.Type protoType = FieldLiteDescriptor.Type.forNumber(typeCode); + switch (protoType) { + case BOOL: + return false; + case INT32: + case INT64: + case SINT32: + case SINT64: + case SFIXED32: + case SFIXED64: + case ENUM: + return 0L; + case UINT32: + case UINT64: + case FIXED32: + case FIXED64: + return UnsignedLong.ZERO; + case FLOAT: + case DOUBLE: + return 0.0d; + case STRING: + return ""; + case BYTES: + return CelByteString.EMPTY; + default: + throw new IllegalArgumentException("Unsupported map scalar type code: " + typeCode); + } + } + static @Nullable Object decodeWireEntries( ImmutableCollection entries, int typeCode, diff --git a/common/src/main/java/dev/cel/common/values/SelectField.java b/common/src/main/java/dev/cel/common/values/SelectField.java index c89d3bf1c..9374ebcf1 100644 --- a/common/src/main/java/dev/cel/common/values/SelectField.java +++ b/common/src/main/java/dev/cel/common/values/SelectField.java @@ -20,6 +20,7 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; +import java.util.Collections; import org.jspecify.annotations.Nullable; /** @@ -40,7 +41,7 @@ public abstract class SelectField { public static final int CEL_MAP_TYPE_CODE = -1; /** Sentinel for a presence-test qualifier, whose 2-tuple carries no type code. */ - public static final int NO_TYPE_CODE = 0; + static final int NO_TYPE_CODE = 0; /** Protobuf field type code for {@code TYPE_MESSAGE} ({@code FieldDescriptorProto.Type}). */ public static final int MESSAGE_TYPE_CODE = 11; @@ -52,28 +53,35 @@ public abstract class SelectField { private static final int GROUP_PROTO_TYPE_CODE = 10; // Unsupported by CEL. /** Protobuf field number of this hop. */ - public abstract int fieldNumber(); + abstract int fieldNumber(); /** Protobuf field name or map key of this hop. */ public abstract String fieldName(); /** - * Protobuf wire type code (1..18, except 10), {@link #CEL_MAP_TYPE_CODE}, or {@link + * Protobuf field type code (1..18, except 10), {@link #CEL_MAP_TYPE_CODE}, or {@link * #NO_TYPE_CODE}. */ - public abstract int typeCode(); + abstract int typeCode(); /** * Default value for this hop, or null if unspecified. When non-null, this must be an immutable * CEL literal value. */ - public abstract @Nullable Object defaultValue(); + abstract @Nullable Object defaultValue(); /** - * Protobuf message type name of this hop (e.g. {@code "google.protobuf.Duration"}, {@code - * "com.example.User"}), or empty string if unknown or not a message field. + * Protobuf message type name associated with this hop (e.g. {@code "google.protobuf.Duration"} or + * {@code "com.example.User"} for singular/repeated message fields, or the map value's message + * type name for map fields with {@link #CEL_MAP_TYPE_CODE}), or empty string if unspecified. */ - public abstract String protoTypeName(); + abstract String protoTypeName(); + + /** + * Map entry decoding specification. Non-null if and only if this is a map field ({@link + * #CEL_MAP_TYPE_CODE}). + */ + abstract @Nullable MapEntrySpec mapEntrySpec(); /** * Creates a presence-test qualifier hop. @@ -93,33 +101,51 @@ public static SelectField create(long fieldNumber, String fieldName) { fieldName, NO_TYPE_CODE, /* defaultValue= */ null, - /* protoTypeName= */ ""); + /* protoTypeName= */ "", + /* mapEntrySpec= */ null); } /** - * Creates a fully-specified field selection hop with type code and optional default value. + * Creates a non-map field selection hop with type code and no default value or protobuf type + * name. Use {@link #createMap} for map fields. * * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's int64 * constant representations. * @param fieldName Protobuf field name. - * @param typeCode Protobuf wire type code or {@link #CEL_MAP_TYPE_CODE}. Takes {@code long} for - * compatibility with CEL's int64 constant representations. - * @param defaultValue Default value for the field, or null if unspecified. + * @param typeCode Protobuf field type code. Takes {@code long} for compatibility with CEL's int64 + * constant representations. + */ + public static SelectField create(long fieldNumber, String fieldName, long typeCode) { + return create( + fieldNumber, fieldName, typeCode, /* defaultValue= */ null, /* protoTypeName= */ ""); + } + + /** + * Creates a non-map field selection hop with type code and default value. Use {@link #createMap} + * for map fields. + * + * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's int64 + * constant representations. + * @param fieldName Protobuf field name. + * @param typeCode Protobuf field type code. Takes {@code long} for compatibility with CEL's int64 + * constant representations. + * @param defaultValue Default value for the field. */ public static SelectField create( - long fieldNumber, String fieldName, long typeCode, @Nullable Object defaultValue) { + long fieldNumber, String fieldName, long typeCode, Object defaultValue) { + checkNotNull(defaultValue); return create(fieldNumber, fieldName, typeCode, defaultValue, /* protoTypeName= */ ""); } /** - * Creates a fully-specified field selection hop with type code, optional default value, and - * protobuf type name. + * Creates a fully-specified non-map field selection hop with type code, optional default value, + * and protobuf type name. Use {@link #createMap} for map fields. * * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's int64 * constant representations. * @param fieldName Protobuf field name. - * @param typeCode Protobuf wire type code or {@link #CEL_MAP_TYPE_CODE}. Takes {@code long} for - * compatibility with CEL's int64 constant representations. + * @param typeCode Protobuf field type code. Takes {@code long} for compatibility with CEL's int64 + * constant representations. * @param defaultValue Default value for the field, or null if unspecified. * @param protoTypeName Protobuf message type name, or empty string if unspecified. */ @@ -129,15 +155,47 @@ public static SelectField create( long typeCode, @Nullable Object defaultValue, String protoTypeName) { - checkArgument( - fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER, - "Field number out of protobuf range: %s", - fieldNumber); - checkNotNull(fieldName); - checkArgument(isSupportedTypeCode(typeCode), "Invalid protobuf type code: %s", typeCode); + return newInstance( + fieldNumber, fieldName, typeCode, defaultValue, protoTypeName, /* mapEntrySpec= */ null); + } + + /** + * Creates a field selection hop for a map field with scalar values. + * + * @param fieldNumber Protobuf field number. + * @param fieldName Protobuf field name. + * @param mapEntrySpec Specification for decoding map entries. + */ + public static SelectField createMap( + long fieldNumber, String fieldName, MapEntrySpec mapEntrySpec) { + return createMap(fieldNumber, fieldName, mapEntrySpec, /* protoTypeName= */ ""); + } + + /** + * Creates a field selection hop for a map field. + * + * @param fieldNumber Protobuf field number. + * @param fieldName Protobuf field name. + * @param mapEntrySpec Specification for decoding map entries. + * @param protoTypeName Protobuf message type name of the map value. Must be non-empty if and only + * if the map value is a message. + */ + public static SelectField createMap( + long fieldNumber, String fieldName, MapEntrySpec mapEntrySpec, String protoTypeName) { + checkNotNull(mapEntrySpec); checkNotNull(protoTypeName); - return new AutoValue_SelectField( - (int) fieldNumber, fieldName, (int) typeCode, defaultValue, protoTypeName); + checkArgument( + (mapEntrySpec.valueTypeCode() == MESSAGE_TYPE_CODE) != protoTypeName.isEmpty(), + "Map value proto type name '%s' is inconsistent with value type code %s", + protoTypeName, + mapEntrySpec.valueTypeCode()); + return newInstance( + fieldNumber, + fieldName, + CEL_MAP_TYPE_CODE, + /* defaultValue= */ Collections.emptyMap(), + protoTypeName, + mapEntrySpec); } /** @@ -153,5 +211,88 @@ public static boolean isSupportedTypeCode(long typeCode) { && typeCode != GROUP_PROTO_TYPE_CODE; } + /** Specification for decoding map entries directly from the wire. */ + @AutoValue + @AutoValue.CopyAnnotations + @Immutable + public abstract static class MapEntrySpec { + + // Mirrors the integral, bool, and string type numbers in + // dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.Type + // Duplicated as int constants to keep :select_field and :planner free of protobuf_lite deps. + private static final int TYPE_INT64 = 3; + private static final int TYPE_UINT64 = 4; + private static final int TYPE_INT32 = 5; + private static final int TYPE_FIXED64 = 6; + private static final int TYPE_FIXED32 = 7; + private static final int TYPE_BOOL = 8; + private static final int TYPE_STRING = 9; + private static final int TYPE_UINT32 = 13; + private static final int TYPE_SFIXED32 = 15; + private static final int TYPE_SFIXED64 = 16; + private static final int TYPE_SINT32 = 17; + private static final int TYPE_SINT64 = 18; + + /** Protobuf field type code ({@code FieldDescriptorProto.Type}) of the map key (tag 1). */ + public abstract int keyTypeCode(); + + /** Protobuf field type code ({@code FieldDescriptorProto.Type}) of the map value (tag 2). */ + public abstract int valueTypeCode(); + + /** + * Creates a new map entry specification. + * + * @param keyTypeCode Protobuf field type code of the map key. Takes {@code long} for CEL int64 + * constant representation. + * @param valueTypeCode Protobuf field type code of the map value. Takes {@code long} for CEL + * int64 constant representation. + */ + public static MapEntrySpec create(long keyTypeCode, long valueTypeCode) { + checkArgument( + isValidMapKeyTypeCode(keyTypeCode), "Invalid map key type code: %s", keyTypeCode); + checkArgument( + isSupportedTypeCode(valueTypeCode) && valueTypeCode != CEL_MAP_TYPE_CODE, + "Invalid map value type code: %s", + valueTypeCode); + return new AutoValue_SelectField_MapEntrySpec((int) keyTypeCode, (int) valueTypeCode); + } + + /** Returns whether {@code typeCode} is an integral, bool, or string type permitted as a key. */ + private static boolean isValidMapKeyTypeCode(long typeCode) { + return typeCode == TYPE_INT64 + || typeCode == TYPE_UINT64 + || typeCode == TYPE_INT32 + || typeCode == TYPE_FIXED64 + || typeCode == TYPE_FIXED32 + || typeCode == TYPE_BOOL + || typeCode == TYPE_STRING + || typeCode == TYPE_UINT32 + || typeCode == TYPE_SFIXED32 + || typeCode == TYPE_SFIXED64 + || typeCode == TYPE_SINT32 + || typeCode == TYPE_SINT64; + } + + MapEntrySpec() {} + } + + private static SelectField newInstance( + long fieldNumber, + String fieldName, + long typeCode, + @Nullable Object defaultValue, + String protoTypeName, + @Nullable MapEntrySpec mapEntrySpec) { + checkArgument( + fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER, + "Field number out of protobuf range: %s", + fieldNumber); + checkNotNull(fieldName); + checkArgument(isSupportedTypeCode(typeCode), "Invalid protobuf type code: %s", typeCode); + checkNotNull(protoTypeName); + return new AutoValue_SelectField( + (int) fieldNumber, fieldName, (int) typeCode, defaultValue, protoTypeName, mapEntrySpec); + } + SelectField() {} } 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 439c8f117..575ed260d 100644 --- a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java +++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java @@ -118,8 +118,7 @@ public void qualify_nested_success(@TestParameter TargetType targetType) { Object target = targetType.createNestedTarget(ImmutableMap.of("inner_key", "nested_value")); ImmutableList fields = ImmutableList.of( - SelectField.create(1L, "outer_key", 11, null), - SelectField.create(2L, "inner_key", 9, "")); + SelectField.create(1L, "outer_key", 11), SelectField.create(2L, "inner_key", 9, "")); Object result = OptimizedSelectTraversal.qualify(target, fields); @@ -129,8 +128,7 @@ public void qualify_nested_success(@TestParameter TargetType targetType) { @Test public void qualify_singleField_missingThrowsException(@TestParameter TargetType targetType) { Object target = targetType.createTarget(ImmutableMap.of("present", "value")); - ImmutableList fields = - ImmutableList.of(SelectField.create(1L, "missing", 9, null)); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "missing", 9)); CelAttributeNotFoundException thrown = assertThrows( 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 ca370410d..f6faf199a 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -375,7 +375,8 @@ public void selectByFieldNumber_renamedMapField_resolvesByFieldNumber() { proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); Object result = - val.selectByFieldNumber(SelectField.create(61L, "renamed_map", -1, ImmutableMap.of())); + val.selectByFieldNumber( + SelectField.createMap(61L, "renamed_map", SelectField.MapEntrySpec.create(9, 9), "")); assertThat(result).isEqualTo(ImmutableMap.of("k", "v")); } @@ -512,8 +513,7 @@ public void qualify_mapField_returnsMap() { proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( - SelectField.create( - 61L, "map_string_string", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of())); + SelectField.createMap(61L, "map_string_string", SelectField.MapEntrySpec.create(9, 9))); Object result = OptimizedSelectTraversal.qualify(val, fields); @@ -531,7 +531,7 @@ public void qualify_nestedMessage_resolvesField() { proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); ImmutableList fields = ImmutableList.of( - SelectField.create(21L, "single_nested_message", 11, null), + SelectField.create(21L, "single_nested_message", 11), SelectField.create(1L, "bb", 5, 0)); Object result = OptimizedSelectTraversal.qualify(val, fields); diff --git a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java index 38cd5b1d2..d29e648ef 100644 --- a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -30,12 +30,14 @@ import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.DefaultLiteDescriptorPool; import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.expr.conformance.proto3.NestedTestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.Map; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -998,6 +1000,19 @@ private enum SelectByFieldNumberTestCase { FieldLiteDescriptor.Type.STRING.getNumber(), ""), "hello"), + MAP_STRING_STRING( + SelectField.createMap( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.MapEntrySpec.create(9, 9)), + ImmutableMap.of("k1", "v1", "k2", "v2")), + MAP_INT32_BYTES( + SelectField.createMap( + TestAllTypes.MAP_INT32_BYTES_FIELD_NUMBER, + "map_int32_bytes", + SelectField.MapEntrySpec.create(5, 12)), + ImmutableMap.of( + 0L, CelByteString.copyFromUtf8("val_for_default_key"), 42L, CelByteString.EMPTY)), DURATION( SelectField.create( TestAllTypes.SINGLE_DURATION_FIELD_NUMBER, @@ -1023,6 +1038,10 @@ public void selectByFieldNumber_decodesExpectedValue( TestAllTypes.newBuilder() .setSingleInt64(99L) .setSingleString("hello") + .putMapStringString("k1", "v1") + .putMapStringString("k2", "v2") + .putMapInt32Bytes(0, ByteString.copyFromUtf8("val_for_default_key")) + .putMapInt32Bytes(42, ByteString.EMPTY) .setSingleDuration(ProtoTimeUtils.toProtoDuration(Duration.ofSeconds(10L, 500L))) .build(); RawProtoMessageLiteValue raw = @@ -1074,7 +1093,7 @@ public void selectByFieldNumber_absentMessageFieldWithoutDescriptor_returnsUnkno Object selected = raw.selectByFieldNumber( SelectField.create( - 21L, "single_nested_message", FieldLiteDescriptor.Type.MESSAGE.getNumber(), null)); + 21L, "single_nested_message", FieldLiteDescriptor.Type.MESSAGE.getNumber())); assertThat(selected).isInstanceOf(RawProtoMessageLiteValue.class); RawProtoMessageLiteValue message = (RawProtoMessageLiteValue) selected; @@ -1082,26 +1101,338 @@ public void selectByFieldNumber_absentMessageFieldWithoutDescriptor_returnsUnkno assertThat(message.celType().name()).isEqualTo("cel.@unknownMessage"); } + @Test + public void selectByFieldNumber_unknownMapFieldWithMapEntrySpec_success( + @TestParameter({"", "cel.@unknownMessage", "cel.expr.conformance.proto3.TestAllTypes"}) + String receiverTypeName) { + TestAllTypes proto = + TestAllTypes.newBuilder() + .putMapStringString("key1", "val1") + .putMapStringString("key2", "val2") + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create(proto.toByteString(), receiverTypeName, EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.MapEntrySpec.create(9, 9)); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result).isEqualTo(ImmutableMap.of("key1", "val1", "key2", "val2")); + } + @Test public void - selectByFieldNumber_unknownMapFieldWithWireEntries_throwsUnsupportedOperationException() { - TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("key", "val").build(); + selectByFieldNumber_mapFieldWithMessageValueWithoutDescriptor_returnsMapOfRawMessage() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .putMapInt64NestedType( + 42L, + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(100L)) + .build()) + .build(); RawProtoMessageLiteValue raw = RawProtoMessageLiteValue.create( proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); - SelectField field = + SelectField field = newMapInt64NestedTypeField(); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result).isInstanceOf(Map.class); + Map map = (Map) result; + assertThat(map).containsKey(42L); + Object val = map.get(42L); + assertThat(val).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue nested = (RawProtoMessageLiteValue) val; + assertThat(nested.celType().name()).isEqualTo("cel.expr.conformance.proto3.NestedTestAllTypes"); + } + + @Test + public void selectByFieldNumber_unknownMapFieldWithMessageValue_multiHopTraversalSuccess() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .putMapInt64NestedType( + 42L, + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(100L)) + .build()) + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField mapField = + SelectField.createMap( + TestAllTypes.MAP_INT64_NESTED_TYPE_FIELD_NUMBER, + "map_int64_nested_type", + SelectField.MapEntrySpec.create( + FieldLiteDescriptor.Type.INT64.getNumber(), SelectField.MESSAGE_TYPE_CODE), + "cel.expr.conformance.proto3.NestedTestAllTypes"); + SelectField payloadField = SelectField.create( + NestedTestAllTypes.PAYLOAD_FIELD_NUMBER, + "payload", + SelectField.MESSAGE_TYPE_CODE, + null, + "cel.expr.conformance.proto3.TestAllTypes"); + SelectField int64Field = + SelectField.create( + TestAllTypes.SINGLE_INT64_FIELD_NUMBER, + "single_int64", + FieldLiteDescriptor.Type.INT64.getNumber(), + 0L); + + Map map = (Map) raw.selectByFieldNumber(mapField); + RawProtoMessageLiteValue nested = (RawProtoMessageLiteValue) map.get(42L); + RawProtoMessageLiteValue payload = + (RawProtoMessageLiteValue) nested.selectByFieldNumber(payloadField); + Object selectedInt64 = payload.selectByFieldNumber(int64Field); + + assertThat(selectedInt64).isEqualTo(100L); + } + + @Test + public void selectByFieldNumber_absentMapFieldWithMapEntrySpec_returnsEmptyMap() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, "map_string_string", - SelectField.CEL_MAP_TYPE_CODE, - ImmutableMap.of()); + SelectField.MapEntrySpec.create(9, 9)); - UnsupportedOperationException e = - assertThrows(UnsupportedOperationException.class, () -> raw.selectByFieldNumber(field)); + Object result = raw.selectByFieldNumber(field); - assertThat(e) - .hasMessageThat() - .contains("Decoding unknown map field from wire bytes is unsupported"); + assertThat(result).isEqualTo(ImmutableMap.of()); + } + + @Test + public void selectByFieldNumber_mapEntrySpecMissingKeyOrValue_usesTypedZeroDefaults() + throws Exception { + ByteString valueOnlyEntry = encode(out -> out.writeDouble(2, 1.5d)); + ByteString keyOnlyEntry = encode(out -> out.writeUInt32(1, 7)); + ByteString wire = + encode( + out -> { + out.writeBytes(TestAllTypes.MAP_UINT32_DOUBLE_FIELD_NUMBER, valueOnlyEntry); + out.writeBytes(TestAllTypes.MAP_UINT32_DOUBLE_FIELD_NUMBER, keyOnlyEntry); + }); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + TestAllTypes.MAP_UINT32_DOUBLE_FIELD_NUMBER, + "map_uint32_double", + SelectField.MapEntrySpec.create( + FieldLiteDescriptor.Type.UINT32.getNumber(), + FieldLiteDescriptor.Type.DOUBLE.getNumber())); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result) + .isEqualTo(ImmutableMap.of(UnsignedLong.ZERO, 1.5d, UnsignedLong.valueOf(7), 0.0d)); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum MapScalarDefaultTestCase { + BOOL(FieldLiteDescriptor.Type.BOOL, false), + INT32(FieldLiteDescriptor.Type.INT32, 0L), + SINT64(FieldLiteDescriptor.Type.SINT64, 0L), + ENUM(FieldLiteDescriptor.Type.ENUM, 0L), + FIXED32(FieldLiteDescriptor.Type.FIXED32, UnsignedLong.ZERO), + UINT64(FieldLiteDescriptor.Type.UINT64, UnsignedLong.ZERO), + FLOAT(FieldLiteDescriptor.Type.FLOAT, 0.0d), + STRING(FieldLiteDescriptor.Type.STRING, ""), + BYTES(FieldLiteDescriptor.Type.BYTES, CelByteString.EMPTY); + + private final FieldLiteDescriptor.Type valueType; + private final Object expectedDefault; + + MapScalarDefaultTestCase(FieldLiteDescriptor.Type valueType, Object expectedDefault) { + this.valueType = valueType; + this.expectedDefault = expectedDefault; + } + } + + @Test + public void selectByFieldNumber_mapEntrySpecMissingScalarValue_usesTypedZeroDefault( + @TestParameter MapScalarDefaultTestCase testCase) throws Exception { + ByteString keyOnlyEntry = encode(out -> out.writeString(1, "k")); + ByteString wire = encode(out -> out.writeBytes(1, keyOnlyEntry)); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + 1L, + "unknown_map", + SelectField.MapEntrySpec.create( + FieldLiteDescriptor.Type.STRING.getNumber(), testCase.valueType.getNumber())); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result).isEqualTo(ImmutableMap.of("k", testCase.expectedDefault)); + } + + @Test + public void selectByFieldNumber_mapEntrySpecMissingMessageValue_returnsEmptyMessage() + throws Exception { + ByteString keyOnlyEntry = encode(out -> out.writeInt64(1, 42L)); + ByteString wire = + encode( + out -> out.writeBytes(TestAllTypes.MAP_INT64_NESTED_TYPE_FIELD_NUMBER, keyOnlyEntry)); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Object result = raw.selectByFieldNumber(newMapInt64NestedTypeField()); + + assertThat(result) + .isEqualTo( + ImmutableMap.of( + 42L, + RawProtoMessageLiteValue.create( + ByteString.EMPTY, + "cel.expr.conformance.proto3.NestedTestAllTypes", + EMPTY_CONVERTER))); + } + + @Test + public void selectByFieldNumber_mapEntrySpecRepeatedMessageValue_mergesFragments() + throws Exception { + ByteString fragment1 = + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleInt64(100L)) + .build() + .toByteString(); + ByteString fragment2 = + NestedTestAllTypes.newBuilder() + .setPayload(TestAllTypes.newBuilder().setSingleBool(true)) + .build() + .toByteString(); + ByteString entry = + encode( + out -> { + out.writeInt64(1, 42L); + out.writeBytes(2, fragment1); + out.writeBytes(2, fragment2); + }); + ByteString wire = + encode(out -> out.writeBytes(TestAllTypes.MAP_INT64_NESTED_TYPE_FIELD_NUMBER, entry)); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Object result = raw.selectByFieldNumber(newMapInt64NestedTypeField()); + + assertThat(result) + .isEqualTo( + ImmutableMap.of( + 42L, + RawProtoMessageLiteValue.create( + fragment1.concat(fragment2), + "cel.expr.conformance.proto3.NestedTestAllTypes", + EMPTY_CONVERTER))); + } + + @Test + public void selectByFieldNumber_mapEntrySpecRepeatedScalarValue_lastOneWins() throws Exception { + ByteString entry = + encode( + out -> { + out.writeString(1, "k"); + out.writeString(2, "first"); + out.writeString(2, "last"); + }); + ByteString wire = + encode(out -> out.writeBytes(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, entry)); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.MapEntrySpec.create(9, 9)); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result).isEqualTo(ImmutableMap.of("k", "last")); + } + + @Test + public void selectByFieldNumber_mapEntrySpecDuplicateKeysAcrossEntries_lastEntryWins() + throws Exception { + ByteString entry1 = + encode( + out -> { + out.writeString(1, "dup"); + out.writeString(2, "first"); + }); + ByteString entry2 = + encode( + out -> { + out.writeString(1, "dup"); + out.writeString(2, "second"); + }); + ByteString wire = + encode( + out -> { + out.writeBytes(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, entry1); + out.writeBytes(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, entry2); + }); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.MapEntrySpec.create(9, 9)); + + Object result = raw.selectByFieldNumber(field); + + assertThat(result).isEqualTo(ImmutableMap.of("dup", "second")); + } + + @Test + public void selectByFieldNumber_mapEntrySpecUnexpectedFieldNumber_throwsIllegalStateException() + throws Exception { + ByteString entry = + encode( + out -> { + out.writeString(1, "k"); + out.writeString(2, "v"); + out.writeString(3, "unexpected"); + }); + ByteString wire = + encode(out -> out.writeBytes(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, entry)); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + wire, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.createMap( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.MapEntrySpec.create(9, 9)); + + IllegalStateException thrown = + assertThrows(IllegalStateException.class, () -> raw.selectByFieldNumber(field)); + + assertThat(thrown).hasMessageThat().contains("Unexpected field number in map entry: 3"); + } + + private static SelectField newMapInt64NestedTypeField() { + return SelectField.createMap( + TestAllTypes.MAP_INT64_NESTED_TYPE_FIELD_NUMBER, + "map_int64_nested_type", + SelectField.MapEntrySpec.create( + FieldLiteDescriptor.Type.INT64.getNumber(), SelectField.MESSAGE_TYPE_CODE), + "cel.expr.conformance.proto3.NestedTestAllTypes"); } private interface WireWriter { diff --git a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java index a64698e13..9e2fb4366 100644 --- a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java +++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java @@ -17,12 +17,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class SelectFieldTest { @Test @@ -36,6 +38,17 @@ public void create_twoArguments_success() { assertThat(field.protoTypeName()).isEmpty(); } + @Test + public void create_threeArguments_success() { + SelectField field = SelectField.create(2L, "bar", 11); + + assertThat(field.fieldNumber()).isEqualTo(2); + assertThat(field.fieldName()).isEqualTo("bar"); + assertThat(field.typeCode()).isEqualTo(11); + assertThat(field.defaultValue()).isNull(); + assertThat(field.protoTypeName()).isEmpty(); + } + @Test public void create_fourArguments_success() { SelectField field = SelectField.create(2L, "bar", 9, "default_str"); @@ -64,20 +77,23 @@ public void create_fiveArgNullProtoTypeName_throwsNullPointerException() { } @Test - public void create_mapTypeCode_success() { - SelectField field = SelectField.create(3L, "map_field", -1, null); - - assertThat(field.typeCode()).isEqualTo(-1); + public void create_twoArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null)); } @Test - public void create_twoArgNullFieldName_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> SelectField.create(1L, null)); + public void create_threeArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 11)); } @Test public void create_fourArgNullFieldName_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 9, null)); + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 9, "default")); + } + + @Test + public void create_fourArgNullDefaultValue_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, "foo", 9, null)); } @Test @@ -107,7 +123,7 @@ public void create_fieldNumberAboveMaximum_throwsIllegalArgumentException() { @Test public void create_typeCodeZero_throwsIllegalArgumentException() { IllegalArgumentException thrown = - assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0)); assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 0"); } @@ -115,7 +131,7 @@ public void create_typeCodeZero_throwsIllegalArgumentException() { @Test public void create_typeCodeAboveMaximum_throwsIllegalArgumentException() { IllegalArgumentException thrown = - assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19)); assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 19"); } @@ -123,7 +139,7 @@ public void create_typeCodeAboveMaximum_throwsIllegalArgumentException() { @Test public void create_typeCodeBelowSentinel_throwsIllegalArgumentException() { IllegalArgumentException thrown = - assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2)); assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: -2"); } @@ -131,17 +147,115 @@ public void create_typeCodeBelowSentinel_throwsIllegalArgumentException() { @Test public void create_typeCodeGroupProto_throwsIllegalArgumentException() { IllegalArgumentException thrown = - assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 10, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 10)); assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 10"); } + @Test + public void createMap_threeArguments_success() { + SelectField.MapEntrySpec mapEntrySpec = SelectField.MapEntrySpec.create(9, 9); + + SelectField field = SelectField.createMap(3L, "map_field", mapEntrySpec); + + assertThat(field.fieldNumber()).isEqualTo(3); + assertThat(field.fieldName()).isEqualTo("map_field"); + assertThat(field.typeCode()).isEqualTo(SelectField.CEL_MAP_TYPE_CODE); + assertThat(field.defaultValue()).isEqualTo(ImmutableMap.of()); + assertThat(field.protoTypeName()).isEmpty(); + assertThat(field.mapEntrySpec()).isEqualTo(mapEntrySpec); + } + + @Test + public void createMap_fourArguments_success() { + SelectField.MapEntrySpec mapEntrySpec = SelectField.MapEntrySpec.create(9, 11); + + SelectField field = + SelectField.createMap( + 3L, "map_field", mapEntrySpec, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(field.fieldNumber()).isEqualTo(3); + assertThat(field.fieldName()).isEqualTo("map_field"); + assertThat(field.typeCode()).isEqualTo(SelectField.CEL_MAP_TYPE_CODE); + assertThat(field.defaultValue()).isEqualTo(ImmutableMap.of()); + assertThat(field.protoTypeName()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(field.mapEntrySpec()).isEqualTo(mapEntrySpec); + } + + @Test + public void mapEntrySpec_create_success() { + SelectField.MapEntrySpec spec = SelectField.MapEntrySpec.create(9, 3); + + assertThat(spec.keyTypeCode()).isEqualTo(9); + assertThat(spec.valueTypeCode()).isEqualTo(3); + } + + @Test + public void mapEntrySpec_validKeyTypeCodes_success( + @TestParameter({ + "3", // INT64 + "4", // UINT64 + "5", // INT32 + "6", // FIXED64 + "7", // FIXED32 + "8", // BOOL + "9", // STRING + "13", // UINT32 + "15", // SFIXED32 + "16", // SFIXED64 + "17", // SINT32 + "18" // SINT64 + }) + long keyTypeCode) { + SelectField.MapEntrySpec spec = SelectField.MapEntrySpec.create(keyTypeCode, 3); + + assertThat(spec.keyTypeCode()).isEqualTo(keyTypeCode); + assertThat(spec.valueTypeCode()).isEqualTo(3); + } + + @Test + public void mapEntrySpec_invalidKeyTypeCode_throwsIllegalArgumentException( + @TestParameter({ + "-1", // CEL_MAP_TYPE_CODE + "0", // NO_TYPE_CODE + "1", // DOUBLE + "2", // FLOAT + "10", // GROUP + "11", // MESSAGE + "12", // BYTES + "14", // ENUM + "19" // OUT_OF_BOUNDS + }) + long keyTypeCode) { + assertThrows( + IllegalArgumentException.class, () -> SelectField.MapEntrySpec.create(keyTypeCode, 3)); + } + + @Test + public void mapEntrySpec_invalidValueTypeCode_throwsIllegalArgumentException( + @TestParameter({ + "-1", // CEL_MAP_TYPE_CODE + "0", // NO_TYPE_CODE + "10", // GROUP + "19" // OUT_OF_BOUNDS + }) + long valueTypeCode) { + assertThrows( + IllegalArgumentException.class, () -> SelectField.MapEntrySpec.create(9, valueTypeCode)); + } + @Test public void equalsAndHashCode_testedProperly() { + SelectField.MapEntrySpec spec1 = SelectField.MapEntrySpec.create(9, 3); + SelectField.MapEntrySpec spec2 = SelectField.MapEntrySpec.create(9, 11); + new EqualsTester() + .addEqualityGroup(spec1, SelectField.MapEntrySpec.create(9, 3)) + .addEqualityGroup(spec2, SelectField.MapEntrySpec.create(9, 11)) .addEqualityGroup(SelectField.create(1L, "foo"), SelectField.create(1L, "foo")) .addEqualityGroup(SelectField.create(2L, "foo"), SelectField.create(2L, "foo")) .addEqualityGroup(SelectField.create(1L, "bar"), SelectField.create(1L, "bar")) + .addEqualityGroup(SelectField.create(1L, "foo", 11), SelectField.create(1L, "foo", 11)) .addEqualityGroup( SelectField.create(1L, "foo", 9, "default"), SelectField.create(1L, "foo", 9, "default")) @@ -150,6 +264,38 @@ public void equalsAndHashCode_testedProperly() { SelectField.create(1L, "foo", 11, null, "google.protobuf.Duration"), SelectField.create(1L, "foo", 11, null, "google.protobuf.Duration")) .addEqualityGroup(SelectField.create(1L, "foo", 11, null, "google.protobuf.Timestamp")) + .addEqualityGroup( + SelectField.createMap(1L, "foo", spec1), SelectField.createMap(1L, "foo", spec1)) + .addEqualityGroup(SelectField.createMap(1L, "foo", spec2, "proto.Name")) + .addEqualityGroup(SelectField.createMap(1L, "foo", spec2, "proto.OtherName")) .testEquals(); } + + @Test + public void createMap_protoTypeNameInconsistentWithValueTypeCode_throws( + @TestParameter InconsistentMapValueTestCase testCase) { + SelectField.MapEntrySpec spec = SelectField.MapEntrySpec.create(9, testCase.valueTypeCode); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> SelectField.createMap(1L, "map_field", spec, testCase.protoTypeName)); + + assertThat(thrown) + .hasMessageThat() + .contains("Map value proto type name '" + testCase.protoTypeName + "' is inconsistent"); + } + + private enum InconsistentMapValueTestCase { + MESSAGE_VALUE_WITHOUT_TYPE_NAME(SelectField.MESSAGE_TYPE_CODE, ""), + SCALAR_VALUE_WITH_TYPE_NAME(9, "cel.expr.conformance.proto3.TestAllTypes"); + + private final long valueTypeCode; + private final String protoTypeName; + + InconsistentMapValueTestCase(long valueTypeCode, String protoTypeName) { + this.valueTypeCode = valueTypeCode; + this.protoTypeName = protoTypeName; + } + } }