Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions conformance/src/test/java/dev/cel/conformance/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,6 @@ _TESTS_TO_SKIP_LEGACY = [

# Skip until fixed.
"fields/qualified_identifier_resolution/map_value_repeat_key_heterogeneous",
# TODO: Add strings.format.quote.
"string_ext/format",
"string_ext/format_errors",

# Future features for CEL 1.0
# TODO: Strong typing support for enums, specified but not implemented.
Expand All @@ -143,10 +140,6 @@ _TESTS_TO_SKIP_LEGACY = [
]

_TESTS_TO_SKIP_PLANNER = [
# TODO: Add strings.format.
"string_ext/format",
"string_ext/format_errors",

# TODO: This is actually a user experience degradation.
# Not worth fixing until we see a concrete need.
"basic/functions/unbound_is_runtime_error",
Expand Down
4 changes: 4 additions & 0 deletions extensions/src/main/java/dev/cel/extensions/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ java_library(
"//checker:checker_builder",
"//common:compiler_common",
"//common/internal",
"//common/internal:date_time_helpers",
"//common/types",
"//common/types:type_providers",
"//common/values",
"//common/values:cel_byte_string",
"//compiler:compiler_builder",
"//extensions:extension_library",
"//runtime",
Expand Down
291 changes: 291 additions & 0 deletions extensions/src/main/java/dev/cel/extensions/CelStringExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,42 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.UnsignedLong;
import com.google.errorprone.annotations.Immutable;
import dev.cel.checker.CelCheckerBuilder;
import dev.cel.common.CelFunctionDecl;
import dev.cel.common.CelOverloadDecl;
import dev.cel.common.internal.CelCodePointArray;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.types.CelType;
import dev.cel.common.types.ListType;
import dev.cel.common.types.SimpleType;
import dev.cel.common.types.TypeType;
import dev.cel.common.values.CelByteString;
import dev.cel.common.values.NullValue;
import dev.cel.compiler.CelCompilerLibrary;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelEvaluationExceptionBuilder;
import dev.cel.runtime.CelFunctionBinding;
import dev.cel.runtime.CelRuntimeBuilder;
import dev.cel.runtime.CelRuntimeLibrary;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/** Internal implementation of CEL string extensions. */
@Immutable
Expand All @@ -58,6 +73,16 @@ public enum Function {
ImmutableList.of(SimpleType.STRING, SimpleType.INT))),
CelFunctionBinding.from(
"string_char_at_int", String.class, Long.class, CelStringExtensions::charAt)),
FORMAT(
CelFunctionDecl.newFunctionDeclaration(
"format",
CelOverloadDecl.newMemberOverload(
"string_format",
"Formats the string using the provided arguments.",
SimpleType.STRING,
ImmutableList.of(SimpleType.STRING, ListType.create(SimpleType.DYN)))),
CelFunctionBinding.from(
"string_format", String.class, List.class, CelStringExtensions::format)),
INDEX_OF(
CelFunctionDecl.newFunctionDeclaration(
"indexOf",
Expand Down Expand Up @@ -404,6 +429,272 @@ private static String join(List<String> stringList, String separator) {
return Joiner.on(separator).join(stringList);
}

private static String format(String formatSpecifier, List<Object> args)
throws CelEvaluationException {
StringBuilder builtStr = new StringBuilder(formatSpecifier.length());
int i = 0;
int argIndex = 0;
while (i < formatSpecifier.length()) {
if (formatSpecifier.charAt(i) == '%') {
if (i + 1 < formatSpecifier.length() && formatSpecifier.charAt(i + 1) == '%') {
builtStr.append('%');
i += 2;
} else {
if (argIndex >= args.size()) {
throw new CelEvaluationException("index " + argIndex + " out of range");
}
Object arg = args.get(argIndex++);
i++; // Skip '%'

int precision = -1;
if (i < formatSpecifier.length() && formatSpecifier.charAt(i) == '.') {
i++;
int start = i;
while (i < formatSpecifier.length() && Character.isDigit(formatSpecifier.charAt(i))) {
i++;
}
if (i == start) {
throw new CelEvaluationException("could not find end of precision specifier");
}
try {
precision = Integer.parseInt(formatSpecifier.substring(start, i));
} catch (NumberFormatException e) {
throw new CelEvaluationException("error while converting precision to integer", e);
}
}

if (i >= formatSpecifier.length()) {
throw new CelEvaluationException("unexpected end of string");
}
char verb = formatSpecifier.charAt(i++);

switch (verb) {
case 's' -> builtStr.append(formatString(arg));
case 'd' -> builtStr.append(formatDecimal(arg));
case 'f' -> builtStr.append(formatFixed(arg, precision));
case 'e' -> builtStr.append(formatScientific(arg, precision));
case 'b' -> builtStr.append(formatBinary(arg));
case 'x', 'X' -> builtStr.append(formatHex(arg, verb == 'X'));
case 'o' -> builtStr.append(formatOctal(arg));
default ->
throw new CelEvaluationException("unrecognized formatting clause \"" + verb + "\"");
}
}
} else {
builtStr.append(formatSpecifier.charAt(i++));
}
}
return builtStr.toString();
}

private static String formatString(Object val) throws CelEvaluationException {
if (val == null) {
return "null";
}
if (val instanceof String s) {
return s;
}
if (val instanceof CelByteString byteString) {
return byteString.toStringUtf8();
}
if (val instanceof Duration duration) {
return DateTimeHelpers.toString(duration);
}
if (val instanceof Instant) {
return val.toString();
}
if (val instanceof Boolean) {
return val.toString();
}
if (val instanceof Long) {
return val.toString();
}
if (val instanceof UnsignedLong) {
return val.toString();
}
if (val instanceof Double d) {
if (d.isNaN()) {
return "NaN";
}
if (d.isInfinite()) {
return d > 0 ? "Infinity" : "-Infinity";
}
return d.toString();
}
if (val instanceof List<?> list) {
return formatList(list);
}
if (val instanceof Map<?, ?> map) {
return formatMap(map);
}
if (val instanceof NullValue) {
return "null";
}
if (val instanceof TypeType typeType) {
return typeType.containingTypeName();
}
if (val instanceof CelType celType) {
return celType.name();
}
throw new CelEvaluationException(
"could not convert argument " + val.getClass().getName() + " to string");
}

private static String formatList(List<?> list) throws CelEvaluationException {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < list.size(); i++) {
sb.append(formatString(list.get(i)));
if (i < list.size() - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}

private static String formatMap(Map<?, ?> map) throws CelEvaluationException {
TreeMap<String, Object> sortedMap = new TreeMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String keyStr = formatString(entry.getKey());
sortedMap.put(keyStr, entry.getValue());
}
StringBuilder sb = new StringBuilder("{");
int i = 0;
for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
sb.append(entry.getKey()).append(": ").append(formatString(entry.getValue()));
if (i < sortedMap.size() - 1) {
sb.append(", ");
}
i++;
}
sb.append("}");
return sb.toString();
}

private static String formatDecimal(Object arg) throws CelEvaluationException {
if (arg instanceof Long || arg instanceof UnsignedLong) {
return arg.toString();
}
if (arg instanceof Double) {
return formatFixed(arg, -1);
}
throw new CelEvaluationException(
"decimal clause can only be used on numbers, was given " + arg.getClass().getName());
}

private static String formatFixed(Object arg, int precision) throws CelEvaluationException {
if (arg instanceof Double d) {
double val = d;
if (Double.isNaN(val)) {
return "NaN";
}
if (Double.isInfinite(val)) {
return val > 0 ? "Infinity" : "-Infinity";
}
int p = precision >= 0 ? precision : 6;
BigDecimal bd = BigDecimal.valueOf(val);
bd = bd.setScale(p, RoundingMode.HALF_EVEN);
return bd.toPlainString();
}
if (arg instanceof Long l) {
return formatFixed((double) l, precision);
}
if (arg instanceof UnsignedLong ulong) {
return formatFixed(ulong.doubleValue(), precision);
}
throw new CelEvaluationException(
"fixed point clause can only be used on doubles, integers, and unsigned integers, was given"
+ " "
+ arg.getClass().getName());
}

private static String formatScientific(Object arg, int precision) throws CelEvaluationException {
if (arg instanceof Double d) {
double val = d;
if (Double.isNaN(val)) {
return "NaN";
}
if (Double.isInfinite(val)) {
return val > 0 ? "Infinity" : "-Infinity";
}
String fmtStr = precision >= 0 ? "%." + precision + "e" : "%.6e";
return String.format(Locale.ROOT, fmtStr, val);
}
if (arg instanceof Long l) {
return formatScientific((double) l, precision);
}
if (arg instanceof UnsignedLong ulong) {
return formatScientific(ulong.doubleValue(), precision);
}
throw new CelEvaluationException(
"scientific clause can only be used on doubles, integers, and unsigned integers, was given "
+ arg.getClass().getName());
}

private static String formatBinary(Object arg) throws CelEvaluationException {
if (arg instanceof Long val) {
if (val < 0) {
if (val == Long.MIN_VALUE) {
return "-1" + "0".repeat(63);
}
return "-" + Long.toBinaryString(-val);
}
return Long.toBinaryString(val);
}
if (arg instanceof UnsignedLong ulong) {
return ulong.toString(2);
}
if (arg instanceof Boolean b) {
return b ? "1" : "0";
}
throw new CelEvaluationException(
"binary clause can only be used on integers and bools, was given "
+ arg.getClass().getName());
}

private static String formatHex(Object arg, boolean upper) throws CelEvaluationException {
String result;
if (arg instanceof Long val) {
if (val < 0) {
if (val == Long.MIN_VALUE) {
result = "-8000000000000000";
} else {
result = "-" + String.format("%x", -val);
}
} else {
result = String.format("%x", val);
}
} else if (arg instanceof UnsignedLong unsignedLong) {
result = unsignedLong.toString(16);
} else if (arg instanceof CelByteString byteString) {
result = HexFormat.of().formatHex(byteString.toByteArray());
} else if (arg instanceof String str) {
result = HexFormat.of().formatHex(str.getBytes(UTF_8));
} else {
throw new CelEvaluationException(
"hex clause can only be used on integers, byte buffers, and strings, was given "
+ arg.getClass().getName());
}
return upper ? result.toUpperCase(Locale.ROOT) : result;
}

private static String formatOctal(Object arg) throws CelEvaluationException {
if (arg instanceof Long val) {
if (val < 0) {
if (val == Long.MIN_VALUE) {
return "-1000000000000000000000";
}
return "-" + String.format("%o", -val);
}
return String.format("%o", val);
}
if (arg instanceof UnsignedLong ulong) {
return ulong.toString(8);
}
throw new CelEvaluationException(
"octal clause can only be used on integers, was given " + arg.getClass().getName());
}

private static Long lastIndexOf(String str, String substr) throws CelEvaluationException {
CelCodePointArray strCpa = CelCodePointArray.fromString(str);
CelCodePointArray substrCpa = CelCodePointArray.fromString(substr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public void getAllFunctionNames() {
"math.bitShiftRight",
"math.sqrt",
"charAt",
"format",
"indexOf",
"join",
"lastIndexOf",
Expand Down
Loading
Loading