Skip to content

Commit 16cb083

Browse files
committed
Add port of debug writer from cel-go
1 parent 785d44c commit 16cb083

File tree

3 files changed

+277
-3
lines changed

3 files changed

+277
-3
lines changed

demo.html

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
</head>
3838
<body>
3939
<script src="dist/index.js"></script>
40+
<script src="dist/to-debug-string.js"></script>
4041
<script>
4142
function execute(expression) {
4243
if (expression.trim() === "") {
@@ -59,7 +60,9 @@
5960
}
6061

6162
function print(thing) {
62-
if (Array.isArray(thing)) {
63+
if (typeof toDebugString === "function") {
64+
return toDebugString(thing);
65+
} else if (Array.isArray(thing)) {
6366
return "[\n" + indent(thing.map(print).join("\n")) + "\n]";
6467
} else if (typeof thing === "object") {
6568
return (

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@
3939
"bundle-init": "rm -rf dist/ &> /dev/null && mkdir dist",
4040
"bundle-cjs": "esbuild index.ts --bundle --format=cjs --outfile=dist/index.cjs && prettier dist/index.cjs --write",
4141
"bundle-browser": "echo 'import * as CEL from \"./\"; module.exports = CEL;' | esbuild --bundle --global-name=CEL --outfile=dist/index.js && prettier dist/index.js --write",
42+
"bundle-debugger": "echo 'import toDebugString from \"./utility/debug/to-debug-string.ts\"; module.exports = toDebugString;' | esbuild --bundle --global-name=toDebugString --outfile=dist/to-debug-string.js && prettier dist/to-debug-string.js --write",
4243
"bundle": "npm run bundle-init && npm run bundle-cjs && npm run bundle-browser",
4344
"extract-conformance-tests": "./utility/extract-conformance-tests/extract.sh",
4445
"prepare": "npm run generate-proto && npm run generate-parser && npm run bundle",
4546
"lint": "prettier . --check",
4647
"format": "prettier . --write",
4748
"check": "tsc --noEmit -p ./tsconfig.json --strict",
48-
"test": "jest",
49+
"test": "jest ./test",
4950
"verify": "npm run lint && npm run check && npm test"
5051
},
5152
"devDependencies": {
@@ -68,6 +69,7 @@
6869
}
6970
},
7071
"utility": {
71-
"celSpec": "https://github.com/google/cel-spec/archive/7bcc79c7cb9a101a66dbbe9c88838855dc4b821d.tar.gz"
72+
"celSpec": "https://github.com/google/cel-spec/archive/7bcc79c7cb9a101a66dbbe9c88838855dc4b821d.tar.gz",
73+
"celGo": "https://github.com/google/cel-go/archive/3545aac7e6d484d3035b1833c4ade036c3f7bacb.tar.gz"
7274
}
7375
}

utility/debug/to-debug-string.ts

+269
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import type {
2+
Constant,
3+
Expr,
4+
Expr_Ident,
5+
Expr_Select,
6+
Expr_Call,
7+
Expr_CreateList,
8+
Expr_CreateStruct,
9+
Expr_Comprehension,
10+
} from "../external/proto/dev/cel/expr/syntax_pb.ts";
11+
12+
import type { Message } from "@bufbuild/protobuf";
13+
14+
const decoder = new TextDecoder();
15+
16+
export default function toDebugString(
17+
expr: Expr,
18+
adorner: Adorner = EmptyAdorner.singleton,
19+
): string {
20+
const writer = new Writer(adorner);
21+
writer.buffer(expr);
22+
23+
return writer.toString();
24+
}
25+
26+
class Writer {
27+
adorner: Adorner;
28+
content: string = "";
29+
indent: number = 0;
30+
lineStart: boolean = true;
31+
32+
constructor(adorner: Adorner) {
33+
this.adorner = adorner;
34+
}
35+
36+
buffer(e?: Expr): void {
37+
if (e == undefined) {
38+
return;
39+
}
40+
41+
switch (e.exprKind.case) {
42+
case "constExpr":
43+
this.append(formatLiteral(e.exprKind.value));
44+
break;
45+
case "identExpr":
46+
this.append(e.exprKind.value.name);
47+
break;
48+
case "selectExpr":
49+
this.appendSelect(e.exprKind.value);
50+
break;
51+
case "callExpr":
52+
this.appendCall(e.exprKind.value);
53+
break;
54+
case "listExpr":
55+
this.appendList(e.exprKind.value);
56+
break;
57+
case "structExpr":
58+
this.appendStruct(e.exprKind.value);
59+
break;
60+
case "comprehensionExpr":
61+
this.appendComprehension(e.exprKind.value);
62+
break;
63+
}
64+
65+
this.adorn(e);
66+
}
67+
68+
appendSelect(sel: Expr_Select): void {
69+
this.buffer(sel.operand);
70+
this.append(".");
71+
this.append(sel.field);
72+
73+
if (sel.testOnly) {
74+
this.append("~test-only~");
75+
}
76+
}
77+
78+
appendCall(call: Expr_Call): void {
79+
if (call.target !== undefined) {
80+
// above check is equivalent to `call.isMemberFunction()`
81+
this.buffer(call.target);
82+
this.append(".");
83+
}
84+
this.append(call.function);
85+
this.append("(");
86+
if (call.args.length > 0) {
87+
this.addIndent();
88+
this.appendLine();
89+
for (let i = 0; i < call.args.length; ++i) {
90+
if (i > 0) {
91+
this.append(",");
92+
this.appendLine();
93+
}
94+
this.buffer(call.args[i]);
95+
}
96+
this.removeIndent();
97+
this.appendLine();
98+
}
99+
this.append(")");
100+
}
101+
102+
appendList(list: Expr_CreateList): void {
103+
this.append("[");
104+
if (list.elements.length > 0) {
105+
this.appendLine();
106+
this.addIndent();
107+
for (let i = 0; i < list.elements.length; ++i) {
108+
if (i > 0) {
109+
this.append(",");
110+
this.appendLine();
111+
}
112+
this.buffer(list.elements[i]);
113+
}
114+
this.removeIndent();
115+
this.appendLine();
116+
}
117+
this.append("]");
118+
}
119+
120+
appendStruct(obj: Expr_CreateStruct) {
121+
this.append(obj.messageName);
122+
this.append("{");
123+
if (obj.entries.length > 0) {
124+
this.appendLine();
125+
this.addIndent();
126+
for (let i = 0; i < obj.entries.length; ++i) {
127+
const entry = obj.entries[i];
128+
if (i > 0) {
129+
this.append(",");
130+
this.appendLine();
131+
}
132+
133+
if (entry.optionalEntry) {
134+
this.append("?");
135+
}
136+
137+
if (entry.keyKind.case === "fieldKey") {
138+
this.append(entry.keyKind.value);
139+
} else {
140+
this.buffer(entry.keyKind.value);
141+
}
142+
143+
this.append(":");
144+
this.buffer(entry.value);
145+
this.adorn(entry);
146+
}
147+
this.removeIndent();
148+
this.appendLine();
149+
}
150+
this.append("}");
151+
}
152+
153+
appendComprehension(comprehension: Expr_Comprehension) {
154+
this.append("__comprehension__(");
155+
this.addIndent();
156+
this.appendLine();
157+
this.append("// Variable");
158+
this.appendLine();
159+
this.append(comprehension.iterVar);
160+
this.append(",");
161+
this.appendLine();
162+
this.append("// Target");
163+
this.appendLine();
164+
this.buffer(comprehension.iterRange);
165+
this.append(",");
166+
this.appendLine();
167+
this.append("// Accumulator");
168+
this.appendLine();
169+
this.append(comprehension.accuVar);
170+
this.append(",");
171+
this.appendLine();
172+
this.append("// Init");
173+
this.appendLine();
174+
this.buffer(comprehension.accuInit);
175+
this.append(",");
176+
this.appendLine();
177+
this.append("// LoopCondition");
178+
this.appendLine();
179+
this.buffer(comprehension.loopCondition);
180+
this.append(",");
181+
this.appendLine();
182+
this.append("// LoopStep");
183+
this.appendLine();
184+
this.buffer(comprehension.loopStep);
185+
this.append(",");
186+
this.appendLine();
187+
this.append("// Result");
188+
this.appendLine();
189+
this.buffer(comprehension.result);
190+
this.append(")");
191+
this.removeIndent();
192+
}
193+
194+
append(s: string) {
195+
this.doIndent();
196+
this.content += s;
197+
}
198+
199+
doIndent() {
200+
if (this.lineStart) {
201+
this.lineStart = false;
202+
this.content += " ".repeat(this.indent);
203+
}
204+
}
205+
206+
adorn(e: Message) {
207+
this.append(this.adorner.GetMetadata(e));
208+
}
209+
210+
appendLine() {
211+
this.content += "\n";
212+
this.lineStart = true;
213+
}
214+
215+
addIndent() {
216+
this.indent++;
217+
}
218+
219+
removeIndent() {
220+
this.indent--;
221+
if (this.indent < 0) {
222+
throw new Error("negative indent");
223+
}
224+
}
225+
226+
toString(): string {
227+
return this.content;
228+
}
229+
}
230+
231+
interface Adorner {
232+
GetMetadata(context: Message): string;
233+
}
234+
235+
class EmptyAdorner implements Adorner {
236+
static readonly singleton = new EmptyAdorner();
237+
private constructor() {}
238+
239+
GetMetadata(): string {
240+
return "";
241+
}
242+
}
243+
244+
function formatLiteral(c: Constant): string {
245+
const kind = c.constantKind;
246+
247+
switch (kind.case) {
248+
case "boolValue":
249+
return kind.value ? "true" : "false";
250+
case "bytesValue":
251+
return `b${JSON.stringify(decoder.decode(kind.value))}`;
252+
case "doubleValue":
253+
if (Math.floor(kind.value) == kind.value) {
254+
return `{value.toString()}.0`;
255+
} else {
256+
return kind.value.toString();
257+
}
258+
case "int64Value":
259+
return kind.value.toString();
260+
case "stringValue":
261+
return JSON.stringify(kind.value);
262+
case "uint64Value":
263+
return kind.value.toString();
264+
case "nullValue":
265+
return "null";
266+
default:
267+
throw new Error(`Unknown constant type: ${kind.case}`);
268+
}
269+
}

0 commit comments

Comments
 (0)