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..7073686b0 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -434,3 +434,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/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java new file mode 100644 index 000000000..9fb08a0a2 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java @@ -0,0 +1,120 @@ +// 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++) { + // Invariant: Select optimization is only applied to structs (proto messages). Maps and + // repeated fields are excluded from optimized field selection, so an absent intermediate + // field is guaranteed to be an unset message field rather than a missing map key or + // invalid field access, safely short-circuiting to false. + 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/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..6b83f0a60 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -29,10 +29,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/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/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"], +)