From 90066d8461644143f60edb2d4e3b1074b58081f9 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Mon, 21 Sep 2026 16:29:44 -0700 Subject: [PATCH] Evaluate cel.@attribute and cel.@hasField in the planner runtime PiperOrigin-RevId: 985581963 --- .../java/dev/cel/common/values/BUILD.bazel | 88 +++ .../cel/common/values/CelValueConverter.java | 87 ++- .../cel/common/values/MutableMapValue.java | 16 +- .../values/OptimizedSelectTraversal.java | 116 +++ .../common/values/OptimizedSelectable.java | 45 ++ .../values/ProtoLiteCelValueConverter.java | 74 +- .../common/values/ProtoMessageLiteValue.java | 78 +- .../values/RawProtoMessageLiteValue.java | 294 +++++++- .../dev/cel/common/values/SelectField.java | 124 +++ .../java/dev/cel/common/values/BUILD.bazel | 6 + .../common/values/CelValueConverterTest.java | 185 +++++ .../values/OptimizedSelectTraversalTest.java | 328 ++++++++ .../ProtoLiteCelValueConverterTest.java | 121 ++- .../values/ProtoMessageLiteValueTest.java | 323 +++++++- .../values/RawProtoMessageLiteValueTest.java | 708 ++++++++++++++---- .../cel/common/values/SelectFieldTest.java | 133 ++++ common/values/BUILD.bazel | 36 + .../optimizer/optimizers/SelectOptimizer.java | 19 + .../dev/cel/optimizer/optimizers/BUILD.bazel | 1 + .../optimizers/SelectOptimizerTest.java | 280 ++++--- .../java/dev/cel/runtime/planner/BUILD.bazel | 38 + .../cel/runtime/planner/EvalAttribute.java | 4 + .../planner/EvalOptionalSelectField.java | 12 +- .../cel/runtime/planner/MaybeAttribute.java | 5 +- .../runtime/planner/NamespacedAttribute.java | 20 +- .../planner/OptimizedSelectPlanner.java | 491 ++++++++++++ .../planner/PresenceTestQualifier.java | 20 +- .../cel/runtime/planner/ProgramPlanner.java | 63 +- .../dev/cel/runtime/planner/Qualifier.java | 21 +- .../runtime/planner/RelativeAttribute.java | 16 +- .../cel/runtime/planner/StringQualifier.java | 32 +- .../java/dev/cel/runtime/planner/BUILD.bazel | 2 + .../runtime/planner/ProgramPlannerTest.java | 507 +++++++++++++ 33 files changed, 3918 insertions(+), 375 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java create mode 100644 common/src/main/java/dev/cel/common/values/OptimizedSelectable.java create mode 100644 common/src/main/java/dev/cel/common/values/SelectField.java create mode 100644 common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java create mode 100644 common/src/test/java/dev/cel/common/values/SelectFieldTest.java create mode 100644 runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java 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 c39eaaa73..895a3410b 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -167,6 +167,7 @@ java_library( ":preadapted_list", "//:auto_value", "//common/annotations", + "//common/exceptions:invalid_argument", "//common/types", "//common/types:type_providers", "@maven//:com_google_errorprone_error_prone_annotations", @@ -218,6 +219,7 @@ cel_android_library( ":preadapted_list_android", "//:auto_value", "//common/annotations", + "//common/exceptions:invalid_argument", "//common/types:type_providers_android", "//common/types:types_android", "@maven//:com_google_errorprone_error_prone_annotations", @@ -323,6 +325,8 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":optimized_selectable", + ":select_field", ":values", "//:auto_value", "//common/annotations", @@ -351,6 +355,8 @@ cel_android_library( ], deps = [ ":base_proto_cel_value_converter_android", + ":optimized_selectable_android", + ":select_field_android", ":values_android", "//:auto_value", "//common/annotations", @@ -434,3 +440,85 @@ cel_android_library( "@maven//:com_google_errorprone_error_prone_annotations", ], ) + +java_library( + name = "select_field", + srcs = ["SelectField.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "select_field_android", + srcs = ["SelectField.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "optimized_selectable", + srcs = ["OptimizedSelectable.java"], + tags = [ + ], + deps = [ + ":select_field", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "optimized_selectable_android", + srcs = ["OptimizedSelectable.java"], + tags = [ + ], + deps = [ + ":select_field_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "optimized_select_traversal", + srcs = ["OptimizedSelectTraversal.java"], + tags = [ + ], + deps = [ + ":optimized_selectable", + ":select_field", + ":values", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "optimized_select_traversal_android", + srcs = ["OptimizedSelectTraversal.java"], + tags = [ + ], + deps = [ + ":optimized_selectable_android", + ":select_field_android", + ":values_android", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "@maven_android//:com_google_guava_guava", + ], +) diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 20deef1d3..3e00be2e6 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -17,8 +17,10 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelInvalidArgumentException; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -26,6 +28,7 @@ import java.util.Optional; import java.util.RandomAccess; import java.util.function.Function; +import org.jspecify.annotations.Nullable; /** * {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link @@ -74,7 +77,7 @@ protected Object mapContainer(Object value, Function mapper) { if (value instanceof List && value instanceof RandomAccess) { List list = (List) value; for (int i = 0; i < list.size(); i++) { - Object element = list.get(i); + Object element = checkListElement(list.get(i), i); Object mapped = mapper.apply(element); if (mapped != element) { @@ -85,7 +88,7 @@ protected Object mapContainer(Object value, Function mapper) { } builder.add(mapped); for (int j = i + 1; j < list.size(); j++) { - builder.add(mapper.apply(list.get(j))); + builder.add(mapper.apply(checkListElement(list.get(j), j))); } return builder.build(); } @@ -100,8 +103,9 @@ protected Object mapContainer(Object value, Function mapper) { Collection collection = (Collection) value; ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(collection.size()); + int index = 0; for (Object element : collection) { - builder.add(mapper.apply(element)); + builder.add(mapper.apply(checkListElement(element, index++))); } return builder.build(); } @@ -112,6 +116,7 @@ protected Object mapContainer(Object value, Function mapper) { while (iterator.hasNext()) { Map.Entry entry = iterator.next(); + checkMapEntry(entry); Object mappedKey = mapper.apply(entry.getKey()); Object mappedValue = mapper.apply(entry.getValue()); @@ -128,6 +133,7 @@ protected Object mapContainer(Object value, Function mapper) { builder.put(mappedKey, mappedValue); while (iterator.hasNext()) { Map.Entry nextEntry = iterator.next(); + checkMapEntry(nextEntry); builder.put(mapper.apply(nextEntry.getKey()), mapper.apply(nextEntry.getValue())); } return builder.buildOrThrow(); @@ -162,6 +168,59 @@ public Object toRuntimeValue(Object value) { return normalizePrimitive(value); } + /** + * Adapts {@code value} for an intermediate field selection hop. + * + *

{@link Map} instances are returned as-is to avoid O(N) whole-map normalization per hop; the + * accessed entry is validated on lookup via {@link #findMapValue} or {@link #containsMapKey}. + * Callers materializing a final evaluation result must use {@link #toRuntimeValue} instead. + */ + public final Object toTraversalTarget(Object value) { + if (value instanceof Map) { + return value; + } + + return toRuntimeValue(value); + } + + /** + * Returns the unadapted value bound to {@code key} in {@code map}, or {@link Optional#empty()} if + * absent. + * + * @throws CelInvalidArgumentException if {@code key} is bound to {@code null}. + */ + public static Optional findMapValue(Map map, Object key) { + Object value = map.get(key); + if (value != null) { + return Optional.of(value); + } + + if (map.containsKey(key)) { + throw new CelInvalidArgumentException( + String.format("Map value cannot be null for key: %s", key)); + } + + return Optional.empty(); + } + + /** + * Returns whether {@code key} is present in {@code map}. + * + * @throws CelInvalidArgumentException if {@code key} is bound to {@code null}. + */ + public static boolean containsMapKey(Map map, Object key) { + if (map.get(key) != null) { + return true; + } + + if (map.containsKey(key)) { + throw new CelInvalidArgumentException( + String.format("Map value cannot be null for key: %s", key)); + } + + return false; + } + protected Object normalizePrimitive(Object value) { Preconditions.checkNotNull(value); @@ -196,6 +255,28 @@ private Object unwrap(CelValue celValue) { return celValue.value(); } + private static void checkMapEntry(Map.Entry entry) { + Object key = entry.getKey(); + if (key == null) { + throw new CelInvalidArgumentException("Map key cannot be null."); + } + + if (entry.getValue() == null) { + throw new CelInvalidArgumentException( + String.format("Map value cannot be null for key: %s", key)); + } + } + + @CanIgnoreReturnValue + private static Object checkListElement(@Nullable Object element, int index) { + if (element == null) { + throw new CelInvalidArgumentException( + String.format("List element cannot be null at index: %d", index)); + } + + return element; + } + protected CelValueConverter() { this.maybeUnwrapFunction = this::maybeUnwrap; this.toRuntimeValueFunction = this::toRuntimeValue; diff --git a/common/src/main/java/dev/cel/common/values/MutableMapValue.java b/common/src/main/java/dev/cel/common/values/MutableMapValue.java index 706436b2e..4f6cfa882 100644 --- a/common/src/main/java/dev/cel/common/values/MutableMapValue.java +++ b/common/src/main/java/dev/cel/common/values/MutableMapValue.java @@ -105,23 +105,13 @@ public Set> entrySet() { @Override public Object select(Object field) { - Object val = internalMap.get(field); - if (val != null) { - return val; - } - if (!internalMap.containsKey(field)) { - throw CelAttributeNotFoundException.forMissingMapKey(field.toString()); - } - throw CelAttributeNotFoundException.of( - String.format("Map value cannot be null for key: %s", field)); + return CelValueConverter.findMapValue(internalMap, field) + .orElseThrow(() -> CelAttributeNotFoundException.forMissingMapKey(field.toString())); } @Override public Optional find(Object field) { - if (internalMap.containsKey(field)) { - return Optional.ofNullable(internalMap.get(field)); - } - return Optional.empty(); + return CelValueConverter.findMapValue(internalMap, field); } @Override diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java new file mode 100644 index 000000000..2c6b2bc08 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java @@ -0,0 +1,116 @@ +// 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. + +package dev.cel.common.values; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import java.util.Optional; + +/** + * Walks a sequence of {@link SelectField} selections, dispatching each field over {@link + * OptimizedSelectable} or {@link SelectableValue}. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class OptimizedSelectTraversal { + + /** + * Qualifies {@code target} through every field in {@code fields} and returns the terminal value. + */ + public static Object qualify(Object target, ImmutableList fields) { + Object current = target; + for (int i = 0; i < fields.size(); i++) { + current = qualifyField(current, fields.get(i)); + } + return current; + } + + /** + * Presence tests the terminal field of {@code fields}, navigating through all preceding fields. + * + *

Absence of any intermediate field short-circuits to {@code false}. + */ + public static boolean hasField(Object target, ImmutableList fields) { + if (fields.isEmpty()) { + return false; + } + Object current = target; + int terminalIndex = fields.size() - 1; + for (int i = 0; i < terminalIndex; i++) { + Optional next = navigateField(current, fields.get(i)); + if (!next.isPresent()) { + return false; + } + current = next.get(); + } + return hasTerminalField(current, fields.get(terminalIndex)); + } + + // SelectableValue is only ever instantiated with String keys in the select path. + @SuppressWarnings("unchecked") + private static Object qualifyField(Object target, SelectField field) { + if (target instanceof ErrorValue) { + return target; + } + if (target instanceof OptimizedSelectable) { + return ((OptimizedSelectable) target).selectByFieldNumber(field); + } + if (target instanceof SelectableValue) { + SelectableValue selectable = (SelectableValue) target; + if (field.defaultValue() != null) { + return selectable + .find(field.fieldName()) + .map(Object.class::cast) + .orElse(field.defaultValue()); + } + return selectable.select(field.fieldName()); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + // SelectableValue is only ever instantiated with String keys in the select path. + @SuppressWarnings("unchecked") + private static Optional navigateField(Object target, SelectField field) { + if (target instanceof ErrorValue) { + return Optional.of(target); + } + if (target instanceof OptimizedSelectable) { + return ((OptimizedSelectable) target).findByFieldNumber(field); + } + if (target instanceof SelectableValue) { + return ((SelectableValue) target).find(field.fieldName()).map(Object.class::cast); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + // SelectableValue is only ever instantiated with String keys in the select path. + @SuppressWarnings("unchecked") + private static boolean hasTerminalField(Object target, SelectField field) { + if (target instanceof ErrorValue) { + return false; + } + if (target instanceof OptimizedSelectable) { + return ((OptimizedSelectable) target).hasFieldByNumber(field); + } + if (target instanceof SelectableValue) { + return ((SelectableValue) target).find(field.fieldName()).isPresent(); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + private OptimizedSelectTraversal() {} +} diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java new file mode 100644 index 000000000..828d15227 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java @@ -0,0 +1,45 @@ +// 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. + +package dev.cel.common.values; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import java.util.Optional; + +/** + * Resolves an optimized field selection within a selection chain rewritten by the select optimizer. + * + *

Implementations resolve individual field selections against themselves by protobuf field + * number. Walking the chain across multiple fields and heterogeneous values belongs to {@link + * OptimizedSelectTraversal}. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public interface OptimizedSelectable { + + /** Selects {@code field}, falling back to its default value or an empty submessage if absent. */ + Object selectByFieldNumber(SelectField field); + + /** Returns whether {@code field} is present. */ + boolean hasFieldByNumber(SelectField field); + + /** + * Returns the value of the field at {@code field} (a scalar or submessage) for an intermediate + * step of a presence test, or empty if absent. + */ + Optional findByFieldNumber(SelectField field); +} 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 093819198..0d53903a5 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Defaults; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -26,6 +25,7 @@ import com.google.common.collect.Multimaps; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.MessageLite; @@ -62,6 +62,9 @@ @Immutable @Internal public final class ProtoLiteCelValueConverter extends BaseProtoCelValueConverter { + static final String MAP_KEY_FIELD_NAME = "key"; + static final String MAP_VALUE_FIELD_NAME = "value"; + private final CelLiteDescriptorPool descriptorPool; public static ProtoLiteCelValueConverter newInstance( @@ -69,6 +72,10 @@ public static ProtoLiteCelValueConverter newInstance( return new ProtoLiteCelValueConverter(celLiteDescriptorPool); } + boolean hasDescriptor(String protoTypeName) { + return descriptorPool.findDescriptor(protoTypeName).isPresent(); + } + private static Object readPrimitiveField( CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { switch (fieldDescriptor.getProtoFieldType()) { @@ -155,22 +162,45 @@ private MessageLite.Builder getDefaultMessageBuilder(String protoTypeName) { Object getDefaultCelValue(String protoTypeName, String fieldName) { MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); - FieldLiteDescriptor fieldDescriptor = messageDescriptor.getByFieldNameOrThrow(fieldName); - - Object defaultValue = getDefaultValue(fieldDescriptor); + return getDefaultCelValue(messageDescriptor.getByFieldNameOrThrow(fieldName)); + } - return toRuntimeValue(defaultValue); + Object getDefaultCelValue(FieldLiteDescriptor fieldDescriptor) { + return toRuntimeValue(getDefaultValue(fieldDescriptor)); } - public Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { + Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { return descriptorPool .findDescriptor(protoTypeName) .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); } - public Optional findDefaultCelValue(String protoTypeName, int fieldNumber) { - return findFieldDescriptor(protoTypeName, fieldNumber) - .map(fieldDescriptor -> toRuntimeValue(getDefaultValue(fieldDescriptor))); + Optional tryDecodeWellKnownProto(ByteString bytes, String protoTypeName) { + Optional wellKnownProto = WellKnownProto.getByTypeName(protoTypeName); + if (!wellKnownProto.isPresent()) { + return Optional.empty(); + } + + return descriptorPool + .findDescriptor(protoTypeName) + .map( + descriptor -> + decodeWellKnownProto(bytes, protoTypeName, descriptor, wellKnownProto.get())); + } + + private Object decodeWellKnownProto( + ByteString bytes, + String protoTypeName, + MessageLiteDescriptor descriptor, + WellKnownProto wellKnownProto) { + try { + MessageLite.Builder builder = descriptor.newMessageBuilder(); + builder.mergeFrom(bytes, ExtensionRegistryLite.getEmptyRegistry()); + return fromWellKnownProto(builder.build(), wellKnownProto); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to decode well-known proto of type: " + protoTypeName, e); + } } @Override @@ -276,16 +306,21 @@ private ImmutableList readPackedRepeatedFields( private Map.Entry readSingleMapEntry( CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + String entryTypeName = fieldDescriptor.getFieldProtoTypeName(); ImmutableMap singleMapEntry = - readAllFields(inputStream.readByteArray(), fieldDescriptor.getFieldProtoTypeName()) - .values(); - Object key = checkNotNull(singleMapEntry.get("key")); - Object value = checkNotNull(singleMapEntry.get("value")); + readAllFields(inputStream.readByteArray(), entryTypeName).values(); + Object key = singleMapEntry.get(MAP_KEY_FIELD_NAME); + if (key == null) { + key = getDefaultCelValue(entryTypeName, MAP_KEY_FIELD_NAME); + } + Object value = singleMapEntry.get(MAP_VALUE_FIELD_NAME); + if (value == null) { + value = getDefaultCelValue(entryTypeName, MAP_VALUE_FIELD_NAME); + } return new AbstractMap.SimpleEntry<>(key, value); } - @VisibleForTesting MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOException { MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); CodedInputStream inputStream = CodedInputStream.newInstance(bytes); @@ -360,19 +395,16 @@ MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOExcepti if (fieldDescriptor.getEncodingType().equals(EncodingType.LIST)) { String fieldName = fieldDescriptor.getFieldName(); List repeatedValues = - repeatedFieldValues.computeIfAbsent( - fieldNumber, - (unused) -> { - List newList = new ArrayList<>(); - fieldValues.put(fieldName, newList); - return newList; - }); + repeatedFieldValues.computeIfAbsent(fieldNumber, (unused) -> new ArrayList<>()); if (payload instanceof Collection) { repeatedValues.addAll((Collection) payload); } else { repeatedValues.add(payload); } + if (!repeatedValues.isEmpty()) { + fieldValues.put(fieldName, repeatedValues); + } } else { fieldValues.put(fieldDescriptor.getFieldName(), payload); } diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java index 99e95ebd3..fffd35794 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -14,19 +14,21 @@ package dev.cel.common.values; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.MessageLite; -import dev.cel.common.annotations.Internal; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import java.io.IOException; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * ProtoMessageLiteValue is a struct value with protobuf support for {@link MessageLite}. @@ -35,10 +37,23 @@ * *

If the codebase has access to full protobuf messages with descriptors, use {@code * ProtoMessageValue} instead. + * + *

Implements {@link OptimizedSelectable} so that select chains can address fields by number: + * + *

    + *
  • Field renames: If a protobuf field is renamed in schema after an AST was compiled, + * resolving by {@link SelectField#fieldNumber()} maps the number to the runtime descriptor's + * current field name, preventing {@code CelAttributeNotFoundException}. + *
  • Version skew / unknown fields: When evaluating payloads serialized by a newer binary + * containing fields absent from the local {@code CelLiteDescriptor}, the unknown wire bytes + * are preserved in {@link #unknownFields()} and decoded on demand using the compile-time wire + * type and default metadata in {@link SelectField}. + *
*/ @AutoValue @Immutable -public abstract class ProtoMessageLiteValue extends StructValue { +public abstract class ProtoMessageLiteValue extends StructValue + implements OptimizedSelectable { @Override public abstract MessageLite value(); @@ -57,12 +72,11 @@ MessageFields messageFields() { } } - @Internal - public ImmutableMap fieldValues() { + ImmutableMap fieldValues() { return messageFields().values(); } - public ImmutableListMultimap unknownFields() { + ImmutableListMultimap unknownFields() { return messageFields().unknowns(); } @@ -84,11 +98,59 @@ public Optional find(String field) { .map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue)); } + @Override + public Object selectByFieldNumber(SelectField field) { + FieldLiteDescriptor fd = findFieldDescriptor(field); + Object known = findKnownFieldValue(fd); + if (known != null) { + return protoLiteCelValueConverter().toRuntimeValue(known); + } + return RawProtoMessageLiteValue.selectWireOrDefault( + field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter()); + } + + @Override + public boolean hasFieldByNumber(SelectField field) { + FieldLiteDescriptor fd = findFieldDescriptor(field); + if (findKnownFieldValue(fd) != null) { + return true; + } + return RawProtoMessageLiteValue.isPresentInWire( + field, fd, unknownFields().get(field.fieldNumber())); + } + + @Override + public Optional findByFieldNumber(SelectField field) { + FieldLiteDescriptor fd = findFieldDescriptor(field); + Object known = findKnownFieldValue(fd); + if (known != null) { + return Optional.of(protoLiteCelValueConverter().toRuntimeValue(known)); + } + return RawProtoMessageLiteValue.navigateWire( + field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter()); + } + + private @Nullable FieldLiteDescriptor findFieldDescriptor(SelectField field) { + return protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), field.fieldNumber()) + .orElse(null); + } + + private @Nullable Object findKnownFieldValue(@Nullable FieldLiteDescriptor fieldDescriptor) { + if (fieldDescriptor == null) { + return null; + } + return fieldValues().get(fieldDescriptor.getFieldName()); + } + public static ProtoMessageLiteValue create( MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) { - Preconditions.checkNotNull(value); - Preconditions.checkNotNull(typeName); + checkNotNull(value); + checkNotNull(typeName); + checkNotNull(protoLiteCelValueConverter); return new AutoValue_ProtoMessageLiteValue( value, StructTypeReference.create(typeName), protoLiteCelValueConverter); } + + ProtoMessageLiteValue() {} } diff --git a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java index cd8990be9..2a3bdf940 100644 --- a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -15,12 +15,15 @@ package dev.cel.common.values; import static com.google.common.base.Preconditions.checkNotNull; +import static dev.cel.common.values.ProtoLiteCelValueConverter.MAP_KEY_FIELD_NAME; +import static dev.cel.common.values.ProtoLiteCelValueConverter.MAP_VALUE_FIELD_NAME; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; 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; @@ -28,15 +31,20 @@ import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; -import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.internal.WellKnownProto; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import org.jspecify.annotations.Nullable; @@ -55,21 +63,25 @@ @Immutable @SuppressWarnings("Immutable") // Immutable wire fields @Internal -public abstract class RawProtoMessageLiteValue - extends StructValue { +public abstract class RawProtoMessageLiteValue extends StructValue + implements OptimizedSelectable { + + private static final String UNKNOWN_MESSAGE_TYPE_NAME = "cel.@unknownMessage"; abstract ByteString rawWireBytes(); + @Override + public abstract CelType celType(); + + abstract ProtoLiteCelValueConverter protoLiteCelValueConverter(); + @Override public RawProtoMessageLiteValue value() { return this; } - @Override - public abstract CelType celType(); - @Memoized - public ImmutableListMultimap unknownFields() { + ImmutableListMultimap unknownFields() { try { CodedInputStream inputStream = rawWireBytes().newCodedInput(); Multimap fields = Multimaps.newMultimap(new TreeMap<>(), ArrayList::new); @@ -85,10 +97,6 @@ public ImmutableListMultimap unknownFields() { } } - public boolean hasField(int fieldNumber) { - return unknownFields().containsKey(fieldNumber); - } - @Override public boolean isZeroValue() { return rawWireBytes().isEmpty(); @@ -97,25 +105,232 @@ public boolean isZeroValue() { /** * Direct field selection by name is unsupported on {@link RawProtoMessageLiteValue} because raw * wire bytes lack message descriptors, and field names are not preserved on the protobuf wire. - * - *

Field traversal on classless messages must be performed via optimized attribute steps - * ({@code cel.@attribute} and {@code cel.@hasField}), where the AST optimizer supplies the - * pre-resolved protobuf field numbers. - * - * @throws CelAttributeNotFoundException always, indicating the field cannot be resolved by name. */ @Override public Object select(String field) { throw CelAttributeNotFoundException.forFieldResolution(field); } + /** + * Direct field presence testing by name is unsupported on {@link RawProtoMessageLiteValue} + * because raw wire bytes lack message descriptors and field names. + */ @Override public Optional find(String field) { - return Optional.empty(); + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Object selectByFieldNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return selectWireOrDefault( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public boolean hasFieldByNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return isPresentInWire(field, fieldDescriptor, unknownFields().get(fieldNumber)); + } + + @Override + public Optional findByFieldNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return navigateWire( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + /** + * Decodes a field value from preserved wire bytes, falling back to schema or default values. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static Object selectWireOrDefault( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (unknowns.isEmpty()) { + return resolveDefault(field, fieldDescriptor, converter); + } + return decodeWireField(field, fieldDescriptor, unknowns, converter); + } + + private static Object decodeWireField( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (fieldDescriptor != null && fieldDescriptor.getEncodingType() == EncodingType.MAP) { + return decodeMapEntries(unknowns, fieldDescriptor, converter); + } + + int typeCode = + fieldDescriptor != null + ? fieldDescriptor.getProtoFieldType().getNumber() + : field.typeCode(); + if (typeCode == SelectField.CEL_MAP_TYPE_CODE) { + throw new UnsupportedOperationException( + "Decoding unknown map field from wire bytes is unsupported: " + field.fieldName()); + } + if (typeCode == SelectField.NO_TYPE_CODE) { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + boolean isRepeated = + fieldDescriptor != null + ? fieldDescriptor.getEncodingType() == EncodingType.LIST + : field.defaultValue() instanceof List; + String protoTypeName = + fieldDescriptor != null + ? fieldDescriptor.getFieldProtoTypeName() + : UNKNOWN_MESSAGE_TYPE_NAME; + + return decodeWireEntries(unknowns, typeCode, protoTypeName, isRepeated, converter); + } + + private static Object resolveDefault( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ProtoLiteCelValueConverter converter) { + if (field.defaultValue() != null) { + return field.defaultValue(); + } + + if (fieldDescriptor == null) { + if (field.typeCode() == FieldLiteDescriptor.Type.MESSAGE.getNumber()) { + return create(ByteString.EMPTY, UNKNOWN_MESSAGE_TYPE_NAME, converter); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + String protoTypeName = fieldDescriptor.getFieldProtoTypeName(); + if (fieldDescriptor.getEncodingType() == EncodingType.SINGULAR + && fieldDescriptor.getJavaType() == JavaType.MESSAGE + && !WellKnownProto.isWrapperType(protoTypeName) + && !converter.hasDescriptor(protoTypeName)) { + return create(ByteString.EMPTY, protoTypeName, converter); + } + + return converter.getDefaultCelValue(fieldDescriptor); + } + + /** + * Returns whether a field has presence in preserved wire bytes. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static boolean isPresentInWire( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns) { + if (unknowns.isEmpty()) { + return false; + } + + boolean isRepeated = + fieldDescriptor != null + ? fieldDescriptor.getEncodingType() == EncodingType.LIST + : field.defaultValue() instanceof List; + int typeCode = + fieldDescriptor != null + ? fieldDescriptor.getProtoFieldType().getNumber() + : field.typeCode(); + + if (!isRepeated) { + return true; + } + + boolean isPackable = + typeCode != FieldLiteDescriptor.Type.STRING.getNumber() + && typeCode != FieldLiteDescriptor.Type.BYTES.getNumber() + && typeCode != FieldLiteDescriptor.Type.MESSAGE.getNumber() + && typeCode != FieldLiteDescriptor.Type.GROUP.getNumber(); + if (!isPackable) { + return true; + } + + for (Object raw : unknowns) { + if (!(raw instanceof ByteString) || !((ByteString) raw).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Navigates a field on preserved wire bytes, returning empty if absent. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static Optional navigateWire( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (!isPresentInWire(field, fieldDescriptor, unknowns)) { + return Optional.empty(); + } + if (fieldDescriptor != null || field.typeCode() != SelectField.NO_TYPE_CODE) { + return Optional.of(selectWireOrDefault(field, fieldDescriptor, unknowns, converter)); + } + Object lastEntry = unknowns.get(unknowns.size() - 1); + if (lastEntry instanceof ByteString) { + return Optional.of( + decodeWireEntries( + unknowns, + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + UNKNOWN_MESSAGE_TYPE_NAME, + /* isRepeated= */ false, + converter)); + } + return Optional.of(lastEntry); + } + + private static ImmutableMap decodeMapEntries( + ImmutableList unknowns, + FieldLiteDescriptor mapFieldDescriptor, + ProtoLiteCelValueConverter converter) { + String entryTypeName = mapFieldDescriptor.getFieldProtoTypeName(); + Object defaultKey = converter.getDefaultCelValue(entryTypeName, MAP_KEY_FIELD_NAME); + Object defaultValue = converter.getDefaultCelValue(entryTypeName, MAP_VALUE_FIELD_NAME); + Map resultMap = new LinkedHashMap<>(); + for (Object raw : unknowns) { + ByteString bytes = requireType(raw, ByteString.class, WireFormat.FieldType.MESSAGE); + try { + ImmutableMap entryFields = + converter.readAllFields(bytes.toByteArray(), entryTypeName).values(); + Object key = entryFields.get(MAP_KEY_FIELD_NAME); + key = (key == null) ? defaultKey : converter.toRuntimeValue(key); + Object value = entryFields.get(MAP_VALUE_FIELD_NAME); + value = (value == null) ? defaultValue : converter.toRuntimeValue(value); + resultMap.put(key, value); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to decode map entry for field: " + mapFieldDescriptor.getFieldName(), e); + } + } + return ImmutableMap.copyOf(resultMap); } - public static @Nullable Object decodeWireEntries( - ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + static @Nullable Object decodeWireEntries( + ImmutableCollection entries, + int typeCode, + String protoTypeName, + boolean isRepeated, + ProtoLiteCelValueConverter converter) { WireFormat.FieldType fieldType = FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(); if (fieldType == WireFormat.FieldType.GROUP) { @@ -130,7 +345,7 @@ public Optional find(String field) { if (fieldType.isPackable() && (raw instanceof ByteString)) { listBuilder.addAll(decodePacked((ByteString) raw, fieldType)); } else { - listBuilder.add(decodeWireValue(raw, fieldType, protoTypeName)); + listBuilder.add(decodeWireValue(raw, fieldType, protoTypeName, converter)); } } return listBuilder.build(); @@ -140,18 +355,26 @@ public Optional find(String field) { for (Object item : entries) { mergedBytes = mergedBytes.concat(requireType(item, ByteString.class, fieldType)); } - return decodeWireValue(mergedBytes, fieldType, protoTypeName); + return decodeWireValue(mergedBytes, fieldType, protoTypeName, converter); } // Protobuf "last one wins" semantics for non-repeated scalar fields - return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName); + return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName, converter); } - static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + static Object decodeWireValue( + Object raw, int typeCode, String protoTypeName, ProtoLiteCelValueConverter converter) { return decodeWireValue( - raw, FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), protoTypeName); + raw, + FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), + protoTypeName, + converter); } - static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + static Object decodeWireValue( + Object raw, + WireFormat.FieldType fieldType, + String protoTypeName, + ProtoLiteCelValueConverter converter) { switch (fieldType) { case DOUBLE: return Double.longBitsToDouble(requireType(raw, Long.class, fieldType)); @@ -180,8 +403,10 @@ static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String case GROUP: throw new UnsupportedOperationException("Groups are not supported"); case MESSAGE: - return RawProtoMessageLiteValue.create( - requireType(raw, ByteString.class, fieldType), protoTypeName); + ByteString msgBytes = requireType(raw, ByteString.class, fieldType); + return converter + .tryDecodeWellKnownProto(msgBytes, protoTypeName) + .orElseGet(() -> create(msgBytes, protoTypeName, converter)); case BYTES: return CelByteString.of(requireType(raw, ByteString.class, fieldType).toByteArray()); case UINT32: @@ -269,15 +494,20 @@ private static ImmutableList decodePacked( } } - public static RawProtoMessageLiteValue create(ByteString rawWireBytes) { - return create(rawWireBytes, ""); + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, ProtoLiteCelValueConverter protoLiteCelValueConverter) { + return create(rawWireBytes, "", protoLiteCelValueConverter); } - public static RawProtoMessageLiteValue create(ByteString rawWireBytes, String protoTypeName) { + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, + String protoTypeName, + ProtoLiteCelValueConverter protoLiteCelValueConverter) { checkNotNull(rawWireBytes); checkNotNull(protoTypeName); + checkNotNull(protoLiteCelValueConverter); return new AutoValue_RawProtoMessageLiteValue( - rawWireBytes, StructTypeReference.create(protoTypeName)); + rawWireBytes, StructTypeReference.create(protoTypeName), protoLiteCelValueConverter); } RawProtoMessageLiteValue() {} diff --git a/common/src/main/java/dev/cel/common/values/SelectField.java b/common/src/main/java/dev/cel/common/values/SelectField.java new file mode 100644 index 000000000..8bee5a0d7 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/SelectField.java @@ -0,0 +1,124 @@ +// 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. + +package dev.cel.common.values; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import org.jspecify.annotations.Nullable; + +/** + * Represents a single field selection in an optimized selection chain. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +@SuppressWarnings("Immutable") // Default value is an immutable CEL literal or null +public abstract class SelectField { + + private static final int MAX_FIELD_NUMBER = 536870911; + + /** CEL-specific type code used to encode CEL maps on the wire. */ + 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; + + /** Protobuf field type code for {@code TYPE_MESSAGE} ({@code FieldDescriptorProto.Type}). */ + public static final int MESSAGE_TYPE_CODE = 11; + + // Mirrors FieldDescriptorProto.Type. Not validated against a protobuf enum because the :values + // target is deliberately protobuf-free; keep in sync with CelLiteDescriptor.FieldLiteDescriptor. + private static final int MIN_PROTO_TYPE_CODE = 1; // TYPE_DOUBLE + private static final int MAX_PROTO_TYPE_CODE = 18; // TYPE_SINT64 + private static final int GROUP_PROTO_TYPE_CODE = 10; // Unsupported by CEL. + + /** Protobuf field number of this hop. */ + public 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 + * #NO_TYPE_CODE}. + */ + public 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(); + + /** + * Creates a presence-test qualifier hop. + * + * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's int64 + * constant representations. + * @param fieldName Protobuf field name. + */ + public static SelectField create(long fieldNumber, String fieldName) { + checkArgument( + fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER, + "Field number out of protobuf range: %s", + fieldNumber); + checkNotNull(fieldName); + return new AutoValue_SelectField( + (int) fieldNumber, fieldName, NO_TYPE_CODE, /* defaultValue= */ null); + } + + /** + * Creates a fully-specified field selection hop with type code and optional default value. + * + * @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. + */ + public static SelectField create( + long fieldNumber, String fieldName, long typeCode, @Nullable Object defaultValue) { + 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 new AutoValue_SelectField((int) fieldNumber, fieldName, (int) typeCode, defaultValue); + } + + /** + * Returns whether {@code typeCode} is a protobuf field type code CEL supports, or the {@link + * #CEL_MAP_TYPE_CODE} sentinel. + */ + public static boolean isSupportedTypeCode(long typeCode) { + if (typeCode == CEL_MAP_TYPE_CODE) { + return true; + } + return typeCode >= MIN_PROTO_TYPE_CODE + && typeCode <= MAX_PROTO_TYPE_CODE + && typeCode != GROUP_PROTO_TYPE_CODE; + } + + SelectField() {} +} diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index baa33ebc3..a6947b979 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -14,14 +14,17 @@ java_library( "//bundle:cel", "//common:cel_ast", "//common:cel_descriptor_util", + "//common:error_codes", "//common:options", "//common/exceptions:attribute_not_found", + "//common/exceptions:invalid_argument", "//common/internal:cel_descriptor_pools", "//common/internal:cel_lite_descriptor_pool", "//common/internal:default_lite_descriptor_pool", "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/internal:proto_time_utils", "//common/types", "//common/types:type_providers", "//common/values", @@ -29,10 +32,13 @@ java_library( "//common/values:cel_value_provider", "//common/values:combined_cel_value_converter", "//common/values:combined_cel_value_provider", + "//common/values:optimized_select_traversal", + "//common/values:optimized_selectable", "//common/values:proto_message_lite_value", "//common/values:proto_message_lite_value_provider", "//common/values:proto_message_value", "//common/values:proto_message_value_provider", + "//common/values:select_field", "//protobuf:cel_lite_descriptor", "//testing/protos:test_all_types_cel_java_proto3", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", diff --git a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java index ccb8e605f..75d9182b3 100644 --- a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java @@ -15,7 +15,16 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import dev.cel.common.CelErrorCode; +import dev.cel.common.exceptions.CelInvalidArgumentException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,4 +59,180 @@ public void unwrap_emptyOptionalValue() { assertThat(result).isEqualTo(Optional.empty()); } + + @Test + public void toRuntimeValue_mapWithNullValue_throws() { + Map map = new HashMap<>(); + map.put("key", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.toRuntimeValue(map)); + + assertThat(e).hasMessageThat().isEqualTo("Map value cannot be null for key: key"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT); + } + + @Test + public void toRuntimeValue_mapWithNullKey_throws() { + Map map = new HashMap<>(); + map.put(null, "value"); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.toRuntimeValue(map)); + + assertThat(e).hasMessageThat().isEqualTo("Map key cannot be null."); + } + + @Test + public void toRuntimeValue_mapWithNullValueAfterAdaptedEntry_throws() { + // The first entry normalizes (Integer -> Long), which diverts mapContainer onto its rebuild + // path. The illegal entry is only reached by the tail loop. + Map map = new LinkedHashMap<>(); + map.put("adapted", 1); + map.put("illegal", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.toRuntimeValue(map)); + + assertThat(e).hasMessageThat().isEqualTo("Map value cannot be null for key: illegal"); + } + + @Test + public void maybeUnwrap_mapWithNullValue_throws() { + Map map = new HashMap<>(); + map.put("key", null); + + CelInvalidArgumentException e = + assertThrows(CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.maybeUnwrap(map)); + + assertThat(e).hasMessageThat().isEqualTo("Map value cannot be null for key: key"); + } + + @Test + public void toTraversalTarget_map_returnsSameInstanceWithoutInspectingEntries() { + Map map = new HashMap<>(); + map.put("illegal", null); + + Object result = CEL_VALUE_CONVERTER.toTraversalTarget(map); + + assertThat(result).isSameInstanceAs(map); + } + + @Test + public void toTraversalTarget_nonMap_normalizes() { + Object result = CEL_VALUE_CONVERTER.toTraversalTarget(1); + + assertThat(result).isEqualTo(1L); + } + + @Test + public void findMapValue_boundKey_returnsValueAsStored() { + Map map = new HashMap<>(); + map.put("key", 1); + + Optional result = CelValueConverter.findMapValue(map, "key"); + + // Unadapted: the caller decides whether this hop materializes or merely traverses. + assertThat(result).hasValue(1); + } + + @Test + public void findMapValue_absentKey_returnsEmpty() { + Optional result = CelValueConverter.findMapValue(new HashMap<>(), "key"); + + assertThat(result).isEmpty(); + } + + @Test + public void findMapValue_nullBoundKey_throws() { + Map map = new HashMap<>(); + map.put("key", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CelValueConverter.findMapValue(map, "key")); + + assertThat(e).hasMessageThat().isEqualTo("Map value cannot be null for key: key"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT); + } + + @Test + public void containsMapKey_boundKey_returnsTrue() { + Map map = new HashMap<>(); + map.put("key", "value"); + + assertThat(CelValueConverter.containsMapKey(map, "key")).isTrue(); + } + + @Test + public void containsMapKey_absentKey_returnsFalse() { + Map map = new HashMap<>(); + map.put("key", "value"); + + assertThat(CelValueConverter.containsMapKey(map, "absent")).isFalse(); + } + + @Test + public void containsMapKey_nullBoundKey_throws() { + Map map = new HashMap<>(); + map.put("key", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CelValueConverter.containsMapKey(map, "key")); + + assertThat(e).hasMessageThat().isEqualTo("Map value cannot be null for key: key"); + } + + @Test + public void toRuntimeValue_listWithNullElement_throws() { + List list = Arrays.asList("a", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.toRuntimeValue(list)); + + assertThat(e).hasMessageThat().isEqualTo("List element cannot be null at index: 1"); + } + + @Test + public void toRuntimeValue_listWithNullElementAfterAdaptedElement_throws() { + // The first element normalizes (Integer -> Long), which diverts mapContainer onto its rebuild + // path. The illegal element is only reached by the tail loop. + List list = Arrays.asList(1, null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.toRuntimeValue(list)); + + assertThat(e).hasMessageThat().isEqualTo("List element cannot be null at index: 1"); + } + + @Test + public void toRuntimeValue_nonRandomAccessCollectionWithNullElement_throws() { + List collection = new LinkedList<>(Arrays.asList("a", null)); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, + () -> CEL_VALUE_CONVERTER.toRuntimeValue(collection)); + + assertThat(e).hasMessageThat().isEqualTo("List element cannot be null at index: 1"); + } + + @Test + public void maybeUnwrap_listWithNullElement_throws() { + // Previously returned the list with the illegal element intact, because the element mapped to + // itself and the zero-allocation path never rebuilt. + List list = Arrays.asList("a", null); + + CelInvalidArgumentException e = + assertThrows( + CelInvalidArgumentException.class, () -> CEL_VALUE_CONVERTER.maybeUnwrap(list)); + + assertThat(e).hasMessageThat().isEqualTo("List element cannot be null at index: 1"); + } } diff --git a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java new file mode 100644 index 000000000..0fd1295c5 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java @@ -0,0 +1,328 @@ +// 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. + +package dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class OptimizedSelectTraversalTest { + + private enum TargetType { + OPTIMIZED_SELECTABLE { + @Override + Object createTarget(Map data) { + return new FakeOptimizedSelectable(data); + } + + @Override + Object createNestedTarget(Map innerData) { + return new FakeOptimizedSelectable( + ImmutableMap.of("outer_key", new FakeOptimizedSelectable(innerData))); + } + }, + SELECTABLE_VALUE { + @Override + Object createTarget(Map data) { + return new FakeSelectableValue(data); + } + + @Override + Object createNestedTarget(Map innerData) { + return new FakeSelectableValue( + ImmutableMap.of("outer_key", new FakeSelectableValue(innerData))); + } + }; + + abstract Object createTarget(Map data); + + abstract Object createNestedTarget(Map innerData); + } + + @SuppressWarnings("Immutable") + private enum NestedPresenceTestCase { + ALL_PRESENT( + ImmutableMap.of("inner_key", "nested_val"), "outer_key", "inner_key", /* expected= */ true), + INTERMEDIATE_MISSING( + ImmutableMap.of("inner_key", "nested_val"), + "missing_outer", + "inner_key", + /* expected= */ false), + TERMINAL_MISSING( + ImmutableMap.of("other_key", "nested_val"), + "outer_key", + "missing_terminal", + /* expected= */ false); + + final ImmutableMap innerData; + final String outerField; + final String innerField; + final boolean expected; + + NestedPresenceTestCase( + ImmutableMap innerData, + String outerField, + String innerField, + boolean expected) { + this.innerData = innerData; + this.outerField = outerField; + this.innerField = innerField; + this.expected = expected; + } + } + + @Test + public void qualify_emptyFields_returnsTargetInstance() { + Object target = new Object(); + + Object result = OptimizedSelectTraversal.qualify(target, ImmutableList.of()); + + assertThat(result).isSameInstanceAs(target); + } + + @Test + public void qualify_singleField_success(@TestParameter TargetType targetType) { + Object target = targetType.createTarget(ImmutableMap.of("key", "value")); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "key", 9, "")); + + Object result = OptimizedSelectTraversal.qualify(target, fields); + + assertThat(result).isEqualTo("value"); + } + + @Test + 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, "")); + + Object result = OptimizedSelectTraversal.qualify(target, fields); + + assertThat(result).isEqualTo("nested_value"); + } + + @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)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(target, fields)); + + assertThat(thrown).hasMessageThat().contains("missing"); + } + + @Test + public void qualify_optimizedSelectable_absentWithDefaultValue_returnsDefault() { + FakeOptimizedSelectable selectable = new FakeOptimizedSelectable(ImmutableMap.of()); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback")); + + Object result = OptimizedSelectTraversal.qualify(selectable, fields); + + assertThat(result).isEqualTo("default_fallback"); + } + + @Test + public void qualify_selectableValue_absentWithDefaultValue_returnsDefault() { + FakeSelectableValue selectable = new FakeSelectableValue(ImmutableMap.of()); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback")); + + Object result = OptimizedSelectTraversal.qualify(selectable, fields); + + assertThat(result).isEqualTo("default_fallback"); + } + + @Test + public void qualify_unsupportedTarget_throwsException() { + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(12345L, fields)); + + assertThat(thrown).hasMessageThat().contains("invalid_field"); + } + + @Test + public void qualify_intermediateUnsupportedTarget_throwsException() { + FakeOptimizedSelectable target = new FakeOptimizedSelectable(ImmutableMap.of("scalar", 999L)); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, "scalar", 3, 0L), SelectField.create(2L, "unreachable", 9, "")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(target, fields)); + + assertThat(thrown).hasMessageThat().contains("unreachable"); + } + + @Test + public void hasField_emptyFields_returnsFalse() { + Object target = new Object(); + + boolean hasField = OptimizedSelectTraversal.hasField(target, ImmutableList.of()); + + assertThat(hasField).isFalse(); + } + + @Test + public void hasField_singleField( + @TestParameter TargetType targetType, + @TestParameter({"present_key", "missing_key"}) String queryKey) { + Object target = targetType.createTarget(ImmutableMap.of("present_key", "val")); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, queryKey)); + + boolean hasField = OptimizedSelectTraversal.hasField(target, fields); + + assertThat(hasField).isEqualTo(queryKey.equals("present_key")); + } + + @Test + public void hasField_nestedFields( + @TestParameter TargetType targetType, @TestParameter NestedPresenceTestCase testCase) { + Object target = targetType.createNestedTarget(testCase.innerData); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, testCase.outerField), + SelectField.create(2L, testCase.innerField)); + + boolean hasField = OptimizedSelectTraversal.hasField(target, fields); + + assertThat(hasField).isEqualTo(testCase.expected); + } + + @Test + public void hasField_intermediateUnsupportedTarget_throwsException() { + FakeOptimizedSelectable target = + new FakeOptimizedSelectable(ImmutableMap.of("scalar_key", 100L)); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "scalar_key"), SelectField.create(2L, "child_key")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(target, fields)); + + assertThat(thrown).hasMessageThat().contains("child_key"); + } + + @Test + public void hasField_unsupportedTarget_throwsException() { + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(12345L, fields)); + + assertThat(thrown).hasMessageThat().contains("invalid_field"); + } + + @Test + public void qualify_errorValue_propagatesError() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2")); + + Object result = OptimizedSelectTraversal.qualify(error, fields); + + assertThat(result).isSameInstanceAs(error); + } + + @Test + public void hasField_errorValue_returnsFalse() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2")); + + boolean result = OptimizedSelectTraversal.hasField(error, fields); + + assertThat(result).isFalse(); + } + + @SuppressWarnings("Immutable") + private static final class FakeOptimizedSelectable implements OptimizedSelectable { + private final ImmutableMap values; + + @Override + public Object selectByFieldNumber(SelectField field) { + Object value = values.get(field.fieldName()); + if (value != null) { + return value; + } + if (field.defaultValue() != null) { + return field.defaultValue(); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + @Override + public boolean hasFieldByNumber(SelectField field) { + return values.containsKey(field.fieldName()); + } + + @Override + public Optional findByFieldNumber(SelectField field) { + return Optional.ofNullable(values.get(field.fieldName())); + } + + FakeOptimizedSelectable(Map values) { + this.values = ImmutableMap.copyOf(values); + } + } + + @SuppressWarnings("Immutable") + private static final class FakeSelectableValue implements SelectableValue { + private final ImmutableMap values; + + @Override + public Object select(String field) { + Object value = values.get(field); + if (value != null) { + return value; + } + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Optional find(String field) { + return Optional.ofNullable(values.get(field)); + } + + FakeSelectableValue(Map values) { + this.values = ImmutableMap.copyOf(values); + } + } +} 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 3b66171e4..0a11c23e5 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 org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -44,13 +45,36 @@ import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.io.IOException; import java.time.Instant; import java.util.LinkedHashMap; +import java.util.NoSuchElementException; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class ProtoLiteCelValueConverterTest { + private static final CelLiteDescriptorPool EMPTY_DESCRIPTOR_POOL = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + throw new NoSuchElementException(protoTypeName); + } + }; + private static final CelLiteDescriptorPool DESCRIPTOR_POOL = DefaultLiteDescriptorPool.newInstance( ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); @@ -307,7 +331,7 @@ public void readAllFields_unknownFieldsWithValues() throws Exception { LinkedHashMap mapBoolDoubleValues = (LinkedHashMap) fields.values().get("map_bool_double"); assertThat(mapBoolDoubleValues).containsExactly(true, 1.5d, false, 2.5d).inOrder(); - Multimap unknownValues = fields.unknowns(); + ImmutableListMultimap unknownValues = fields.unknowns(); assertThat(unknownValues) .containsExactly( 2500, @@ -326,4 +350,99 @@ public void readAllFields_unknownFieldsWithValues() throws Exception { ByteString.copyFromUtf8("\n\003bar\020\005")) .inOrder(); } + + @Test + public void getDefaultCelValue_fieldDescriptor_returnsDefault() { + FieldLiteDescriptor fieldDescriptor = + DESCRIPTOR_POOL + .getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes") + .getByFieldNameOrThrow("single_string"); + + Object defaultValue = PROTO_LITE_CEL_VALUE_CONVERTER.getDefaultCelValue(fieldDescriptor); + + assertThat(defaultValue).isEqualTo(""); + } + + @Test + public void getDefaultCelValue_nestedMessageWithoutDescriptor_throwsNoSuchElementException() { + FieldLiteDescriptor nestedMsgField = + DESCRIPTOR_POOL + .getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes") + .getByFieldNameOrThrow("single_nested_message"); + ProtoLiteCelValueConverter converterWithoutNested = + ProtoLiteCelValueConverter.newInstance(EMPTY_DESCRIPTOR_POOL); + + assertThrows( + NoSuchElementException.class, + () -> converterWithoutNested.getDefaultCelValue(nestedMsgField)); + } + + @Test + public void tryDecodeWellKnownProto_validBytes_returnsDecodedValue() { + Int32Value int32Value = Int32Value.of(42); + + Optional decoded = + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + int32Value.toByteString(), "google.protobuf.Int32Value"); + + assertThat(decoded).hasValue(42L); + } + + @Test + public void tryDecodeWellKnownProto_notWellKnownType_returnsEmpty() { + Optional decoded = + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(decoded).isEmpty(); + } + + @Test + public void tryDecodeWellKnownProto_missingDescriptor_returnsEmpty() { + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(EMPTY_DESCRIPTOR_POOL); + + Optional decoded = + converter.tryDecodeWellKnownProto(ByteString.EMPTY, "google.protobuf.Int32Value"); + + assertThat(decoded).isEmpty(); + } + + @Test + public void tryDecodeWellKnownProto_invalidBytes_throwsIllegalArgumentException() { + ByteString corruptBytes = ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte) 0xFF}); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + corruptBytes, "google.protobuf.Int32Value")); + + assertThat(exception) + .hasMessageThat() + .contains("Failed to decode well-known proto of type: google.protobuf.Int32Value"); + assertThat(exception).hasCauseThat().isInstanceOf(IOException.class); + } + + @Test + public void tryDecodeWellKnownProto_anyType_throwsUnsupportedOperationException() { + UnsupportedOperationException exception = + assertThrows( + UnsupportedOperationException.class, + () -> + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + ByteString.EMPTY, "google.protobuf.Any")); + + assertThat(exception).hasMessageThat().contains("ANY_VALUE"); + } + + @Test + public void hasDescriptor_returnsExpectedResult() { + assertThat( + PROTO_LITE_CEL_VALUE_CONVERTER.hasDescriptor( + "cel.expr.conformance.proto3.TestAllTypes")) + .isTrue(); + assertThat(PROTO_LITE_CEL_VALUE_CONVERTER.hasDescriptor("unknown.Type")).isFalse(); + } } diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java index 88799878e..ca370410d 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -15,6 +15,7 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -37,6 +38,7 @@ import com.google.protobuf.UInt64Value; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.internal.CelLiteDescriptorPool; import dev.cel.common.internal.DefaultLiteDescriptorPool; import dev.cel.expr.conformance.proto3.TestAllTypes; @@ -46,11 +48,12 @@ import java.io.ByteArrayOutputStream; import java.time.Duration; import java.time.Instant; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) -public class ProtoMessageLiteValueTest { +public final class ProtoMessageLiteValueTest { private static final CelLiteDescriptorPool DESCRIPTOR_POOL = DefaultLiteDescriptorPool.newInstance( ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); @@ -280,4 +283,322 @@ public void unknownFields_retainsUnknownWireFields() throws Exception { .valuesForKey(1000) .containsExactly(ByteString.copyFromUtf8("hello unknown")); } + + @Test + public void selectByFieldNumber_knownField_returnsValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default")); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void selectByFieldNumber_unknownWireField_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 42L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectByFieldNumber(SelectField.create(999L, "unknown_field", 3, 0L)); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void selectByFieldNumber_unknownRepeatedWireField_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 10L); + cos.writeInt64(999, 20L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectByFieldNumber( + SelectField.create(999L, "unknown_repeated", 3, ImmutableList.of())); + + assertThat((Iterable) result).containsExactly(10L, 20L).inOrder(); + } + + @Test + public void selectByFieldNumber_renamedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectByFieldNumber(SelectField.create(14L, "renamed_string", 9, "default")); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void selectByFieldNumber_unknownFieldCollidesWithKnownFieldName_returnsUnknownFieldValue() + throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 42L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()) + .toBuilder() + .setSingleString("known_field_14") + .build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectByFieldNumber(SelectField.create(999L, "single_string", 3, 0L)); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void selectByFieldNumber_renamedMapField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectByFieldNumber(SelectField.create(61L, "renamed_map", -1, ImmutableMap.of())); + + assertThat(result).isEqualTo(ImmutableMap.of("k", "v")); + } + + @Test + public void selectByFieldNumber_renamedRepeatedField_resolvesByFieldNumber() { + TestAllTypes proto = + TestAllTypes.newBuilder().addRepeatedInt64(10L).addRepeatedInt64(20L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectByFieldNumber(SelectField.create(32L, "renamed_repeated", 3, ImmutableList.of())); + + assertThat(result).isEqualTo(ImmutableList.of(10L, 20L)); + } + + @Test + public void findByFieldNumber_intermediateUnknownSubmessage_returnsRawProtoMessage() + throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeString(1, "inner"); + subCos.flush(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(998, ByteString.copyFrom(subBaos.toByteArray())); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Optional nav = val.findByFieldNumber(SelectField.create(998L, "unknown_submessage")); + + assertThat(nav.map(v -> v instanceof RawProtoMessageLiteValue)).hasValue(true); + } + + @Test + public void hasFieldByNumber_knownField_returnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(14L, "single_string"))).isTrue(); + } + + @Test + public void hasFieldByNumber_unknownFieldPresent_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 42L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(999L, "unknown_present"))).isTrue(); + } + + @Test + public void hasFieldByNumber_unknownFieldAbsent_returnsFalse() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(888L, "unknown_absent"))).isFalse(); + } + + @Test + public void hasFieldByNumber_renamedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(14L, "renamed_string"))).isTrue(); + } + + @Test + public void hasFieldByNumber_renamedMapField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(61L, "renamed_map"))).isTrue(); + } + + @Test + public void hasFieldByNumber_renamedRepeatedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().addRepeatedInt64(10L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasFieldByNumber(SelectField.create(32L, "renamed_repeated"))).isTrue(); + } + + @Test + public void qualify_emptyList_returnsSameInstance() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(OptimizedSelectTraversal.qualify(val, ImmutableList.of())).isSameInstanceAs(val); + } + + @Test + public void hasField_emptyList_returnsFalse() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(OptimizedSelectTraversal.hasField(val, ImmutableList.of())).isFalse(); + } + + @Test + public void qualify_mapField_returnsMap() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + ImmutableList fields = + ImmutableList.of( + SelectField.create( + 61L, "map_string_string", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of())); + + Object result = OptimizedSelectTraversal.qualify(val, fields); + + assertThat(result).isEqualTo(ImmutableMap.of("k", "v")); + } + + @Test + public void qualify_nestedMessage_resolvesField() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + ImmutableList fields = + ImmutableList.of( + SelectField.create(21L, "single_nested_message", 11, null), + SelectField.create(1L, "bb", 5, 0)); + + Object result = OptimizedSelectTraversal.qualify(val, fields); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void hasField_nestedMessage_resolvesPresence() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat( + OptimizedSelectTraversal.hasField( + val, + ImmutableList.of( + SelectField.create(21L, "single_nested_message"), + SelectField.create(1L, "bb")))) + .isTrue(); + assertThat( + OptimizedSelectTraversal.hasField( + val, + ImmutableList.of( + SelectField.create(21L, "single_nested_message"), + SelectField.create(99L, "missing")))) + .isFalse(); + } + + @Test + public void qualify_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + ImmutableList fields = + ImmutableList.of( + SelectField.create(2L, "single_int64", 3, 0L), + SelectField.create(3L, "leaf_field", 9, "")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(val, fields)); + + assertThat(thrown).hasMessageThat().contains("leaf_field"); + } + + @Test + public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + ImmutableList fields = + ImmutableList.of( + SelectField.create(2L, "single_int64"), SelectField.create(3L, "leaf_field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(val, fields)); + + assertThat(thrown).hasMessageThat().contains("leaf_field"); + } } 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 8f5ac623a..180883f60 100644 --- a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -18,25 +18,64 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.ByteString; import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.Int64Value; +import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.internal.CelLiteDescriptorPool; +import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; import java.io.ByteArrayOutputStream; +import java.time.Duration; +import java.util.NoSuchElementException; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class RawProtoMessageLiteValueTest { + private static final ProtoLiteCelValueConverter CONVERTER = + ProtoLiteCelValueConverter.newInstance( + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor()))); + + private static final ProtoLiteCelValueConverter EMPTY_CONVERTER = + ProtoLiteCelValueConverter.newInstance(DefaultLiteDescriptorPool.newInstance()); + + private static Object decodeWireEntries( + ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + return RawProtoMessageLiteValue.decodeWireEntries( + entries, typeCode, protoTypeName, isRepeated, EMPTY_CONVERTER); + } + + private static Object decodeWireValue( + Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + return RawProtoMessageLiteValue.decodeWireValue(raw, fieldType, protoTypeName, EMPTY_CONVERTER); + } + + private static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + return RawProtoMessageLiteValue.decodeWireValue(raw, typeCode, protoTypeName, EMPTY_CONVERTER); + } + @Test public void create_accessorsAndType() { ByteString bytes = ByteString.copyFromUtf8("test"); - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, "custom.Message"); + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(bytes, "custom.Message", EMPTY_CONVERTER); assertThat(value.rawWireBytes()).isEqualTo(bytes); assertThat(value.value()).isSameInstanceAs(value); @@ -44,9 +83,9 @@ public void create_accessorsAndType() { } @Test - public void create_singleArgDefaultsEmptyTypeName() { + public void create_defaultsEmptyTypeName() { ByteString bytes = ByteString.copyFromUtf8("test"); - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes); + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, EMPTY_CONVERTER); assertThat(value.rawWireBytes()).isEqualTo(bytes); assertThat(value.celType().name()).isEmpty(); @@ -55,22 +94,23 @@ public void create_singleArgDefaultsEmptyTypeName() { @Test public void select_throwsCelAttributeNotFoundException() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); assertThrows(CelAttributeNotFoundException.class, () -> value.select("field")); } @Test - public void find_returnsEmptyOptional() { + public void find_throwsCelAttributeNotFoundException() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); - assertThat(value.find("field")).isEmpty(); + assertThrows(CelAttributeNotFoundException.class, () -> value.find("field")); } @Test public void isZeroValue_emptyBytes_returnsTrue() { - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(ByteString.EMPTY); + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, EMPTY_CONVERTER); assertThat(value.isZeroValue()).isTrue(); } @@ -78,22 +118,27 @@ public void isZeroValue_emptyBytes_returnsTrue() { @Test public void isZeroValue_nonEmptyBytes_returnsFalse() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data")); + RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data"), EMPTY_CONVERTER); assertThat(value.isZeroValue()).isFalse(); } @Test - public void hasField_returnsExpectedPresence() throws Exception { + public void hasFieldByNumber_scalarField_returnsExpectedPresence() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); CodedOutputStream cos = CodedOutputStream.newInstance(baos); cos.writeInt64(1, 42L); cos.flush(); RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray()), EMPTY_CONVERTER); + + SelectField field1 = + SelectField.create(1L, "single_int64", FieldLiteDescriptor.Type.INT64.getNumber(), 0L); + SelectField field2 = + SelectField.create(2L, "single_int64", FieldLiteDescriptor.Type.INT64.getNumber(), 0L); - assertThat(value.hasField(1)).isTrue(); - assertThat(value.hasField(2)).isFalse(); + assertThat(value.hasFieldByNumber(field1)).isTrue(); + assertThat(value.hasFieldByNumber(field2)).isFalse(); } @Test @@ -107,7 +152,7 @@ public void unknownFields_parsesWireTags() throws Exception { cos.flush(); RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray()), EMPTY_CONVERTER); assertThat(value.unknownFields()).valuesForKey(1).containsExactly(42L); assertThat(value.unknownFields()).valuesForKey(2).containsExactly(100); @@ -120,13 +165,13 @@ public void unknownFields_parsesWireTags() throws Exception { @Test public void decodeWireEntries_emptySingularEntries_returnsNull() { Object intResult = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", /* isRepeated= */ false); Object messageResult = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.MESSAGE.getNumber(), "custom.Message", @@ -139,7 +184,7 @@ public void decodeWireEntries_emptySingularEntries_returnsNull() { @Test public void decodeWireEntries_emptyRepeatedEntries_returnsEmptyList() { Object result = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -151,7 +196,7 @@ public void decodeWireEntries_emptyRepeatedEntries_returnsEmptyList() { @Test public void decodeWireEntries_nonRepeated_lastOneWins() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(10L, 20L, 30L), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -163,7 +208,7 @@ public void decodeWireEntries_nonRepeated_lastOneWins() { @Test public void decodeWireEntries_repeatedUnpacked() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(10L, 20L, 30L), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -182,7 +227,7 @@ public void decodeWireEntries_packedInt32() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -200,7 +245,7 @@ public void decodeWireEntries_packedInt64() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -217,7 +262,7 @@ public void decodeWireEntries_packedUint32() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.UINT32.getNumber(), "custom.Message", @@ -234,7 +279,7 @@ public void decodeWireEntries_packedUint64() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.UINT64.getNumber(), "custom.Message", @@ -252,7 +297,7 @@ public void decodeWireEntries_packedSint32AndSint64() throws Exception { cos32.flush(); Object decoded32 = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos32.toByteArray())), FieldLiteDescriptor.Type.SINT32.getNumber(), "custom.Message", @@ -267,7 +312,7 @@ public void decodeWireEntries_packedSint32AndSint64() throws Exception { cos64.flush(); Object decoded64 = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos64.toByteArray())), FieldLiteDescriptor.Type.SINT64.getNumber(), "custom.Message", @@ -287,7 +332,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { cos.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(0, 4)), FieldLiteDescriptor.Type.FIXED32.getNumber(), "custom.Message", @@ -295,7 +340,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(10L))); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(4, 12)), FieldLiteDescriptor.Type.FIXED64.getNumber(), "custom.Message", @@ -303,7 +348,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(20L))); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(12, 16)), FieldLiteDescriptor.Type.SFIXED32.getNumber(), "custom.Message", @@ -311,7 +356,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(-30L)); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(16, 24)), FieldLiteDescriptor.Type.SFIXED64.getNumber(), "custom.Message", @@ -328,7 +373,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosBool.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosBool.toByteArray())), FieldLiteDescriptor.Type.BOOL.getNumber(), "custom.Message", @@ -341,7 +386,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosFloat.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosFloat.toByteArray())), FieldLiteDescriptor.Type.FLOAT.getNumber(), "custom.Message", @@ -354,7 +399,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosDouble.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosDouble.toByteArray())), FieldLiteDescriptor.Type.DOUBLE.getNumber(), "custom.Message", @@ -367,7 +412,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosEnum.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosEnum.toByteArray())), FieldLiteDescriptor.Type.ENUM.getNumber(), "custom.Message", @@ -378,99 +423,72 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { @Test public void decodeWireValue_allScalarWireTypes() { assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( Double.doubleToRawLongBits(2.5d), WireFormat.FieldType.DOUBLE, "custom.Message")) .isEqualTo(2.5d); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( Float.floatToRawIntBits(1.5f), WireFormat.FieldType.FLOAT, "custom.Message")) .isEqualTo(1.5d); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.INT64, "custom.Message")) - .isEqualTo(42L); + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT64, "custom.Message")).isEqualTo(42L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.INT32, "custom.Message")) - .isEqualTo(42L); + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT32, "custom.Message")).isEqualTo(42L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.UINT64, "custom.Message")) + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT64, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(42L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.UINT32, "custom.Message")) + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT32, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(42L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 100, WireFormat.FieldType.FIXED32, "custom.Message")) + assertThat(decodeWireValue(100, WireFormat.FieldType.FIXED32, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(100L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.FIXED64, "custom.Message")) + assertThat(decodeWireValue(100L, WireFormat.FieldType.FIXED64, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(100L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - -50, WireFormat.FieldType.SFIXED32, "custom.Message")) + assertThat(decodeWireValue(-50, WireFormat.FieldType.SFIXED32, "custom.Message")) .isEqualTo(-50L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - -50L, WireFormat.FieldType.SFIXED64, "custom.Message")) + assertThat(decodeWireValue(-50L, WireFormat.FieldType.SFIXED64, "custom.Message")) .isEqualTo(-50L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 1L, WireFormat.FieldType.BOOL, "custom.Message")) - .isEqualTo(true); + assertThat(decodeWireValue(1L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(true); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 0L, WireFormat.FieldType.BOOL, "custom.Message")) - .isEqualTo(false); + assertThat(decodeWireValue(0L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(false); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("hello"), WireFormat.FieldType.STRING, "custom.Message")) .isEqualTo("hello"); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("bytes"), WireFormat.FieldType.BYTES, "custom.Message")) .isEqualTo(CelByteString.of("bytes".getBytes(UTF_8))); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( 1L, // zigzag 1 -> -1 WireFormat.FieldType.SINT32, "custom.Message")) .isEqualTo(-1L); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( 1L, // zigzag 1 -> -1 WireFormat.FieldType.SINT64, "custom.Message")) .isEqualTo(-1L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 3L, WireFormat.FieldType.ENUM, "custom.Message")) - .isEqualTo(3L); + assertThat(decodeWireValue(3L, WireFormat.FieldType.ENUM, "custom.Message")).isEqualTo(3L); } @Test public void decodeWireValue_messageType_returnsRawProtoMessageLiteValue() { Object submessage = - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("raw"), WireFormat.FieldType.MESSAGE, "sub.Message"); assertThat(submessage).isInstanceOf(RawProtoMessageLiteValue.class); @@ -484,9 +502,7 @@ public void decodeWireValue_groupType_throwsUnsupportedOperationException() { UnsupportedOperationException thrown = assertThrows( UnsupportedOperationException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - rawBytes, WireFormat.FieldType.GROUP, "group.Message")); + () -> decodeWireValue(rawBytes, WireFormat.FieldType.GROUP, "group.Message")); assertThat(thrown).hasMessageThat().contains("Groups are not supported"); } @@ -500,7 +516,7 @@ public void decodeWireEntries_groupType_throwsUnsupportedOperationException() { assertThrows( UnsupportedOperationException.class, () -> - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( rawEntries, groupTypeCode, "group.Message", /* isRepeated= */ false)); assertThat(thrown).hasMessageThat().contains("Groups are not supported"); @@ -512,30 +528,22 @@ public void decodeWireEntries_invalidTypeCode_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireEntries( - rawEntries, 999, "custom.Message", /* isRepeated= */ false)); + () -> decodeWireEntries(rawEntries, 999, "custom.Message", /* isRepeated= */ false)); } @Test public void decodeWireValue_invalidTypeCode_throws() { - assertThrows( - IllegalArgumentException.class, - () -> RawProtoMessageLiteValue.decodeWireValue(42L, 0, "custom.Message")); + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 0, "custom.Message")); - assertThrows( - IllegalArgumentException.class, - () -> RawProtoMessageLiteValue.decodeWireValue(42L, 999, "custom.Message")); + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 999, "custom.Message")); } @Test public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { Object decodedHigh = - RawProtoMessageLiteValue.decodeWireValue( - 0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); + decodeWireValue(0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); Object decodedNegative = - RawProtoMessageLiteValue.decodeWireValue( - 0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); + decodeWireValue(0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); assertThat(decodedHigh).isEqualTo(5L); assertThat(decodedNegative).isEqualTo(-2147483648L); @@ -543,9 +551,7 @@ public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { @Test public void decodeWireValue_enumHighBits_truncatedToSigned32Bit() { - Object decodedHigh = - RawProtoMessageLiteValue.decodeWireValue( - 0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); + Object decodedHigh = decodeWireValue(0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); assertThat(decodedHigh).isEqualTo(5L); } @@ -555,33 +561,25 @@ public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { IllegalArgumentException thrownInt64 = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - "not a long", WireFormat.FieldType.INT64, "custom.Message")); + () -> decodeWireValue("not a long", WireFormat.FieldType.INT64, "custom.Message")); assertThat(thrownInt64).hasMessageThat().contains("Expected Long for wire type INT64"); IllegalArgumentException thrownString = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.STRING, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.STRING, "custom.Message")); assertThat(thrownString).hasMessageThat().contains("Expected ByteString for wire type STRING"); IllegalArgumentException thrownBytes = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.BYTES, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.BYTES, "custom.Message")); assertThat(thrownBytes).hasMessageThat().contains("Expected ByteString for wire type BYTES"); IllegalArgumentException thrownMessage = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.MESSAGE, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.MESSAGE, "custom.Message")); assertThat(thrownMessage) .hasMessageThat() .contains("Expected ByteString for wire type MESSAGE"); @@ -589,17 +587,13 @@ public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { IllegalArgumentException thrownFloat = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.FLOAT, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.FLOAT, "custom.Message")); assertThat(thrownFloat).hasMessageThat().contains("Expected Integer for wire type FLOAT"); IllegalArgumentException thrownDouble = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100, WireFormat.FieldType.DOUBLE, "custom.Message")); + () -> decodeWireValue(100, WireFormat.FieldType.DOUBLE, "custom.Message")); assertThat(thrownDouble).hasMessageThat().contains("Expected Long for wire type DOUBLE"); } @@ -610,9 +604,7 @@ public void decodeWireValue_invalidUtf8String_throwsIllegalArgumentException() { IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - invalidUtf8, WireFormat.FieldType.STRING, "custom.Message")); + () -> decodeWireValue(invalidUtf8, WireFormat.FieldType.STRING, "custom.Message")); assertThat(thrown).hasMessageThat().contains("Invalid UTF-8 in string field"); } @@ -631,7 +623,7 @@ public void decodeWireEntries_multiChunkPackedRepeated() throws Exception { cos2.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of( ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), FieldLiteDescriptor.Type.INT32.getNumber(), @@ -650,7 +642,7 @@ public void decodeWireEntries_mixedPackedAndUnpackedRepeated() throws Exception cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(1L, ByteString.copyFrom(baos.toByteArray()), 4L), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -672,7 +664,7 @@ public void decodeWireEntries_singularMessage_mergesChunks() throws Exception { cos2.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of( ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), FieldLiteDescriptor.Type.MESSAGE.getNumber(), @@ -687,18 +679,14 @@ public void decodeWireEntries_singularMessage_mergesChunks() throws Exception { @Test public void decodeWireValue_uint32HighBit_correctUnsignedLong() { - Object decoded = - RawProtoMessageLiteValue.decodeWireValue( - 0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); + Object decoded = decodeWireValue(0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); } @Test public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { - Object decoded = - RawProtoMessageLiteValue.decodeWireValue( - -1, WireFormat.FieldType.FIXED32, "custom.Message"); + Object decoded = decodeWireValue(-1, WireFormat.FieldType.FIXED32, "custom.Message"); assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); } @@ -706,7 +694,7 @@ public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { @Test public void decodeWireEntries_repeatedString() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), FieldLiteDescriptor.Type.STRING.getNumber(), "custom.Message", @@ -718,7 +706,7 @@ public void decodeWireEntries_repeatedString() { @Test public void decodeWireEntries_repeatedBytes() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), FieldLiteDescriptor.Type.BYTES.getNumber(), "custom.Message", @@ -733,7 +721,7 @@ public void decodeWireEntries_repeatedBytes() { @Test public void decodeWireEntries_repeatedMessage() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("msg1"), ByteString.copyFromUtf8("msg2")), FieldLiteDescriptor.Type.MESSAGE.getNumber(), "sub.Message", @@ -741,8 +729,10 @@ public void decodeWireEntries_repeatedMessage() { assertThat((Iterable) decoded) .containsExactly( - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("msg1"), "sub.Message"), - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("msg2"), "sub.Message")) + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg1"), "sub.Message", EMPTY_CONVERTER), + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg2"), "sub.Message", EMPTY_CONVERTER)) .inOrder(); } @@ -755,7 +745,7 @@ public void decodeWireEntries_packedTruncated_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(truncated), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -763,4 +753,450 @@ public void decodeWireEntries_packedTruncated_throwsIllegalStateException() { assertThat(thrown).hasMessageThat().contains("Failed to parse packed repeated field"); } + + @Test + public void selectByFieldNumber_presentOnWire_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(14, "hello"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "")); + + assertThat(val).isEqualTo("hello"); + } + + @Test + public void selectByFieldNumber_absentWithDefaultValue_returnsDefault() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default")); + + assertThat(val).isEqualTo("default"); + } + + @Test + public void selectByFieldNumber_withConverter_resolvesDescriptor() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string")); + + assertThat(val).isEqualTo(""); + } + + @Test + public void + selectByFieldNumber_absentSubmessageWithMissingChildDescriptor_returnsEmptyRawProtoMessageLiteValue() { + CelLiteDescriptorPool poolWithoutNested = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals("cel.expr.conformance.proto3.TestAllTypes")) { + return DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())) + .findDescriptor(protoTypeName); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutNested); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object val = raw.selectByFieldNumber(SelectField.create(21L, "single_nested_message")); + + assertThat(val).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue rawChild = (RawProtoMessageLiteValue) val; + assertThat(rawChild.rawWireBytes()).isEqualTo(ByteString.EMPTY); + assertThat(rawChild.celType().name()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } + + @Test + public void + selectByFieldNumber_absentRepeatedMessageWithMissingChildDescriptor_returnsEmptyList() { + CelLiteDescriptorPool poolWithoutNested = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals("cel.expr.conformance.proto3.TestAllTypes")) { + return DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())) + .findDescriptor(protoTypeName); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutNested); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object val = + raw.selectByFieldNumber( + SelectField.create( + TestAllTypes.REPEATED_NESTED_MESSAGE_FIELD_NUMBER, "repeated_nested_message")); + + assertThat(val).isEqualTo(ImmutableList.of()); + } + + @Test + public void selectByFieldNumber_unknownFieldWithoutTypeCode_throwsCelAttributeNotFoundException() + throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(999, "unknown"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + SelectField selectField = SelectField.create(999L, "unknown_field"); + + assertThrows(CelAttributeNotFoundException.class, () -> raw.selectByFieldNumber(selectField)); + } + + @Test + public void hasFieldByNumber_wirePresent_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "present"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"))) + .isTrue(); + } + + @Test + public void hasFieldByNumber_wireAbsent_returnsFalse() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"))) + .isFalse(); + } + + @Test + public void hasFieldByNumber_emptyPackedRepeated_returnsFalse( + @TestParameter boolean withDescriptor) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, ByteString.EMPTY); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + SelectField selectField = + withDescriptor + ? SelectField.create(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, "repeated_int32") + : SelectField.create( + TestAllTypes.REPEATED_INT32_FIELD_NUMBER, + "repeated_int32", + FieldLiteDescriptor.Type.INT32.getNumber(), + ImmutableList.of()); + + assertThat(raw.hasFieldByNumber(selectField)).isFalse(); + } + + @Test + public void hasFieldByNumber_nonEmptyPackedRepeated_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + ByteArrayOutputStream packed = new ByteArrayOutputStream(); + CodedOutputStream packedCos = CodedOutputStream.newInstance(packed); + packedCos.writeInt32NoTag(42); + packedCos.flush(); + cos.writeBytes( + TestAllTypes.REPEATED_INT32_FIELD_NUMBER, ByteString.copyFrom(packed.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, "repeated_int32"))) + .isTrue(); + } + + @Test + public void hasFieldByNumber_emptyByteStringOnScalarPackableField_returnsTrue( + @TestParameter boolean withDescriptor) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeByteArray(TestAllTypes.SINGLE_INT32_FIELD_NUMBER, new byte[0]); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + SelectField selectField = + withDescriptor + ? SelectField.create(TestAllTypes.SINGLE_INT32_FIELD_NUMBER, "single_int32") + : SelectField.create( + TestAllTypes.SINGLE_INT32_FIELD_NUMBER, + "single_int32", + FieldLiteDescriptor.Type.INT32.getNumber(), + 0); + + assertThat(raw.hasFieldByNumber(selectField)).isTrue(); + } + + @Test + public void findByFieldNumber_intermediatePresent_returnsSubmessage() throws Exception { + ByteArrayOutputStream subBaos1 = new ByteArrayOutputStream(); + CodedOutputStream subCos1 = CodedOutputStream.newInstance(subBaos1); + subCos1.writeInt32(1, 42); + subCos1.flush(); + + ByteArrayOutputStream subBaos2 = new ByteArrayOutputStream(); + CodedOutputStream subCos2 = CodedOutputStream.newInstance(subBaos2); + subCos2.writeInt32(2, 84); + subCos2.flush(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(21, ByteString.copyFrom(subBaos1.toByteArray())); + cos.writeBytes(21, ByteString.copyFrom(subBaos2.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + Optional nav = raw.findByFieldNumber(SelectField.create(21L, "single_nested_message")); + + RawProtoMessageLiteValue expected = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(subBaos1.toByteArray()) + .concat(ByteString.copyFrom(subBaos2.toByteArray())), + "cel.@unknownMessage", + EMPTY_CONVERTER); + assertThat(nav).hasValue(expected); + } + + @Test + public void findByFieldNumber_intermediateAbsent_returnsEmpty() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Optional nav = raw.findByFieldNumber(SelectField.create(999L, "absent")); + + assertThat(nav).isEmpty(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum SelectByFieldNumberTestCase { + INT64(SelectField.create(TestAllTypes.SINGLE_INT64_FIELD_NUMBER, "single_int64"), 99L), + STRING(SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"), "hello"), + MAP_STRING_STRING( + SelectField.create(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, "map_string_string"), + ImmutableMap.of("k1", "v1", "k2", "v2")), + MAP_INT32_BYTES( + SelectField.create(TestAllTypes.MAP_INT32_BYTES_FIELD_NUMBER, "map_int32_bytes"), + ImmutableMap.of( + 0L, CelByteString.copyFromUtf8("val_for_default_key"), 42L, CelByteString.EMPTY)), + DURATION( + SelectField.create(TestAllTypes.SINGLE_DURATION_FIELD_NUMBER, "single_duration"), + Duration.ofSeconds(10L, 500L)), + INT64_WRAPPER( + SelectField.create(TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER, "single_int64_wrapper"), + 12345L); + + private final SelectField selectField; + private final Object expectedValue; + + private SelectByFieldNumberTestCase(SelectField selectField, Object expectedValue) { + this.selectField = selectField; + this.expectedValue = expectedValue; + } + } + + @Test + public void selectByFieldNumber_withDescriptor_decodesExpectedValue( + @TestParameter SelectByFieldNumberTestCase testCase) { + TestAllTypes proto = + 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))) + .setSingleInt64Wrapper(Int64Value.of(12345L)) + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object selected = raw.selectByFieldNumber(testCase.selectField); + + assertThat(selected).isEqualTo(testCase.expectedValue); + } + + @Test + public void findByFieldNumber_scalarField_returnsScalar(@TestParameter boolean withDescriptor) { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + + Optional nav = + raw.findByFieldNumber( + SelectField.create(TestAllTypes.SINGLE_INT64_FIELD_NUMBER, "single_int64")); + + assertThat(nav).hasValue(99L); + } + + @Test + public void selectByFieldNumber_unsetWrapperFieldWithoutWrapperDescriptor_returnsNullValue() { + MessageLiteDescriptor testAllTypesDesc = + TestAllTypesCelDescriptor.getDescriptor() + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + CelLiteDescriptorPool poolWithoutWrappers = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals(testAllTypesDesc.getProtoTypeName())) { + return Optional.of(testAllTypesDesc); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return findDescriptor(messageLite.getClass().getName()); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutWrappers); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object result = + raw.selectByFieldNumber( + SelectField.create( + TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER, "single_int64_wrapper")); + + assertThat(result).isEqualTo(NullValue.NULL_VALUE); + } + + @Test + public void findByFieldNumber_typedFieldWithoutDescriptor_returnsSelectedValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleUint32(123).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Optional nav = + raw.findByFieldNumber( + SelectField.create( + TestAllTypes.SINGLE_UINT32_FIELD_NUMBER, + "single_uint32", + FieldLiteDescriptor.Type.UINT32.getNumber(), + 0L)); + + assertThat(nav).hasValue(UnsignedLong.fromLongBits(123L)); + } + + @Test + public void selectByFieldNumber_absentMessageFieldWithoutDescriptor_returnsUnknownMessage() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create(ByteString.EMPTY, EMPTY_CONVERTER); + + Object selected = + raw.selectByFieldNumber( + SelectField.create( + 21L, "single_nested_message", FieldLiteDescriptor.Type.MESSAGE.getNumber(), null)); + + assertThat(selected).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue message = (RawProtoMessageLiteValue) selected; + assertThat(message.rawWireBytes()).isEqualTo(ByteString.EMPTY); + assertThat(message.celType().name()).isEqualTo("cel.@unknownMessage"); + } + + @Test + public void + selectByFieldNumber_unknownMapFieldWithWireEntries_throwsUnsupportedOperationException() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("key", "val").build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.create( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.CEL_MAP_TYPE_CODE, + null); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> raw.selectByFieldNumber(field)); + + assertThat(e) + .hasMessageThat() + .contains("Decoding unknown map field from wire bytes is unsupported"); + } } diff --git a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java new file mode 100644 index 000000000..ba9dc7008 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java @@ -0,0 +1,133 @@ +// 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. + +package dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.testing.EqualsTester; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class SelectFieldTest { + + @Test + public void create_twoArguments_success() { + SelectField field = SelectField.create(1L, "foo"); + + assertThat(field.fieldNumber()).isEqualTo(1); + assertThat(field.fieldName()).isEqualTo("foo"); + assertThat(field.typeCode()).isEqualTo(SelectField.NO_TYPE_CODE); + assertThat(field.defaultValue()).isNull(); + } + + @Test + public void create_fourArguments_success() { + SelectField field = SelectField.create(2L, "bar", 9, "default_str"); + + assertThat(field.fieldNumber()).isEqualTo(2); + assertThat(field.fieldName()).isEqualTo("bar"); + assertThat(field.typeCode()).isEqualTo(9); + assertThat(field.defaultValue()).isEqualTo("default_str"); + } + + @Test + public void create_mapTypeCode_success() { + SelectField field = SelectField.create(3L, "map_field", -1, null); + + assertThat(field.typeCode()).isEqualTo(-1); + } + + @Test + public void create_twoArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null)); + } + + @Test + public void create_fourArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 9, null)); + } + + @Test + public void create_fieldNumberBelowMinimum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(0L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 0"); + } + + @Test + public void create_fieldNumberNegative_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(-1L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: -1"); + } + + @Test + public void create_fieldNumberAboveMaximum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(536870912L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 536870912"); + } + + @Test + public void create_typeCodeZero_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 0"); + } + + @Test + public void create_typeCodeAboveMaximum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 19"); + } + + @Test + public void create_typeCodeBelowSentinel_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: -2"); + } + + @Test + public void create_typeCodeGroupProto_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 10, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 10"); + } + + @Test + public void equalsAndHashCode_testedProperly() { + new EqualsTester() + .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", 9, "default"), + SelectField.create(1L, "foo", 9, "default")) + .addEqualityGroup(SelectField.create(1L, "foo", 9, "other_default")) + .testEquals(); + } +} diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 9853289a9..192f01de8 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -126,3 +126,39 @@ cel_android_library( name = "base_proto_message_value_provider_android", exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider_android"], ) + +java_library( + name = "select_field", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:select_field"], +) + +cel_android_library( + name = "select_field_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:select_field_android"], +) + +java_library( + name = "optimized_selectable", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_selectable"], +) + +cel_android_library( + name = "optimized_selectable_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_selectable_android"], +) + +java_library( + name = "optimized_select_traversal", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_select_traversal"], +) + +cel_android_library( + name = "optimized_select_traversal_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_select_traversal_android"], +) diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java index c50c07c29..61cd425c5 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -115,6 +115,17 @@ * * *

Map indexing and non-protobuf selects pass through untouched. + * + *

Rename Resilience & Dynamic Type Limitations: + * + *

    + *
  • Protobuf Extensions: Extension fields are not rename-resilient; runtime lookup + * resolves extensions by their fully qualified name rather than field number. + *
  • Dynamic & Unpacked Payloads: When traversing values typed as {@code dyn}, unpacked + * from {@code google.protobuf.Any}, or evaluated via classless wire payloads ({@code + * RawProtoMessageLiteValue}), no cross-check between the embedded field number and field name + * is performed at runtime. + *
*/ public final class SelectOptimizer implements CelAstOptimizer { @@ -131,6 +142,14 @@ public final class SelectOptimizer implements CelAstOptimizer { private static final TypeParamType TYPE_PARAM_T = TypeParamType.create("T"); + /** + * Declaration for {@code cel.@attribute(operand, qualifiers, typeIdent) -> T}. + * + *

The 3rd argument ({@code TypeType.create(TYPE_PARAM_T)}) binds type parameter {@code T} to + * the static type identifier of the leaf field so that type checking preserves the exact result + * type rather than erasing to {@code dyn}, and enables plan-time integrity validation between the + * leaf hop's wire type code and its static type. + */ @VisibleForTesting static final CelFunctionDecl CEL_ATTRIBUTE_FUNCTION_DECL = CelFunctionDecl.newFunctionDeclaration( diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel index 1fd34709a..f7c2ef401 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel @@ -22,6 +22,7 @@ java_library( "//common/ast", "//common/navigation:mutable_navigation", "//common/types", + "//common/values:cel_byte_string", "//extensions", "//extensions:optional_library", # "//java/com/google/testing/testsize:annotations", diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java index 83057e44d..9dc88589b 100644 --- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java +++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java @@ -18,11 +18,14 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; import dev.cel.expr.ParsedExpr; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.TextFormat; @@ -42,9 +45,11 @@ import dev.cel.common.types.MapType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.CelByteString; import dev.cel.expr.conformance.proto2.NestedTestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypesProto; import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelExtensions; import dev.cel.optimizer.CelAstOptimizer; import dev.cel.optimizer.CelOptimizer; import dev.cel.optimizer.CelOptimizerFactory; @@ -52,10 +57,11 @@ import dev.cel.parser.CelStandardMacro; import dev.cel.parser.CelUnparser; import dev.cel.parser.CelUnparserFactory; -import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelFunctionBinding; import dev.cel.runtime.CelRuntime.Program; import dev.cel.testing.CelRuntimeFlavor; +import java.time.Duration; +import java.time.Instant; import java.util.List; import java.util.stream.LongStream; import org.junit.Before; @@ -570,100 +576,181 @@ public void optimizeAndEvaluate_withHasFieldFunctionBinding_evaluatesSuccessfull assertThat(result).isEqualTo(true); } - @Test - public void optimizeAndEvaluate_withSelectOnMapValue_evaluatesSuccessfully() throws Exception { - Cel celWithBinding = - cel.toCelBuilder() - .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) - .addFunctionBindings( - CelFunctionBinding.from( - "cel_attribute_list", - ImmutableList.of(Object.class, List.class, Object.class), - args -> 42L)) - .build(); - CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) - .addAstOptimizers( - SelectOptimizer.newInstance( - SelectOptimizerOptions.newBuilder().build(), - TestAllTypes.getDescriptor().getFile())) - .build(); - CelAbstractSyntaxTree ast = celWithBinding.compile("map_var_msg.key.single_int64").getAst(); + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum NativeSelectEvaluationTestCase { + POPULATED_PROTO3_MESSAGE( + "msg.single_nested_message.bb", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build()), + 42), + UNSET_INTERMEDIATE_PROTO3_MESSAGE( + "msg.single_nested_message.bb", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + 0), + PROTO2_CUSTOM_DEFAULT( + "proto2_msg.single_int64", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + -64L), + PROTO2_CUSTOM_DEFAULT_INT32( + "proto2_msg.single_int32", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + -32L), + PROTO2_CUSTOM_DEFAULT_UINT64( + "proto2_msg.single_uint64", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + UnsignedLong.valueOf(64)), + PROTO2_CUSTOM_DEFAULT_DOUBLE( + "proto2_msg.single_double", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + 6.4d), + PROTO2_CUSTOM_DEFAULT_BOOL( + "proto2_msg.single_bool", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + true), + PROTO2_CUSTOM_DEFAULT_STRING( + "proto2_msg.single_string", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + "empty"), + PROTO2_CUSTOM_DEFAULT_BYTES( + "proto2_msg.single_bytes", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + CelByteString.of("none".getBytes(UTF_8))), + PROTO3_DEFAULT_MAP( + "msg.map_string_string", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + ImmutableMap.of()), + PROTO3_DEFAULT_LIST( + "msg.repeated_int32", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + ImmutableList.of()), + PROTO3_DEFAULT_DURATION( + "msg.single_duration", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + Duration.ZERO), + PROTO3_DEFAULT_TIMESTAMP( + "msg.single_timestamp", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + Instant.EPOCH), + DEEPLY_NESTED_PROTO2_MESSAGE_POPULATED( + "nested_msg.child.payload.single_int64", + ImmutableMap.of( + "nested_msg", + NestedTestAllTypes.newBuilder() + .setChild( + NestedTestAllTypes.newBuilder() + .setPayload( + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt64(999L))) + .build()), + 999L), + DEEPLY_NESTED_PROTO2_MESSAGE_UNSET( + "nested_msg.child.payload.single_int64", + ImmutableMap.of("nested_msg", NestedTestAllTypes.getDefaultInstance()), + -64L), + HAS_FIELD_INTERMEDIATE_UNSET( + "has(msg.single_nested_message.bb)", + ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), + false), + HAS_FIELD_PROTO3_IMPLICIT_PRESENCE_DEFAULT( + "has(msg.single_nested_message.bb)", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(0)) + .build()), + false), + HAS_FIELD_PROTO3_FIELD_PRESENT( + "has(msg.single_nested_message.bb)", + ImmutableMap.of( + "msg", + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build()), + true), + HAS_FIELD_PROTO3_OPTIONAL_SCALAR_EXPLICIT_PRESENCE( + "has(msg.optional_bool)", + ImmutableMap.of("msg", TestAllTypes.newBuilder().setOptionalBool(false).build()), + true), + HAS_FIELD_PROTO3_OPTIONAL_SCALAR_UNSET( + "has(msg.optional_bool)", ImmutableMap.of("msg", TestAllTypes.getDefaultInstance()), false), + HAS_FIELD_PROTO2_SCALAR_SET_TO_DEFAULT( + "has(proto2_msg.single_int32)", + ImmutableMap.of( + "proto2_msg", + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder().setSingleInt32(-32).build()), + true), + HAS_FIELD_PROTO2_SCALAR_UNSET( + "has(proto2_msg.single_int32)", + ImmutableMap.of( + "proto2_msg", dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance()), + false), + SELECT_ON_MAP_VALUE_POPULATED( + "map_var_msg.key.single_nested_message.bb", + ImmutableMap.of( + "map_var_msg", + ImmutableMap.of( + "key", + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build())), + 42), + SELECT_ON_MAP_VALUE_UNSET( + "map_var_msg.key.single_nested_message.bb", + ImmutableMap.of("map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance())), + 0), + HAS_FIELD_ON_MAP_VALUE_PRESENT( + "has(map_var_msg.key.single_nested_message)", + ImmutableMap.of( + "map_var_msg", + ImmutableMap.of( + "key", + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build())), + true), + HAS_FIELD_ON_MAP_VALUE_ABSENT( + "has(map_var_msg.key.single_nested_message)", + ImmutableMap.of("map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance())), + false); - CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); - Object result = - celWithBinding - .createProgram(optimizedAst) - .eval( - ImmutableMap.of( - "map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance()))); + private final String expression; + private final ImmutableMap input; + private final Object expectedResult; - assertThat(result).isEqualTo(42L); - assertThat(optimizedAst.getSource().getExtensions()) - .contains(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + NativeSelectEvaluationTestCase( + String expression, ImmutableMap input, Object expectedResult) { + this.expression = expression; + this.input = input; + this.expectedResult = expectedResult; + } } @Test - public void optimizeAndEvaluate_withHasOnMapValue_evaluatesSuccessfully() throws Exception { - Cel celWithBinding = - cel.toCelBuilder() - .addFunctionDeclarations(SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL) - .addFunctionBindings( - CelFunctionBinding.from( - "cel_has_field_list", Object.class, List.class, (target, path) -> true)) - .build(); - CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) - .addAstOptimizers( - SelectOptimizer.newInstance( - SelectOptimizerOptions.newBuilder().build(), - TestAllTypes.getDescriptor().getFile())) - .build(); - CelAbstractSyntaxTree ast = - celWithBinding.compile("has(map_var_msg.key.single_nested_message)").getAst(); + public void optimizeAndEvaluate_nativeSelectAndHasField_matchesUnoptimized( + @TestParameter NativeSelectEvaluationTestCase testCase) throws Exception { + assumeTrue(runtimeFlavor == CelRuntimeFlavor.PLANNER); + CelAbstractSyntaxTree ast = cel.compile(testCase.expression).getAst(); + CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast); + Program unoptimizedProgram = cel.createProgram(ast); + Program optimizedProgram = cel.createProgram(optimizedAst); - CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); - Object result = - celWithBinding - .createProgram(optimizedAst) - .eval( - ImmutableMap.of( - "map_var_msg", ImmutableMap.of("key", TestAllTypes.getDefaultInstance()))); + Object unoptimizedResult = unoptimizedProgram.eval(testCase.input); + Object optimizedResult = optimizedProgram.eval(testCase.input); - assertThat(result).isEqualTo(true); - assertThat(optimizedAst.getSource().getExtensions()) - .contains(SelectOptimizer.SELECT_OPTIMIZATION_AST_EXTENSION_TAG); + assertThat(unoptimizedResult).isEqualTo(testCase.expectedResult); + assertThat(optimizedResult).isEqualTo(testCase.expectedResult); } - @Test - public void optimizeAndEvaluate_withMissingMapKey_throwsEvaluationException() throws Exception { - Cel celWithBinding = - cel.toCelBuilder() - .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL) - .addFunctionBindings( - CelFunctionBinding.from( - "cel_attribute_list", - ImmutableList.of(Object.class, List.class, Object.class), - args -> 42L)) - .build(); - CelOptimizer optimizer = - CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding) - .addAstOptimizers( - SelectOptimizer.newInstance( - SelectOptimizerOptions.newBuilder().build(), - TestAllTypes.getDescriptor().getFile())) - .build(); - CelAbstractSyntaxTree ast = celWithBinding.compile("map_var_msg.key.single_int64").getAst(); - - CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); - Program program = celWithBinding.createProgram(optimizedAst); - ImmutableMap input = ImmutableMap.of("map_var_msg", ImmutableMap.of()); - - CelEvaluationException exception = - assertThrows(CelEvaluationException.class, () -> program.eval(input)); - - assertThat(exception).hasMessageThat().contains("key 'key' is not present in map"); - } @Test public void options_toBuilder_preservesValues() { @@ -1039,4 +1126,25 @@ public void optimize_binaryOperationBetweenOptimizedSelects_resolvesSingleOverlo CelReference addReference = optimizedAst.getReferenceOrThrow(optimizedAst.getExpr().id()); assertThat(addReference.overloadIds()).containsExactly("add_int64"); } + + @Test + public void optimize_optionalSelect_passesThroughUntouched() throws Exception { + Cel celWithOptional = + cel.toCelBuilder() + .addCompilerLibraries(CelExtensions.optional()) + .addRuntimeLibraries(CelExtensions.optional()) + .build(); + CelOptimizer optimizer = + CelOptimizerFactory.standardCelOptimizerBuilder(celWithOptional) + .addAstOptimizers( + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), + TestAllTypes.getDescriptor().getFile())) + .build(); + CelAbstractSyntaxTree ast = celWithOptional.compile("msg.?single_nested_message.bb").getAst(); + + CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast); + + assertThat(optimizedAst.getExpr()).isEqualTo(ast.getExpr()); + } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index a8882f539..71a21ef6f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -40,6 +40,7 @@ java_library( ":eval_unary", ":eval_var_args_call", ":eval_zero_arity", + ":optimized_select_planner", ":planned_interpretable", ":planned_program", "//:auto_value", @@ -504,6 +505,24 @@ java_library( ], ) +java_library( + name = "optimized_select_planner", + srcs = ["OptimizedSelectPlanner.java"], + deps = [ + ":attribute", + ":eval_attribute", + ":planned_interpretable", + "//common/ast", + "//common/values", + "//common/values:cel_byte_string", + "//common/values:optimized_select_traversal", + "//common/values:select_field", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + java_library( name = "eval_or", srcs = ["EvalOr.java"], @@ -645,6 +664,7 @@ cel_android_library( ":eval_unary_android", ":eval_var_args_call_android", ":eval_zero_arity_android", + ":optimized_select_planner_android", ":planned_interpretable_android", ":planned_program_android", "//:auto_value", @@ -1110,6 +1130,24 @@ cel_android_library( ], ) +cel_android_library( + name = "optimized_select_planner_android", + srcs = ["OptimizedSelectPlanner.java"], + deps = [ + ":attribute_android", + ":eval_attribute_android", + ":planned_interpretable_android", + "//common/ast:ast_android", + "//common/values:cel_byte_string", + "//common/values:optimized_select_traversal_android", + "//common/values:select_field_android", + "//common/values:values_android", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + cel_android_library( name = "eval_or_android", srcs = ["EvalOr.java"], diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java index 56ea8a832..43bf5ca44 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -39,6 +39,10 @@ public EvalAttribute addQualifier(CelExpr expr, Qualifier qualifier) { return create(expr, newAttribute); } + Attribute attribute() { + return attr; + } + static EvalAttribute create(CelExpr expr, Attribute attr) { return new EvalAttribute(expr, attr); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java index 4122a6e8e..8cfd1e437 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOptionalSelectField.java @@ -43,8 +43,15 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { operandValue = opt.get(); } - Object runtimeOperandValue = celValueConverter.toRuntimeValue(operandValue); + // The operand arrives already adapted, so re-materializing it would re-scan every entry of a + // map for nothing. Traversing keeps that O(1); the selected value is materialized by the + // attribute below. + Object runtimeOperandValue = celValueConverter.toTraversalTarget(operandValue); if (runtimeOperandValue instanceof AccumulatedUnknowns) { + Object resultValue = EvalHelpers.evalStrictly(selectAttribute, resolver, frame); + if (resultValue instanceof AccumulatedUnknowns) { + return resultValue; + } return runtimeOperandValue; } @@ -56,6 +63,9 @@ Object evalInternal(GlobalResolver resolver, ExecutionFrame frame) { SelectableValue selectableValue = (SelectableValue) runtimeOperandValue; hasField = selectableValue.find(field).isPresent(); } else if (runtimeOperandValue instanceof Map) { + // A plain containsKey suffices: every producer of a map operand (attribute resolution, + // function dispatch, async completion, map construction) materializes it first, so an entry + // bound to an illegal Java null has already been rejected by then. hasField = ((Map) runtimeOperandValue).containsKey(field); } if (!hasField) { diff --git a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java index 1506eb180..af685d75d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/MaybeAttribute.java @@ -51,13 +51,12 @@ public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { @Override public Attribute addQualifier(Qualifier qualifier) { - Object strQualifier = qualifier.value(); ImmutableList.Builder augmentedNamesBuilder = ImmutableList.builder(); ImmutableList.Builder attributesBuilder = ImmutableList.builder(); for (NamespacedAttribute attr : attributes) { - if (strQualifier instanceof String && attr.qualifiers().isEmpty()) { + if (qualifier instanceof StringQualifier && attr.qualifiers().isEmpty()) { for (String varName : attr.candidateVariableNames()) { - augmentedNamesBuilder.add(varName + "." + strQualifier); + augmentedNamesBuilder.add(varName + "." + qualifier.value()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java index 01673923d..5228ea669 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/NamespacedAttribute.java @@ -182,21 +182,23 @@ public NamespacedAttribute addQualifier(Qualifier qualifier) { .build()); } - private static Object applyQualifiers( + static Object applyQualifiers( Object value, CelValueConverter celValueConverter, ImmutableList qualifiers) { if (value instanceof AccumulatedUnknowns) { return value; } - Object obj = celValueConverter.toRuntimeValue(value); - - // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier element = qualifiers.get(i); - obj = element.qualify(obj); - obj = celValueConverter.toRuntimeValue(obj); + // Each Qualifier accepts and returns a traversal target, so only the root operand needs + // adapting on the way in and only the terminal value is materialized on the way out. + Object obj = value; + if (!qualifiers.isEmpty()) { + obj = celValueConverter.toTraversalTarget(obj); + // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated + for (int i = 0; i < qualifiers.size(); i++) { + obj = qualifiers.get(i).qualify(obj); + } } - return celValueConverter.maybeUnwrap(obj); + return celValueConverter.maybeUnwrap(celValueConverter.toRuntimeValue(obj)); } private static Optional findPartialMatchingPattern( diff --git a/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java new file mode 100644 index 000000000..aa52550f0 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/OptimizedSelectPlanner.java @@ -0,0 +1,491 @@ +// 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. + +package dev.cel.runtime.planner; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelExpr.CelCall; +import dev.cel.common.ast.CelExpr.ExprKind.Kind; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.OptimizedSelectTraversal; +import dev.cel.common.values.OptionalValue; +import dev.cel.common.values.SelectField; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** + * Plans optimizer-rewritten select chains ({@code cel.@attribute}) and presence tests ({@code + * cel.@hasField}) into {@link EvalAttribute} instances decorated with {@link + * OptimizedSelectQualifier}. + */ +@Immutable +final class OptimizedSelectPlanner { + + static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute"; + static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField"; + + private final AttributeFactory attributeFactory; + private final CelValueConverter celValueConverter; + + /** + * Functional interface for planning an operand expression into a {@link PlannedInterpretable}. + */ + @FunctionalInterface + interface OperandPlanner { + PlannedInterpretable plan(CelExpr expr); + } + + PlannedInterpretable plan(CelExpr expr, String functionName, OperandPlanner operandPlanner) { + ImmutableList args = expr.call().args(); + ImmutableList selectFields; + boolean isPresenceTest; + switch (functionName) { + case CEL_ATTRIBUTE_FUNCTION_NAME: + checkArgument( + args.size() == 3, "Expected 3 arguments for %s, found %s", functionName, args.size()); + String typeIdent = extractQualifiedName(args.get(2)); + selectFields = unpackAttributeFields(args.get(1), typeIdent); + isPresenceTest = false; + break; + case CEL_HAS_FIELD_FUNCTION_NAME: + checkArgument( + args.size() == 2, "Expected 2 arguments for %s, found %s", functionName, args.size()); + selectFields = unpackHasFieldFields(args.get(1)); + isPresenceTest = true; + break; + default: + throw new IllegalArgumentException( + "Unsupported optimized select function: " + functionName); + } + + PlannedInterpretable operand = operandPlanner.plan(args.get(0)); + Attribute qualified = resolveBaseAttribute(operand); + + int lastIndex = selectFields.size() - 1; + for (int i = 0; i < lastIndex; i++) { + qualified = qualified.addQualifier(new PassthroughQualifier(selectFields.get(i).fieldName())); + } + qualified = + qualified.addQualifier( + new OptimizedSelectQualifier(selectFields, celValueConverter, isPresenceTest)); + return EvalAttribute.create(expr, qualified); + } + + private Attribute resolveBaseAttribute(PlannedInterpretable operand) { + if (operand instanceof EvalAttribute) { + return ((EvalAttribute) operand).attribute(); + } + return attributeFactory.newRelativeAttribute(operand); + } + + private static ImmutableList unpackAttributeFields( + CelExpr qualifiersExpr, String typeIdent) { + ImmutableList elements = unpackQualifierHops(qualifiersExpr); + ImmutableList.Builder fieldsBuilder = + ImmutableList.builderWithExpectedSize(elements.size()); + for (int i = 0; i < elements.size(); i++) { + boolean isLeaf = (i == elements.size() - 1); + CelExpr hopExpr = elements.get(i); + ImmutableList hopElements = unpackHopElements(hopExpr); + checkArgument( + hopElements.size() == 3 || hopElements.size() == 4, + "Expected qualifier hop for cel.@attribute to contain 3 or 4 elements, found: %s", + hopElements.size()); + checkArgument( + isLeaf || hopElements.size() == 3, + "Non-leaf qualifier hop must not contain a default value: %s", + hopExpr); + long fieldNumber = parseFieldNumber(hopElements.get(0)); + String fieldName = parseFieldName(hopElements.get(1)); + long rawTypeCode = parseTypeCode(hopElements.get(2)); + checkArgument( + SelectField.isSupportedTypeCode(rawTypeCode), + "Invalid protobuf type code: %s", + rawTypeCode); + checkArgument( + isLeaf || rawTypeCode == SelectField.MESSAGE_TYPE_CODE, + "Non-leaf qualifier hop must have MESSAGE type code (11), found: %s", + rawTypeCode); + Object defaultValue = + (hopElements.size() == 4) ? resolveDefaultValue(hopElements.get(3)) : null; + if (isLeaf) { + validateLeafTypeIdent((int) rawTypeCode, defaultValue, typeIdent); + } + fieldsBuilder.add(SelectField.create(fieldNumber, fieldName, rawTypeCode, defaultValue)); + } + return fieldsBuilder.build(); + } + + private static ImmutableList unpackHasFieldFields(CelExpr qualifiersExpr) { + ImmutableList elements = unpackQualifierHops(qualifiersExpr); + ImmutableList.Builder fieldsBuilder = + ImmutableList.builderWithExpectedSize(elements.size()); + for (CelExpr hopExpr : elements) { + ImmutableList hopElements = unpackHopElements(hopExpr); + checkArgument( + hopElements.size() == 2, + "Expected qualifier hop for cel.@hasField to contain 2 elements, found: %s", + hopElements.size()); + long fieldNumber = parseFieldNumber(hopElements.get(0)); + String fieldName = parseFieldName(hopElements.get(1)); + fieldsBuilder.add(SelectField.create(fieldNumber, fieldName)); + } + return fieldsBuilder.build(); + } + + private static ImmutableList unpackQualifierHops(CelExpr qualifiersExpr) { + checkArgument( + qualifiersExpr.getKind() == Kind.LIST, + "Expected qualifiers argument to be a list, found: %s", + qualifiersExpr.getKind()); + ImmutableList elements = qualifiersExpr.list().elements(); + checkArgument(!elements.isEmpty(), "Expected qualifiers list to be non-empty"); + return elements; + } + + private static ImmutableList unpackHopElements(CelExpr hopExpr) { + checkArgument( + hopExpr.getKind() == Kind.LIST, + "Expected qualifier hop to be a list, found: %s", + hopExpr.getKind()); + return hopExpr.list().elements(); + } + + private static long parseFieldNumber(CelExpr expr) { + checkArgument( + expr.getKind() == Kind.CONSTANT + && expr.constant().getKind() == CelConstant.Kind.INT64_VALUE, + "Expected qualifier hop field number to be an int64 constant, found: %s", + expr); + return expr.constant().int64Value(); + } + + private static String parseFieldName(CelExpr expr) { + checkArgument( + expr.getKind() == Kind.CONSTANT + && expr.constant().getKind() == CelConstant.Kind.STRING_VALUE, + "Expected qualifier hop field name to be a string constant, found: %s", + expr); + return expr.constant().stringValue(); + } + + private static long parseTypeCode(CelExpr expr) { + checkArgument( + expr.getKind() == Kind.CONSTANT + && expr.constant().getKind() == CelConstant.Kind.INT64_VALUE, + "Expected qualifier hop type code to be an int64 constant, found: %s", + expr); + return expr.constant().int64Value(); + } + + private static void validateLeafTypeIdent( + int leafTypeCode, @Nullable Object defaultValue, String typeIdent) { + checkArgument( + defaultValue != null || leafTypeCode == SelectField.MESSAGE_TYPE_CODE, + "Leaf hop with type code %s must specify a default value", + leafTypeCode); + checkArgument( + (leafTypeCode == SelectField.CEL_MAP_TYPE_CODE) == typeIdent.equals("map"), + "Leaf type code %s is incompatible with typeIdent '%s'", + leafTypeCode, + typeIdent); + checkArgument( + (defaultValue instanceof Map) == typeIdent.equals("map"), + "Leaf default value %s is incompatible with typeIdent '%s'", + defaultValue, + typeIdent); + checkArgument( + (defaultValue instanceof List) == typeIdent.equals("list"), + "Leaf default value %s is incompatible with typeIdent '%s'", + defaultValue, + typeIdent); + if (typeIdent.equals("map") || typeIdent.equals("list")) { + return; + } + if (leafTypeCode == SelectField.MESSAGE_TYPE_CODE) { + checkArgument( + !isScalarTypeIdent(typeIdent), + "Leaf MESSAGE type code (11) is incompatible with scalar typeIdent '%s'", + typeIdent); + boolean isWellKnownMessage = + typeIdent.equals("google.protobuf.Duration") + || typeIdent.equals("google.protobuf.Timestamp"); + checkArgument( + isWellKnownMessage == (defaultValue != null), + "Leaf default value for message type '%s' is invalid or missing: %s", + typeIdent, + defaultValue); + if (defaultValue != null) { + if (typeIdent.equals("google.protobuf.Duration")) { + checkArgument( + defaultValue.equals(Duration.ZERO), + "Leaf default value for Duration must be Duration.ZERO, found: %s", + defaultValue); + } else { + checkArgument( + defaultValue.equals(Instant.EPOCH), + "Leaf default value for Timestamp must be Instant.EPOCH, found: %s", + defaultValue); + } + } + return; + } + String expectedScalarIdent = expectedScalarTypeIdent(leafTypeCode); + checkArgument( + typeIdent.equals(expectedScalarIdent), + "Leaf type code %s (expected '%s') is incompatible with typeIdent '%s'", + leafTypeCode, + expectedScalarIdent, + typeIdent); + validateScalarDefaultValue(expectedScalarIdent, defaultValue); + } + + private static void validateScalarDefaultValue(String scalarTypeIdent, Object defaultValue) { + switch (scalarTypeIdent) { + case "bool": + checkArgument( + defaultValue instanceof Boolean, + "Leaf default value %s is incompatible with typeIdent 'bool'", + defaultValue); + break; + case "int": + checkArgument( + defaultValue instanceof Long, + "Leaf default value %s is incompatible with typeIdent 'int'", + defaultValue); + break; + case "uint": + checkArgument( + defaultValue instanceof UnsignedLong, + "Leaf default value %s is incompatible with typeIdent 'uint'", + defaultValue); + break; + case "double": + checkArgument( + defaultValue instanceof Double, + "Leaf default value %s is incompatible with typeIdent 'double'", + defaultValue); + break; + case "string": + checkArgument( + defaultValue instanceof String, + "Leaf default value %s is incompatible with typeIdent 'string'", + defaultValue); + break; + case "bytes": + checkArgument( + defaultValue instanceof CelByteString, + "Leaf default value %s is incompatible with typeIdent 'bytes'", + defaultValue); + break; + default: + throw new IllegalArgumentException("Unsupported scalar typeIdent: " + scalarTypeIdent); + } + } + + private static boolean isScalarTypeIdent(String typeIdent) { + switch (typeIdent) { + case "double": + case "int": + case "uint": + case "bool": + case "string": + case "bytes": + return true; + default: + return false; + } + } + + private static String expectedScalarTypeIdent(int leafTypeCode) { + switch (leafTypeCode) { + case 1: // DOUBLE + case 2: // FLOAT + return "double"; + case 3: // INT64 + case 5: // INT32 + case 14: // ENUM + case 15: // SFIXED32 + case 16: // SFIXED64 + case 17: // SINT32 + case 18: // SINT64 + return "int"; + case 4: // UINT64 + case 6: // FIXED64 + case 7: // FIXED32 + case 13: // UINT32 + return "uint"; + case 8: // BOOL + return "bool"; + case 9: // STRING + return "string"; + case 12: // BYTES + return "bytes"; + default: + throw new IllegalStateException("Unexpected leaf type code: " + leafTypeCode); + } + } + + private static Object resolveDefaultValue(CelExpr defaultExpr) { + switch (defaultExpr.getKind()) { + case CONSTANT: + return resolveConstant(defaultExpr.constant()); + case LIST: + if (defaultExpr.list().elements().isEmpty()) { + return ImmutableList.of(); + } + break; + case MAP: + if (defaultExpr.map().entries().isEmpty()) { + return ImmutableMap.of(); + } + break; + case CALL: + CelCall call = defaultExpr.call(); + if (call.function().equals("duration") + && call.args().size() == 1 + && call.args().get(0).getKind() == Kind.CONSTANT + && call.args().get(0).constant().getKind() == CelConstant.Kind.STRING_VALUE + && call.args().get(0).constant().stringValue().equals("0s")) { + return Duration.ZERO; + } + if (call.function().equals("timestamp") + && call.args().size() == 1 + && call.args().get(0).getKind() == Kind.CONSTANT + && call.args().get(0).constant().getKind() == CelConstant.Kind.INT64_VALUE + && call.args().get(0).constant().int64Value() == 0L) { + return Instant.EPOCH; + } + break; + default: + break; + } + throw new IllegalArgumentException("Unsupported default value expression: " + defaultExpr); + } + + private static Object resolveConstant(CelConstant celConstant) { + switch (celConstant.getKind()) { + case BOOLEAN_VALUE: + return celConstant.booleanValue(); + case INT64_VALUE: + return celConstant.int64Value(); + case UINT64_VALUE: + return celConstant.uint64Value(); + case DOUBLE_VALUE: + return celConstant.doubleValue(); + case STRING_VALUE: + return celConstant.stringValue(); + case BYTES_VALUE: + return celConstant.bytesValue(); + default: + throw new IllegalArgumentException("Unsupported kind: " + celConstant.getKind()); + } + } + + private static String extractQualifiedName(CelExpr expr) { + if (expr.getKind() == Kind.IDENT) { + return expr.ident().name(); + } + if (expr.getKind() == Kind.SELECT) { + return extractQualifiedName(expr.select().operand()) + "." + expr.select().field(); + } + throw new IllegalArgumentException( + "Expected type identifier argument to be an IDENT or SELECT, found: " + expr.getKind()); + } + + private static void validateTarget(Object target) { + if (target instanceof OptionalValue) { + throw new UnsupportedOperationException( + "Optional operands are not yet supported by the select-optimized runtime"); + } + } + + @Immutable + private static final class PassthroughQualifier implements Qualifier { + private final String fieldName; + + @Override + public Object value() { + return fieldName; + } + + /** + * Returns the operand unchanged: this qualifier exists only to contribute {@code fieldName} to + * the attribute path for unknown resolution. The operand is already a traversal target, so + * returning it verbatim upholds the {@link Qualifier#qualify} contract. + */ + @Override + public Object qualify(Object operand) { + return operand; + } + + private PassthroughQualifier(String fieldName) { + this.fieldName = checkNotNull(fieldName); + } + } + + @Immutable + private static final class OptimizedSelectQualifier implements Qualifier { + private final ImmutableList fields; + private final CelValueConverter celValueConverter; + private final boolean isPresenceTest; + + @Override + public Object value() { + return fields.get(fields.size() - 1).fieldName(); + } + + @Override + public Object qualify(Object operand) { + validateTarget(operand); + if (isPresenceTest) { + // A boolean is already a valid traversal target. + return OptimizedSelectTraversal.hasField(operand, fields); + } + return celValueConverter.toTraversalTarget(OptimizedSelectTraversal.qualify(operand, fields)); + } + + private OptimizedSelectQualifier( + ImmutableList fields, + CelValueConverter celValueConverter, + boolean isPresenceTest) { + this.fields = checkNotNull(fields); + this.celValueConverter = checkNotNull(celValueConverter); + this.isPresenceTest = isPresenceTest; + } + } + + static OptimizedSelectPlanner create( + AttributeFactory attributeFactory, CelValueConverter celValueConverter) { + return new OptimizedSelectPlanner(attributeFactory, celValueConverter); + } + + private OptimizedSelectPlanner( + AttributeFactory attributeFactory, CelValueConverter celValueConverter) { + this.attributeFactory = checkNotNull(attributeFactory); + this.celValueConverter = checkNotNull(celValueConverter); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java index a93ec74b7..24e2ef5df 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PresenceTestQualifier.java @@ -17,6 +17,7 @@ import static dev.cel.runtime.planner.MissingAttribute.newMissingField; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.SelectableValue; import java.util.Map; @@ -32,14 +33,21 @@ public Object value() { return value; } + /** + * Returns a boolean, or a {@link MissingAttribute} sentinel when the operand cannot be presence + * tested. Both are already traversal targets, so no adaptation is required. + * + *

Throws {@code CelInvalidArgumentException} when the key is bound to an illegal Java {@code + * null}. The type is named in prose rather than an {@code @throws} tag so that this package need + * not depend on the exception target purely for documentation. + */ @Override @SuppressWarnings("unchecked") // SelectableValue cast is safe - public Object qualify(Object obj) { - if (obj instanceof SelectableValue) { - return ((SelectableValue) obj).find(value).isPresent(); - } else if (obj instanceof Map) { - Map map = (Map) obj; - return map.containsKey(value); + public Object qualify(Object operand) { + if (operand instanceof SelectableValue) { + return ((SelectableValue) operand).find(value).isPresent(); + } else if (operand instanceof Map) { + return CelValueConverter.containsMapKey((Map) operand, value); } return newMissingField(value.toString()); diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index 47b7cf552..3eed3329c 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -75,6 +75,7 @@ public final class ProgramPlanner { private final CelOptions options; private final CelValueConverter celValueConverter; private final ImmutableSet lateBoundFunctionNames; + private final OptimizedSelectPlanner optimizedSelectPlanner; // CelAsyncEvaluationOptions is an immutable value object. @SuppressWarnings("Immutable") @@ -137,38 +138,44 @@ private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) { CelSelect select = celExpr.select(); PlannedInterpretable operand = plan(select.operand(), ctx); - InterpretableAttribute attribute; - if (operand instanceof EvalAttribute) { - attribute = (EvalAttribute) operand; - } else { - attribute = EvalAttribute.create(celExpr, attributeFactory.newRelativeAttribute(operand)); - } + InterpretableAttribute attribute = EvalAttribute.create(celExpr, resolveBaseAttribute(operand)); if (select.testOnly()) { attribute = EvalTestOnly.create(celExpr, attribute); } - Qualifier qualifier = StringQualifier.create(select.field()); + Qualifier qualifier = StringQualifier.create(select.field(), celValueConverter); return attribute.addQualifier(celExpr, qualifier); } + private Attribute resolveBaseAttribute(PlannedInterpretable operand) { + if (operand instanceof EvalAttribute) { + return ((EvalAttribute) operand).attribute(); + } + return attributeFactory.newRelativeAttribute(operand); + } + private PlannedInterpretable planConstant(CelExpr expr, CelConstant celConstant) { + return EvalConstant.create(expr, resolveConstant(celConstant)); + } + + private static Object resolveConstant(CelConstant celConstant) { switch (celConstant.getKind()) { case NULL_VALUE: - return EvalConstant.create(expr, celConstant.nullValue()); + return celConstant.nullValue(); case BOOLEAN_VALUE: - return EvalConstant.create(expr, celConstant.booleanValue()); + return celConstant.booleanValue(); case INT64_VALUE: - return EvalConstant.create(expr, celConstant.int64Value()); + return celConstant.int64Value(); case UINT64_VALUE: - return EvalConstant.create(expr, celConstant.uint64Value()); + return celConstant.uint64Value(); case DOUBLE_VALUE: - return EvalConstant.create(expr, celConstant.doubleValue()); + return celConstant.doubleValue(); case STRING_VALUE: - return EvalConstant.create(expr, celConstant.stringValue()); + return celConstant.stringValue(); case BYTES_VALUE: - return EvalConstant.create(expr, celConstant.bytesValue()); + return celConstant.bytesValue(); default: throw new IllegalStateException("Unsupported kind: " + celConstant.getKind()); } @@ -249,6 +256,16 @@ private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { ResolvedFunction resolvedFunction = resolveFunction(expr, ctx.referenceMap()); String functionName = resolvedFunction.functionName(); + // Intercept optimizer-rewritten select chains (cel.@attribute and cel.@hasField) for direct + // traversal via OptimizedSelectTraversal (proto field number lookup on OptimizedSelectable, + // map key lookup on Map, and SelectableValue fallback). If the caller registered a custom + // overload for either function in the dispatcher, defer to normal function dispatch. + if ((functionName.equals(OptimizedSelectPlanner.CEL_ATTRIBUTE_FUNCTION_NAME) + || functionName.equals(OptimizedSelectPlanner.CEL_HAS_FIELD_FUNCTION_NAME)) + && !hasCustomOverload(resolvedFunction, functionName)) { + return optimizedSelectPlanner.plan(expr, functionName, operandExpr -> plan(operandExpr, ctx)); + } + CelExpr target = resolvedFunction.target().orElse(null); int argCount = expr.call().args().size(); if (target != null) { @@ -411,14 +428,9 @@ private Optional maybeInterceptOptionalCalls( if (functionName.equals(Operator.OPTIONAL_SELECT.getFunction())) { String field = expr.call().args().get(1).constant().stringValue(); - InterpretableAttribute attribute; - if (evaluatedArgs[0] instanceof EvalAttribute) { - attribute = (EvalAttribute) evaluatedArgs[0]; - } else { - attribute = - EvalAttribute.create(expr, attributeFactory.newRelativeAttribute(evaluatedArgs[0])); - } - Qualifier qualifier = StringQualifier.create(field); + InterpretableAttribute attribute = + EvalAttribute.create(expr, resolveBaseAttribute(evaluatedArgs[0])); + Qualifier qualifier = StringQualifier.create(field, celValueConverter); PlannedInterpretable selectAttribute = attribute.addQualifier(expr, qualifier); return Optional.of( @@ -634,6 +646,11 @@ private Optional toQualifiedName(CelExpr operand) { return Optional.empty(); } + private boolean hasCustomOverload(ResolvedFunction resolvedFunction, String functionName) { + return resolvedFunction.overloadId().flatMap(dispatcher::findOverload).isPresent() + || dispatcher.findOverload(functionName).isPresent(); + } + @AutoValue abstract static class ResolvedFunction { @@ -765,5 +782,7 @@ private ProgramPlanner( this.asyncExecutor = asyncExecutor; this.attributeFactory = AttributeFactory.newAttributeFactory(container, typeProvider, celValueConverter); + this.optimizedSelectPlanner = + OptimizedSelectPlanner.create(attributeFactory, celValueConverter); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java index 82e48e95a..c277cb8aa 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/Qualifier.java @@ -22,7 +22,26 @@ */ @Immutable interface Qualifier { + /** + * The key this step qualifies by. + * + *

Every qualifier on this path keys by {@code String}: index expressions such as {@code m[1]} + * are planned as function dispatches instead of qualifiers. Map lookups below therefore read a + * raw {@code java.util.Map} without normalizing the key. Introducing a qualifier over a + * non-string key would break that, because a normalized CEL {@code Long} would silently miss in a + * {@code Map} supplied by the caller. + */ Object value(); - Object qualify(Object value); + /** + * Applies this qualification step to {@code operand} and returns the qualified value. + * + *

{@code operand} is a traversal target, and implementations must return a traversal target so + * that the next step in the chain can consume it directly (see {@link + * dev.cel.common.values.CelValueConverter#toTraversalTarget}). An implementation that surfaces a + * value straight out of a backing container, such as a raw {@code java.util.Map} entry, must + * therefore adapt it. An implementation that returns its operand unchanged, or that already + * produces a {@code CelValue}, need not. + */ + Object qualify(Object operand); } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java index 38f733c79..2293d2057 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/RelativeAttribute.java @@ -17,7 +17,6 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; import dev.cel.common.values.CelValueConverter; -import dev.cel.runtime.AccumulatedUnknowns; import dev.cel.runtime.GlobalResolver; /** @@ -34,20 +33,7 @@ final class RelativeAttribute implements Attribute { @Override public Object resolve(long exprId, GlobalResolver ctx, ExecutionFrame frame) { Object obj = EvalHelpers.evalStrictly(operand, ctx, frame); - if (obj instanceof AccumulatedUnknowns) { - return obj; - } - - obj = celValueConverter.toRuntimeValue(obj); - - // Avoid enhanced for loop to prevent UnmodifiableIterator from being allocated - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier element = qualifiers.get(i); - obj = element.qualify(obj); - obj = celValueConverter.toRuntimeValue(obj); - } - - return celValueConverter.maybeUnwrap(obj); + return NamespacedAttribute.applyQualifiers(obj, celValueConverter, qualifiers); } @Override diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java index 21a4b6721..54ff31a2d 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/StringQualifier.java @@ -16,6 +16,7 @@ import com.google.errorprone.annotations.Immutable; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.OptionalValue; import dev.cel.common.values.SelectableValue; import java.util.Map; @@ -25,6 +26,7 @@ final class StringQualifier implements Qualifier { private final String value; + private final CelValueConverter celValueConverter; @Override public String value() { @@ -32,8 +34,14 @@ public String value() { } @Override + public Object qualify(Object operand) { + // Single exit point: the map branch below surfaces a raw entry, so the result is adapted here + // rather than in each branch, which keeps the traversal-target contract impossible to miss. + return celValueConverter.toTraversalTarget(select(operand)); + } + @SuppressWarnings("unchecked") // Qualifications on maps/structs must be a string - public Object qualify(Object obj) { + private Object select(Object obj) { if (obj instanceof OptionalValue) { OptionalValue opt = (OptionalValue) obj; if (!opt.isZeroValue()) { @@ -49,29 +57,19 @@ public Object qualify(Object obj) { } if (obj instanceof Map) { - Map map = (Map) obj; - Object mapVal = map.get(value); - - if (mapVal != null) { - return mapVal; - } - - if (!map.containsKey(value)) { - throw CelAttributeNotFoundException.forMissingMapKey(value); - } - - throw CelAttributeNotFoundException.of( - String.format("Map value cannot be null for key: %s", value)); + return CelValueConverter.findMapValue((Map) obj, value) + .orElseThrow(() -> CelAttributeNotFoundException.forMissingMapKey(value)); } throw CelAttributeNotFoundException.forFieldResolution(value); } - static StringQualifier create(String value) { - return new StringQualifier(value); + static StringQualifier create(String value, CelValueConverter celValueConverter) { + return new StringQualifier(value, celValueConverter); } - private StringQualifier(String value) { + private StringQualifier(String value, CelValueConverter celValueConverter) { this.value = value; + this.celValueConverter = celValueConverter; } } diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel index 53240ff87..998d6d49a 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -23,7 +23,9 @@ java_library( "//common:error_codes", "//common:options", "//common/ast", + "//common/exceptions:attribute_not_found", "//common/exceptions:divide_by_zero", + "//common/exceptions:invalid_argument", "//common/exceptions:runtime_exception", "//common/internal:cel_descriptor_pools", "//common/internal:default_message_factory", diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index ebf8e1cdb..d5647c6a7 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -41,7 +41,9 @@ import dev.cel.common.CelSource; import dev.cel.common.ast.CelConstant; import dev.cel.common.ast.CelExpr; +import dev.cel.common.exceptions.CelAttributeNotFoundException; import dev.cel.common.exceptions.CelDivideByZeroException; +import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.common.internal.CelDescriptorPool; import dev.cel.common.internal.DefaultDescriptorPool; import dev.cel.common.internal.DefaultMessageFactory; @@ -61,6 +63,7 @@ import dev.cel.common.values.CelValueConverter; import dev.cel.common.values.CelValueProvider; import dev.cel.common.values.NullValue; +import dev.cel.common.values.OptionalValue; import dev.cel.common.values.ProtoCelValueConverter; import dev.cel.common.values.ProtoMessageValueProvider; import dev.cel.compiler.CelCompiler; @@ -87,6 +90,11 @@ import dev.cel.runtime.RuntimeEquality; import dev.cel.runtime.RuntimeHelpers; import dev.cel.runtime.standard.TypeFunction; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -1574,6 +1582,505 @@ public void plan_variableAsCelUnknownSet_propagatesUnknown() throws Exception { .isEqualTo(CelUnknownSet.create(CelAttribute.create("custom_msg"))); } + @Test + public void plan_select_javaMapScalarLeaf_normalizesIntegerToLong() throws Exception { + ImmutableMap vars = ImmutableMap.of("map_var", ImmutableMap.of("int_key", 42)); + CelAbstractSyntaxTree ast = compile("map_var.int_key"); + + Object result = PLANNER.plan(ast).eval(vars); + + // Asserted by type rather than value: Truth's Subject#isEqualTo treats a boxed Integer and + // Long holding the same value as equal, so isEqualTo(42L) alone would not detect a miss. + assertThat(result).isInstanceOf(Long.class); + assertThat(result).isEqualTo(42L); + } + + @Test + public void plan_select_javaMapLeaf_normalizesMapEntriesToLong() throws Exception { + ImmutableMap innerMap = ImmutableMap.of("int_key", 42); + ImmutableMap vars = + ImmutableMap.of("map_var", ImmutableMap.of("inner", innerMap)); + CelAbstractSyntaxTree ast = compile("map_var.inner"); + + Object result = PLANNER.plan(ast).eval(vars); + + assertThat(result).isEqualTo(ImmutableMap.of("int_key", 42L)); + } + + @Test + public void plan_ident_javaMapRoot_normalizesMapEntriesToLong() throws Exception { + ImmutableMap vars = ImmutableMap.of("map_var", ImmutableMap.of("int_key", 42)); + CelAbstractSyntaxTree ast = compile("map_var"); + + Object result = PLANNER.plan(ast).eval(vars); + + assertThat(result).isEqualTo(ImmutableMap.of("int_key", 42L)); + } + + @Test + public void plan_celHasField_optionalRootOperand_throwsUnsupportedOperationException() + throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@hasField(optional_var, [[1, 'single_int32']])"); + Program program = PLANNER.plan(ast); + ImmutableMap input = + ImmutableMap.of("optional_var", OptionalValue.create(TestAllTypes.getDefaultInstance())); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(UnsupportedOperationException.class); + } + + @Test + public void plan_celHasField_nonSelectableIntermediate_throwsCelAttributeNotFoundException() + throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@hasField(msg, [[1, 'single_int32'], [2, 'nested_field']])"); + Program program = PLANNER.plan(ast); + ImmutableMap input = + ImmutableMap.of("msg", TestAllTypes.newBuilder().setSingleInt32(42).build()); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class); + } + + @Test + public void plan_presenceTest_javaMapWithAbsentKey_returnsFalse() throws Exception { + Program program = PLANNER.plan(compile("has(map_var.absent_key)")); + + Object result = program.eval(ImmutableMap.of("map_var", ImmutableMap.of("present", "value"))); + + assertThat(result).isEqualTo(false); + } + + @Test + public void plan_presenceTest_javaMapWithNullValue_throwsCelInvalidArgumentException() + throws Exception { + Map mapWithNull = new HashMap<>(); + mapWithNull.put("null_leaf", null); + Program program = PLANNER.plan(compile("has(map_var.null_leaf)")); + ImmutableMap input = ImmutableMap.of("map_var", mapWithNull); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(CelInvalidArgumentException.class); + assertThat(e).hasMessageThat().contains("Map value cannot be null for key: null_leaf"); + } + + @Test + public void plan_optionalSelect_javaMapWithNullValue_throwsCelInvalidArgumentException() + throws Exception { + // End-to-end behavior only: the operand attribute is materialized before the optional select + // inspects it, so the illegal entry is rejected during adaptation rather than by the presence + // test itself. + Map mapWithNull = new HashMap<>(); + mapWithNull.put("null_leaf", null); + Program program = PLANNER.plan(compile("map_var.?null_leaf")); + ImmutableMap input = ImmutableMap.of("map_var", mapWithNull); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(CelInvalidArgumentException.class); + assertThat(e).hasMessageThat().contains("Map value cannot be null for key: null_leaf"); + } + + @Test + public void plan_optionalSelect_javaMapWithAbsentKey_returnsEmpty() throws Exception { + // The operand attribute is materialized rather than traversed, so this map must be free of + // illegal entries for the absent-key path to be reachable at all. + Program program = PLANNER.plan(compile("map_var.?absent_key")); + + Object result = program.eval(ImmutableMap.of("map_var", ImmutableMap.of("present", "value"))); + + assertThat(result).isEqualTo(Optional.empty()); + } + + @Test + public void plan_celHasField_maybeAttributeCandidatePrecedence_knownHigherPriorityWins() + throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst("cel.@hasField(b, [[1, 'single_int32']])"); + Program program = PLANNER.plan(ast); + TestAllTypes knownMsg = TestAllTypes.newBuilder().setSingleInt32(99).build(); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("cel.expr.conformance.proto3.b", knownMsg), + CelAttributePattern.create("b") + .qualify(CelAttribute.Qualifier.ofString("single_int32"))); + + Object result = program.eval(partialVars); + + assertThat(result).isEqualTo(true); + } + + @Test + public void + plan_celHasField_maybeAttributeCandidatePrecedence_unknownHigherPriorityWinsOverKnownFallback() + throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst("cel.@hasField(b, [[1, 'single_int32']])"); + Program program = PLANNER.plan(ast); + TestAllTypes knownMsg = TestAllTypes.newBuilder().setSingleInt32(99).build(); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("b", knownMsg), + CelAttributePattern.fromQualifiedIdentifier( + "cel.expr.conformance.proto3.b.single_int32")); + + Object result = program.eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) result).attributes()) + .containsExactly( + CelAttribute.fromQualifiedIdentifier("cel.expr.conformance.proto3.b.single_int32")); + } + + @Test + public void plan_celHasField_maybeAttributeCandidatePrecedence_unknownReturnedWhenHigherAbsent() + throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst("cel.@hasField(b, [[1, 'single_int32']])"); + Program program = PLANNER.plan(ast); + PartialVars partialVars = + PartialVars.of( + CelAttributePattern.create("b") + .qualify(CelAttribute.Qualifier.ofString("single_int32"))); + + Object result = program.eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + } + + @Test + public void plan_celAttribute_missingMapKey_throwsEvaluationException() throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@attribute(map_var.key, [[2, 'single_int64', 3, 0]], int)"); + Program program = PLANNER.plan(ast); + ImmutableMap input = ImmutableMap.of("map_var", ImmutableMap.of()); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + assertThat(e).hasMessageThat().contains("key 'key' is not present in map"); + } + + @Test + public void plan_celAttribute_nullBoundMapKey_throwsEvaluationException() throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@attribute(map_var.null_key, [[2, 'single_int64', 3, 0]], int)"); + Program program = PLANNER.plan(ast); + Map mapWithNull = new HashMap<>(); + mapWithNull.put("null_key", null); + ImmutableMap input = ImmutableMap.of("map_var", mapWithNull); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(input)); + + assertThat(e).hasCauseThat().isInstanceOf(CelInvalidArgumentException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT); + assertThat(e).hasMessageThat().contains("Map value cannot be null for key: null_key"); + } + + @Test + public void plan_celAttribute_unboundRootVariable_throwsEvaluationException() throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@attribute(msg, [[2, 'single_int64', 3, 0]], int)"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + + assertThat(e).hasCauseThat().isInstanceOf(CelAttributeNotFoundException.class); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + assertThat(e).hasMessageThat().contains("msg"); + } + + @Test + public void plan_celAttribute_withPartialVarsTargetUnknown_returnsUnknown() throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst( + "cel.@attribute(msg, [[21, 'single_nested_message', 11], [1, 'bb', 5, 0]], int)"); + Program program = PLANNER.plan(ast); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build(); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("msg", msg), + CelAttributePattern.fromQualifiedIdentifier("msg.single_nested_message.bb")); + + Object result = program.eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) result).attributes()) + .containsExactly(CelAttribute.fromQualifiedIdentifier("msg.single_nested_message.bb")); + } + + @Test + public void plan_celAttribute_withPartialVarsSiblingUnknown_evaluatesSuccessfully() + throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst( + "cel.@attribute(msg, [[21, 'single_nested_message', 11], [1, 'bb', 5, 0]], int)"); + Program program = PLANNER.plan(ast); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleInt64(10L) + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build(); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("msg", msg), + CelAttributePattern.fromQualifiedIdentifier("msg.single_int64")); + + Object result = program.eval(partialVars); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void plan_celHasField_errorInOperand_propagatesError() throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst("cel.@hasField(error(), [[1, 'single_int64']])"); + Program program = PLANNER.plan(ast); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> program.eval(ImmutableMap.of())); + + assertThat(e).hasMessageThat().contains("error"); + } + + @Test + public void plan_optionalSelect_onOptimizedPrefix_withPartialVarsUnknown_returnsUnknown() + throws Exception { + CelAbstractSyntaxTree prefixAst = + parseSelectAst( + "cel.@attribute(msg, [[21, 'single_nested_message', 11]]," + + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage)"); + CelAbstractSyntaxTree wrapperAst = CEL_COMPILER.parse("dummy.?bb").getAst(); + CelExpr selectCall = wrapperAst.getExpr(); + CelExpr combinedExpr = + selectCall.toBuilder() + .setCall(selectCall.call().toBuilder().setArg(0, prefixAst.getExpr()).build()) + .build(); + CelAbstractSyntaxTree combinedAst = + CelAbstractSyntaxTree.newParsedAst(combinedExpr, wrapperAst.getSource()); + Program program = PLANNER.plan(combinedAst); + TestAllTypes msg = + TestAllTypes.newBuilder() + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(42)) + .build(); + PartialVars partialVars = + PartialVars.of( + ImmutableMap.of("msg", msg), + CelAttributePattern.fromQualifiedIdentifier("msg.single_nested_message.bb")); + + Object result = program.eval(partialVars); + + assertThat(result).isInstanceOf(CelUnknownSet.class); + assertThat(((CelUnknownSet) result).attributes()) + .containsExactly(CelAttribute.fromQualifiedIdentifier("msg.single_nested_message.bb")); + } + + @Test + public void plan_celAttribute_invalidAstIntegrity_throwsIllegalArgumentException( + @TestParameter InvalidSelectAstTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst(testCase.expression); + + CelEvaluationException e = assertThrows(CelEvaluationException.class, () -> PLANNER.plan(ast)); + + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(e).hasMessageThat().contains(testCase.expectedErrorSubstring); + } + + @Test + public void plan_celAttribute_customOverload_bypassesOptimizedSelectPlanner() throws Exception { + DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); + builder.addOverload( + "cel.@attribute", + "cel.@attribute", + ImmutableList.of(Object.class, Object.class, Object.class), + /* isStrict= */ false, + args -> "custom_attribute_called"); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + builder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@attribute(msg, [[2, 'single_int64', 3, 0]], int)"); + + Program program = planner.plan(ast); + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo("custom_attribute_called"); + } + + @Test + public void plan_celHasField_customOverload_bypassesOptimizedSelectPlanner() throws Exception { + DefaultDispatcher.Builder builder = DefaultDispatcher.newBuilder(); + builder.addOverload( + "cel.@hasField", + "cel.@hasField", + ImmutableList.of(Object.class, Object.class), + /* isStrict= */ false, + args -> true); + ProgramPlanner planner = + ProgramPlanner.newPlanner( + TYPE_PROVIDER, + VALUE_PROVIDER, + builder.build(), + CEL_VALUE_CONVERTER, + CEL_CONTAINER, + CEL_OPTIONS, + ImmutableSet.of(), + CelAsyncEvaluationOptions.defaultOptions(), + /* asyncExecutor= */ null); + CelAbstractSyntaxTree ast = parseSelectAst("cel.@hasField(msg, [[2, 'single_int64']])"); + + Program program = planner.plan(ast); + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(true); + } + + @Test + public void plan_celAttribute_populatedField_returnsPopulatedValue() throws Exception { + CelAbstractSyntaxTree ast = + parseSelectAst("cel.@attribute(msg, [[2, 'single_int64', 3, 0]], int)"); + Program program = PLANNER.plan(ast); + TestAllTypes msg = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + + Object result = program.eval(ImmutableMap.of("msg", msg)); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void plan_celAttribute_defaultValues_returnsExpectedDefault( + @TestParameter DefaultValueTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = parseSelectAst(testCase.expression); + Program program = PLANNER.plan(ast); + + Object result = program.eval(ImmutableMap.of("msg", TestAllTypes.getDefaultInstance())); + + assertThat(result).isEqualTo(testCase.expected); + } + + private static CelAbstractSyntaxTree parseSelectAst(String expression) throws Exception { + String parseable = + expression + .replace("cel.@attribute", "cel_attribute") + .replace("cel.@hasField", "cel_has_field"); + CelAbstractSyntaxTree parsed = CEL_COMPILER.parse(parseable).getAst(); + CelExpr root = parsed.getExpr(); + String targetFn = + root.call().function().equals("cel_attribute") ? "cel.@attribute" : "cel.@hasField"; + CelExpr rewrittenRoot = + root.toBuilder().setCall(root.call().toBuilder().setFunction(targetFn).build()).build(); + return CelAbstractSyntaxTree.newParsedAst(rewrittenRoot, parsed.getSource()); + } + + /** + * Malformed {@code cel.@attribute} / {@code cel.@hasField} ASTs. + * + *

{@code SelectOptimizer} is the only in-tree producer of these calls and cannot emit a + * malformed hop, so these cases are unreachable from any top-level API. The plan-time validation + * exists because the call is a serialized contract carried in a checked AST, which may be + * persisted and re-planned by a binary that did not produce it. + * + *

This list is deliberately one case per validation category rather than exhaustive: + * enumerating every rejection would pin the exact error copy of an internal contract without + * covering any additional behavior. + */ + private enum InvalidSelectAstTestCase { + WRONG_ARG_COUNT( + "cel.@attribute(msg, [[1, 'single_int32', 5]])", + "Expected 3 arguments for cel.@attribute, found 2"), + MALFORMED_HOP( + "cel.@attribute(msg, [[1, 'single_int32']], int)", + "Expected qualifier hop for cel.@attribute to contain 3 or 4 elements"), + TYPE_CODE_IDENT_MISMATCH( + "cel.@attribute(msg, [[1, 'single_int32', 5, 0]], string)", + "Leaf type code 5 (expected 'int') is incompatible with typeIdent 'string'"), + MESSAGE_WITH_SCALAR_TYPE_IDENT( + "cel.@attribute(msg, [[1, 'single_int32', 11]], int)", + "Leaf MESSAGE type code (11) is incompatible with scalar typeIdent 'int'"), + NON_LEAF_HOP_WITH_DEFAULT_VALUE( + "cel.@attribute(msg, [[21, 'single_nested_message', 11, 0], [1, 'bb', 5, 0]], int)", + "Non-leaf qualifier hop must not contain a default value"), + NON_LEAF_HOP_NOT_MESSAGE( + "cel.@attribute(msg, [[21, 'single_nested_message', 5], [1, 'bb', 5, 0]], int)", + "Non-leaf qualifier hop must have MESSAGE type code (11)"), + SCALAR_DEFAULT_VALUE_TYPE_MISMATCH( + "cel.@attribute(msg, [[1, 'single_int32', 5, 'bogus']], int)", + "Leaf default value bogus is incompatible with typeIdent 'int'"), + DURATION_MISSING_DEFAULT_VALUE( + "cel.@attribute(msg, [[18, 'single_duration', 11]], google.protobuf.Duration)", + "Leaf default value for message type 'google.protobuf.Duration' is invalid or missing:" + + " null"), + TIMESTAMP_MISSING_DEFAULT_VALUE( + "cel.@attribute(msg, [[19, 'single_timestamp', 11]], google.protobuf.Timestamp)", + "Leaf default value for message type 'google.protobuf.Timestamp' is invalid or missing:" + + " null"), + MESSAGE_UNEXPECTED_DEFAULT_VALUE( + "cel.@attribute(msg, [[21, 'single_nested_message', 11, 0]]," + + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage)", + "Leaf default value for message type" + + " 'cel.expr.conformance.proto3.TestAllTypes.NestedMessage' is invalid or missing: 0"), + HAS_FIELD_WRONG_ARG_COUNT( + "cel.@hasField(msg)", "Expected 2 arguments for cel.@hasField, found 1"), + HAS_FIELD_MALFORMED_HOP( + "cel.@hasField(msg, [[1]])", + "Expected qualifier hop for cel.@hasField to contain 2 elements"); + + private final String expression; + private final String expectedErrorSubstring; + + InvalidSelectAstTestCase(String expression, String expectedErrorSubstring) { + this.expression = expression; + this.expectedErrorSubstring = expectedErrorSubstring; + } + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum DefaultValueTestCase { + BOOL("cel.@attribute(msg, [[13, 'single_bool', 8, false]], bool)", false), + STRING("cel.@attribute(msg, [[14, 'single_string', 9, '']], string)", ""), + BYTES("cel.@attribute(msg, [[15, 'single_bytes', 12, b'']], bytes)", CelByteString.EMPTY), + INT64("cel.@attribute(msg, [[2, 'single_int64', 3, 0]], int)", 0L), + UINT64("cel.@attribute(msg, [[4, 'single_uint64', 4, 0u]], uint)", UnsignedLong.ZERO), + DOUBLE("cel.@attribute(msg, [[1, 'single_double', 1, 0.0]], double)", 0.0d), + LIST("cel.@attribute(msg, [[32, 'repeated_int64', 3, []]], list)", ImmutableList.of()), + MAP("cel.@attribute(msg, [[61, 'map_string_string', -1, {}]], map)", ImmutableMap.of()), + DURATION( + "cel.@attribute(msg, [[18, 'single_duration', 11, duration('0s')]]," + + " google.protobuf.Duration)", + Duration.ZERO), + TIMESTAMP( + "cel.@attribute(msg, [[19, 'single_timestamp', 11, timestamp(0)]]," + + " google.protobuf.Timestamp)", + Instant.EPOCH); + + private final String expression; + private final Object expected; + + DefaultValueTestCase(String expression, Object expected) { + this.expression = expression; + this.expected = expected; + } + } + private CelAbstractSyntaxTree compile(String expression) throws Exception { return compile(CEL_COMPILER, expression); }