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..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,7 +35,7 @@ import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; -import java.util.function.Function; +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; @@ -60,6 +60,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); } @@ -98,25 +113,43 @@ abstract static class BaseListT extends ListT { this.size = size; } - @SuppressWarnings("unchecked") + 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) { + 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"); @@ -138,27 +171,27 @@ 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++) { 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)); + 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) { @@ -167,12 +200,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 +331,7 @@ public Val next() { } @Override + @SuppressWarnings("removal") public T convertToNative(Class typeDesc) { throw new UnsupportedOperationException("IMPLEMENT ME??"); } @@ -358,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 - : otherValue.convertToNative(componentType); - } - return new GenericListT(adapter, newArray); + return concatValues(array.length, otherList); } @Override @@ -414,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 @@ -440,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; } @@ -460,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 d96ef276..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("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,26 +151,29 @@ 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(), v.convertToNative(Value.class)); + struct.putFields( + adapter.valueToNative(k, String.class), adapter.valueToNative(v, Value.class)); }); 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(TypeAdapter adapter) { + Map r = new HashMap<>(); + map.forEach( + (k, v) -> + r.put( + adapter.valueToNative(k, Object.class), adapter.valueToNative(v, Object.class))); return r; } @@ -223,8 +243,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/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..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("unchecked") + @SuppressWarnings("removal") @Override public T convertToNative(Class typeDesc) { - return (T) MapT.newMaybeWrappedMap(adapter, toJavaMap()).convertToNative(typeDesc); + return toNative(adapter, typeDesc); + } + + @Override + public T toNative(TypeAdapter conversionAdapter, Class typeDesc) { + return conversionAdapter.valueToNative( + MapT.newMaybeWrappedMap(adapter, toJavaMap()), typeDesc); } @Override @@ -683,13 +689,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/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..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 @@ -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; @@ -84,12 +86,18 @@ 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; 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 +346,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 +358,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 +432,22 @@ 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 -> + value instanceof DoubleT ? valueToDouble(value) : valueToNative(value, Double.class); + case FLOAT -> valueToNative(value, Float.class); + 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 -> toNativeMapInt(value); + case UINT32, FIXED32 -> toUnsignedInt(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 -> { if (value == NullT.NullValue) { if (field.getEnumType().getFullName().equals("google.protobuf.NullValue")) { @@ -442,13 +455,27 @@ 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 toNativeMapInt(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 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) { + 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 +502,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 +617,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..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 @@ -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,23 @@ 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.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; /** - * 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 +165,175 @@ 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)); + } + 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); + } + + /** + * 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..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,15 +28,63 @@ 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() { Map javaMap = ImmutableMap.of( @@ -102,6 +150,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..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 @@ -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,164 @@ 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(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 { + 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(); @@ -186,7 +331,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 +346,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 +361,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 +380,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 +403,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 +433,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 +472,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 +484,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 +523,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 +616,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( + 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[] {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[] {uintOf(4)}), + new Object[] {4L}); // loses "ULong" here 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(5)}), new Val[] {uintOf(5)}); + expectValueToNative(reg, doubleOf(5.5d), 5.5f); + expectValueToNative(reg, doubleOf(-5.5d), -5.5d); 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[] {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 +849,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..54b19e71 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/ref/TypeAdapterTest.java @@ -0,0 +1,424 @@ +/* + * 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.StringT.stringOf; +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") +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}); + 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(); + + 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/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; +} 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"); }