Skip to content

Commit 4c775f3

Browse files
l46kokcopybara-github
authored andcommitted
Add an interface for selectable values
PiperOrigin-RevId: 587152574
1 parent 5cf5755 commit 4c775f3

File tree

11 files changed

+214
-160
lines changed

11 files changed

+214
-160
lines changed

common/src/main/java/dev/cel/common/values/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ CEL_VALUES_SOURCES = [
2424
"NullValue.java",
2525
"OpaqueValue.java",
2626
"OptionalValue.java",
27+
"SelectableValue.java",
2728
"StringValue.java",
2829
"StructValue.java",
2930
"TimestampValue.java",
@@ -72,7 +73,9 @@ java_library(
7273
":cel_byte_string",
7374
":cel_value",
7475
"//:auto_value",
76+
"//common:error_codes",
7577
"//common:options",
78+
"//common:runtime_exception",
7679
"//common/annotations",
7780
"//common/types",
7881
"//common/types:type_providers",

common/src/main/java/dev/cel/common/values/MapValue.java

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
import com.google.errorprone.annotations.CanIgnoreReturnValue;
1818
import com.google.errorprone.annotations.DoNotCall;
1919
import com.google.errorprone.annotations.Immutable;
20+
import dev.cel.common.CelErrorCode;
21+
import dev.cel.common.CelRuntimeException;
2022
import dev.cel.common.types.CelType;
2123
import dev.cel.common.types.MapType;
2224
import dev.cel.common.types.SimpleType;
2325
import java.util.Map;
26+
import java.util.Optional;
2427
import java.util.function.BiFunction;
2528
import java.util.function.Function;
2629
import org.jspecify.nullness.Nullable;
@@ -33,8 +36,8 @@
3336
*/
3437
@Immutable(containerOf = {"K", "V"})
3538
public abstract class MapValue<K extends CelValue, V extends CelValue> extends CelValue
36-
implements Map<K, V> {
37-
39+
implements Map<K, V>, SelectableValue<K> {
40+
3841
private static final MapType MAP_TYPE = MapType.create(SimpleType.DYN, SimpleType.DYN);
3942

4043
@Override
@@ -48,24 +51,30 @@ public boolean isZeroValue() {
4851
@Override
4952
@SuppressWarnings("unchecked")
5053
public V get(Object key) {
51-
return get((K) key);
54+
return select((K) key);
5255
}
5356

54-
public V get(K key) {
55-
if (!has(key)) {
56-
throw new IllegalArgumentException(
57-
String.format("key '%s' is not present in map.", key.value()));
58-
}
59-
return value().get(key);
57+
@Override
58+
@SuppressWarnings("unchecked")
59+
public V select(K field) {
60+
return (V)
61+
find(field)
62+
.orElseThrow(
63+
() ->
64+
new CelRuntimeException(
65+
new IllegalArgumentException(
66+
String.format("key '%s' is not present in map.", field.value())),
67+
CelErrorCode.ATTRIBUTE_NOT_FOUND));
6068
}
6169

6270
@Override
63-
public CelType celType() {
64-
return MAP_TYPE;
71+
public Optional<CelValue> find(K field) {
72+
return value().containsKey(field) ? Optional.of(value().get(field)) : Optional.empty();
6573
}
6674

67-
public boolean has(K key) {
68-
return value().containsKey(key);
75+
@Override
76+
public CelType celType() {
77+
return MAP_TYPE;
6978
}
7079

7180
/**

common/src/main/java/dev/cel/common/values/OptionalValue.java

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import dev.cel.common.types.OptionalType;
2121
import dev.cel.common.types.SimpleType;
2222
import java.util.NoSuchElementException;
23+
import java.util.Optional;
2324
import org.jspecify.nullness.Nullable;
2425

2526
/**
@@ -29,7 +30,8 @@
2930
*/
3031
@AutoValue
3132
@Immutable(containerOf = "E")
32-
public abstract class OptionalValue<E extends CelValue> extends CelValue {
33+
public abstract class OptionalValue<E extends CelValue> extends CelValue
34+
implements SelectableValue<CelValue> {
3335
private static final OptionalType OPTIONAL_TYPE = OptionalType.create(SimpleType.DYN);
3436

3537
/** Sentinel value representing an empty optional ('optional.none()' in CEL) */
@@ -65,51 +67,21 @@ public OptionalType celType() {
6567
* <li>map[?key] -> key in map ? optional{map[key]} : optional.none()
6668
* </ol>
6769
*/
68-
@SuppressWarnings("unchecked")
69-
public OptionalValue<CelValue> select(CelValue field) {
70-
if (isZeroValue()) {
71-
return EMPTY;
72-
}
73-
74-
CelValue celValue = value();
75-
if (celValue instanceof MapValue) {
76-
MapValue<CelValue, CelValue> mapValue = (MapValue<CelValue, CelValue>) celValue;
77-
if (!mapValue.has(field)) {
78-
return EMPTY;
79-
}
80-
81-
return OptionalValue.create(mapValue.get(field));
82-
} else if (celValue instanceof StructValue) {
83-
StructValue structValue = (StructValue) celValue;
84-
StringValue stringField = (StringValue) field;
85-
if (!structValue.hasField(stringField.value())) {
86-
return EMPTY;
87-
}
88-
89-
return OptionalValue.create(structValue.select(stringField.value()));
90-
}
91-
92-
throw new UnsupportedOperationException("Unsupported select on: " + celValue);
70+
@Override
71+
public CelValue select(CelValue field) {
72+
return find(field).orElse(EMPTY);
9373
}
9474

95-
/** Presence test with optional semantics on maps and structs. */
96-
@SuppressWarnings("unchecked") // Unchecked cast of MapValue flagged due to type erasure.
97-
public boolean hasField(CelValue field) {
75+
@Override
76+
@SuppressWarnings("unchecked")
77+
public Optional<CelValue> find(CelValue field) {
9878
if (isZeroValue()) {
99-
return false;
100-
}
101-
102-
CelValue celValue = value();
103-
if (celValue instanceof MapValue) {
104-
MapValue<CelValue, CelValue> mapValue = (MapValue<CelValue, CelValue>) celValue;
105-
return mapValue.has(field);
106-
} else if (celValue instanceof StructValue) {
107-
StructValue structValue = (StructValue) celValue;
108-
StringValue stringField = (StringValue) field;
109-
return structValue.hasField(stringField.value());
79+
return Optional.empty();
11080
}
11181

112-
throw new UnsupportedOperationException("Unsupported presence test on: " + celValue);
82+
SelectableValue<CelValue> selectableValue = (SelectableValue<CelValue>) value();
83+
Optional<CelValue> selectedField = selectableValue.find(field);
84+
return selectedField.map(OptionalValue::create);
11385
}
11486

11587
public static <E extends CelValue> OptionalValue<E> create(E value) {

common/src/main/java/dev/cel/common/values/ProtoMessageValue.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
import dev.cel.common.internal.CelDescriptorPool;
2424
import dev.cel.common.types.CelType;
2525
import dev.cel.common.types.StructTypeReference;
26+
import java.util.Optional;
2627

2728
/** ProtoMessageValue is a struct value with protobuf support. */
2829
@AutoValue
2930
@Immutable
30-
public abstract class ProtoMessageValue extends StructValue {
31+
public abstract class ProtoMessageValue extends StructValue<StringValue> {
3132

3233
@Override
3334
public abstract Message value();
@@ -45,23 +46,18 @@ public boolean isZeroValue() {
4546
}
4647

4748
@Override
48-
public boolean hasField(String fieldName) {
49-
FieldDescriptor fieldDescriptor =
50-
findField(celDescriptorPool(), value().getDescriptorForType(), fieldName);
51-
52-
if (fieldDescriptor.isRepeated()) {
53-
return value().getRepeatedFieldCount(fieldDescriptor) > 0;
54-
}
55-
56-
return value().hasField(fieldDescriptor);
49+
public CelValue select(StringValue field) {
50+
return find(field)
51+
.orElseThrow(() -> new IllegalArgumentException("Undefined field: " + field.value()));
5752
}
5853

5954
@Override
60-
public CelValue select(String fieldName) {
55+
public Optional<CelValue> find(StringValue field) {
6156
FieldDescriptor fieldDescriptor =
62-
findField(celDescriptorPool(), value().getDescriptorForType(), fieldName);
57+
findField(celDescriptorPool(), value().getDescriptorForType(), field.value());
6358

64-
return protoCelValueConverter().fromProtoMessageFieldToCelValue(value(), fieldDescriptor);
59+
return Optional.of(
60+
protoCelValueConverter().fromProtoMessageFieldToCelValue(value(), fieldDescriptor));
6561
}
6662

6763
public static ProtoMessageValue create(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.common.values;
16+
17+
import java.util.Optional;
18+
19+
/**
20+
* SelectableValue is an interface for representing a value that supports field selection and
21+
* presence tests. Few examples are structs, protobuf messages, maps and optional values.
22+
*/
23+
public interface SelectableValue<T extends CelValue> {
24+
25+
/**
26+
* Performs field selection. The behavior depends on the concrete implementation of the value
27+
* being selected. For structs and maps, this must throw an exception if the field does not exist.
28+
* For optional values, this will return an {@code optional.none()}.
29+
*/
30+
CelValue select(T field);
31+
32+
/**
33+
* Finds the field. This will return an {@link Optional#empty()} if the field does not exist. This
34+
* can be used for presence testing.
35+
*/
36+
Optional<CelValue> find(T field);
37+
}

common/src/main/java/dev/cel/common/values/StructValue.java

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,11 @@
2222
* <p>Users may extend from this class to provide a custom struct that CEL can understand (ex:
2323
* POJOs). Custom struct implementations must provide all functionalities denoted in the CEL
2424
* specification, such as field selection, presence testing and new object creation.
25+
*
26+
* <p>For an expression `e` selecting a field `f`, `e.f` must throw an exception if `f` does not
27+
* exist in the struct (i.e: hasField returns false). If the field exists but is not set, the
28+
* implementation should return an appropriate default value based on the struct's semantics.
2529
*/
2630
@Immutable
27-
public abstract class StructValue extends CelValue {
28-
29-
/**
30-
* Performs field selection. For an expression `e` selecting a field `f`, `e.f` must throw an
31-
* exception if `f` does not exist in the struct (i.e: hasField returns false). If the field
32-
* exists but is not set, the implementation should return an appropriate default value based on
33-
* the struct's semantics.
34-
*/
35-
public abstract CelValue select(String fieldName);
36-
37-
/**
38-
* Performs presence test on a field.
39-
*
40-
* @return true iff the field exists in the struct.
41-
*/
42-
public abstract boolean hasField(String fieldName);
43-
}
31+
public abstract class StructValue<T extends CelValue> extends CelValue
32+
implements SelectableValue<T> {}

common/src/test/java/dev/cel/common/values/ImmutableMapValueTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.common.collect.ImmutableMap;
2121
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
2222
import com.google.testing.junit.testparameterinjector.TestParameters;
23+
import dev.cel.common.CelRuntimeException;
2324
import dev.cel.common.types.MapType;
2425
import dev.cel.common.types.SimpleType;
2526
import org.junit.Test;
@@ -65,21 +66,21 @@ public void get_nonExistentKey_throws() {
6566
ImmutableMapValue<IntValue, StringValue> mapValue =
6667
ImmutableMapValue.create(ImmutableMap.of(one, hello));
6768

68-
IllegalArgumentException exception =
69-
assertThrows(IllegalArgumentException.class, () -> mapValue.get(IntValue.create(100L)));
70-
assertThat(exception).hasMessageThat().isEqualTo("key '100' is not present in map.");
69+
CelRuntimeException exception =
70+
assertThrows(CelRuntimeException.class, () -> mapValue.get(IntValue.create(100L)));
71+
assertThat(exception).hasMessageThat().contains("key '100' is not present in map.");
7172
}
7273

7374
@Test
7475
@TestParameters("{key: 1, expectedResult: true}")
7576
@TestParameters("{key: 100, expectedResult: false}")
76-
public void has_success(long key, boolean expectedResult) {
77+
public void find_success(long key, boolean expectedResult) {
7778
IntValue one = IntValue.create(1L);
7879
StringValue hello = StringValue.create("hello");
7980
ImmutableMapValue<IntValue, StringValue> mapValue =
8081
ImmutableMapValue.create(ImmutableMap.of(one, hello));
8182

82-
assertThat(mapValue.has(IntValue.create(key))).isEqualTo(expectedResult);
83+
assertThat(mapValue.find(IntValue.create(key)).isPresent()).isEqualTo(expectedResult);
8384
}
8485

8586
@Test

0 commit comments

Comments
 (0)