@telorun/templating 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/builtins.d.ts +11 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +16 -0
- package/dist/cel/analyze.d.ts +2 -1
- package/dist/cel/analyze.d.ts.map +1 -1
- package/dist/cel/analyze.js +4 -5
- package/dist/cel/catalog.d.ts.map +1 -1
- package/dist/cel/catalog.js +27 -0
- package/dist/cel/diagnose.d.ts +50 -0
- package/dist/cel/diagnose.d.ts.map +1 -0
- package/dist/cel/diagnose.js +223 -0
- package/dist/cel/environment.d.ts +7 -0
- package/dist/cel/environment.d.ts.map +1 -1
- package/dist/cel/environment.js +19 -5
- package/dist/cel/walk.d.ts +17 -1
- package/dist/cel/walk.d.ts.map +1 -1
- package/dist/cel/walk.js +21 -3
- package/dist/engine.d.ts +110 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engines/cel.d.ts +13 -9
- package/dist/engines/cel.d.ts.map +1 -1
- package/dist/engines/cel.js +86 -28
- package/dist/engines/include.d.ts +28 -0
- package/dist/engines/include.d.ts.map +1 -0
- package/dist/engines/include.js +142 -0
- package/dist/engines/literal.js +1 -1
- package/dist/engines/ref.js +1 -1
- package/dist/engines/sql.d.ts.map +1 -1
- package/dist/engines/sql.js +32 -5
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/manifest-schemas.d.ts +0 -16
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +0 -95
- package/dist/sentinel.d.ts +37 -0
- package/dist/sentinel.d.ts.map +1 -1
- package/dist/sentinel.js +54 -0
- package/package.json +2 -2
- package/src/builtins.ts +17 -0
- package/src/cel/analyze.ts +4 -7
- package/src/cel/catalog.ts +27 -0
- package/src/cel/diagnose.ts +293 -0
- package/src/cel/environment.ts +21 -5
- package/src/cel/walk.ts +37 -4
- package/src/engine.ts +116 -3
- package/src/engines/cel.ts +91 -31
- package/src/engines/include.ts +164 -0
- package/src/engines/literal.ts +1 -1
- package/src/engines/ref.ts +1 -1
- package/src/engines/sql.ts +41 -7
- package/src/index.ts +36 -5
- package/src/manifest-schemas.ts +0 -98
- package/src/sentinel.ts +62 -0
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ Built to be language-agnostic and infinitely extensible.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Reconcile your manifest into a running backend
|
|
26
|
-
$ telo ./examples/
|
|
26
|
+
$ telo ./examples/todo-app
|
|
27
27
|
|
|
28
|
-
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:
|
|
28
|
+
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8077"}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Why use Telo?
|
package/dist/builtins.d.ts
CHANGED
|
@@ -8,6 +8,17 @@ import type { TemplatingEngine } from "./engine.js";
|
|
|
8
8
|
* another (e.g. `cel + literal`); always ship the same set. */
|
|
9
9
|
export declare const builtinEngines: readonly TemplatingEngine[];
|
|
10
10
|
export declare function createDefaultRegistry(): TemplatingEngineRegistry;
|
|
11
|
+
/**
|
|
12
|
+
* The type a tag always produces, or undefined when its produced type is a
|
|
13
|
+
* function of the slot rather than of the tag (`!cel`, `!ref`).
|
|
14
|
+
*
|
|
15
|
+
* The single reader of `TemplatingEngine.producedType`, so a consumer asks the
|
|
16
|
+
* registry what a tag produces instead of recognising tag names — the same seam
|
|
17
|
+
* `fileClaims` opened for payload membership. Reads the default registry
|
|
18
|
+
* because a produced type is a property of the engine, not of a host's
|
|
19
|
+
* configuration, and every host ships the same built-in set.
|
|
20
|
+
*/
|
|
21
|
+
export declare function producedTypeOf(engineName: string): Record<string, unknown> | undefined;
|
|
11
22
|
/** Memoized singleton: returns the default registry. Hosts that don't need
|
|
12
23
|
* per-instance isolation (precompile, the analyzer's tagged-value walker)
|
|
13
24
|
* should use this so they share the same registry instance the YAML tag
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAOrD,CAAC;AAEF,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAEtF;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
|
package/dist/builtins.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { celEngine } from "./engines/cel.js";
|
|
2
|
+
import { includeBytesEngine, includeTextEngine } from "./engines/include.js";
|
|
2
3
|
import { literalEngine } from "./engines/literal.js";
|
|
3
4
|
import { refEngine } from "./engines/ref.js";
|
|
4
5
|
import { sqlEngine } from "./engines/sql.js";
|
|
@@ -11,6 +12,8 @@ import { TemplatingEngineRegistry } from "./registry.js";
|
|
|
11
12
|
* another (e.g. `cel + literal`); always ship the same set. */
|
|
12
13
|
export const builtinEngines = [
|
|
13
14
|
celEngine,
|
|
15
|
+
includeBytesEngine,
|
|
16
|
+
includeTextEngine,
|
|
14
17
|
literalEngine,
|
|
15
18
|
refEngine,
|
|
16
19
|
sqlEngine,
|
|
@@ -22,6 +25,19 @@ export function createDefaultRegistry() {
|
|
|
22
25
|
}
|
|
23
26
|
return registry;
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The type a tag always produces, or undefined when its produced type is a
|
|
30
|
+
* function of the slot rather than of the tag (`!cel`, `!ref`).
|
|
31
|
+
*
|
|
32
|
+
* The single reader of `TemplatingEngine.producedType`, so a consumer asks the
|
|
33
|
+
* registry what a tag produces instead of recognising tag names — the same seam
|
|
34
|
+
* `fileClaims` opened for payload membership. Reads the default registry
|
|
35
|
+
* because a produced type is a property of the engine, not of a host's
|
|
36
|
+
* configuration, and every host ships the same built-in set.
|
|
37
|
+
*/
|
|
38
|
+
export function producedTypeOf(engineName) {
|
|
39
|
+
return defaultRegistry().get(engineName)?.producedType?.();
|
|
40
|
+
}
|
|
25
41
|
let defaultRegistryCache;
|
|
26
42
|
/** Memoized singleton: returns the default registry. Hosts that don't need
|
|
27
43
|
* per-instance isolation (precompile, the analyzer's tagged-value walker)
|
package/dist/cel/analyze.d.ts
CHANGED
|
@@ -29,7 +29,8 @@ export declare function findNullableAccessIssues(node: ASTNode, contextSchema: R
|
|
|
29
29
|
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
30
30
|
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
31
31
|
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
32
|
-
* reach inside
|
|
32
|
+
* reach inside a `live` value type — one whose consumption has effects, so its
|
|
33
|
+
* contents exist only for a consumer that drains it.
|
|
33
34
|
* Returns null when the chain is valid or the schema is too open to judge.
|
|
34
35
|
*/
|
|
35
36
|
export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AA0DD;;+DAE+D;AAC/D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAqBnC,UAAU,aAAa;IACrB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;CAChB;AAoFD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,aAAa,EAAE,CAIjB;AA2FD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAsBf"}
|
package/dist/cel/analyze.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLiveSlot } from "@telorun/sdk";
|
|
1
2
|
/**
|
|
2
3
|
* Extract all member-access chains from a CEL AST.
|
|
3
4
|
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
@@ -254,7 +255,8 @@ function walkNullable(node, nonNull, boundVars, issues, schema) {
|
|
|
254
255
|
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
255
256
|
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
256
257
|
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
257
|
-
* reach inside
|
|
258
|
+
* reach inside a `live` value type — one whose consumption has effects, so its
|
|
259
|
+
* contents exist only for a consumer that drains it.
|
|
258
260
|
* Returns null when the chain is valid or the schema is too open to judge.
|
|
259
261
|
*/
|
|
260
262
|
export function validateChainAgainstSchema(chain, schema) {
|
|
@@ -268,10 +270,7 @@ export function validateChainAgainstSchema(chain, schema) {
|
|
|
268
270
|
return null;
|
|
269
271
|
if (key in props) {
|
|
270
272
|
const propSchema = props[key];
|
|
271
|
-
if (propSchema &&
|
|
272
|
-
typeof propSchema === "object" &&
|
|
273
|
-
propSchema["x-telo-stream"] === true &&
|
|
274
|
-
i < chain.length - 1) {
|
|
273
|
+
if (isLiveSlot(propSchema) && i < chain.length - 1) {
|
|
275
274
|
const path = chain.slice(0, i + 1).join(".");
|
|
276
275
|
return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
|
|
277
276
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAGA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAyE5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAGA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAyE5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EA+hBlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
|
package/dist/cel/catalog.js
CHANGED
|
@@ -540,6 +540,33 @@ export const CEL_FUNCTIONS = [
|
|
|
540
540
|
hostBacked: false,
|
|
541
541
|
build: () => () => BigInt(Math.floor(Date.now() / 1000)),
|
|
542
542
|
},
|
|
543
|
+
// Timestamp conversions. cel-go's standard library defines both and cel-js
|
|
544
|
+
// ships neither, which is what made an instant a one-way door: timestamp
|
|
545
|
+
// arithmetic and the `getFullYear` / `getHours` family already work, but
|
|
546
|
+
// nothing converted the result back into a value a manifest field accepts,
|
|
547
|
+
// so an expiry could be computed and not stored. Semantics follow cel-go
|
|
548
|
+
// exactly — RFC 3339 and epoch SECONDS — so `int(timestamp)` and
|
|
549
|
+
// `timestamp(int)` round-trip in one unit.
|
|
550
|
+
{
|
|
551
|
+
name: "string",
|
|
552
|
+
signature: "string(timestamp): string",
|
|
553
|
+
register: ["string(google.protobuf.Timestamp): string"],
|
|
554
|
+
category: "conversion",
|
|
555
|
+
summary: "Format an instant as RFC 3339 (ISO-8601, UTC).",
|
|
556
|
+
deterministic: true,
|
|
557
|
+
hostBacked: false,
|
|
558
|
+
build: () => (t) => t.toISOString(),
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "int",
|
|
562
|
+
signature: "int(timestamp): int",
|
|
563
|
+
register: ["int(google.protobuf.Timestamp): int"],
|
|
564
|
+
category: "conversion",
|
|
565
|
+
summary: "Epoch seconds of an instant (the unit `timestamp(int)` reads back).",
|
|
566
|
+
deterministic: true,
|
|
567
|
+
hostBacked: false,
|
|
568
|
+
build: () => (t) => BigInt(Math.floor(t.getTime() / 1000)),
|
|
569
|
+
},
|
|
543
570
|
// UUID
|
|
544
571
|
{
|
|
545
572
|
name: "uuidv1",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ASTNode, Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
import type { CallSite, EngineDiagnostic } from "../engine.js";
|
|
3
|
+
/** Classifies every function call in a CEL expression against the environment's
|
|
4
|
+
* own function registry.
|
|
5
|
+
*
|
|
6
|
+
* This exists because cel-js reports one sentence for three unrelated mistakes
|
|
7
|
+
* — a name that does not exist, a name called in the wrong form, and a genuine
|
|
8
|
+
* type mismatch all surface as `found no matching overload for 'f(...)'`. Two
|
|
9
|
+
* of those readings actively mislead: the message names argument types, so the
|
|
10
|
+
* repair for `startsWith(key, 'x')` looks like a cast, when the real fix is
|
|
11
|
+
* `key.startsWith('x')` and no cast helps.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here reads cel-js's message text. `Environment.getDefinitions()`
|
|
14
|
+
* reports every registered signature — cel-js builtins and Telo's catalog
|
|
15
|
+
* alike — with its call form and parameters, and the AST distinguishes `f(x)`
|
|
16
|
+
* from `x.f()` structurally. Name existence, call form and arity are therefore
|
|
17
|
+
* decidable by lookup, which is what keeps a cel-js version bump from silently
|
|
18
|
+
* degrading this back into the passthrough it replaced. */
|
|
19
|
+
/** One registered signature, reduced to what classification needs. */
|
|
20
|
+
interface FnEntry {
|
|
21
|
+
readonly signature: string;
|
|
22
|
+
readonly form: "global" | "receiver";
|
|
23
|
+
/** Parameter count, excluding the receiver for a receiver form. */
|
|
24
|
+
readonly arity: number;
|
|
25
|
+
}
|
|
26
|
+
export interface FunctionIndex {
|
|
27
|
+
readonly byName: ReadonlyMap<string, readonly FnEntry[]>;
|
|
28
|
+
}
|
|
29
|
+
/** Registry view of an environment, memoized: environments are rebuilt per
|
|
30
|
+
* analysis path, but each is immutable once built. */
|
|
31
|
+
export declare function functionIndex(env: Environment): FunctionIndex;
|
|
32
|
+
export interface CallAudit {
|
|
33
|
+
readonly diagnostics: readonly EngineDiagnostic[];
|
|
34
|
+
readonly calls: readonly CallSite[];
|
|
35
|
+
/** Names that resolve, but that no registered signature accepts as written.
|
|
36
|
+
* The caller appends their signatures to a type-check failure it could not
|
|
37
|
+
* otherwise explain. */
|
|
38
|
+
readonly unresolved: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** Classify every call in `ast`. Runs unconditionally rather than only after a
|
|
41
|
+
* failed type-check, because a type-check reports its first error and stops:
|
|
42
|
+
* an expression with two bad calls would otherwise fix one, re-run, and
|
|
43
|
+
* discover the next. */
|
|
44
|
+
export declare function auditCalls(source: string, ast: ASTNode, env: Environment): CallAudit;
|
|
45
|
+
/** Registered signatures for names a type-check failure mentions, so the
|
|
46
|
+
* residual says what the function actually accepts rather than only echoing
|
|
47
|
+
* what the author wrote. */
|
|
48
|
+
export declare function explainUnresolved(names: readonly string[], env: Environment): string;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=diagnose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/cel/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAiB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE9E;;;;;;;;;;;;;;;4DAe4D;AAE5D,sEAAsE;AACtE,UAAU,OAAO;IACf,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;CAC1D;AAWD;uDACuD;AACvD,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAiB7D;AAuJD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC;;6BAEyB;IACzB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;yBAGyB;AACzB,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,CA0DpF;AAED;;6BAE6B;AAC7B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAIpF"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { CEL_FUNCTIONS } from "./catalog.js";
|
|
2
|
+
/** Determinism is Telo catalog metadata; cel-js builtins carry none. Absent
|
|
3
|
+
* means "no signal", never "deterministic" — consumers of
|
|
4
|
+
* `CallSite.deterministic` must not read undefined as a guarantee. */
|
|
5
|
+
const DETERMINISM = new Map(CEL_FUNCTIONS.map((f) => [f.name, f.deterministic]));
|
|
6
|
+
const INDEX_CACHE = new WeakMap();
|
|
7
|
+
/** Registry view of an environment, memoized: environments are rebuilt per
|
|
8
|
+
* analysis path, but each is immutable once built. */
|
|
9
|
+
export function functionIndex(env) {
|
|
10
|
+
const cached = INDEX_CACHE.get(env);
|
|
11
|
+
if (cached)
|
|
12
|
+
return cached;
|
|
13
|
+
const byName = new Map();
|
|
14
|
+
for (const fn of env.getDefinitions().functions) {
|
|
15
|
+
const form = fn.receiverType === null ? "global" : "receiver";
|
|
16
|
+
const entry = { signature: fn.signature, form, arity: fn.params?.length ?? 0 };
|
|
17
|
+
const entries = byName.get(fn.name);
|
|
18
|
+
if (entries)
|
|
19
|
+
entries.push(entry);
|
|
20
|
+
else
|
|
21
|
+
byName.set(fn.name, [entry]);
|
|
22
|
+
}
|
|
23
|
+
const index = { byName };
|
|
24
|
+
INDEX_CACHE.set(env, index);
|
|
25
|
+
return index;
|
|
26
|
+
}
|
|
27
|
+
/** Macros the parser expands rather than dispatching through the registry, so
|
|
28
|
+
* they never appear in `getDefinitions()` and would otherwise classify as
|
|
29
|
+
* unknown names.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately short: most macros ARE registered, and the caller only reports
|
|
32
|
+
* this audit when the type-checker already rejected the expression, so a macro
|
|
33
|
+
* missing from here degrades to "no extra explanation" rather than to a false
|
|
34
|
+
* error on valid CEL. That is what keeps a cel-js upgrade from turning a new
|
|
35
|
+
* macro into a manifest this analyzer refuses. */
|
|
36
|
+
const MACROS = new Set(["optMap", "optFlatMap"]);
|
|
37
|
+
function isNode(v) {
|
|
38
|
+
return typeof v === "object" && v !== null && "op" in v;
|
|
39
|
+
}
|
|
40
|
+
/** Every non-macro call in the expression, in source order. */
|
|
41
|
+
function collectCalls(root, index) {
|
|
42
|
+
const out = [];
|
|
43
|
+
visit(root);
|
|
44
|
+
return out.sort((a, b) => a.start - b.start);
|
|
45
|
+
function visit(node) {
|
|
46
|
+
const args = node.args;
|
|
47
|
+
if (node.op === "call" || node.op === "rcall") {
|
|
48
|
+
const tuple = args;
|
|
49
|
+
const name = tuple[0];
|
|
50
|
+
if (typeof name === "string" && !MACROS.has(name)) {
|
|
51
|
+
const receiver = node.op === "rcall" ? tuple[1] : undefined;
|
|
52
|
+
const rawArgs = node.op === "rcall" ? tuple[2] : tuple[1];
|
|
53
|
+
const callArgs = (Array.isArray(rawArgs) ? rawArgs : []).filter(isNode);
|
|
54
|
+
out.push({
|
|
55
|
+
name,
|
|
56
|
+
form: node.op === "rcall" ? "receiver" : "global",
|
|
57
|
+
arity: callArgs.length,
|
|
58
|
+
start: node.start,
|
|
59
|
+
end: node.end,
|
|
60
|
+
...(index.byName.has(name) ? { deterministic: DETERMINISM.get(name) } : {}),
|
|
61
|
+
...(isNode(receiver) ? { receiver } : {}),
|
|
62
|
+
args: callArgs,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const arg of Array.isArray(args) ? args : [args]) {
|
|
67
|
+
if (isNode(arg))
|
|
68
|
+
visit(arg);
|
|
69
|
+
else if (Array.isArray(arg))
|
|
70
|
+
for (const item of arg)
|
|
71
|
+
if (isNode(item))
|
|
72
|
+
visit(item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Node shapes that can carry a `.` on their right without reparsing
|
|
77
|
+
* differently. Everything else — an operator expression, a literal that would
|
|
78
|
+
* sit against the dot — is parenthesized when moved into receiver position. */
|
|
79
|
+
const SELF_DELIMITING = new Set(["id", ".", ".?", "call", "rcall", "[]", "[?]"]);
|
|
80
|
+
/** Source text of a node. When it is about to become a receiver it may need
|
|
81
|
+
* parentheses: an identifier, member chain, index or call is self-delimiting,
|
|
82
|
+
* but an operator expression or a bare literal against a `.` is not. */
|
|
83
|
+
function nodeText(source, node, asReceiver = false) {
|
|
84
|
+
const text = source.slice(node.start, node.end);
|
|
85
|
+
if (!asReceiver)
|
|
86
|
+
return text;
|
|
87
|
+
return SELF_DELIMITING.has(node.op) ? text : `(${text})`;
|
|
88
|
+
}
|
|
89
|
+
/** The same call written in the other form, or undefined when the shape does
|
|
90
|
+
* not allow it (a global call with no arguments has no receiver to move). */
|
|
91
|
+
function transpose(source, call) {
|
|
92
|
+
if (call.form === "global") {
|
|
93
|
+
const [first, ...rest] = call.args;
|
|
94
|
+
if (!first)
|
|
95
|
+
return undefined;
|
|
96
|
+
const argText = rest.map((a) => nodeText(source, a));
|
|
97
|
+
return `${nodeText(source, first, true)}.${call.name}(${argText.join(", ")})`;
|
|
98
|
+
}
|
|
99
|
+
if (!call.receiver)
|
|
100
|
+
return undefined;
|
|
101
|
+
const argText = [nodeText(source, call.receiver), ...call.args.map((a) => nodeText(source, a))];
|
|
102
|
+
return `${call.name}(${argText.join(", ")})`;
|
|
103
|
+
}
|
|
104
|
+
/** Splice a rewritten call back into the full source. The fix always carries
|
|
105
|
+
* the whole corrected source, so a consumer applies it by replacing the
|
|
106
|
+
* scalar. */
|
|
107
|
+
function spliceFix(source, call, rewritten) {
|
|
108
|
+
return { replacement: source.slice(0, call.start) + rewritten + source.slice(call.end) };
|
|
109
|
+
}
|
|
110
|
+
/** Replace only the called name, leaving arguments untouched. The offset is
|
|
111
|
+
* derived rather than searched: a receiver whose own text contains the name
|
|
112
|
+
* (`slice.slice(1)`) would defeat a first-occurrence replace. */
|
|
113
|
+
function renameFix(source, call, to) {
|
|
114
|
+
const searchFrom = call.receiver ? call.receiver.end : call.start;
|
|
115
|
+
const at = source.indexOf(call.name, searchFrom);
|
|
116
|
+
if (at === -1 || at >= call.end)
|
|
117
|
+
return undefined;
|
|
118
|
+
return { replacement: source.slice(0, at) + to + source.slice(at + call.name.length) };
|
|
119
|
+
}
|
|
120
|
+
/** Arity the call would need in the other form: moving a receiver in adds an
|
|
121
|
+
* argument, moving it out removes one. */
|
|
122
|
+
function transposedArity(call) {
|
|
123
|
+
return call.form === "global" ? call.arity - 1 : call.arity + 1;
|
|
124
|
+
}
|
|
125
|
+
function accepts(entries, form, arity) {
|
|
126
|
+
return entries.some((e) => e.form === form && e.arity === arity);
|
|
127
|
+
}
|
|
128
|
+
const listOf = (names) => [...new Set(names)].sort().join(", ");
|
|
129
|
+
/** Names registered in exactly the form and arity the author wrote — the only
|
|
130
|
+
* ones that could replace this call with no further edits. Ranked by shared
|
|
131
|
+
* prefix, which is what reaches `nowIso` from `now`; edit distance never
|
|
132
|
+
* would (3 characters against a 3-character name). Returns [] when nothing
|
|
133
|
+
* shares a prefix, so the caller lists what is legal here instead of
|
|
134
|
+
* guessing. */
|
|
135
|
+
function candidates(call, index) {
|
|
136
|
+
const written = call.name.toLowerCase();
|
|
137
|
+
return callableHere(call, index)
|
|
138
|
+
.filter((name) => {
|
|
139
|
+
const lower = name.toLowerCase();
|
|
140
|
+
return lower.startsWith(written) || written.startsWith(lower);
|
|
141
|
+
})
|
|
142
|
+
.sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
143
|
+
}
|
|
144
|
+
/** Every name callable in exactly the position written — same form, same
|
|
145
|
+
* argument count. Arity is what makes the list usable: a receiver's type is
|
|
146
|
+
* `dyn` at analysis time so every method is nominally reachable, and printing
|
|
147
|
+
* all forty says nothing. */
|
|
148
|
+
function callableHere(call, index) {
|
|
149
|
+
const names = [];
|
|
150
|
+
for (const [name, entries] of index.byName) {
|
|
151
|
+
if (name !== call.name && accepts(entries, call.form, call.arity))
|
|
152
|
+
names.push(name);
|
|
153
|
+
}
|
|
154
|
+
return names;
|
|
155
|
+
}
|
|
156
|
+
function signaturesOf(name, index) {
|
|
157
|
+
return (index.byName.get(name) ?? []).map((e) => e.signature);
|
|
158
|
+
}
|
|
159
|
+
/** Spread-friendly optional `fix`, so an undecidable rewrite simply omits the
|
|
160
|
+
* field rather than carrying `undefined` into the diagnostic. */
|
|
161
|
+
const withFix = (fix) => (fix ? { fix } : {});
|
|
162
|
+
/** Classify every call in `ast`. Runs unconditionally rather than only after a
|
|
163
|
+
* failed type-check, because a type-check reports its first error and stops:
|
|
164
|
+
* an expression with two bad calls would otherwise fix one, re-run, and
|
|
165
|
+
* discover the next. */
|
|
166
|
+
export function auditCalls(source, ast, env) {
|
|
167
|
+
const index = functionIndex(env);
|
|
168
|
+
const diagnostics = [];
|
|
169
|
+
const unresolved = [];
|
|
170
|
+
const calls = collectCalls(ast, index);
|
|
171
|
+
for (const call of calls) {
|
|
172
|
+
const entries = index.byName.get(call.name);
|
|
173
|
+
const noun = call.form === "receiver" ? "method" : "function";
|
|
174
|
+
if (!entries) {
|
|
175
|
+
const near = candidates(call, index);
|
|
176
|
+
const hint = near.length > 0
|
|
177
|
+
? `Closest taking ${call.arity} argument${call.arity === 1 ? "" : "s"}: ${near
|
|
178
|
+
.slice(0, 5)
|
|
179
|
+
.map((n) => `\`${n}\``)
|
|
180
|
+
.join(", ")}.`
|
|
181
|
+
: `Every ${noun} taking ${call.arity} argument${call.arity === 1 ? "" : "s"}: ${listOf(callableHere(call, index))}.`;
|
|
182
|
+
diagnostics.push({
|
|
183
|
+
code: "CEL_UNKNOWN_FUNCTION",
|
|
184
|
+
message: `there is no ${noun} \`${call.name}\`. ${hint} Full list: \`telo cel functions\`.`,
|
|
185
|
+
// A single candidate is an unambiguous rename; several are a menu, and
|
|
186
|
+
// applying an arbitrary one would be a guess wearing a fix's clothes.
|
|
187
|
+
...(near.length === 1 ? withFix(renameFix(source, call, near[0])) : {}),
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (accepts(entries, call.form, call.arity))
|
|
192
|
+
continue;
|
|
193
|
+
const otherForm = call.form === "global" ? "receiver" : "global";
|
|
194
|
+
if (accepts(entries, otherForm, transposedArity(call))) {
|
|
195
|
+
const rewritten = transpose(source, call);
|
|
196
|
+
const written = source.slice(call.start, call.end);
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
code: "CEL_WRONG_CALL_FORM",
|
|
199
|
+
message: `\`${call.name}\` is ${call.form === "global"
|
|
200
|
+
? "a method, not a global function — call it on the value"
|
|
201
|
+
: "a global function, not a method — pass the value to it"}:` +
|
|
202
|
+
(rewritten ? `\n write: ${rewritten}\n not: ${written}` : "") +
|
|
203
|
+
`\nRegistered: ${listOf(signaturesOf(call.name, index))}.`,
|
|
204
|
+
...withFix(rewritten ? spliceFix(source, call, rewritten) : undefined),
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
unresolved.push(call.name);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
diagnostics,
|
|
212
|
+
calls: calls.map(({ receiver: _receiver, args: _args, ...site }) => site),
|
|
213
|
+
unresolved,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** Registered signatures for names a type-check failure mentions, so the
|
|
217
|
+
* residual says what the function actually accepts rather than only echoing
|
|
218
|
+
* what the author wrote. */
|
|
219
|
+
export function explainUnresolved(names, env) {
|
|
220
|
+
const index = functionIndex(env);
|
|
221
|
+
const signatures = [...new Set(names)].flatMap((n) => signaturesOf(n, index));
|
|
222
|
+
return signatures.length > 0 ? ` Registered: ${listOf(signatures)}.` : "";
|
|
223
|
+
}
|
|
@@ -24,5 +24,12 @@ export type { CelHandlers } from "./catalog.js";
|
|
|
24
24
|
* Returns `[signature]` unchanged when no `?` is present or the signature
|
|
25
25
|
* cannot be parsed. */
|
|
26
26
|
export declare function deriveSignatures(signature: string): string[];
|
|
27
|
+
/** The environment before any Telo function is registered — i.e. exactly
|
|
28
|
+
* cel-js's own built-ins. Documentation needs to tell the two apart, and
|
|
29
|
+
* subtracting by signature TEXT does not work: cel-js normalizes a declared
|
|
30
|
+
* `list` to `list<dyn>`, so the catalog's documented spelling and the
|
|
31
|
+
* registered one differ for a third of the entries. Asking for the base set
|
|
32
|
+
* directly needs no matching at all. */
|
|
33
|
+
export declare function celBuiltinFunctions(): ReturnType<Environment["getDefinitions"]>["functions"];
|
|
27
34
|
export declare function buildCelEnvironment(handlers?: Partial<CelHandlers>): Environment;
|
|
28
35
|
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cel/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAoBhD;;;;;;;;;;;;;;;yDAeyD;AACzD;;;;;wBAKwB;AACxB,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB5D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cel/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAoBhD;;;;;;;;;;;;;;;yDAeyD;AACzD;;;;;wBAKwB;AACxB,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB5D;AAYD;;;;;yCAKyC;AACzC,wBAAgB,mBAAmB,IAAI,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC,WAAW,CAAC,CAE5F;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAcpF"}
|
package/dist/cel/environment.js
CHANGED
|
@@ -66,13 +66,27 @@ export function deriveSignatures(signature) {
|
|
|
66
66
|
return `${name}(${allParams.join(", ")}): ${returnType}`;
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
/** cel-go defaults HomogeneousAggregateLiterals OFF: heterogeneous list/map
|
|
70
|
+
* literals unify to `dyn` rather than erroring. cel-js flips that default to
|
|
71
|
+
* strict; we align with cel-go so manifests (dyn-heavy: request, rows, …)
|
|
72
|
+
* don't hit false positives the runtime evaluates fine. */
|
|
73
|
+
const ENVIRONMENT_OPTIONS = {
|
|
74
|
+
unlistedVariablesAreDyn: true,
|
|
75
|
+
enableOptionalTypes: true,
|
|
76
|
+
homogeneousAggregateLiterals: false,
|
|
77
|
+
};
|
|
78
|
+
/** The environment before any Telo function is registered — i.e. exactly
|
|
79
|
+
* cel-js's own built-ins. Documentation needs to tell the two apart, and
|
|
80
|
+
* subtracting by signature TEXT does not work: cel-js normalizes a declared
|
|
81
|
+
* `list` to `list<dyn>`, so the catalog's documented spelling and the
|
|
82
|
+
* registered one differ for a third of the entries. Asking for the base set
|
|
83
|
+
* directly needs no matching at all. */
|
|
84
|
+
export function celBuiltinFunctions() {
|
|
85
|
+
return new Environment(ENVIRONMENT_OPTIONS).getDefinitions().functions;
|
|
86
|
+
}
|
|
69
87
|
export function buildCelEnvironment(handlers = {}) {
|
|
70
88
|
const h = { ...STUB_HANDLERS, ...handlers };
|
|
71
|
-
|
|
72
|
-
// literals unify to `dyn` rather than erroring. cel-js flips that default to
|
|
73
|
-
// strict; we align with cel-go so manifests (dyn-heavy: request, rows, …)
|
|
74
|
-
// don't hit false positives the runtime evaluates fine.
|
|
75
|
-
let env = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true, homogeneousAggregateLiterals: false });
|
|
89
|
+
let env = new Environment(ENVIRONMENT_OPTIONS);
|
|
76
90
|
for (const fn of CEL_FUNCTIONS) {
|
|
77
91
|
const impl = fn.build(h);
|
|
78
92
|
// `register` lists one cel-js signature per arity (overloaded functions).
|
package/dist/cel/walk.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/** How an emitted expression sits in the scalar it came from. A repair can be
|
|
2
|
+
* applied by replacing the scalar only when the expression covers all of it;
|
|
3
|
+
* otherwise the literal text around it would be lost.
|
|
4
|
+
*
|
|
5
|
+
* `wrapper` is the delimiter text to restore around a corrected expression —
|
|
6
|
+
* empty for a tagged sentinel (whose scalar *is* the expression), `${{` / `}}`
|
|
7
|
+
* for the legacy interpolation form. Carrying it as data rather than letting
|
|
8
|
+
* each consumer re-derive it is what keeps `${{ … }}` a first-class fix site
|
|
9
|
+
* instead of an exclusion: the two surfaces differ only by these delimiters. */
|
|
10
|
+
export interface CelSurface {
|
|
11
|
+
readonly whole: boolean;
|
|
12
|
+
readonly wrapper?: {
|
|
13
|
+
readonly prefix: string;
|
|
14
|
+
readonly suffix: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
1
17
|
/** Walks `value` and emits each templated source segment with its dotted
|
|
2
18
|
* path (e.g. `routes[0].handler.body`) and the engine that owns it.
|
|
3
19
|
*
|
|
@@ -9,5 +25,5 @@
|
|
|
9
25
|
* routing through the registry stays generic so adding a third engine
|
|
10
26
|
* that wants real analysis doesn't require touching the walker.
|
|
11
27
|
* - Compiled values are skipped so a precompiled tree won't be re-walked. */
|
|
12
|
-
export declare function walkCelExpressions(value: unknown, path: string, cb: (source: string, path: string, engineName: string) => void): void;
|
|
28
|
+
export declare function walkCelExpressions(value: unknown, path: string, cb: (source: string, path: string, engineName: string, surface: CelSurface) => void): void;
|
|
13
29
|
//# sourceMappingURL=walk.d.ts.map
|
package/dist/cel/walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../../src/cel/walk.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;8EAU8E;AAC9E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../../src/cel/walk.ts"],"names":[],"mappings":"AAGA;;;;;;;;iFAQiF;AACjF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAID;;;;;;;;;;8EAU8E;AAC9E,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,IAAI,GAClF,IAAI,CAyCN"}
|
package/dist/cel/walk.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isTaggedSentinel } from "../sentinel.js";
|
|
2
2
|
import { TEMPLATE_REGEX } from "./compile.js";
|
|
3
|
+
const OPEN = "${{";
|
|
3
4
|
/** Walks `value` and emits each templated source segment with its dotted
|
|
4
5
|
* path (e.g. `routes[0].handler.body`) and the engine that owns it.
|
|
5
6
|
*
|
|
@@ -13,12 +14,29 @@ import { TEMPLATE_REGEX } from "./compile.js";
|
|
|
13
14
|
* - Compiled values are skipped so a precompiled tree won't be re-walked. */
|
|
14
15
|
export function walkCelExpressions(value, path, cb) {
|
|
15
16
|
if (isTaggedSentinel(value)) {
|
|
16
|
-
cb(value.source, path, value.engine);
|
|
17
|
+
cb(value.source, path, value.engine, { whole: true });
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
if (typeof value === "string") {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const matches = [...value.matchAll(TEMPLATE_REGEX)];
|
|
22
|
+
for (const m of matches) {
|
|
23
|
+
const expr = m[1].trim();
|
|
24
|
+
// A string that is nothing but one interpolation is as whole as a
|
|
25
|
+
// tagged scalar — the delimiters are the only difference, and they are
|
|
26
|
+
// handed back as the wrapper.
|
|
27
|
+
const only = matches.length === 1 && m.index === 0 && m[0].length === value.length;
|
|
28
|
+
const lead = /^\s*/.exec(m[0].slice(OPEN.length))[0].length;
|
|
29
|
+
cb(expr, path, "cel", {
|
|
30
|
+
whole: only,
|
|
31
|
+
...(only
|
|
32
|
+
? {
|
|
33
|
+
wrapper: {
|
|
34
|
+
prefix: value.slice(0, OPEN.length + lead),
|
|
35
|
+
suffix: value.slice(OPEN.length + lead + expr.length),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
22
40
|
}
|
|
23
41
|
return;
|
|
24
42
|
}
|