@bufbuild/cel-spec
Version:
CEL definitions and test data
425 lines (424 loc) • 14.3 kB
JavaScript
"use strict";
// Copyright 2024-2025 Buf Technologies, Inc.
//
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.KindAdorner = void 0;
exports.toDebugString = toDebugString;
const decoder = new TextDecoder();
/**
* Returns adorned debug output for the given expression tree, following cel-go.
*
* @private Caution: This functions requires ES2024 features.
*/
function toDebugString(expr, adorner = EmptyAdorner.singleton) {
const writer = new Writer(adorner);
writer.buffer(expr);
return writer.toString();
}
class Writer {
constructor(adorner) {
this.content = "";
this.indent = 0;
this.lineStart = true;
this.adorner = adorner;
}
buffer(e) {
if (e == undefined) {
return;
}
switch (e.exprKind.case) {
case "constExpr":
this.append(formatLiteral(e.exprKind.value));
break;
case "identExpr":
this.append(e.exprKind.value.name);
break;
case "selectExpr":
this.appendSelect(e.exprKind.value);
break;
case "callExpr":
this.appendCall(e.exprKind.value);
break;
case "listExpr":
this.appendList(e.exprKind.value);
break;
case "structExpr":
this.appendStruct(e.exprKind.value);
break;
case "comprehensionExpr":
this.appendComprehension(e.exprKind.value);
break;
}
this.adorn(e);
}
appendSelect(sel) {
this.buffer(sel.operand);
this.append(".");
this.append(sel.field);
if (sel.testOnly) {
this.append("~test-only~");
}
}
appendCall(call) {
if (call.target !== undefined) {
// above check is equivalent to `call.isMemberFunction()`
this.buffer(call.target);
this.append(".");
}
this.append(call.function);
this.append("(");
if (call.args.length > 0) {
this.addIndent();
this.appendLine();
for (let i = 0; i < call.args.length; ++i) {
if (i > 0) {
this.append(",");
this.appendLine();
}
this.buffer(call.args[i]);
}
this.removeIndent();
this.appendLine();
}
this.append(")");
}
appendList(list) {
this.append("[");
if (list.elements.length > 0) {
this.appendLine();
this.addIndent();
for (let i = 0; i < list.elements.length; ++i) {
if (i > 0) {
this.append(",");
this.appendLine();
}
this.buffer(list.elements[i]);
}
this.removeIndent();
this.appendLine();
}
this.append("]");
}
appendStruct(obj) {
this.append(obj.messageName);
this.append("{");
if (obj.entries.length > 0) {
this.appendLine();
this.addIndent();
for (let i = 0; i < obj.entries.length; ++i) {
const entry = obj.entries[i];
if (i > 0) {
this.append(",");
this.appendLine();
}
if (entry.optionalEntry) {
this.append("?");
}
if (entry.keyKind.case === "fieldKey") {
this.append(entry.keyKind.value);
}
else {
this.buffer(entry.keyKind.value);
}
this.append(":");
this.buffer(entry.value);
this.adorn(entry);
}
this.removeIndent();
this.appendLine();
}
this.append("}");
}
appendComprehension(comprehension) {
this.append("__comprehension__(");
this.addIndent();
this.appendLine();
this.append("// Variable");
this.appendLine();
this.append(comprehension.iterVar);
this.append(",");
this.appendLine();
this.append("// Target");
this.appendLine();
this.buffer(comprehension.iterRange);
this.append(",");
this.appendLine();
this.append("// Accumulator");
this.appendLine();
this.append(comprehension.accuVar);
this.append(",");
this.appendLine();
this.append("// Init");
this.appendLine();
this.buffer(comprehension.accuInit);
this.append(",");
this.appendLine();
this.append("// LoopCondition");
this.appendLine();
this.buffer(comprehension.loopCondition);
this.append(",");
this.appendLine();
this.append("// LoopStep");
this.appendLine();
this.buffer(comprehension.loopStep);
this.append(",");
this.appendLine();
this.append("// Result");
this.appendLine();
this.buffer(comprehension.result);
this.append(")");
this.removeIndent();
}
append(s) {
this.doIndent();
this.content += s;
}
doIndent() {
if (this.lineStart) {
this.lineStart = false;
this.content += " ".repeat(this.indent);
}
}
adorn(e) {
this.append(this.adorner.GetMetadata(e));
}
appendLine() {
this.content += "\n";
this.lineStart = true;
}
addIndent() {
this.indent++;
}
removeIndent() {
this.indent--;
if (this.indent < 0) {
throw new Error("negative indent");
}
}
toString() {
return this.content;
}
}
class EmptyAdorner {
constructor() { }
GetMetadata(_context) {
return "";
}
}
EmptyAdorner.singleton = new EmptyAdorner();
class KindAdorner {
constructor() { }
GetMetadata(context) {
let valueType;
if (isExpr(context)) {
valueType = getExprType(context);
}
else if (isEntry(context)) {
valueType = "*expr.Expr_CreateStruct_Entry";
}
else {
throw new Error("unexpected message type: " + context.$typeName);
}
return `^#${valueType}#`;
}
}
exports.KindAdorner = KindAdorner;
KindAdorner.singleton = new KindAdorner();
function formatLiteral(c) {
const kind = c.constantKind;
switch (kind.case) {
case "boolValue":
return kind.value ? "true" : "false";
case "bytesValue":
return quoteBytes(kind.value);
case "doubleValue":
// these are the bounds where Go's default formatting switches to exponential
if (kind.value < 1e6 && kind.value > -1e6) {
return (Object.is(kind.value, -0) ? "-" : "") + kind.value.toString();
}
// workaround for https://github.com/golang/go/issues/70862
return kind.value.toExponential().replace(/e\+([0-9])$/, "e+0$1");
case "int64Value":
return kind.value.toString();
case "stringValue":
return quoteString(kind.value);
case "uint64Value":
return `${kind.value.toString()}u`;
case "nullValue":
return "null";
default:
throw new Error(`Unknown constant type: ${kind.case}`);
}
}
// @ts-expect-error - The regex flag `v` is only available in ES2024 or later
const unprintableExp = /[^\p{L}\p{N}\p{S}\p{P}\p{Cs} ]/v;
// @ts-expect-error - The regex flag `v` is only available in ES2024 or later
const unprintableExpGlobal = /[^\p{L}\p{N}\p{S}\p{P}\p{Cs} ]/gv;
const segmenter =
// @ts-expect-error - Intl.Segmenter is only available in ES2022 or later
new Intl.Segmenter("en");
function isPrintable(c) {
if (unprintableExp.test(c.normalize())) {
return false;
}
try {
// We want to verify that the string does not contain any lone surrogates.
// Ideally, we would use String.isWellFormed, but it's only available in ES2024,
// and we would have to target ES2024 for the entire package to use it.
// As a workaround, we rely on encodeURI raising an error on lone surrogates,
// and can stay at a more widely supported target.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#encoding_a_lone_surrogate_throws
encodeURI(c);
}
catch (_) {
return false;
}
return true;
}
function quoteBytes(bytes) {
const replacement = String.fromCharCode(0xfffd);
let byteString = "";
let i = 0;
while (i < bytes.length) {
let length = 1;
const character = bytes[i] < 0x80
? String.fromCharCode(bytes[i])
: bytes[i] < 0xc0
? "" // continuation
: bytes[i] < 0xe0
? // biome-ignore lint/suspicious/noAssignInExpressions: do not want to remove the ternary expression
decoder.decode(bytes.slice(i, i + (length = 2)))
: bytes[i] < 0xf0
? // biome-ignore lint/suspicious/noAssignInExpressions: do not want to remove the ternary expression
decoder.decode(bytes.slice(i, i + (length = 3)))
: bytes[i] < 0xf5
? // biome-ignore lint/suspicious/noAssignInExpressions: do not want to remove the ternary expression
decoder.decode(bytes.slice(i, i + (length = 4)))
: ""; // unused
// this is a bit subtle; either
// - we got an unexpected continuation byte, in which case `character` is an empty string
// - we got an unexpected unused byte, in which case `character` is an empty string
// - we got the first byte of a multibyte code point, but the subsequent bytes weren't valid and
// decoding failed, and the decoder returned the replacement character for one or more bytes
// in the byte sequence
// - we got a literal replacement byte UTF-8 sequence (0xef, 0xbf, 0xbd), which we treat the
// same way as if it were a failure because we're just going to encode the escaped bytes in
// either case
// - we successfully decoded a single character but it isn't printable
//
// only if none of these things is true can we return the unescaped decoded character
if (character.length !== 1 ||
character === replacement ||
unprintableExp.test(character)) {
byteString += formatSpecial("\\x" + bytes[i].toString(16).padStart(2, "0"));
i++;
}
else {
byteString += formatSpecial(character);
i += length;
}
}
return 'b"' + byteString + '"';
}
function quoteString(text) {
return '"' + escapeString(text) + '"';
}
function formatSpecial(c) {
if (c === "\\x07" || c === "\\u0007") {
return "\\a";
}
if (c === "\\x08" || c === "\\u0008") {
return "\\b";
}
if (c === "\\x0c" || c === "\\u000c") {
return "\\f";
}
if (c === "\\x0a" || c === "\\u000a") {
return "\\n";
}
if (c === "\\x0d" || c === "\\u000d") {
return "\\r";
}
if (c === "\\x09" || c === "\\u0009") {
return "\\t";
}
if (c === "\\x0b" || c === "\\u000b") {
return "\\v";
}
if (c === "\\") {
return "\\\\";
}
if (c === '"') {
return '\\"';
}
return c;
}
function escapeString(text) {
return [...segmenter.segment(text)]
.map((s) => {
if (isPrintable(s.segment)) {
return formatSpecial(s.segment);
}
return formatSpecial(
// @ts-expect-error - string.replaceAll is only available in ES2021 or later
s.segment.replaceAll(unprintableExpGlobal, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")));
})
.join("");
}
function getExprType(e) {
switch (e.exprKind.case) {
case "constExpr":
return getConstantType(e.exprKind.value);
case "identExpr":
return "*expr.Expr_IdentExpr";
case "selectExpr":
return "*expr.Expr_SelectExpr";
case "callExpr":
return "*expr.Expr_CallExpr";
case "listExpr":
return "*expr.Expr_ListExpr";
case "structExpr":
return "*expr.Expr_StructExpr";
case "comprehensionExpr":
return "*expr.Expr_ComprehensionExpr";
default:
throw new Error("unexpected expression type: " + e.exprKind.case);
}
}
function getConstantType(c) {
switch (c.constantKind.case) {
case "nullValue":
return "*expr.Constant_NullValue";
case "boolValue":
return "*expr.Constant_BoolValue";
case "int64Value":
return "*expr.Constant_Int64Value";
case "uint64Value":
return "*expr.Constant_Uint64Value";
case "doubleValue":
return "*expr.Constant_DoubleValue";
case "stringValue":
return "*expr.Constant_StringValue";
case "bytesValue":
return "*expr.Constant_BytesValue";
default:
throw new Error("unexpected constant type: " + c.constantKind.case);
}
}
function isExpr(m) {
return m.$typeName === "cel.expr.Expr";
}
function isEntry(m) {
return m.$typeName === "cel.expr.Expr.CreateStruct.Entry";
}