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,59 @@ 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 new CelInvalidArgumentException (
200+ String .format ("Map value cannot be null for key: %s" , key ));
201+ }
202+
203+ return Optional .empty ();
204+ }
205+
206+ /**
207+ * Returns whether {@code key} is present in {@code map}.
208+ *
209+ * @throws CelInvalidArgumentException if {@code key} is bound to {@code null}.
210+ */
211+ public static boolean containsMapKey (Map <?, ?> map , Object key ) {
212+ if (map .get (key ) != null ) {
213+ return true ;
214+ }
215+
216+ if (map .containsKey (key )) {
217+ throw new CelInvalidArgumentException (
218+ String .format ("Map value cannot be null for key: %s" , key ));
219+ }
220+
221+ return false ;
222+ }
223+
165224 protected Object normalizePrimitive (Object value ) {
166225 Preconditions .checkNotNull (value );
167226
@@ -196,6 +255,28 @@ private Object unwrap(CelValue celValue) {
196255 return celValue .value ();
197256 }
198257
258+ private static void checkMapEntry (Map .Entry <?, ?> entry ) {
259+ Object key = entry .getKey ();
260+ if (key == null ) {
261+ throw new CelInvalidArgumentException ("Map key cannot be null." );
262+ }
263+
264+ if (entry .getValue () == null ) {
265+ throw new CelInvalidArgumentException (
266+ String .format ("Map value cannot be null for key: %s" , key ));
267+ }
268+ }
269+
270+ @ CanIgnoreReturnValue
271+ private static Object checkListElement (@ Nullable Object element , int index ) {
272+ if (element == null ) {
273+ throw new CelInvalidArgumentException (
274+ String .format ("List element cannot be null at index: %d" , index ));
275+ }
276+
277+ return element ;
278+ }
279+
199280 protected CelValueConverter () {
200281 this .maybeUnwrapFunction = this ::maybeUnwrap ;
201282 this .toRuntimeValueFunction = this ::toRuntimeValue ;
0 commit comments