From adb18629ec5b14ea0db8aa749a847f449035fd87 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 16 Oct 2026 19:21:48 +0200 Subject: [PATCH 1/4] Centralize native value conversion Make TypeAdapter the bidirectional conversion boundary while retaining the deprecated Val conversion fallback. Preserve scalar ranges, configured container adapters, and protobuf built-in and object type values; validate uint32 map entries before narrowing. --- .../conformance/SimpleConformanceTest.java | 24 +- .../types/TypeAdapterConversionBench.java | 280 ++++++++++++++++++ .../projectnessie/cel/common/types/BoolT.java | 2 +- .../cel/common/types/BytesT.java | 2 +- .../cel/common/types/DoubleT.java | 2 +- .../cel/common/types/DurationT.java | 2 +- .../projectnessie/cel/common/types/Err.java | 1 + .../projectnessie/cel/common/types/IntT.java | 2 +- .../cel/common/types/IteratorT.java | 1 + .../projectnessie/cel/common/types/ListT.java | 15 +- .../projectnessie/cel/common/types/MapT.java | 4 +- .../projectnessie/cel/common/types/NullT.java | 2 +- .../cel/common/types/ObjectT.java | 7 +- .../cel/common/types/OptionalT.java | 1 + .../cel/common/types/StringT.java | 2 +- .../cel/common/types/TimestampT.java | 2 +- .../projectnessie/cel/common/types/TypeT.java | 1 + .../projectnessie/cel/common/types/Types.java | 6 +- .../projectnessie/cel/common/types/UintT.java | 2 +- .../cel/common/types/UnknownT.java | 2 +- .../common/types/pb/DefaultTypeAdapter.java | 42 ++- .../cel/common/types/pb/FieldDescription.java | 4 +- .../cel/common/types/pb/PbObjectT.java | 17 +- .../common/types/pb/ProtoTypeRegistry.java | 76 ++++- .../cel/common/types/ref/TypeAdapter.java | 160 +++++++++- .../common/types/ref/TypeAdapterSupport.java | 179 ++++++++++- .../cel/common/types/ref/Val.java | 18 +- .../cel/extension/EncodersLib.java | 5 +- .../cel/extension/NetworkLib.java | 4 +- .../cel/extension/StringsLib.java | 6 +- .../java/org/projectnessie/cel/CELTest.java | 2 + .../cel/common/types/ListTest.java | 1 + .../cel/common/types/MapTest.java | 2 + .../cel/common/types/ProviderTest.java | 113 ++++--- .../cel/common/types/TypeTest.java | 19 ++ .../cel/common/types/pb/PbObjectTest.java | 2 + .../cel/common/types/ref/TypeAdapterTest.java | 214 +++++++++++++ .../cel/interpreter/InterpreterTest.java | 2 + .../cel/types/jackson/JacksonObjectT.java | 1 + .../cel/types/jackson/JacksonRegistry.java | 35 +++ .../cel/types/jackson3/Jackson3Registry.java | 35 +++ .../cel/types/jackson3/JacksonObjectT.java | 1 + .../org/projectnessie/cel/tools/Script.java | 2 +- .../cel/tools/ScriptHostTest.java | 6 +- 44 files changed, 1187 insertions(+), 119 deletions(-) create mode 100644 core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java create mode 100644 core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java diff --git a/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java index 825a5d61..220546a3 100644 --- a/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java +++ b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java @@ -405,7 +405,7 @@ private static ExprValue eval(SimpleTest test, Ast ast) { EvalResult res = program.eval(args); if (!isError(res.getVal())) { - return refValueToExprValue(res.getVal()); + return refValueToExprValue(env.getTypeAdapter(), res.getVal()); } Err err = (Err) res.getVal(); @@ -672,23 +672,25 @@ private static Val valueToRefValue(TypeAdapter adapter, Value v) { } } - private static ExprValue refValueToExprValue(Val res) { + private static ExprValue refValueToExprValue(TypeAdapter adapter, Val res) { if (isUnknown(res)) { return ExprValue.newBuilder() .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) .build(); } - return ExprValue.newBuilder().setValue(refValueToValue(res)).build(); + return ExprValue.newBuilder().setValue(refValueToValue(adapter, res)).build(); } - private static Value refValueToValue(Val res) { + private static Value refValueToValue(TypeAdapter adapter, Val res) { switch (res.type().typeEnum()) { case Bool: return Value.newBuilder().setBoolValue(res.booleanValue()).build(); case Bytes: - return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); + return Value.newBuilder() + .setBytesValue(adapter.valueToNative(res, ByteString.class)) + .build(); case Double: - return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); + return Value.newBuilder().setDoubleValue(adapter.valueToDouble(res)).build(); case Int: return Value.newBuilder().setInt64Value(res.intValue()).build(); case Null: @@ -701,17 +703,17 @@ private static Value refValueToValue(Val res) { return Value.newBuilder().setUint64Value(res.intValue()).build(); case Duration: return Value.newBuilder() - .setObjectValue(Any.pack(res.convertToNative(Duration.class))) + .setObjectValue(Any.pack(adapter.valueToNative(res, Duration.class))) .build(); case Timestamp: return Value.newBuilder() - .setObjectValue(Any.pack(res.convertToNative(Timestamp.class))) + .setObjectValue(Any.pack(adapter.valueToNative(res, Timestamp.class))) .build(); case List: Lister lister = (Lister) res; ListValue.Builder elements = ListValue.newBuilder(); for (IteratorT i = lister.iterator(); i.hasNext() == True; ) { - elements.addValues(refValueToValue(i.next())); + elements.addValues(refValueToValue(adapter, i.next())); } return Value.newBuilder().setListValue(elements).build(); case Map: @@ -721,8 +723,8 @@ private static Value refValueToValue(Val res) { Val key = i.next(); entries .addEntriesBuilder() - .setKey(refValueToValue(key)) - .setValue(refValueToValue(mapper.get(key))); + .setKey(refValueToValue(adapter, key)) + .setValue(refValueToValue(adapter, mapper.get(key))); } return Value.newBuilder().setMapValue(entries).build(); case Object: diff --git a/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java b/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java new file mode 100644 index 00000000..465fb879 --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/common/types/TypeAdapterConversionBench.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.common.types; + +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; + +import com.google.api.expr.v1alpha1.ParsedExpr; +import com.google.protobuf.Any; +import com.google.protobuf.Value; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.common.types.ref.TypeRegistry; +import org.projectnessie.cel.common.types.ref.Val; + +@Warmup(iterations = 1, time = 1500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 300, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@SuppressWarnings({"deprecation", "removal"}) +public class TypeAdapterConversionBench { + + @State(Scope.Thread) + public static class ConversionState { + TypeRegistry registry; + + boolean primitiveBoolean; + long primitiveLong; + double primitiveDouble; + + Object nativeBoolean; + Object nativeLong; + Object nativeDouble; + Object nativeString; + Object nativeList; + Object nativeMap; + Object nativeMessage; + + BoolT boolValue; + IntT intValue; + UintT uintValue; + DoubleT doubleValue; + Val stringValue; + Val listValue; + Val mapValue; + Val messageValue; + + @Setup + public void init() { + ParsedExpr parsedExpr = ParsedExpr.getDefaultInstance(); + registry = newRegistry(parsedExpr); + + primitiveBoolean = true; + primitiveLong = 42L; + primitiveDouble = 42.5d; + + nativeBoolean = Boolean.TRUE; + nativeLong = Long.valueOf(42L); + nativeDouble = Double.valueOf(42.5d); + nativeString = "forty-two"; + nativeList = List.of(1L, 2L, 3L); + nativeMap = Map.of("one", 1L, "two", 2L); + nativeMessage = parsedExpr; + + boolValue = BoolT.True; + intValue = intOf(42L); + uintValue = uintOf(42L); + doubleValue = doubleOf(42.5d); + stringValue = stringOf("forty-two"); + listValue = registry.nativeToValue(nativeList); + mapValue = registry.nativeToValue(nativeMap); + messageValue = registry.nativeToValue(nativeMessage); + } + } + + @Benchmark + public Val boxedNativeToValueBoolean(ConversionState state) { + return state.registry.nativeToValue(state.nativeBoolean); + } + + @Benchmark + public Val boxedNativeToValueLong(ConversionState state) { + return state.registry.nativeToValue(state.nativeLong); + } + + @Benchmark + public Val boxedNativeToValueDouble(ConversionState state) { + return state.registry.nativeToValue(state.nativeDouble); + } + + @Benchmark + public Val boxedNativeToValueString(ConversionState state) { + return state.registry.nativeToValue(state.nativeString); + } + + @Benchmark + public Val boxedNativeToValueList(ConversionState state) { + return state.registry.nativeToValue(state.nativeList); + } + + @Benchmark + public Val boxedNativeToValueMap(ConversionState state) { + return state.registry.nativeToValue(state.nativeMap); + } + + @Benchmark + public Val boxedNativeToValueMessage(ConversionState state) { + return state.registry.nativeToValue(state.nativeMessage); + } + + @Benchmark + public Val primitiveNativeToValueBoolean(ConversionState state) { + return state.registry.nativeToValue(state.primitiveBoolean); + } + + @Benchmark + public Val primitiveNativeToValueLong(ConversionState state) { + return state.registry.nativeToValue(state.primitiveLong); + } + + @Benchmark + public Val primitiveNativeToValueDouble(ConversionState state) { + return state.registry.nativeToValue(state.primitiveDouble); + } + + @Benchmark + public Boolean directConvertBoolToBoolean(ConversionState state) { + return state.boolValue.convertToNative(Boolean.class); + } + + @Benchmark + public Long directConvertIntToLong(ConversionState state) { + return state.intValue.convertToNative(Long.class); + } + + @Benchmark + public Long directConvertUintToLong(ConversionState state) { + return state.uintValue.convertToNative(Long.class); + } + + @Benchmark + public Double directConvertDoubleToDouble(ConversionState state) { + return state.doubleValue.convertToNative(Double.class); + } + + @Benchmark + public String directConvertStringToString(ConversionState state) { + return state.stringValue.convertToNative(String.class); + } + + @Benchmark + public List directConvertListToList(ConversionState state) { + return state.listValue.convertToNative(List.class); + } + + @Benchmark + public Map directConvertMapToMap(ConversionState state) { + return state.mapValue.convertToNative(Map.class); + } + + @Benchmark + public Value directConvertBoolToProtoValue(ConversionState state) { + return state.boolValue.convertToNative(Value.class); + } + + @Benchmark + public Any directConvertIntToAny(ConversionState state) { + return state.intValue.convertToNative(Any.class); + } + + @Benchmark + public ParsedExpr directConvertMessageToGenerated(ConversionState state) { + return state.messageValue.convertToNative(ParsedExpr.class); + } + + @Benchmark + public Boolean adapterConvertBoolToBoolean(ConversionState state) { + return state.registry.valueToNative(state.boolValue, Boolean.class); + } + + @Benchmark + public Long adapterConvertIntToLong(ConversionState state) { + return state.registry.valueToNative(state.intValue, Long.class); + } + + @Benchmark + public Double adapterConvertDoubleToDouble(ConversionState state) { + return state.registry.valueToNative(state.doubleValue, Double.class); + } + + @Benchmark + public String adapterConvertStringToString(ConversionState state) { + return state.registry.valueToNative(state.stringValue, String.class); + } + + @Benchmark + public List adapterConvertListToList(ConversionState state) { + return state.registry.valueToNative(state.listValue, List.class); + } + + @Benchmark + public Map adapterConvertMapToMap(ConversionState state) { + return state.registry.valueToNative(state.mapValue, Map.class); + } + + @Benchmark + public ParsedExpr adapterConvertMessageToGenerated(ConversionState state) { + return state.registry.valueToNative(state.messageValue, ParsedExpr.class); + } + + @Benchmark + public boolean adapterBooleanValue(ConversionState state) { + return state.registry.valueToBoolean(state.boolValue); + } + + @Benchmark + public long adapterIntValue(ConversionState state) { + return state.registry.valueToLong(state.intValue); + } + + @Benchmark + public long adapterUintValue(ConversionState state) { + return state.registry.valueToLong(state.uintValue); + } + + @Benchmark + public double adapterDoubleValue(ConversionState state) { + return state.registry.valueToDouble(state.doubleValue); + } + + @Benchmark + public boolean directBooleanValue(ConversionState state) { + return state.boolValue.booleanValue(); + } + + @Benchmark + public long directIntValue(ConversionState state) { + return state.intValue.intValue(); + } + + @Benchmark + public long directUintValue(ConversionState state) { + return state.uintValue.intValue(); + } + + @Benchmark + public double directDoubleValue(ConversionState state) { + return state.doubleValue.doubleValue(); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java index 2868ad4e..0f1eb4c1 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BoolT.java @@ -65,7 +65,7 @@ public Val compare(Val other) { } /** ConvertToNative implements the ref.Val interface method. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Boolean.class || typeDesc == boolean.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java index ca2025bb..7cdeff37 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/BytesT.java @@ -102,7 +102,7 @@ public Val compare(Val other) { } /** ConvertToNative implements the ref.Val interface method. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == ByteString.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java index fed76630..3510d9af 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java @@ -83,7 +83,7 @@ public Val add(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Double.class || typeDesc == double.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java index fde847bd..618ce9f9 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DurationT.java @@ -158,7 +158,7 @@ public Val compare(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Duration.class.isAssignableFrom(typeDesc)) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Err.java b/core/src/main/java/org/projectnessie/cel/common/types/Err.java index e84ff00c..1000d012 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Err.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Err.java @@ -200,6 +200,7 @@ public Val getErr() { /** ConvertToNative implements ref.Val.ConvertToNative. */ @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(error); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java index 056575a0..7c4dc00b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java @@ -120,7 +120,7 @@ public double doubleValue() { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Long.class || typeDesc == long.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java b/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java index 911bf857..bfa6ebb5 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IteratorT.java @@ -67,6 +67,7 @@ public Val next() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java index a202e70b..12aad198 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java @@ -35,7 +35,6 @@ import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; -import java.util.function.Function; import org.projectnessie.cel.common.operators.Operator; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.Type; @@ -98,7 +97,7 @@ abstract static class BaseListT extends ListT { this.size = size; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc.isArray()) { @@ -147,14 +146,14 @@ private ListValue toPbListValue() { int s = (int) size; for (int i = 0; i < s; i++) { Val v = get(intOf(i)); - Value e = v.convertToNative(Value.class); + Value e = adapter.valueToNative(v, Value.class); list.addValues(e); } return list.build(); } private List toJavaList() { - return asList(convertToNative(Object[].class)); + return asList((Object[]) toJavaArray(Object[].class)); } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -167,12 +166,9 @@ private Object toJavaArray(Class typeDesc) { } Object array = Array.newInstance(compType, s); - Function fixForTarget = Function.identity(); - for (int i = 0; i < s; i++) { Val v = get(intOf(i)); - Object e = v.convertToNative(compType); - e = fixForTarget.apply(e); + Object e = adapter.valueToNative(v, compType); Array.set(array, i, e); } return array; @@ -301,6 +297,7 @@ public Val next() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException("IMPLEMENT ME??"); } @@ -366,7 +363,7 @@ public Val add(Val other) { newArray[array.length + i] = componentType.isInstance(otherValue) ? otherValue - : otherValue.convertToNative(componentType); + : adapter.valueToNative(otherValue, componentType); } return new GenericListT(adapter, newArray); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java index d96ef276..ce44420b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java @@ -107,7 +107,7 @@ static final class ValMapT extends MapT { this.map = map; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Map.class.isAssignableFrom(typeDesc) || typeDesc == Object.class) { @@ -145,7 +145,7 @@ private Struct toPbStruct() { if (k.type().typeEnum() != TypeEnum.String) { throw new IllegalArgumentException("bad key type"); } - struct.putFields(k.value().toString(), v.convertToNative(Value.class)); + struct.putFields(k.value().toString(), adapter.valueToNative(v, Value.class)); }); return struct.build(); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java index 3ef11a2e..10c56a3c 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/NullT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/NullT.java @@ -42,7 +42,7 @@ public final class NullT extends BaseVal { private static final Any PbAny = Any.pack(PbValue); /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Integer.class || typeDesc == int.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java b/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java index e7f8c100..44c3f2cc 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ObjectT.java @@ -29,7 +29,7 @@ import org.projectnessie.cel.common.types.traits.FieldTester; import org.projectnessie.cel.common.types.traits.Indexer; -public abstract class ObjectT extends BaseVal implements FieldTester, Indexer, TypeAdapter { +public abstract class ObjectT extends BaseVal implements FieldTester, Indexer { protected final TypeAdapter adapter; protected final Object value; protected final TypeDescription typeDesc; @@ -78,11 +78,6 @@ public Object value() { return value; } - @Override - public Val nativeToValue(Object value) { - return adapter.nativeToValue(value); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java index 4bbffbc3..f816c7e3 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/OptionalT.java @@ -87,6 +87,7 @@ public Val getValue() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { if (typeDesc == Val.class || typeDesc == OptionalT.class) { return typeDesc.cast(this); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java index b275e4c7..c4c22af7 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/StringT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/StringT.java @@ -90,7 +90,7 @@ public Val add(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == String.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java index 3d9e9bfb..ecb49b5d 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TimestampT.java @@ -207,7 +207,7 @@ public Val compare(Val other) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == ZonedDateTime.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java index 17e8f62f..b5ff2d1f 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/TypeT.java @@ -95,6 +95,7 @@ public double doubleValue() { /** ConvertToNative implements ref.Val.ConvertToNative. */ @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException("type conversion not supported for 'type'"); } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/Types.java b/core/src/main/java/org/projectnessie/cel/common/types/Types.java index 36d6b63b..601ca50d 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/Types.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/Types.java @@ -17,11 +17,13 @@ import static org.projectnessie.cel.common.types.BytesT.BytesType; import static org.projectnessie.cel.common.types.DoubleT.DoubleType; +import static org.projectnessie.cel.common.types.DurationT.DurationType; import static org.projectnessie.cel.common.types.IntT.IntType; import static org.projectnessie.cel.common.types.ListT.ListType; import static org.projectnessie.cel.common.types.MapT.MapType; import static org.projectnessie.cel.common.types.NullT.NullType; import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.TimestampT.TimestampType; import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.UintT.UintType; @@ -39,17 +41,19 @@ private Types() {} typeNameToTypeValue.put(BoolT.BoolType.typeName(), BoolT.BoolType); typeNameToTypeValue.put(BytesType.typeName(), BytesType); typeNameToTypeValue.put(DoubleType.typeName(), DoubleType); + typeNameToTypeValue.put(DurationType.typeName(), DurationType); typeNameToTypeValue.put(NullType.typeName(), NullType); typeNameToTypeValue.put(IntType.typeName(), IntType); typeNameToTypeValue.put(ListType.typeName(), ListType); typeNameToTypeValue.put(MapType.typeName(), MapType); typeNameToTypeValue.put(StringType.typeName(), StringType); + typeNameToTypeValue.put(TimestampType.typeName(), TimestampType); typeNameToTypeValue.put(TypeType.typeName(), TypeType); typeNameToTypeValue.put(UintType.typeName(), UintType); } public static Type getTypeByName(String typeName) { - return null; + return typeNameToTypeValue.get(typeName); } public static BoolT boolOf(boolean b) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java index b392ed2a..2d2f8ee7 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java @@ -96,7 +96,7 @@ public double doubleValue() { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc == Long.class || typeDesc == long.class || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java b/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java index 7630d266..f31d71a2 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UnknownT.java @@ -42,7 +42,7 @@ private UnknownT(long value) { } /** ConvertToNative implements ref.Val.ConvertToNative. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (Long.class.isAssignableFrom(typeDesc) diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java index 097bb003..44f44fb2 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/DefaultTypeAdapter.java @@ -18,6 +18,7 @@ import static org.projectnessie.cel.common.types.Err.anyWithEmptyType; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.Err.unknownType; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; import static org.projectnessie.cel.common.types.pb.PbTypeDescription.typeNameFromMessage; import static org.projectnessie.cel.common.types.ref.TypeAdapterSupport.maybeNativeToValue; @@ -26,7 +27,9 @@ import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.Val; /** defaultTypeAdapter converts go native types to CEL values. */ @@ -50,6 +53,41 @@ public Val nativeToValue(Object value) { return Err.unsupportedRefValConversionErr(value); } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + /** * nativeToValue returns the converted (ref.Val, true) of a conversion is found, otherwise (nil, * false) @@ -102,7 +140,9 @@ static Object maybeUnwrapValue(Object value) { case STRING_VALUE: return v.getStringValue(); case TYPE_VALUE: - return Types.getTypeByName(v.getTypeValue()); + String typeName = v.getTypeValue(); + Type type = Types.getTypeByName(typeName); + return type != null ? type : newObjectTypeValue(typeName); case UINT64_VALUE: return ULong.valueOf(v.getUint64Value()); case OBJECT_VALUE: diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java index 0953b0f1..cf4d5f8e 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java @@ -540,10 +540,10 @@ private ProtoMapT(TypeAdapter adapter, FieldDescriptor field, List entries) { this.valueDesc = field.getMessageType().findFieldByNumber(2); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { - return (T) MapT.newMaybeWrappedMap(adapter, toJavaMap()).convertToNative(typeDesc); + return adapter.valueToNative(MapT.newMaybeWrappedMap(adapter, toJavaMap()), typeDesc); } @Override diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java index fae8e40e..df771cbd 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/PbObjectT.java @@ -80,7 +80,7 @@ public Val get(Val index) { if (fd == null) { return noSuchField(protoFieldStr); } - return nativeToValue(fd.getField(value, adapter)); + return adapter.nativeToValue(fd.getField(value, adapter)); } @Override @@ -98,7 +98,18 @@ public Val equal(Val other) { return boolOf(message().equals(otherObject.message())); } - @SuppressWarnings("unchecked") + /** + * Converts this value to a compatible protobuf or CEL representation. + * + *

Supported targets include the wrapped generated message class, {@link DynamicMessage}, + * {@link Any}, selected protobuf JSON {@link Value} representations, {@link Message}, {@link + * Val}, and {@link PbObjectT}. Conversion to another generated protobuf class rebuilds the + * message from its protobuf data. + * + * @throws IllegalArgumentException if the requested target is not supported + * @throws RuntimeException if rebuilding a generated message fails + */ + @SuppressWarnings({"removal", "unchecked"}) @Override public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { @@ -127,7 +138,7 @@ public T convertToNative(Class typeDesc) { return (T) Value.newBuilder().setStringValue(fieldMaskJsonValue((FieldMask) value)).build(); } if (value instanceof Timestamp) { - return adapter.nativeToValue(value).convertToNative(typeDesc); + return adapter.valueToNative(adapter.nativeToValue(value), typeDesc); } } if (typeDesc.isAssignableFrom(this.typeDesc.reflectType()) || typeDesc == Object.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java index 1f007f90..7ae0365d 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java @@ -23,6 +23,8 @@ import static org.projectnessie.cel.common.types.Err.anyWithEmptyType; import static org.projectnessie.cel.common.types.Err.newErr; import static org.projectnessie.cel.common.types.Err.noSuchField; +import static org.projectnessie.cel.common.types.Err.rangeError; +import static org.projectnessie.cel.common.types.Err.throwErrorAsIllegalStateException; import static org.projectnessie.cel.common.types.Err.unknownType; import static org.projectnessie.cel.common.types.Err.unsupportedRefValConversionErr; import static org.projectnessie.cel.common.types.IntT.IntType; @@ -90,6 +92,7 @@ import org.projectnessie.cel.common.types.TypeT; import org.projectnessie.cel.common.types.ref.FieldGetter; import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Lister; @@ -338,7 +341,7 @@ && isNullPrunedMessageField(fieldDesc) && value instanceof Lister) { return toNativeRepeatedFieldValue((Lister) value, fieldDesc); } - return value.convertToNative(field.reflectType()); + return valueToNative(value, field.reflectType()); } private Object toNativeRepeatedFieldValue(Lister value, FieldDescriptor fieldDesc) { @@ -350,7 +353,7 @@ private Object toNativeRepeatedFieldValue(Lister value, FieldDescriptor fieldDes if (element == NullT.NullValue && isNullPrunedMessageField(fieldDesc)) { continue; } - converted.add(element.convertToNative(elementType)); + converted.add(valueToNative(element, elementType)); } return converted; } @@ -424,17 +427,17 @@ private Object toProtoMapStructure(FieldDescription field, MapT value) { return entries; } - private static Object toNativeMapEntryValue(Val value, FieldDescriptor field) { + private Object toNativeMapEntryValue(Val value, FieldDescriptor field) { return switch (field.getType()) { - case DOUBLE -> value.convertToNative(Double.class); - case FLOAT -> value.convertToNative(Float.class); - case INT64, SINT64, SFIXED64 -> value.convertToNative(Long.class); - case UINT64, FIXED64 -> value.convertToNative(ULong.class).longValue(); - case INT32, SINT32, SFIXED32 -> value.convertToNative(Integer.class); - case UINT32, FIXED32 -> value.convertToNative(ULong.class).intValue(); - case BOOL -> value.convertToNative(Boolean.class); - case STRING -> value.convertToNative(String.class); - case BYTES -> value.convertToNative(ByteString.class); + case DOUBLE -> valueToDouble(value); + case FLOAT -> valueToNative(value, Float.class); + case INT64, SINT64, SFIXED64 -> valueToLong(value); + case UINT64, FIXED64 -> valueToNative(value, ULong.class).longValue(); + case INT32, SINT32, SFIXED32 -> valueToInt(value); + case UINT32, FIXED32 -> toUnsignedInt(value); + case BOOL -> valueToBoolean(value); + case STRING -> valueToNative(value, String.class); + case BYTES -> valueToNative(value, ByteString.class); case ENUM -> { if (value == NullT.NullValue) { if (field.getEnumType().getFullName().equals("google.protobuf.NullValue")) { @@ -442,13 +445,21 @@ private static Object toNativeMapEntryValue(Val value, FieldDescriptor field) { } throw new IllegalArgumentException("null is only valid for google.protobuf.NullValue"); } - yield value.convertToNative(Integer.class); + yield valueToInt(value); } - case MESSAGE -> value.convertToNative(messageNativeType(field)); + case MESSAGE -> valueToNative(value, messageNativeType(field)); case GROUP -> throw new IllegalArgumentException("protobuf maps cannot contain group values"); }; } + private int toUnsignedInt(Val value) { + long unsigned = valueToNative(value, ULong.class).longValue(); + if (Long.compareUnsigned(unsigned, 0xffff_ffffL) > 0) { + throwErrorAsIllegalStateException(rangeError(Long.toUnsignedString(unsigned), "uint32")); + } + return (int) unsigned; + } + /** * Converts {@code value}, of the map-field {@code fieldDesc} from its Java {@link Map} * representation to the protobuf-y {@code {@link List}<{@link MapEntry}>} representation. @@ -475,7 +486,7 @@ private Object toProtoMapStructure(FieldDescriptor fieldDesc, Object value) { continue; } if (!(v instanceof Message)) { - v = nativeToValue(v).convertToNative(messageNativeType(valueType)); + v = valueToNative(nativeToValue(v), messageNativeType(valueType)); } } @@ -590,6 +601,41 @@ public Val nativeToValue(Object value) { return unsupportedRefValConversionErr(value); } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + void registerAllTypes(FileDescription fd) { for (String typeName : fd.getTypeNames()) { registerType(newObjectTypeValue(typeName)); diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java index 81c88fea..7e2c1116 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapter.java @@ -16,9 +16,165 @@ package org.projectnessie.cel.common.types.ref; /** - * TypeAdapter converts native Go values of varying type and complexity to equivalent CEL values. + * Converts between host-language Java values and runtime CEL values. + * + *

An adapter is part of an environment's type integration and may be reused by every program + * created from that environment. Configure any mutable adapter state before sharing it. {@link + * #nativeToValue(Object)} should return a non-null CEL value, including a CEL error value for an + * unsupported runtime input when appropriate; Java {@code null} as input normally represents CEL + * null, subject to the concrete adapter's contract. + * + *

The primitive overloads delegate to the object conversion by default. Implementations may + * override them, but each override must have the same CEL semantics as the boxed representation. + * CEL-to-Java conversion methods throw when the requested target representation is incompatible. */ +@FunctionalInterface public interface TypeAdapter { - /** NativeToValue converts the input `value` to a CEL `ref.Val`. */ + /** + * Converts {@code value} to a CEL value. + * + * @param value host value, possibly {@code null} if supported by the adapter + * @return the corresponding non-null CEL value + */ Val nativeToValue(Object value); + + /** + * Converts a primitive boolean to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(boolean value) { + return nativeToValue(Boolean.valueOf(value)); + } + + /** + * Converts a primitive byte to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(byte value) { + return nativeToValue(Byte.valueOf(value)); + } + + /** + * Converts a primitive short to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(short value) { + return nativeToValue(Short.valueOf(value)); + } + + /** + * Converts a primitive int to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(int value) { + return nativeToValue(Integer.valueOf(value)); + } + + /** + * Converts a primitive character according to this adapter's boxed {@link Character} contract. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(char value) { + return nativeToValue(Character.valueOf(value)); + } + + /** + * Converts a primitive long to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(long value) { + return nativeToValue(Long.valueOf(value)); + } + + /** + * Converts a primitive float to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(float value) { + return nativeToValue(Float.valueOf(value)); + } + + /** + * Converts a primitive double to a CEL value. + * + * @param value primitive value + * @return the corresponding CEL value + */ + default Val nativeToValue(double value) { + return nativeToValue(Double.valueOf(value)); + } + + /** + * Converts a CEL value to the requested Java representation. + * + * @param value CEL value to convert + * @param targetType requested Java class or primitive class + * @return the converted Java value + * @throws RuntimeException if the conversion is unsupported or out of range + */ + default T valueToNative(Val value, Class targetType) { + return TypeAdapterSupport.valueToNative(this, value, targetType); + } + + /** + * Converts a CEL boolean value to a primitive boolean. + * + * @param value CEL value to convert + * @return the primitive value + * @throws RuntimeException if {@code value} is not convertible to a Java boolean + */ + default boolean valueToBoolean(Val value) { + return TypeAdapterSupport.valueToBoolean(value); + } + + /** + * Converts a CEL int or uint value to a range-checked primitive {@code int}. + * + * @param value CEL value to convert + * @return the primitive value + * @throws RuntimeException if {@code value} is not an integer or is outside the Java {@code int} + * range + */ + default int valueToInt(Val value) { + return TypeAdapterSupport.valueToInt(value); + } + + /** + * Converts a CEL integer value to its primitive long representation. + * + *

CEL signed integers use a Java {@code long}. Unsigned values preserve their raw unsigned + * bits in the returned {@code long}. + * + * @param value CEL value to convert + * @return the primitive value or raw unsigned bits + * @throws RuntimeException if {@code value} is not convertible to a Java {@code long} + */ + default long valueToLong(Val value) { + return TypeAdapterSupport.valueToLong(value); + } + + /** + * Converts a CEL double value to a primitive double. + * + * @param value CEL value to convert + * @return the primitive value + * @throws RuntimeException if {@code value} is not convertible to a Java {@code double} + */ + default double valueToDouble(Val value) { + return TypeAdapterSupport.valueToDouble(value); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java index 9c029a3d..9a6aec2b 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java @@ -18,6 +18,7 @@ import static org.projectnessie.cel.common.types.BytesT.bytesOf; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.DurationT.durationOf; +import static org.projectnessie.cel.common.types.Err.rangeError; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.ListT.newDoubleArrayList; import static org.projectnessie.cel.common.types.ListT.newGenericArrayList; @@ -56,10 +57,21 @@ import java.util.Optional; import java.util.function.BiFunction; import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.UnknownT; /** - * Helper class for {@link TypeAdapter} implementations to convert from a Java type to a CEL type. + * Standard conversion helpers for {@link TypeAdapter} implementations. + * + *

{@link #maybeNativeToValue(TypeAdapter, Object)} recognizes the built-in Java, Protobuf, and + * aggregate representations handled by CEL-Java. Custom adapters commonly call it first and handle + * a {@code null} return as “not recognized.” The primitive methods and CEL-to-Java methods provide + * the default semantics used by {@link TypeAdapter}. */ public final class TypeAdapterSupport { private TypeAdapterSupport() {} @@ -151,4 +163,169 @@ public static Val maybeNativeToValue(TypeAdapter a, Object value) { return null; } + + /** + * Returns the standard CEL boolean value. + * + * @param value primitive value + * @return CEL boolean + */ + public static Val nativeToValue(boolean value) { + return boolOf(value); + } + + /** + * Returns a CEL int value. + * + * @param value primitive value + * @return CEL int + */ + public static Val nativeToValue(byte value) { + return intOf(value); + } + + /** + * Returns a CEL int value. + * + * @param value primitive value + * @return CEL int + */ + public static Val nativeToValue(short value) { + return intOf(value); + } + + /** + * Returns a CEL int value. + * + * @param value primitive value + * @return CEL int + */ + public static Val nativeToValue(int value) { + return intOf(value); + } + + /** + * Returns a CEL int value. + * + * @param value primitive value + * @return CEL int + */ + public static Val nativeToValue(long value) { + return intOf(value); + } + + /** + * Returns a CEL double value. + * + * @param value primitive value + * @return CEL double + */ + public static Val nativeToValue(float value) { + return doubleOf(value); + } + + /** + * Returns a CEL double value. + * + * @param value primitive value + * @return CEL double + */ + public static Val nativeToValue(double value) { + return doubleOf(value); + } + + /** + * Converts a CEL value to the requested Java representation using primitive specializations where + * available. + * + * @param adapter adapter whose primitive conversion methods define the conversion semantics + * @param value CEL value to convert + * @param targetType requested Java class or primitive class + * @return the converted Java value + * @throws RuntimeException if conversion is unsupported or out of range + */ + @SuppressWarnings("unchecked") + public static T valueToNative(TypeAdapter adapter, Val value, Class targetType) { + if (targetType == boolean.class) { + return (T) Boolean.valueOf(adapter.valueToBoolean(value)); + } + if (targetType == int.class) { + return (T) Integer.valueOf(adapter.valueToInt(value)); + } + if (targetType == long.class) { + return (T) Long.valueOf(adapter.valueToLong(value)); + } + if (targetType == double.class) { + return (T) Double.valueOf(adapter.valueToDouble(value)); + } + return legacyValueToNative(value, targetType); + } + + /** + * Converts a CEL boolean to a Java primitive. + * + * @param value CEL value to convert + * @return primitive boolean + * @throws RuntimeException if {@code value} is not convertible to boolean + */ + public static boolean valueToBoolean(Val value) { + if (value instanceof BoolT boolValue) { + return boolValue.booleanValue(); + } + return legacyValueToNative(value, boolean.class); + } + + /** + * Converts a CEL int or uint to a range-checked Java {@code int}. + * + * @param value CEL value to convert + * @return primitive value + * @throws RuntimeException if {@code value} is not an integer or is outside the Java {@code int} + * range + */ + public static int valueToInt(Val value) { + if (value instanceof IntT || value instanceof UintT) { + long longValue = value.intValue(); + if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { + Err.throwErrorAsIllegalStateException(rangeError(longValue, "Java int")); + } + return (int) longValue; + } + return legacyValueToNative(value, int.class); + } + + /** + * Converts a CEL int or uint to Java {@code long} bits. + * + *

For CEL uint, the returned primitive preserves the unsigned value's raw bits. + * + * @param value CEL value to convert + * @return primitive signed value or raw unsigned bits + * @throws RuntimeException if {@code value} is not convertible to {@code long} + */ + public static long valueToLong(Val value) { + if (value instanceof IntT || value instanceof UintT || value instanceof UnknownT) { + return value.intValue(); + } + return legacyValueToNative(value, long.class); + } + + /** + * Converts a CEL double to a Java primitive. + * + * @param value CEL value to convert + * @return primitive double + * @throws RuntimeException if {@code value} is not convertible to {@code double} + */ + public static double valueToDouble(Val value) { + if (value instanceof DoubleT doubleValue) { + return doubleValue.doubleValue(); + } + return legacyValueToNative(value, double.class); + } + + @SuppressWarnings("removal") + private static T legacyValueToNative(Val value, Class targetType) { + return value.convertToNative(targetType); + } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java index 113285ba..dd47d650 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/Val.java @@ -21,9 +21,14 @@ */ public interface Val { /** - * ConvertToNative converts the Value to a native Go struct according to the reflected type - * description, or error if the conversion is not feasible. + * Converts the value to a native Java value according to the requested type, or reports an error + * if the conversion is not feasible. + * + * @deprecated Use {@link TypeAdapter#valueToNative(Val, Class)} with the adapter associated with + * the evaluation environment. {@code DefaultTypeAdapter.Instance} can be used for + * context-free built-in values. */ + @Deprecated(forRemoval = true) T convertToNative(Class typeDesc); /** @@ -42,8 +47,13 @@ public interface Val { Type type(); /** - * Value returns the raw value of the instance which may not be directly compatible with the - * expression language types. + * Returns the underlying representation. + * + *

The result is implementation-specific and is not necessarily accepted directly by other CEL + * operations. Prefer {@link TypeAdapter#valueToNative(Val, Class)} when a particular Java + * representation is required. + * + * @return the underlying representation, possibly {@code null} for CEL null */ Object value(); diff --git a/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java index 2f3972aa..1b90817b 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/EncodersLib.java @@ -29,6 +29,7 @@ import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.types.BytesT; import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.interpreter.functions.Overload; @@ -73,7 +74,7 @@ private static Val base64Encode(Val value) { if (!(value instanceof BytesT)) { return noSuchOverload(value, BASE64_ENCODE, BASE64_ENCODE_OVERLOAD, new Val[] {value}); } - byte[] bytes = value.convertToNative(byte[].class); + byte[] bytes = DefaultTypeAdapter.Instance.valueToNative(value, byte[].class); return stringOf(Base64.getEncoder().encodeToString(bytes)); } @@ -81,7 +82,7 @@ private static Val base64Decode(Val value) { if (!(value instanceof StringT)) { return noSuchOverload(value, BASE64_DECODE, BASE64_DECODE_OVERLOAD, new Val[] {value}); } - String text = value.convertToNative(String.class); + String text = DefaultTypeAdapter.Instance.valueToNative(value, String.class); try { return bytesOf(Base64.getDecoder().decode(text)); } catch (IllegalArgumentException e) { diff --git a/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java index 2e53bd97..71e6a8e5 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/NetworkLib.java @@ -348,7 +348,7 @@ private IpT(byte[] bytes, int family) { } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) public T convertToNative(Class typeDesc) { if (typeDesc == String.class || typeDesc == Object.class) { return (T) canonical; @@ -440,7 +440,7 @@ private CidrT(IpT ip, int prefix) { } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"removal", "unchecked"}) public T convertToNative(Class typeDesc) { if (typeDesc == String.class || typeDesc == Object.class) { return (T) canonical; diff --git a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java index 8cc65c47..3b8d0b3e 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java @@ -28,6 +28,7 @@ import org.projectnessie.cel.checker.Decls; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Indexer; import org.projectnessie.cel.common.types.traits.Sizer; @@ -808,7 +809,8 @@ private static String renderStringClause(Val value) { return switch (value.type().typeEnum()) { case String -> value.value().toString(); case Bool -> Boolean.toString(value.booleanValue()); - case Bytes -> new String(value.convertToNative(byte[].class), UTF_8); + case Bytes -> + new String(DefaultTypeAdapter.Instance.valueToNative(value, byte[].class), UTF_8); case Int -> Long.toString(value.intValue()); case Uint -> Long.toUnsignedString(value.intValue()); case Double -> renderDouble(value.doubleValue()); @@ -871,7 +873,7 @@ private static String renderHexClause(Val value, boolean upperCase) { case Int -> Long.toHexString(value.intValue()); case Uint -> Long.toUnsignedString(value.intValue(), 16); case String -> bytesToHex(value.value().toString().getBytes(UTF_8)); - case Bytes -> bytesToHex(value.convertToNative(byte[].class)); + case Bytes -> bytesToHex(DefaultTypeAdapter.Instance.valueToNative(value, byte[].class)); default -> throw new FormatException( "error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given %s", diff --git a/core/src/test/java/org/projectnessie/cel/CELTest.java b/core/src/test/java/org/projectnessie/cel/CELTest.java index 356a2b4f..8b58ac2d 100644 --- a/core/src/test/java/org/projectnessie/cel/CELTest.java +++ b/core/src/test/java/org/projectnessie/cel/CELTest.java @@ -295,6 +295,7 @@ void Abbrevs_Parsed() { } @Test + @SuppressWarnings("removal") void Abbrevs_Disambiguation() { Env env = newEnv( @@ -341,6 +342,7 @@ void CustomEnv() { } @Test + @SuppressWarnings("removal") void CustomEnvCanSubsetStandardLibrary() { Env withoutStdLib = newCustomEnv(declarations(Decls.newVar("resource.name", Decls.String))); AstIssuesTuple missingStartsWith = diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java index 8c25f5f5..de6a823b 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ListTest.java @@ -217,6 +217,7 @@ void listConstruction(TestData tc) { checkList(doubleTc, doubleListVal); } + @SuppressWarnings("removal") Lister checkList(TestData tc, Val listVal) { assertThat(listVal) .isInstanceOf(Lister.class) diff --git a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java index bd22dedd..13ea2f2b 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java @@ -37,6 +37,7 @@ public class MapTest { @Test + @SuppressWarnings("removal") void heterogenousKeys() { Map javaMap = ImmutableMap.of( @@ -102,6 +103,7 @@ void heterogenousKeys() { } @Test + @SuppressWarnings("removal") void mapToProtobufStructRequiresStringKeys() { MapT stringKeyMap = (MapT) newWrappedMap(newRegistry(), ImmutableMap.of(stringOf("one"), doubleOf(1.0d))); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java index b969f316..1f65cc35 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java @@ -186,7 +186,7 @@ void typeRegistryNewValue_OneofFields() { mapOf("string_value", stringOf("oneof"))))))); assertThat(exp).matches(v -> !Err.isError(v)); - CheckedExpr ce = exp.convertToNative(CheckedExpr.class); + CheckedExpr ce = reg.valueToNative(exp, CheckedExpr.class); assertThat(ce) .extracting(CheckedExpr::getExpr) .extracting(Expr::getConstExpr) @@ -201,7 +201,7 @@ void typeRegistryNewValue_WrapperFields() { reg.newValue( "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", intOf(123))); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce) .extracting(TestAllTypes::getSingleInt32Wrapper) .extracting(Int32Value::getValue) @@ -216,7 +216,7 @@ void typeRegistryNewValue_NullWrapperFieldIsUnset() { "cel.expr.conformance.proto3.TestAllTypes", mapOf("single_int32_wrapper", NullValue)); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce.hasSingleInt32Wrapper()).isFalse(); } @@ -235,7 +235,7 @@ void typeRegistryNewValue_NullMessageFieldsAreUnset() { NullValue)); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce.hasSingleNestedMessage()).isFalse(); assertThat(ce.hasSingleDuration()).isFalse(); assertThat(ce.hasSingleTimestamp()).isFalse(); @@ -258,7 +258,7 @@ void typeRegistryNewValue_RepeatedMessageFieldNullsArePruned() { newGenericArrayList(reg, new Val[] {intOf(1), NullValue}))); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce.getRepeatedTimestampList()) .containsExactly(Timestamp.newBuilder().setSeconds(1).build()); assertThat(ce.getRepeatedDurationList()) @@ -288,7 +288,7 @@ reg, mapOf(true, NullValue, false, durationOf(Duration.ofSeconds(1)))), newMaybeWrappedMap(reg, mapOf(true, NullValue, false, intOf(1))))); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes ce = exp.convertToNative(TestAllTypes.class); + TestAllTypes ce = reg.valueToNative(exp, TestAllTypes.class); assertThat(ce.getMapBoolTimestampMap()) .containsExactlyEntriesOf(mapOf(false, Timestamp.newBuilder().setSeconds(1).build())); assertThat(ce.getMapBoolDurationMap()) @@ -327,7 +327,7 @@ void typeRegistryNewValue_ConvertsMapEntriesDirectly() { new DirectConversionMap(reg, mapOf(true, nestedMessage)))); assertThat(exp).matches(v -> !Err.isError(v)); - TestAllTypes value = exp.convertToNative(TestAllTypes.class); + TestAllTypes value = reg.valueToNative(exp, TestAllTypes.class); assertThat(value.getMapBoolBytesMap()).containsEntry(true, ByteString.copyFromUtf8("bytes")); assertThat(value.getMapBoolInt32Map()).containsEntry(true, Integer.MIN_VALUE); assertThat(value.getMapBoolUint32Map()).containsEntry(true, -1); @@ -339,6 +339,21 @@ void typeRegistryNewValue_ConvertsMapEntriesDirectly() { .containsEntry(true, TestAllTypes.NestedMessage.newBuilder().setBb(42).build()); } + @Test + void typeRegistryNewValue_RejectsOutOfRangeUint32MapEntries() { + TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); + + Val value = + reg.newValue( + "cel.expr.conformance.proto3.TestAllTypes", + mapOf( + "map_bool_uint32", + new DirectConversionMap(reg, mapOf(true, uintOf(0x1_0000_0000L))))); + + assertThat(value).matches(Err::isError); + assertThat(value.toString()).contains("range error converting 4294967296 to uint32"); + } + @Test void typeRegistryNewValue_InvalidNullFieldAssignmentsReturnErrors() { TypeRegistry reg = newRegistry(TestAllTypes.getDefaultInstance()); @@ -363,6 +378,7 @@ private DirectConversionMap(TypeRegistry registry, Map value) { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new AssertionError("protobuf map conversion must not materialize a native Java map"); } @@ -455,67 +471,80 @@ void convertToNative() { TypeRegistry reg = newRegistry(ParsedExpr.getDefaultInstance()); // Core type conversion tests. - expectValueToNative(True, true); - expectValueToNative(True, True); + expectValueToNative(reg, True, true); + expectValueToNative(reg, True, True); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {True, False}), new Object[] {true, false}); expectValueToNative( - newGenericArrayList(reg, new Val[] {True, False}), new Object[] {true, false}); - expectValueToNative(newGenericArrayList(reg, new Val[] {True, False}), new Val[] {True, False}); - expectValueToNative(intOf(-1), -1); - expectValueToNative(intOf(2), 2L); - expectValueToNative(intOf(-1), -1); - expectValueToNative(newGenericArrayList(reg, new Val[] {intOf(4)}), new Object[] {4L}); - expectValueToNative(newGenericArrayList(reg, new Val[] {intOf(5)}), new Val[] {intOf(5)}); - expectValueToNative(uintOf(3), ULong.valueOf(3)); - expectValueToNative(uintOf(4), ULong.valueOf(4)); - expectValueToNative(uintOf(5), 5); + reg, newGenericArrayList(reg, new Val[] {True, False}), new Val[] {True, False}); + expectValueToNative(reg, intOf(-1), -1); + expectValueToNative(reg, intOf(2), 2L); + expectValueToNative(reg, intOf(-1), -1); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {intOf(4)}), new Object[] {4L}); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {intOf(5)}), new Val[] {intOf(5)}); + expectValueToNative(reg, uintOf(3), ULong.valueOf(3)); + expectValueToNative(reg, uintOf(4), ULong.valueOf(4)); + expectValueToNative(reg, uintOf(5), 5); expectValueToNative( - newGenericArrayList(reg, new Val[] {uintOf(4)}), new Object[] {4L}); // loses "ULong" here - expectValueToNative(newGenericArrayList(reg, new Val[] {uintOf(5)}), new Val[] {uintOf(5)}); - expectValueToNative(doubleOf(5.5d), 5.5f); - expectValueToNative(doubleOf(-5.5d), -5.5d); - expectValueToNative(newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Object[] {-5.5}); + reg, + newGenericArrayList(reg, new Val[] {uintOf(4)}), + new Object[] {4L}); // loses "ULong" here expectValueToNative( - newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Val[] {doubleOf(-5.5)}); - expectValueToNative(doubleOf(-5.5), doubleOf(-5.5)); - expectValueToNative(stringOf("hello"), "hello"); - expectValueToNative(stringOf("hello"), stringOf("hello")); - expectValueToNative(NullValue, NULL_VALUE); - expectValueToNative(NullValue, NullValue); - expectValueToNative(newGenericArrayList(reg, new Val[] {NullValue}), new Object[] {null}); - expectValueToNative(newGenericArrayList(reg, new Val[] {NullValue}), new Val[] {NullValue}); - expectValueToNative(bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); - expectValueToNative(bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + reg, newGenericArrayList(reg, new Val[] {uintOf(5)}), new Val[] {uintOf(5)}); + expectValueToNative(reg, doubleOf(5.5d), 5.5f); + expectValueToNative(reg, doubleOf(-5.5d), -5.5d); expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Object[] {-5.5}); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {doubleOf(-5.5)}), new Val[] {doubleOf(-5.5)}); + expectValueToNative(reg, doubleOf(-5.5), doubleOf(-5.5)); + expectValueToNative(reg, stringOf("hello"), "hello"); + expectValueToNative(reg, stringOf("hello"), stringOf("hello")); + expectValueToNative(reg, NullValue, NULL_VALUE); + expectValueToNative(reg, NullValue, NullValue); + expectValueToNative(reg, newGenericArrayList(reg, new Val[] {NullValue}), new Object[] {null}); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {NullValue}), new Val[] {NullValue}); + expectValueToNative(reg, bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + expectValueToNative(reg, bytesOf("world"), "world".getBytes(StandardCharsets.UTF_8)); + expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Object[] {ByteString.copyFromUtf8("hello")}); expectValueToNative( - newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Val[] {bytesOf("hello")}); + reg, newGenericArrayList(reg, new Val[] {bytesOf("hello")}), new Val[] {bytesOf("hello")}); expectValueToNative( + reg, newGenericArrayList(reg, new Val[] {intOf(1), intOf(2), intOf(3)}), new Object[] {1L, 2L, 3L}); - expectValueToNative(durationOf(Duration.ofSeconds(500)), Duration.ofSeconds(500)); + expectValueToNative(reg, durationOf(Duration.ofSeconds(500)), Duration.ofSeconds(500)); expectValueToNative( + reg, durationOf(Duration.ofSeconds(500)), com.google.protobuf.Duration.newBuilder().setSeconds(500).build()); - expectValueToNative(durationOf(Duration.ofSeconds(500)), durationOf(Duration.ofSeconds(500))); expectValueToNative( + reg, durationOf(Duration.ofSeconds(500)), durationOf(Duration.ofSeconds(500))); + expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), Instant.ofEpochSecond(12345, 0).atZone(ZoneIdZ)); expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), timestampOf(Timestamp.newBuilder().setSeconds(12345).build())); expectValueToNative( + reg, timestampOf(Timestamp.newBuilder().setSeconds(12345).build()), Timestamp.newBuilder().setSeconds(12345).build()); expectValueToNative( - newMaybeWrappedMap(reg, mapOf(1L, 1L, 2L, 1L, 3L, 1L)), mapOf(1L, 1L, 2L, 1L, 3L, 1L)); + reg, newMaybeWrappedMap(reg, mapOf(1L, 1L, 2L, 1L, 3L, 1L)), mapOf(1L, 1L, 2L, 1L, 3L, 1L)); // Null conversion tests. - expectValueToNative(NullValue, NULL_VALUE); + expectValueToNative(reg, NullValue, NULL_VALUE); // Proto conversion tests. ParsedExpr parsedExpr = ParsedExpr.getDefaultInstance(); - expectValueToNative(reg.nativeToValue(parsedExpr), parsedExpr); + expectValueToNative(reg, reg.nativeToValue(parsedExpr), parsedExpr); } @Test @@ -675,8 +704,8 @@ void unsupportedConversion() { static class nonConvertible {} - static void expectValueToNative(Val in, Object out) { - Object val = in.convertToNative(out.getClass()); + static void expectValueToNative(TypeRegistry registry, Val in, Object out) { + Object val = registry.valueToNative(in, out.getClass()); assertThat(val).isNotNull(); if (val instanceof byte[]) { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java index e08ca174..7261ec02 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/TypeTest.java @@ -29,7 +29,9 @@ import static org.projectnessie.cel.common.types.TypeT.TypeType; import static org.projectnessie.cel.common.types.UintT.UintType; +import com.google.api.expr.v1alpha1.Value; import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.Val; @@ -63,6 +65,23 @@ void typeType() { assertThat(TypeType.type()).isSameAs(TypeType); } + @Test + void typeValueNamesAdaptToBuiltInAndObjectTypes() { + assertThat(Types.getTypeByName("int")).isSameAs(IntType); + assertThat(Types.getTypeByName("google.protobuf.Duration")).isSameAs(DurationType); + assertThat(Types.getTypeByName("google.protobuf.Timestamp")).isSameAs(TimestampType); + assertThat(Types.getTypeByName("example.CustomType")).isNull(); + + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(Value.getDefaultInstance()); + assertThat(registry.nativeToValue(Value.newBuilder().setTypeValue("int").build())) + .isSameAs(IntType); + + Val objectType = + registry.nativeToValue(Value.newBuilder().setTypeValue("example.CustomType").build()); + assertThat(objectType).isInstanceOf(Type.class); + assertThat(((Type) objectType).typeName()).isEqualTo("example.CustomType"); + } + @Test void objectTypeEqualsBuiltInTypeWithSameName() { assertThat(TypeT.newObjectTypeValue("google.protobuf.Timestamp").equal(TimestampType)) diff --git a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java index 01608869..2a81be88 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/pb/PbObjectTest.java @@ -67,6 +67,7 @@ void newProtoObject() { } @Test + @SuppressWarnings("removal") void protoObjectConvertToNative() throws Exception { TypeRegistry reg = newRegistry(Expr.getDefaultInstance()); ParsedExpr msg = @@ -96,6 +97,7 @@ void protoObjectConvertToNative() throws Exception { } @Test + @SuppressWarnings("removal") void wellKnownProtoObjectsConvertToJsonValue() { TypeRegistry reg = newRegistry(Empty.getDefaultInstance(), FieldMask.getDefaultInstance()); diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java new file mode 100644 index 00000000..06e62c2e --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2021 The Authors of CEL-Java + * + * 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 + * + * http://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 org.projectnessie.cel.common.types.ref; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; + +@SuppressWarnings("removal") +class TypeAdapterTest { + + private static final TypeAdapter DEFAULT_ADAPTER = DefaultTypeAdapter.Instance; + + @Test + void primitiveInboundDefaultsPreserveExactWrapperType() { + TrackingAdapter adapter = new TrackingAdapter(); + + adapter.nativeToValue(true); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Boolean.class).isEqualTo(true); + + adapter.nativeToValue((byte) 1); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Byte.class).isEqualTo((byte) 1); + + adapter.nativeToValue((short) 2); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Short.class).isEqualTo((short) 2); + + adapter.nativeToValue(3); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Integer.class).isEqualTo(3); + + adapter.nativeToValue('4'); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Character.class).isEqualTo('4'); + + adapter.nativeToValue(5L); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Long.class).isEqualTo(5L); + + adapter.nativeToValue(6.0f); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Float.class).isEqualTo(6.0f); + + adapter.nativeToValue(7.0d); + assertThat(adapter.lastNativeValue).isExactlyInstanceOf(Double.class).isEqualTo(7.0d); + } + + @Test + void builtInPrimitiveInboundConversions() { + assertThat(DEFAULT_ADAPTER.nativeToValue(true)).isSameAs(True); + assertThat(DEFAULT_ADAPTER.nativeToValue(false)).isSameAs(False); + assertThat(DEFAULT_ADAPTER.nativeToValue((byte) -1)).isEqualTo(intOf(-1)); + assertThat(DEFAULT_ADAPTER.nativeToValue((short) 2)).isEqualTo(intOf(2)); + assertThat(DEFAULT_ADAPTER.nativeToValue(3)).isEqualTo(intOf(3)); + assertThat(DEFAULT_ADAPTER.nativeToValue(4L)).isEqualTo(intOf(4)); + assertThat(DEFAULT_ADAPTER.nativeToValue(5.25f)).isEqualTo(doubleOf(5.25)); + assertThat(DEFAULT_ADAPTER.nativeToValue(6.5d)).isEqualTo(doubleOf(6.5)); + assertThat(DEFAULT_ADAPTER.nativeToValue('x')).matches(Err::isError); + } + + @Test + void primitiveClassTokenDispatchesThroughPrimitiveOverride() { + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return DEFAULT_ADAPTER.nativeToValue(value); + } + + @Override + public int valueToInt(Val value) { + return 42; + } + }; + + assertThat(adapter.valueToNative(intOf(1), int.class)).isEqualTo(42); + assertThat(adapter.valueToNative(intOf(1), Integer.class)).isEqualTo(1); + } + + @Test + void primitiveOutboundConversionsMatchLegacyConversions() { + assertThat(DEFAULT_ADAPTER.valueToBoolean(True)).isEqualTo(True.convertToNative(boolean.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(False, boolean.class)) + .isEqualTo(False.convertToNative(boolean.class)); + + assertThat(DEFAULT_ADAPTER.valueToInt(intOf(Integer.MIN_VALUE))) + .isEqualTo(intOf(Integer.MIN_VALUE).convertToNative(int.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(intOf(Integer.MAX_VALUE), int.class)) + .isEqualTo(intOf(Integer.MAX_VALUE).convertToNative(int.class)); + + assertThat(DEFAULT_ADAPTER.valueToLong(intOf(Long.MIN_VALUE))) + .isEqualTo(intOf(Long.MIN_VALUE).convertToNative(long.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(uintOf(-1), long.class)) + .isEqualTo(uintOf(-1).convertToNative(long.class)); + + assertThat(DEFAULT_ADAPTER.valueToDouble(doubleOf(Double.NEGATIVE_INFINITY))) + .isEqualTo(doubleOf(Double.NEGATIVE_INFINITY).convertToNative(double.class)); + assertThat(DEFAULT_ADAPTER.valueToNative(doubleOf(Double.NaN), double.class)).isNaN(); + } + + @Test + void primitiveOutboundConversionsPreserveRangeAndWrongTypeFailures() { + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToInt(intOf((long) Integer.MAX_VALUE + 1))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("range error converting 2147483648 to Java int"); + assertThatThrownBy( + () -> DEFAULT_ADAPTER.valueToNative(intOf((long) Integer.MIN_VALUE - 1), int.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("range error converting -2147483649 to Java int"); + + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToBoolean(intOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'int' to 'boolean'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToInt(doubleOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'double' to 'int'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToLong(True)) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'bool' to 'long'"); + assertThatThrownBy(() -> DEFAULT_ADAPTER.valueToDouble(intOf(1))) + .isInstanceOf(RuntimeException.class) + .hasMessage("native type conversion error from 'int' to 'double'"); + } + + @Test + void customValUsesLegacyConversionFallback() { + LegacyVal value = new LegacyVal(); + + assertThat(DEFAULT_ADAPTER.valueToNative(value, String.class)).isEqualTo("legacy"); + assertThat(value.lastTargetType).isEqualTo(String.class); + assertThat(DEFAULT_ADAPTER.valueToBoolean(value)).isTrue(); + assertThat(value.lastTargetType).isEqualTo(boolean.class); + } + + private static final class TrackingAdapter implements TypeAdapter { + private Object lastNativeValue; + + @Override + public Val nativeToValue(Object value) { + lastNativeValue = value; + return NullT.NullValue; + } + } + + private static final class LegacyVal implements Val { + private Class lastTargetType; + + @Override + @SuppressWarnings("unchecked") + public T convertToNative(Class typeDesc) { + lastTargetType = typeDesc; + if (typeDesc == String.class) { + return (T) "legacy"; + } + if (typeDesc == boolean.class) { + return (T) Boolean.TRUE; + } + throw new AssertionError("unexpected target type " + typeDesc); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return other == this ? True : False; + } + + @Override + public Type type() { + return StringType; + } + + @Override + public Object value() { + return "legacy"; + } + + @Override + public boolean booleanValue() { + return true; + } + + @Override + public long intValue() { + return 0; + } + + @Override + public double doubleValue() { + return 0; + } + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index 45625645..875d79a2 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -145,6 +145,7 @@ public TypeEnum typeEnum() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } @@ -188,6 +189,7 @@ public double doubleValue() { private static final class ReceiverTestVal extends BaseVal implements Receiver { @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException(); } diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java index 2b1e07a0..0267bd69 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonObjectT.java @@ -78,6 +78,7 @@ public Val get(Val index) { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { return (T) value; diff --git a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java index 1db799e2..c276f48a 100644 --- a/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java +++ b/jackson/src/main/java/org/projectnessie/cel/types/jackson/JacksonRegistry.java @@ -149,6 +149,41 @@ public Val nativeToValue(Object value) { } } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java index 85c83e88..6faa3600 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/Jackson3Registry.java @@ -160,6 +160,41 @@ public Val nativeToValue(Object value) { } } + @Override + public Val nativeToValue(boolean value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(byte value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(short value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(int value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(long value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(float value) { + return TypeAdapterSupport.nativeToValue(value); + } + + @Override + public Val nativeToValue(double value) { + return TypeAdapterSupport.nativeToValue(value); + } + synchronized JacksonEnumDescription enumDescription(Class clazz) { if (!Enum.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("only enum allowed here"); diff --git a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java index 4ab3ab36..7018a4f5 100644 --- a/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java +++ b/jackson3/src/main/java/org/projectnessie/cel/types/jackson3/JacksonObjectT.java @@ -78,6 +78,7 @@ public Val get(Val index) { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { if (typeDesc.isAssignableFrom(value.getClass())) { return (T) value; diff --git a/tools/src/main/java/org/projectnessie/cel/tools/Script.java b/tools/src/main/java/org/projectnessie/cel/tools/Script.java index 2ee6d939..98188dbc 100644 --- a/tools/src/main/java/org/projectnessie/cel/tools/Script.java +++ b/tools/src/main/java/org/projectnessie/cel/tools/Script.java @@ -71,6 +71,6 @@ private T evaluate(Class resultType, Object arguments) throws ScriptExecu return (T) result; } - return result.convertToNative(resultType); + return env.getTypeAdapter().valueToNative(result, resultType); } } diff --git a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java index 76a48286..13ec6425 100644 --- a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java +++ b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java @@ -450,7 +450,7 @@ public List getProgramOptions() { if (!(keyVal instanceof StringT) || !(defaultVal instanceof StringT)) { return newErr("invalid arguments to getAttribute"); } - String key = keyVal.convertToNative(String.class); + String key = keyVal.value().toString(); if ("error".equals(key)) { return newErr("forced getAttribute error"); } @@ -485,8 +485,8 @@ public List getProgramOptions() { if (!(valueVal instanceof StringT) || !(prefixVal instanceof StringT)) { return newErr("invalid arguments to extractAfter"); } - String value = valueVal.convertToNative(String.class); - String prefix = prefixVal.convertToNative(String.class); + String value = valueVal.value().toString(); + String prefix = prefixVal.value().toString(); if ("error".equals(prefix)) { return newErr("forced extractAfter error"); } From 1db646182ac67f911c7d13fb72a9158e1217a6ec Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 21 Oct 2026 19:31:45 +0200 Subject: [PATCH 2/4] Fix native map conversion and unsigned protobuf map round trips Route native map keys and values through the configured adapter while preserving raw value access. Normalize uint32 and fixed32 map entries without sign extension, retaining output range validation. Add regression coverage for nested conversions, null and type values, generated and dynamic unsigned maps, and out-of-range keys and values. --- .../projectnessie/cel/common/types/MapT.java | 15 +++-- .../cel/common/types/pb/FieldDescription.java | 13 ++-- .../cel/common/types/MapTest.java | 47 +++++++++++++++ .../cel/common/types/ProviderTest.java | 60 +++++++++++++++++++ .../src/testFixtures/proto/proto-map.proto | 7 +++ 5 files changed, 129 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java index ce44420b..6328d1f6 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java @@ -150,10 +150,12 @@ private Struct toPbStruct() { return struct.build(); } - @SuppressWarnings({"rawtypes", "unchecked"}) - private Map toJavaMap() { - Map r = new HashMap(); - map.forEach((k, v) -> r.put(k.value(), v.value())); + private Map toJavaMap() { + Map r = new HashMap<>(); + map.forEach( + (k, v) -> + r.put( + adapter.valueToNative(k, Object.class), adapter.valueToNative(v, Object.class))); return r; } @@ -223,8 +225,9 @@ public Val equal(Val other) { @Override public Object value() { // TODO this is expensive :( - Map nativeMap = toJavaMap(); - return nativeMap; + Map rawMap = new HashMap<>(); + map.forEach((k, v) -> rawMap.put(k.value(), v.value())); + return rawMap; } @Override diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java index cf4d5f8e..64476a1a 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java @@ -683,13 +683,12 @@ private Object rawMapEntryValue(Object entry, int fieldNumber) { } private static Object normalizeUnsignedValue(FieldDescriptor desc, Object value) { - FieldDescriptor.Type type = desc.getType(); - if (value instanceof Number - && (type == FieldDescriptor.Type.UINT32 - || type == FieldDescriptor.Type.UINT64 - || type == FieldDescriptor.Type.FIXED32 - || type == FieldDescriptor.Type.FIXED64)) { - return ULong.valueOf(((Number) value).longValue()); + if (value instanceof Number number) { + return switch (desc.getType()) { + case UINT32, FIXED32 -> ULong.valueOf(Integer.toUnsignedLong(number.intValue())); + case UINT64, FIXED64 -> ULong.valueOf(number.longValue()); + default -> value; + }; } return value; } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java index 13ea2f2b..3d5d02e6 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/MapTest.java @@ -28,14 +28,61 @@ import com.google.common.collect.ImmutableMap; import com.google.protobuf.Struct; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.Val; public class MapTest { + @Test + void nativeMapConversionUsesAdapterRecursively() { + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return DefaultTypeAdapter.Instance.nativeToValue(value); + } + + @Override + public T valueToNative(Val value, Class targetType) { + if (targetType == Object.class && value instanceof IntT) { + return targetType.cast(value.intValue() + 100); + } + return TypeAdapter.super.valueToNative(value, targetType); + } + }; + Val nested = newWrappedMap(adapter, Map.of(intOf(1), intOf(2))); + Val list = ListT.newValArrayList(adapter, new Val[] {intOf(3), nested}); + Val map = newWrappedMap(adapter, Map.of(intOf(4), list, stringOf("nested"), nested)); + + Map expected = + Map.of(104L, List.of(103L, Map.of(101L, 102L)), "nested", Map.of(101L, 102L)); + assertThat((Object) adapter.valueToNative(map, Map.class)).isEqualTo(expected); + assertThat(adapter.valueToNative(map, Object.class)).isEqualTo(expected); + assertThat(nested.value()).isEqualTo(Map.of(1L, 2L)); + } + + @Test + void nativeMapConversionPreservesNullAndTypeSemantics() { + TypeAdapter adapter = DefaultTypeAdapter.Instance; + Val nullMap = newWrappedMap(adapter, Map.of(stringOf("null"), NullT.NullValue)); + assertThat((Object) adapter.valueToNative(nullMap, Map.class)) + .isEqualTo(Collections.singletonMap("null", null)); + assertThat(nullMap.value()).isEqualTo(Map.of("null", NullT.NullValue.value())); + + Val typeMap = newWrappedMap(adapter, Map.of(stringOf("type"), IntT.IntType)); + assertThat(typeMap.value()).isEqualTo(Map.of("type", "int")); + assertThatThrownBy(() -> adapter.valueToNative(typeMap, Map.class)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("type conversion not supported for 'type'"); + } + @Test @SuppressWarnings("removal") void heterogenousKeys() { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java index 1f65cc35..c785e1ad 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java @@ -45,6 +45,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; import com.google.protobuf.DoubleValue; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; @@ -60,20 +61,79 @@ import java.time.Instant; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Indexer; import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.proto.tests.ProtoTestTypes.UnsignedMaps; import org.projectnessie.cel.test.proto3.OutOfOrderEnumOuterClass; public class ProviderTest { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void unsignedMapEntriesRoundTrip(boolean dynamic) throws Exception { + UnsignedMaps generated = + UnsignedMaps.newBuilder() + .putUint32Map(0x80000000, 0x80000000) + .putUint32Map(0xffffffff, 0xffffffff) + .putFixed32Map(0x80000000, 0x80000000) + .putFixed32Map(0xffffffff, 0xffffffff) + .putUint64Map(Long.MIN_VALUE, Long.MIN_VALUE) + .putUint64Map(-1L, -1L) + .putFixed64Map(Long.MIN_VALUE, Long.MIN_VALUE) + .putFixed64Map(-1L, -1L) + .build(); + Message input = + dynamic + ? DynamicMessage.parseFrom(UnsignedMaps.getDescriptor(), generated.toByteString()) + : generated; + TypeRegistry registry = newRegistry(input); + Val value = registry.nativeToValue(input); + Map fields = new HashMap<>(); + for (String name : new String[] {"uint32_map", "fixed32_map", "uint64_map", "fixed64_map"}) { + MapT map = (MapT) ((Indexer) value).get(stringOf(name)); + long[] expected = + name.contains("32") + ? new long[] {0x80000000L, 0xffffffffL} + : new long[] {Long.MIN_VALUE, -1L}; + for (long entry : expected) { + assertThat(map.get(uintOf(entry))).isEqualTo(uintOf(entry)); + } + fields.put(name, map); + } + Val rebuilt = registry.newValue(input.getDescriptorForType().getFullName(), fields); + assertThat(rebuilt).matches(v -> !Err.isError(v)); + Message output = registry.valueToNative(rebuilt, Message.class); + assertThat(output.getAllFields()).isEqualTo(input.getAllFields()); + } + + @ParameterizedTest + @ValueSource(strings = {"uint32_map", "fixed32_map"}) + void unsignedMapEntriesRejectOutOfRangeKeysAndValues(String field) { + TypeRegistry registry = newRegistry(UnsignedMaps.getDefaultInstance()); + for (Map entries : + new Map[] { + Map.of(uintOf(0x1_0000_0000L), uintOf(1)), Map.of(uintOf(1), uintOf(0x1_0000_0000L)) + }) { + Val value = + registry.newValue( + UnsignedMaps.getDescriptor().getFullName(), + Map.of(field, newMaybeWrappedMap(registry, entries))); + assertThat(value).matches(Err::isError); + assertThat(value.toString()).contains("range error converting 4294967296 to uint32"); + } + } + @Test void typeRegistryCopy() { TypeRegistry reg = newEmptyRegistry(); diff --git a/generated-pb/src/testFixtures/proto/proto-map.proto b/generated-pb/src/testFixtures/proto/proto-map.proto index 2765db9d..b4573346 100644 --- a/generated-pb/src/testFixtures/proto/proto-map.proto +++ b/generated-pb/src/testFixtures/proto/proto-map.proto @@ -8,3 +8,10 @@ message EventA { bool property_bool = 5; map map_str = 11; } + +message UnsignedMaps { + map uint32_map = 1; + map fixed32_map = 2; + map uint64_map = 3; + map fixed64_map = 4; +} From e6c18ed07f77c58c7d527eb3dd395a9a311f99e9 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 22 Oct 2026 08:58:53 +0200 Subject: [PATCH 3/4] Preserve adapter hooks and boxed custom value conversions Use the calling adapter when converting built-in list elements, including string arrays and split results. Preserve legacy string-array element adaptation and custom list conversions. Preserve boxed conversion targets for custom protobuf map entries while retaining built-in primitive fast paths. Cover Java and protobuf list outputs, split overloads, null string-array elements, and boxed-only map keys and values. --- .../projectnessie/cel/common/types/ListT.java | 44 +++++++--- .../common/types/pb/ProtoTypeRegistry.java | 26 ++++-- .../common/types/ref/TypeAdapterSupport.java | 4 + .../cel/common/types/ProviderTest.java | 85 +++++++++++++++++++ .../cel/common/types/ref/TypeAdapterTest.java | 71 ++++++++++++++++ 5 files changed, 213 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java index 12aad198..44470f6c 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java @@ -59,6 +59,21 @@ public static Val newStringArrayList(String[] value) { return newGenericArrayList(v -> stringOf((String) v), value); } + /** + * Converts a list using the supplied adapter for its elements. + * + *

Built-in lists retain their inbound adapter for element access. Outbound conversion uses the + * calling adapter, including for lists created by extension functions. Custom list + * implementations retain their legacy conversion fallback. + */ + @SuppressWarnings("removal") + public static T toNative(TypeAdapter adapter, ListT value, Class targetType) { + if (value instanceof BaseListT list) { + return list.convertToNative(adapter, targetType); + } + return value.convertToNative(targetType); + } + public static Val newGenericArrayList(TypeAdapter adapter, Object[] value) { return new GenericListT(adapter, value); } @@ -97,25 +112,30 @@ abstract static class BaseListT extends ListT { this.size = size; } - @SuppressWarnings({"removal", "unchecked"}) + @SuppressWarnings("removal") @Override public T convertToNative(Class typeDesc) { + return convertToNative(adapter, typeDesc); + } + + @SuppressWarnings("unchecked") + private T convertToNative(TypeAdapter adapter, Class typeDesc) { if (typeDesc.isArray()) { - Object array = toJavaArray(typeDesc); + Object array = toJavaArray(adapter, typeDesc); return (T) array; } if (typeDesc == List.class || typeDesc == Object.class) { - return (T) toJavaList(); + return (T) toJavaList(adapter); } if (typeDesc == ListValue.class) { - return (T) toPbListValue(); + return (T) toPbListValue(adapter); } if (typeDesc == Value.class) { - return (T) toPbValue(); + return (T) toPbValue(adapter); } if (typeDesc == Any.class) { - ListValue v = toPbListValue(); + ListValue v = toPbListValue(adapter); // Descriptor anyDesc = Any.getDescriptor(); // FieldDescriptor anyFieldTypeUrl = anyDesc.findFieldByName("type_url"); // FieldDescriptor anyFieldValue = anyDesc.findFieldByName("value"); @@ -137,11 +157,11 @@ public T convertToNative(Class typeDesc) { String.format("Unsupported conversion of '%s' to '%s'", ListType, typeDesc.getName())); } - private Value toPbValue() { - return Value.newBuilder().setListValue(toPbListValue()).build(); + private Value toPbValue(TypeAdapter adapter) { + return Value.newBuilder().setListValue(toPbListValue(adapter)).build(); } - private ListValue toPbListValue() { + private ListValue toPbListValue(TypeAdapter adapter) { ListValue.Builder list = ListValue.newBuilder(); int s = (int) size; for (int i = 0; i < s; i++) { @@ -152,12 +172,12 @@ private ListValue toPbListValue() { return list.build(); } - private List toJavaList() { - return asList((Object[]) toJavaArray(Object[].class)); + private List toJavaList(TypeAdapter adapter) { + return asList((Object[]) toJavaArray(adapter, Object[].class)); } @SuppressWarnings({"rawtypes", "unchecked"}) - private Object toJavaArray(Class typeDesc) { + private Object toJavaArray(TypeAdapter adapter, Class typeDesc) { int s = (int) size; Class compType = typeDesc.getComponentType(); if (compType == Enum.class) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java index 7ae0365d..ee5908ba 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/ProtoTypeRegistry.java @@ -86,10 +86,15 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.projectnessie.cel.common.ULong; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.IteratorT; import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.UnknownT; import org.projectnessie.cel.common.types.ref.FieldGetter; import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapterSupport; @@ -429,13 +434,18 @@ private Object toProtoMapStructure(FieldDescription field, MapT value) { private Object toNativeMapEntryValue(Val value, FieldDescriptor field) { return switch (field.getType()) { - case DOUBLE -> valueToDouble(value); + case DOUBLE -> + value instanceof DoubleT ? valueToDouble(value) : valueToNative(value, Double.class); case FLOAT -> valueToNative(value, Float.class); - case INT64, SINT64, SFIXED64 -> valueToLong(value); + case INT64, SINT64, SFIXED64 -> + value instanceof IntT || value instanceof UintT || value instanceof UnknownT + ? valueToLong(value) + : valueToNative(value, Long.class); case UINT64, FIXED64 -> valueToNative(value, ULong.class).longValue(); - case INT32, SINT32, SFIXED32 -> valueToInt(value); + case INT32, SINT32, SFIXED32 -> toNativeMapInt(value); case UINT32, FIXED32 -> toUnsignedInt(value); - case BOOL -> valueToBoolean(value); + case BOOL -> + value instanceof BoolT ? valueToBoolean(value) : valueToNative(value, Boolean.class); case STRING -> valueToNative(value, String.class); case BYTES -> valueToNative(value, ByteString.class); case ENUM -> { @@ -445,13 +455,19 @@ private Object toNativeMapEntryValue(Val value, FieldDescriptor field) { } throw new IllegalArgumentException("null is only valid for google.protobuf.NullValue"); } - yield valueToInt(value); + yield toNativeMapInt(value); } case MESSAGE -> valueToNative(value, messageNativeType(field)); case GROUP -> throw new IllegalArgumentException("protobuf maps cannot contain group values"); }; } + private int toNativeMapInt(Val value) { + return value instanceof IntT || value instanceof UintT + ? valueToInt(value) + : valueToNative(value, Integer.class); + } + private int toUnsignedInt(Val value) { long unsigned = valueToNative(value, ULong.class).longValue(); if (Long.compareUnsigned(unsigned, 0xffff_ffffL) > 0) { diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java index 9a6aec2b..44c61b2f 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java @@ -61,6 +61,7 @@ import org.projectnessie.cel.common.types.DoubleT; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.ListT; import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.UintT; import org.projectnessie.cel.common.types.UnknownT; @@ -258,6 +259,9 @@ public static T valueToNative(TypeAdapter adapter, Val value, Class targe if (targetType == double.class) { return (T) Double.valueOf(adapter.valueToDouble(value)); } + if (value instanceof ListT list) { + return ListT.toNative(adapter, list, targetType); + } return legacyValueToNative(value, targetType); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java index c785e1ad..371d84f2 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ProviderTest.java @@ -79,6 +79,91 @@ public class ProviderTest { + @ParameterizedTest + @ValueSource(strings = {"bool", "int32", "int64", "double", "enum"}) + void protobufMapsAcceptBoxedOnlyCustomValues(String fieldType) { + TypeRegistry registry = newRegistry(TestAllTypes.getDefaultInstance()); + Object nativeValue = + switch (fieldType) { + case "bool" -> true; + case "int32", "enum" -> 1; + case "int64" -> 2L; + case "double" -> 3.0d; + default -> throw new AssertionError(fieldType); + }; + Val custom = new BoxedOnlyVal(registry.nativeToValue(nativeValue), nativeValue); + String typeName = TestAllTypes.getDescriptor().getFullName(); + if (!fieldType.equals("enum")) { + Val scalar = registry.newValue(typeName, Map.of("single_" + fieldType, custom)); + assertThat(scalar).matches(v -> !Err.isError(v)); + } + Val value = + registry.newValue( + typeName, + Map.of( + "map_bool_" + fieldType, new DirectConversionMap(registry, Map.of(true, custom)))); + assertThat(value).matches(v -> !Err.isError(v)); + TestAllTypes message = registry.valueToNative(value, TestAllTypes.class); + TestAllTypes.Builder expected = TestAllTypes.newBuilder(); + switch (fieldType) { + case "bool" -> expected.putMapBoolBool(true, true); + case "int32" -> expected.putMapBoolInt32(true, 1); + case "int64" -> expected.putMapBoolInt64(true, 2L); + case "double" -> expected.putMapBoolDouble(true, 3.0d); + case "enum" -> expected.putMapBoolEnumValue(true, 1); + default -> throw new AssertionError(fieldType); + } + assertThat(message).isEqualTo(expected.build()); + if (fieldType.equals("bool") || fieldType.equals("int32") || fieldType.equals("int64")) { + Val key = + registry.newValue( + typeName, + Map.of( + "map_" + fieldType + "_bool", + new DirectConversionMap(registry, Map.of(custom, True)))); + assertThat(key).matches(v -> !Err.isError(v)); + } + } + + private static final class BoxedOnlyVal extends org.projectnessie.cel.common.types.ref.BaseVal { + private final Val delegate; + private final Object nativeValue; + + private BoxedOnlyVal(Val delegate, Object nativeValue) { + this.delegate = delegate; + this.nativeValue = nativeValue; + } + + @Override + @SuppressWarnings("removal") + public T convertToNative(Class targetType) { + if (targetType != nativeValue.getClass()) { + throw new UnsupportedOperationException("unsupported target " + targetType); + } + return targetType.cast(nativeValue); + } + + @Override + public Val convertToType(org.projectnessie.cel.common.types.ref.Type typeValue) { + return delegate.convertToType(typeValue); + } + + @Override + public Val equal(Val other) { + return delegate.equal(other instanceof BoxedOnlyVal boxed ? boxed.delegate : other); + } + + @Override + public org.projectnessie.cel.common.types.ref.Type type() { + return delegate.type(); + } + + @Override + public Object value() { + return nativeValue; + } + } + @ParameterizedTest @ValueSource(booleans = {false, true}) void unsignedMapEntriesRoundTrip(boolean dynamic) throws Exception { diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java index 06e62c2e..143abc2b 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java @@ -22,18 +22,89 @@ import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.StringT.StringType; +import static org.projectnessie.cel.common.types.StringT.stringOf; import static org.projectnessie.cel.common.types.UintT.uintOf; +import com.google.protobuf.Any; +import com.google.protobuf.ListValue; +import com.google.protobuf.Value; +import java.util.List; +import java.util.Locale; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.EnvOption; import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.ListT; import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.extension.StringsLib; @SuppressWarnings("removal") class TypeAdapterTest { private static final TypeAdapter DEFAULT_ADAPTER = DefaultTypeAdapter.Instance; + @Test + void stringArraysPreserveLegacyNullElementAdaptation() { + Val list = DEFAULT_ADAPTER.nativeToValue(new String[] {null}); + Val element = ((ListT) list).get(intOf(0)); + + assertThat(element).isInstanceOf(StringT.class); + assertThat(element.value()).isNull(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "array", + "list", + "factory", + "['a', 'b']", + "'a,b'.split(',')", + "'a,b'.split(',', 2)" + }) + void stringListsUseConfiguredOutboundAdapter(String source) throws Exception { + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return TypeAdapterSupport.maybeNativeToValue(this, value); + } + + @Override + public T valueToNative(Val value, Class targetType) { + if (value instanceof StringT) { + return DEFAULT_ADAPTER.valueToNative( + stringOf(((String) value.value()).toUpperCase(Locale.ROOT)), targetType); + } + return TypeAdapter.super.valueToNative(value, targetType); + } + }; + Env env = Env.newEnv(EnvOption.customTypeAdapter(adapter), StringsLib.strings()); + Val value = + switch (source) { + case "array" -> adapter.nativeToValue(new String[] {"a", "b"}); + case "list" -> adapter.nativeToValue(List.of("a", "b")); + case "factory" -> ListT.newStringArrayList(new String[] {"a", "b"}); + default -> env.program(env.compile(source).getAst()).eval(java.util.Map.of()).getVal(); + }; + assertThat(adapter.valueToNative(value, String[].class)).containsExactly("A", "B"); + assertThat((Object) adapter.valueToNative(value, List.class)).isEqualTo(List.of("A", "B")); + assertThat(adapter.valueToNative(value, Object.class)).isEqualTo(List.of("A", "B")); + ListValue expected = + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("A")) + .addValues(Value.newBuilder().setStringValue("B")) + .build(); + assertThat(adapter.valueToNative(value, ListValue.class)).isEqualTo(expected); + assertThat(adapter.valueToNative(value, Value.class)) + .isEqualTo(Value.newBuilder().setListValue(expected).build()); + assertThat(adapter.valueToNative(value, Any.class).unpack(ListValue.class)).isEqualTo(expected); + } + @Test void primitiveInboundDefaultsPreserveExactWrapperType() { TrackingAdapter adapter = new TrackingAdapter(); From 235d3daf204d3f2cdf2693cd686187fd95333cce Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 22 Oct 2026 11:01:39 +0200 Subject: [PATCH 4/4] Keep aggregate evaluation separate from outbound conversion Pass the calling adapter through built-in map conversion, including protobuf-backed maps and nested Java and protobuf output. Preserve custom map legacy conversion and raw value access. Retain CEL elements when concatenating array-backed and Java-backed lists so outbound hooks cannot alter evaluation or narrow element types. Preserve unsigned raw representations in concatenation snapshots. Cover delegated inbound adapters, nested map exports, protobuf maps, and typed-array concatenation. --- .../projectnessie/cel/common/types/ListT.java | 55 ++++--- .../projectnessie/cel/common/types/MapT.java | 38 +++-- .../cel/common/types/pb/FieldDescription.java | 10 +- .../common/types/ref/TypeAdapterSupport.java | 4 + .../cel/common/types/ref/TypeAdapterTest.java | 139 ++++++++++++++++++ 5 files changed, 211 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java index 44470f6c..7a0abb74 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ListT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ListT.java @@ -35,6 +35,7 @@ import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; +import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.operators.Operator; import org.projectnessie.cel.common.types.ref.BaseVal; import org.projectnessie.cel.common.types.ref.Type; @@ -112,6 +113,19 @@ abstract static class BaseListT extends ListT { this.size = size; } + final Val concatValues(int thisSize, Lister other) { + // Concatenation is CEL evaluation; outbound conversion belongs to result export. + int otherSize = (int) other.size().intValue(); + Val[] values = new Val[thisSize + otherSize]; + for (int i = 0; i < thisSize; i++) { + values[i] = get(intOf(i)); + } + for (int i = 0; i < otherSize; i++) { + values[thisSize + i] = other.get(intOf(i)); + } + return new ValListT(adapter, values, true); + } + @SuppressWarnings("removal") @Override public T convertToNative(Class typeDesc) { @@ -375,17 +389,7 @@ public Val add(Val other) { if (!(other instanceof Lister otherList)) { return noSuchOverload(this, "add", other); } - int otherSize = (int) otherList.size().intValue(); - Object[] newArray = Arrays.copyOf(array, array.length + otherSize); - Class componentType = array.getClass().getComponentType(); - for (int i = 0; i < otherSize; i++) { - Val otherValue = otherList.get(intOf(i)); - newArray[array.length + i] = - componentType.isInstance(otherValue) - ? otherValue - : adapter.valueToNative(otherValue, componentType); - } - return new GenericListT(adapter, newArray); + return concatValues(array.length, otherList); } @Override @@ -431,15 +435,7 @@ public Val add(Val other) { if (!(other instanceof Lister otherList)) { return noSuchOverload(this, "add", other); } - int otherSize = (int) otherList.size().intValue(); - Object[] newArray = new Object[list.size() + otherSize]; - for (int i = 0; i < list.size(); i++) { - newArray[i] = list.get(i); - } - for (int i = 0; i < otherSize; i++) { - newArray[list.size() + i] = otherList.get(intOf(i)); - } - return new GenericListT(adapter, newArray); + return concatValues(list.size(), otherList); } @Override @@ -457,17 +453,27 @@ public Val get(Val index) { static final class ValListT extends BaseListT { private final Val[] array; + // Concatenation snapshots keep raw uint values distinguishable from signed integers. + private final boolean preserveUnsignedRepresentation; ValListT(TypeAdapter adapter, Val[] array) { + this(adapter, array, false); + } + + ValListT(TypeAdapter adapter, Val[] array, boolean preserveUnsignedRepresentation) { super(adapter, array.length); this.array = array; + this.preserveUnsignedRepresentation = preserveUnsignedRepresentation; } @Override public Object value() { Object[] nativeArray = new Object[array.length]; for (int i = 0; i < array.length; i++) { - nativeArray[i] = array[i].value(); + nativeArray[i] = + preserveUnsignedRepresentation && array[i] instanceof UintT + ? ULong.valueOf(array[i].intValue()) + : array[i].value(); } return nativeArray; } @@ -477,18 +483,21 @@ public Val add(Val other) { if (!(other instanceof Lister otherLister)) { return noSuchOverload(this, "add", other); } + boolean preserveUnsigned = + preserveUnsignedRepresentation + || other instanceof ValListT values && values.preserveUnsignedRepresentation; if (other instanceof ValListT) { Val[] otherArray = ((ValListT) other).array; Val[] newArray = Arrays.copyOf(array, array.length + otherArray.length); System.arraycopy(otherArray, 0, newArray, array.length, otherArray.length); - return new ValListT(adapter, newArray); + return new ValListT(adapter, newArray, preserveUnsigned); } else { int otherSIze = (int) otherLister.size().intValue(); Val[] newArray = Arrays.copyOf(array, array.length + otherSIze); for (int i = 0; i < otherSIze; i++) { newArray[array.length + i] = otherLister.get(intOf(i)); } - return new ValListT(adapter, newArray); + return new ValListT(adapter, newArray, preserveUnsigned); } } diff --git a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java index 6328d1f6..2ffcf3ac 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/MapT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/MapT.java @@ -54,6 +54,17 @@ public static Val newWrappedMap(TypeAdapter adapter, Map value) { return new ValMapT(adapter, value); } + /** + * Converts this map using the calling adapter for its entries. + * + *

Built-in maps override this method to separate inbound adaptation from outbound conversion. + * Custom map implementations retain their legacy conversion fallback. + */ + @SuppressWarnings("removal") + public T toNative(TypeAdapter adapter, Class targetType) { + return convertToNative(targetType); + } + @SuppressWarnings("unchecked") public static Val newMaybeWrappedMap(TypeAdapter adapter, Map value) { boolean alreadyWrapped = true; @@ -107,20 +118,26 @@ static final class ValMapT extends MapT { this.map = map; } - @SuppressWarnings({"removal", "unchecked"}) + @SuppressWarnings("removal") @Override public T convertToNative(Class typeDesc) { + return toNative(adapter, typeDesc); + } + + @SuppressWarnings("unchecked") + @Override + public T toNative(TypeAdapter adapter, Class typeDesc) { if (Map.class.isAssignableFrom(typeDesc) || typeDesc == Object.class) { - return (T) toJavaMap(); + return (T) toJavaMap(adapter); } if (typeDesc == Struct.class) { - return (T) toPbStruct(); + return (T) toPbStruct(adapter); } if (typeDesc == Value.class) { - return (T) toPbValue(); + return (T) toPbValue(adapter); } if (typeDesc == Any.class) { - Struct v = toPbStruct(); + Struct v = toPbStruct(adapter); // DynamicMessage dyn = DynamicMessage.newBuilder(v).build(); // return (T) Any.newBuilder().mergeFrom(dyn).build(); return (T) @@ -134,23 +151,24 @@ public T convertToNative(Class typeDesc) { "native type conversion error from '%s' to '%s'", MapType, typeDesc.getName())); } - private Value toPbValue() { - return Value.newBuilder().setStructValue(toPbStruct()).build(); + private Value toPbValue(TypeAdapter adapter) { + return Value.newBuilder().setStructValue(toPbStruct(adapter)).build(); } - private Struct toPbStruct() { + private Struct toPbStruct(TypeAdapter adapter) { Struct.Builder struct = Struct.newBuilder(); map.forEach( (k, v) -> { if (k.type().typeEnum() != TypeEnum.String) { throw new IllegalArgumentException("bad key type"); } - struct.putFields(k.value().toString(), adapter.valueToNative(v, Value.class)); + struct.putFields( + adapter.valueToNative(k, String.class), adapter.valueToNative(v, Value.class)); }); return struct.build(); } - private Map toJavaMap() { + private Map toJavaMap(TypeAdapter adapter) { Map r = new HashMap<>(); map.forEach( (k, v) -> diff --git a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java index 64476a1a..1af5b41c 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/pb/FieldDescription.java @@ -540,10 +540,16 @@ private ProtoMapT(TypeAdapter adapter, FieldDescriptor field, List entries) { this.valueDesc = field.getMessageType().findFieldByNumber(2); } - @SuppressWarnings({"removal", "unchecked"}) + @SuppressWarnings("removal") @Override public T convertToNative(Class typeDesc) { - return adapter.valueToNative(MapT.newMaybeWrappedMap(adapter, toJavaMap()), typeDesc); + return toNative(adapter, typeDesc); + } + + @Override + public T toNative(TypeAdapter conversionAdapter, Class typeDesc) { + return conversionAdapter.valueToNative( + MapT.newMaybeWrappedMap(adapter, toJavaMap()), typeDesc); } @Override diff --git a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java index 44c61b2f..4c2e81f9 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/ref/TypeAdapterSupport.java @@ -62,6 +62,7 @@ import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.UintT; import org.projectnessie.cel.common.types.UnknownT; @@ -262,6 +263,9 @@ public static T valueToNative(TypeAdapter adapter, Val value, Class targe if (value instanceof ListT list) { return ListT.toNative(adapter, list, targetType); } + if (value instanceof MapT map) { + return map.toNative(adapter, targetType); + } return legacyValueToNative(value, targetType); } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java index 143abc2b..54b19e71 100644 --- a/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java @@ -26,20 +26,32 @@ import static org.projectnessie.cel.common.types.UintT.uintOf; import com.google.protobuf.Any; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.ListValue; +import com.google.protobuf.Message; +import com.google.protobuf.Struct; import com.google.protobuf.Value; +import dev.cel.expr.conformance.proto3.TestAllTypes; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.projectnessie.cel.Env; import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.ULong; import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; import org.projectnessie.cel.common.types.ListT; import org.projectnessie.cel.common.types.NullT; import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.traits.Indexer; +import org.projectnessie.cel.common.types.traits.Lister; import org.projectnessie.cel.extension.StringsLib; @SuppressWarnings("removal") @@ -47,6 +59,133 @@ class TypeAdapterTest { private static final TypeAdapter DEFAULT_ADAPTER = DefaultTypeAdapter.Instance; + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void nestedMapsUseCallingAdapterForJavaAndProtobufExports(boolean dynamic) throws Exception { + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return DEFAULT_ADAPTER.nativeToValue(value); + } + + @Override + public T valueToNative(Val value, Class targetType) { + if (value instanceof StringT) { + value = stringOf(((String) value.value()).toUpperCase(Locale.ROOT)); + } else if (value instanceof IntT) { + value = intOf(value.intValue() + 100); + } + return TypeAdapter.super.valueToNative(value, targetType); + } + }; + Map raw = Map.of("items", List.of(1L, Map.of("leaf", 2L))); + Val value = adapter.nativeToValue(raw); + Map expectedJava = Map.of("ITEMS", List.of(101L, Map.of("LEAF", 102L))); + assertThat((Object) adapter.valueToNative(value, Map.class)).isEqualTo(expectedJava); + assertThat(adapter.valueToNative(value, Object.class)).isEqualTo(expectedJava); + assertThat(value.value()).isEqualTo(raw); + Struct expectedProto = + Struct.newBuilder() + .putFields( + "ITEMS", + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setNumberValue(101)) + .addValues( + Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields( + "LEAF", + Value.newBuilder().setNumberValue(102).build())))) + .build()) + .build(); + assertThat(adapter.valueToNative(value, Struct.class)).isEqualTo(expectedProto); + assertThat(adapter.valueToNative(value, Value.class)) + .isEqualTo(Value.newBuilder().setStructValue(expectedProto).build()); + assertThat(adapter.valueToNative(value, Any.class).unpack(Struct.class)) + .isEqualTo(expectedProto); + Val numericKeys = adapter.nativeToValue(Map.of(1L, 2L)); + assertThat((Object) adapter.valueToNative(numericKeys, Map.class)) + .isEqualTo(Map.of(101L, 102L)); + + TestAllTypes generated = TestAllTypes.newBuilder().putMapBoolInt64(true, 1L).build(); + Message message = + dynamic + ? DynamicMessage.parseFrom(TestAllTypes.getDescriptor(), generated.toByteString()) + : generated; + ProtoTypeRegistry registry = ProtoTypeRegistry.newRegistry(message); + Val protoMap = ((Indexer) registry.nativeToValue(message)).get(stringOf("map_bool_int64")); + assertThat((Object) adapter.valueToNative(protoMap, Map.class)).isEqualTo(Map.of(true, 101L)); + } + + @ParameterizedTest + @ValueSource(strings = {"boxed", "objects", "list", "primitive"}) + void concatenationDoesNotInvokeOutboundHooks(String source) { + AtomicInteger conversions = new AtomicInteger(); + TypeAdapter adapter = + new TypeAdapter() { + @Override + public Val nativeToValue(Object value) { + return value instanceof Val val + ? val + : TypeAdapterSupport.maybeNativeToValue(this, value); + } + + @Override + public T valueToNative(Val value, Class targetType) { + if (value instanceof IntT) { + conversions.incrementAndGet(); + value = intOf(value.intValue() + 100); + } + return TypeAdapter.super.valueToNative(value, targetType); + } + }; + Object input = + switch (source) { + case "boxed" -> new Long[] {1L}; + case "objects" -> new Object[] {1L}; + case "list" -> List.of(1L); + case "primitive" -> new long[] {1L}; + default -> throw new AssertionError(source); + }; + Env env = + Env.newEnv( + EnvOption.customTypeAdapter(adapter), + EnvOption.declarations(Decls.newVar("xs", Decls.newListType(Decls.Int)))); + Val result = + env.program(env.compile("(xs + [2])[1] == 2").getAst()).eval(Map.of("xs", input)).getVal(); + assertThat(result).isSameAs(True); + Val concatenated = + env.program(env.compile("xs + [2]").getAst()).eval(Map.of("xs", input)).getVal(); + assertThat(((Lister) concatenated).get(intOf(0))).isEqualTo(intOf(1)); + assertThat(((Lister) concatenated).get(intOf(1))).isEqualTo(intOf(2)); + assertThat(conversions).hasValue(0); + assertThat(adapter.valueToNative(concatenated, Long[].class)).containsExactly(101L, 102L); + assertThat(conversions).hasValue(2); + } + + @Test + void typedArrayConcatenationPreservesCelElementTypes() { + Val[] suffix = {uintOf(2), NullT.NullValue, StringType, Err.newErr("element error")}; + Lister left = (Lister) DEFAULT_ADAPTER.nativeToValue(new Long[] {1L}); + Lister combined = (Lister) left.add(ListT.newValArrayList(DEFAULT_ADAPTER, suffix)); + assertThat(combined.get(intOf(0))).isEqualTo(intOf(1)); + for (int i = 0; i < suffix.length; i++) { + assertThat(combined.get(intOf(i + 1))).isSameAs(suffix[i]); + } + Lister unsigned = + (Lister) left.add(ListT.newValArrayList(DEFAULT_ADAPTER, new Val[] {uintOf(-1L)})); + Val repeated = unsigned.add(left); + assertThat((Object[]) repeated.value()).containsExactly(1L, ULong.valueOf(-1L), 1L); + assertThat(DEFAULT_ADAPTER.valueToNative(repeated, Object[].class)) + .containsExactly(1L, -1L, 1L); + assertThat((Object[]) ListT.newValArrayList(DEFAULT_ADAPTER, new Val[] {uintOf(2)}).value()) + .containsExactly(2L); + } + @Test void stringArraysPreserveLegacyNullElementAdaptation() { Val list = DEFAULT_ADAPTER.nativeToValue(new String[] {null});