1717import com .google .common .base .Preconditions ;
1818import com .google .common .collect .ImmutableList ;
1919import com .google .common .collect .ImmutableMap ;
20+ import com .google .errorprone .annotations .CanIgnoreReturnValue ;
2021import com .google .errorprone .annotations .Immutable ;
2122import dev .cel .common .annotations .Internal ;
23+ import dev .cel .common .exceptions .CelInvalidArgumentException ;
2224import java .util .Collection ;
2325import java .util .Iterator ;
2426import java .util .List ;
2527import java .util .Map ;
2628import java .util .Optional ;
2729import java .util .RandomAccess ;
2830import java .util .function .Function ;
31+ import org .jspecify .annotations .Nullable ;
2932
3033/**
3134 * {@code CelValueConverter} handles bidirectional conversion between native Java objects to {@link
@@ -74,7 +77,7 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
7477 if (value instanceof List && value instanceof RandomAccess ) {
7578 List <Object > list = (List <Object >) value ;
7679 for (int i = 0 ; i < list .size (); i ++) {
77- Object element = list .get (i );
80+ Object element = checkListElement ( list .get (i ), i );
7881 Object mapped = mapper .apply (element );
7982
8083 if (mapped != element ) {
@@ -85,7 +88,7 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
8588 }
8689 builder .add (mapped );
8790 for (int j = i + 1 ; j < list .size (); j ++) {
88- builder .add (mapper .apply (list .get (j )));
91+ builder .add (mapper .apply (checkListElement ( list .get (j ), j )));
8992 }
9093 return builder .build ();
9194 }
@@ -100,8 +103,9 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
100103 Collection <Object > collection = (Collection <Object >) value ;
101104 ImmutableList .Builder <Object > builder =
102105 ImmutableList .builderWithExpectedSize (collection .size ());
106+ int index = 0 ;
103107 for (Object element : collection ) {
104- builder .add (mapper .apply (element ));
108+ builder .add (mapper .apply (checkListElement ( element , index ++) ));
105109 }
106110 return builder .build ();
107111 }
@@ -112,6 +116,7 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
112116
113117 while (iterator .hasNext ()) {
114118 Map .Entry <Object , Object > entry = iterator .next ();
119+ checkMapEntry (entry );
115120 Object mappedKey = mapper .apply (entry .getKey ());
116121 Object mappedValue = mapper .apply (entry .getValue ());
117122
@@ -128,6 +133,7 @@ protected Object mapContainer(Object value, Function<Object, Object> mapper) {
128133 builder .put (mappedKey , mappedValue );
129134 while (iterator .hasNext ()) {
130135 Map .Entry <Object , Object > nextEntry = iterator .next ();
136+ checkMapEntry (nextEntry );
131137 builder .put (mapper .apply (nextEntry .getKey ()), mapper .apply (nextEntry .getValue ()));
132138 }
133139 return builder .buildOrThrow ();
@@ -162,6 +168,57 @@ public Object toRuntimeValue(Object value) {
162168 return normalizePrimitive (value );
163169 }
164170
171+ /**
172+ * Adapts {@code value} for an intermediate field selection hop.
173+ *
174+ * <p>{@link Map} instances are returned as-is to avoid O(N) whole-map normalization per hop; the
175+ * accessed entry is validated on lookup via {@link #findMapValue} or {@link #containsMapKey}.
176+ * Callers materializing a final evaluation result must use {@link #toRuntimeValue} instead.
177+ */
178+ public final Object toTraversalTarget (Object value ) {
179+ if (value instanceof Map ) {
180+ return value ;
181+ }
182+
183+ return toRuntimeValue (value );
184+ }
185+
186+ /**
187+ * Returns the unadapted value bound to {@code key} in {@code map}, or {@link Optional#empty()} if
188+ * absent.
189+ *
190+ * @throws CelInvalidArgumentException if {@code key} is bound to {@code null}.
191+ */
192+ public static Optional <Object > findMapValue (Map <?, ?> map , Object key ) {
193+ Object value = map .get (key );
194+ if (value != null ) {
195+ return Optional .of (value );
196+ }
197+
198+ if (map .containsKey (key )) {
199+ throw nullMapValue (key );
200+ }
201+
202+ return Optional .empty ();
203+ }
204+
205+ /**
206+ * Returns whether {@code key} is present in {@code map}.
207+ *
208+ * @throws CelInvalidArgumentException if {@code key} is bound to {@code null}.
209+ */
210+ public static boolean containsMapKey (Map <?, ?> map , Object key ) {
211+ if (map .get (key ) != null ) {
212+ return true ;
213+ }
214+
215+ if (map .containsKey (key )) {
216+ throw nullMapValue (key );
217+ }
218+
219+ return false ;
220+ }
221+
165222 protected Object normalizePrimitive (Object value ) {
166223 Preconditions .checkNotNull (value );
167224
@@ -196,6 +253,36 @@ private Object unwrap(CelValue celValue) {
196253 return celValue .value ();
197254 }
198255
256+ private static void checkMapEntry (Map .Entry <?, ?> entry ) {
257+ Object key = entry .getKey ();
258+ if (key == null ) {
259+ throw new CelInvalidArgumentException ("Map key cannot be null." );
260+ }
261+
262+ if (entry .getValue () == null ) {
263+ throw nullMapValue (key );
264+ }
265+ }
266+
267+ @ CanIgnoreReturnValue
268+ private static Object checkListElement (@ Nullable Object element , int index ) {
269+ if (element == null ) {
270+ throw new CelInvalidArgumentException (
271+ String .format ("List element cannot be null at index: %d" , index ));
272+ }
273+
274+ return element ;
275+ }
276+
277+ /**
278+ * Uses {@code INVALID_ARGUMENT} rather than {@code ATTRIBUTE_NOT_FOUND} so presence tests and
279+ * optional selects do not treat a null-bound key as absent.
280+ */
281+ private static CelInvalidArgumentException nullMapValue (Object key ) {
282+ return new CelInvalidArgumentException (
283+ String .format ("Map value cannot be null for key: %s" , key ));
284+ }
285+
199286 protected CelValueConverter () {
200287 this .maybeUnwrapFunction = this ::maybeUnwrap ;
201288 this .toRuntimeValueFunction = this ::toRuntimeValue ;
0 commit comments