@telorun/templating 0.8.0 → 0.10.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/cel/catalog.d.ts.map +1 -1
- package/dist/cel/catalog.js +127 -0
- package/package.json +3 -2
- package/src/cel/catalog.ts +132 -0
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ metadata:
|
|
|
55
55
|
A complete feedback collection REST API — no code, pure YAML.
|
|
56
56
|
Persists entries to SQLite and serves them over HTTP.
|
|
57
57
|
imports:
|
|
58
|
-
Http: std/http-server@0.
|
|
59
|
-
Sql: std/sql@0.
|
|
58
|
+
Http: std/http-server@0.11.0
|
|
59
|
+
Sql: std/sql@0.9.0
|
|
60
60
|
targets:
|
|
61
61
|
- !ref Migrations
|
|
62
62
|
- !ref Server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"
|
|
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,EA8elD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
|
package/dist/cel/catalog.js
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
1
2
|
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
3
|
+
/** RE2 regex engine for the CEL `regex*` functions — `re2js`, a pure-JS port of
|
|
4
|
+
* Google's RE2. The dialect is RE2 (linear-time, no backtracking, ReDoS-safe) —
|
|
5
|
+
* never JS `RegExp` — so a manifest's regex behaves the same across runtimes.
|
|
6
|
+
* Flags map a trailing option string to RE2 flags; inline `(?s)` etc. work in
|
|
7
|
+
* the pattern too.
|
|
8
|
+
*
|
|
9
|
+
* Why pure-JS and not a native RE2:
|
|
10
|
+
* - The Rust `regex` crate via napi (N-API) is faster, but shipping it means a
|
|
11
|
+
* cross-platform prebuild matrix + per-triple npm packages — a whole release
|
|
12
|
+
* subsystem for one function family.
|
|
13
|
+
* - The `re2` npm package (Google's C++ RE2) is simpler to depend on, but it's a
|
|
14
|
+
* nan/V8 addon and **does not load under Bun** (incomplete V8 C++ ABI →
|
|
15
|
+
* `undefined symbol`). Telo's CLI and test suite run on Bun, so that's a
|
|
16
|
+
* non-starter.
|
|
17
|
+
* `re2js` is pure JS: zero native addons, runs identically on Node, Bun, and the
|
|
18
|
+
* browser (keeps this package — and the analyzer — browser-safe), and needs no
|
|
19
|
+
* prebuilds. The cost is throughput vs. native, which is irrelevant for the
|
|
20
|
+
* small strings CEL manifests run regex over. */
|
|
21
|
+
const RE2_FLAG = {
|
|
22
|
+
i: RE2JS.CASE_INSENSITIVE,
|
|
23
|
+
m: RE2JS.MULTILINE,
|
|
24
|
+
s: RE2JS.DOTALL,
|
|
25
|
+
};
|
|
26
|
+
const compileRe2 = (fn, pattern, flags) => {
|
|
27
|
+
let bits = 0;
|
|
28
|
+
for (const c of flags ?? "") {
|
|
29
|
+
if (c === "g")
|
|
30
|
+
continue; // global is implicit in replaceAll / find-loop
|
|
31
|
+
const bit = RE2_FLAG[c];
|
|
32
|
+
if (bit === undefined)
|
|
33
|
+
throw new Error(`${fn}: unknown regex flag '${c}' (supported: i, m, s)`);
|
|
34
|
+
bits |= bit;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return RE2JS.compile(pattern, bits);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Error(`${fn}: invalid RE2 pattern ${JSON.stringify(pattern)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
2
43
|
const num = (x) => Number(x);
|
|
3
44
|
const minMax = (list, isMin) => {
|
|
4
45
|
if (!Array.isArray(list) || list.length === 0)
|
|
@@ -130,6 +171,24 @@ export const CEL_FUNCTIONS = [
|
|
|
130
171
|
hostBacked: false,
|
|
131
172
|
build: () => (list) => sortList(list),
|
|
132
173
|
},
|
|
174
|
+
{
|
|
175
|
+
name: "range",
|
|
176
|
+
signature: "range(int): list<int>",
|
|
177
|
+
category: "collection",
|
|
178
|
+
summary: "Integers [0, n-1] (empty for n <= 0); materializes indices for an unknown-length list.",
|
|
179
|
+
deterministic: true,
|
|
180
|
+
hostBacked: false,
|
|
181
|
+
build: () => (n) => Array.from({ length: Math.max(0, Math.trunc(num(n))) }, (_unused, i) => BigInt(i)),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "enumerate",
|
|
185
|
+
signature: "enumerate(list): list",
|
|
186
|
+
category: "collection",
|
|
187
|
+
summary: "Pair each element with its zero-based position as {index, value}.",
|
|
188
|
+
deterministic: true,
|
|
189
|
+
hostBacked: false,
|
|
190
|
+
build: () => (list) => list.map((value, i) => ({ index: BigInt(i), value })),
|
|
191
|
+
},
|
|
133
192
|
// Strings
|
|
134
193
|
{
|
|
135
194
|
name: "lower",
|
|
@@ -176,6 +235,74 @@ export const CEL_FUNCTIONS = [
|
|
|
176
235
|
hostBacked: false,
|
|
177
236
|
build: () => (s, sep) => s.split(sep),
|
|
178
237
|
},
|
|
238
|
+
{
|
|
239
|
+
name: "regexReplace",
|
|
240
|
+
signature: "regexReplace(string, string, string, string?): string",
|
|
241
|
+
category: "string",
|
|
242
|
+
summary: "Replace every regex match (RE2 syntax) with a replacement ($1 backrefs); flags like 'i', 'm', 's'.",
|
|
243
|
+
deterministic: true,
|
|
244
|
+
hostBacked: false,
|
|
245
|
+
build: () => (s, pattern, replacement, flags) => compileRe2("regexReplace", pattern, flags).matcher(s).replaceAll(replacement),
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "regexExtract",
|
|
249
|
+
signature: "regexExtract(string, string, string?): string",
|
|
250
|
+
category: "string",
|
|
251
|
+
summary: "First whole match of a regex (RE2 syntax), or '' when there is none.",
|
|
252
|
+
deterministic: true,
|
|
253
|
+
hostBacked: false,
|
|
254
|
+
build: () => (s, pattern, flags) => {
|
|
255
|
+
const m = compileRe2("regexExtract", pattern, flags).matcher(s);
|
|
256
|
+
return m.find() ? (m.group() ?? "") : "";
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: "regexExtractAll",
|
|
261
|
+
signature: "regexExtractAll(string, string, string?): list<string>",
|
|
262
|
+
category: "string",
|
|
263
|
+
summary: "Every whole match of a regex (RE2 syntax), in order.",
|
|
264
|
+
deterministic: true,
|
|
265
|
+
hostBacked: false,
|
|
266
|
+
build: () => (s, pattern, flags) => {
|
|
267
|
+
const m = compileRe2("regexExtractAll", pattern, flags).matcher(s);
|
|
268
|
+
const out = [];
|
|
269
|
+
while (m.find())
|
|
270
|
+
out.push(m.group() ?? "");
|
|
271
|
+
return out;
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "regexGroups",
|
|
276
|
+
signature: "regexGroups(string, string, string?): list<string>",
|
|
277
|
+
category: "string",
|
|
278
|
+
summary: "Capture groups of the first regex match (RE2 syntax); empty list when there is no match.",
|
|
279
|
+
deterministic: true,
|
|
280
|
+
hostBacked: false,
|
|
281
|
+
build: () => (s, pattern, flags) => {
|
|
282
|
+
const m = compileRe2("regexGroups", pattern, flags).matcher(s);
|
|
283
|
+
if (!m.find())
|
|
284
|
+
return [];
|
|
285
|
+
return Array.from({ length: m.groupCount() }, (_unused, i) => m.group(i + 1) ?? "");
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "trimPrefix",
|
|
290
|
+
signature: "trimPrefix(string, string): string",
|
|
291
|
+
category: "string",
|
|
292
|
+
summary: "Strip a leading prefix if present.",
|
|
293
|
+
deterministic: true,
|
|
294
|
+
hostBacked: false,
|
|
295
|
+
build: () => (s, prefix) => (s.startsWith(prefix) ? s.slice(prefix.length) : s),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "trimSuffix",
|
|
299
|
+
signature: "trimSuffix(string, string): string",
|
|
300
|
+
category: "string",
|
|
301
|
+
summary: "Strip a trailing suffix if present.",
|
|
302
|
+
deterministic: true,
|
|
303
|
+
hostBacked: false,
|
|
304
|
+
build: () => (s, suffix) => suffix && s.endsWith(suffix) ? s.slice(0, s.length - suffix.length) : s,
|
|
305
|
+
},
|
|
179
306
|
// Math
|
|
180
307
|
{
|
|
181
308
|
name: "abs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/templating",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@marcbachmann/cel-js": "^7.6.1",
|
|
39
|
+
"re2js": "^2.8.3",
|
|
39
40
|
"uuid": "^10.0.0",
|
|
40
41
|
"yaml": "^2.8.3"
|
|
41
42
|
},
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@types/uuid": "^10.0.0",
|
|
45
46
|
"typescript": "^5.0.0",
|
|
46
47
|
"vitest": "^2.1.8",
|
|
47
|
-
"@telorun/sdk": "0.
|
|
48
|
+
"@telorun/sdk": "0.26.0"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
51
|
"@telorun/sdk": "*"
|
package/src/cel/catalog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
1
2
|
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
2
3
|
|
|
3
4
|
/** Host-injected functions that need platform APIs the templating package must
|
|
@@ -15,6 +16,47 @@ export interface CelHandlers {
|
|
|
15
16
|
json: (value: unknown) => string;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** RE2 regex engine for the CEL `regex*` functions — `re2js`, a pure-JS port of
|
|
20
|
+
* Google's RE2. The dialect is RE2 (linear-time, no backtracking, ReDoS-safe) —
|
|
21
|
+
* never JS `RegExp` — so a manifest's regex behaves the same across runtimes.
|
|
22
|
+
* Flags map a trailing option string to RE2 flags; inline `(?s)` etc. work in
|
|
23
|
+
* the pattern too.
|
|
24
|
+
*
|
|
25
|
+
* Why pure-JS and not a native RE2:
|
|
26
|
+
* - The Rust `regex` crate via napi (N-API) is faster, but shipping it means a
|
|
27
|
+
* cross-platform prebuild matrix + per-triple npm packages — a whole release
|
|
28
|
+
* subsystem for one function family.
|
|
29
|
+
* - The `re2` npm package (Google's C++ RE2) is simpler to depend on, but it's a
|
|
30
|
+
* nan/V8 addon and **does not load under Bun** (incomplete V8 C++ ABI →
|
|
31
|
+
* `undefined symbol`). Telo's CLI and test suite run on Bun, so that's a
|
|
32
|
+
* non-starter.
|
|
33
|
+
* `re2js` is pure JS: zero native addons, runs identically on Node, Bun, and the
|
|
34
|
+
* browser (keeps this package — and the analyzer — browser-safe), and needs no
|
|
35
|
+
* prebuilds. The cost is throughput vs. native, which is irrelevant for the
|
|
36
|
+
* small strings CEL manifests run regex over. */
|
|
37
|
+
const RE2_FLAG: Record<string, number> = {
|
|
38
|
+
i: RE2JS.CASE_INSENSITIVE,
|
|
39
|
+
m: RE2JS.MULTILINE,
|
|
40
|
+
s: RE2JS.DOTALL,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const compileRe2 = (fn: string, pattern: string, flags?: string): RE2JS => {
|
|
44
|
+
let bits = 0;
|
|
45
|
+
for (const c of flags ?? "") {
|
|
46
|
+
if (c === "g") continue; // global is implicit in replaceAll / find-loop
|
|
47
|
+
const bit = RE2_FLAG[c];
|
|
48
|
+
if (bit === undefined) throw new Error(`${fn}: unknown regex flag '${c}' (supported: i, m, s)`);
|
|
49
|
+
bits |= bit;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return RE2JS.compile(pattern, bits);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${fn}: invalid RE2 pattern ${JSON.stringify(pattern)}: ${e instanceof Error ? e.message : String(e)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
18
60
|
export type CelFunctionCategory =
|
|
19
61
|
| "conversion"
|
|
20
62
|
| "time"
|
|
@@ -195,6 +237,26 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
|
|
|
195
237
|
hostBacked: false,
|
|
196
238
|
build: () => (list: unknown[]) => sortList(list),
|
|
197
239
|
},
|
|
240
|
+
{
|
|
241
|
+
name: "range",
|
|
242
|
+
signature: "range(int): list<int>",
|
|
243
|
+
category: "collection",
|
|
244
|
+
summary: "Integers [0, n-1] (empty for n <= 0); materializes indices for an unknown-length list.",
|
|
245
|
+
deterministic: true,
|
|
246
|
+
hostBacked: false,
|
|
247
|
+
build: () => (n: unknown) =>
|
|
248
|
+
Array.from({ length: Math.max(0, Math.trunc(num(n))) }, (_unused, i) => BigInt(i)),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "enumerate",
|
|
252
|
+
signature: "enumerate(list): list",
|
|
253
|
+
category: "collection",
|
|
254
|
+
summary: "Pair each element with its zero-based position as {index, value}.",
|
|
255
|
+
deterministic: true,
|
|
256
|
+
hostBacked: false,
|
|
257
|
+
build: () => (list: unknown[]) =>
|
|
258
|
+
list.map((value, i) => ({ index: BigInt(i), value })),
|
|
259
|
+
},
|
|
198
260
|
// Strings
|
|
199
261
|
{
|
|
200
262
|
name: "lower",
|
|
@@ -241,6 +303,76 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
|
|
|
241
303
|
hostBacked: false,
|
|
242
304
|
build: () => (s: string, sep: string) => s.split(sep),
|
|
243
305
|
},
|
|
306
|
+
{
|
|
307
|
+
name: "regexReplace",
|
|
308
|
+
signature: "regexReplace(string, string, string, string?): string",
|
|
309
|
+
category: "string",
|
|
310
|
+
summary:
|
|
311
|
+
"Replace every regex match (RE2 syntax) with a replacement ($1 backrefs); flags like 'i', 'm', 's'.",
|
|
312
|
+
deterministic: true,
|
|
313
|
+
hostBacked: false,
|
|
314
|
+
build: () => (s: string, pattern: string, replacement: string, flags?: string) =>
|
|
315
|
+
compileRe2("regexReplace", pattern, flags).matcher(s).replaceAll(replacement),
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "regexExtract",
|
|
319
|
+
signature: "regexExtract(string, string, string?): string",
|
|
320
|
+
category: "string",
|
|
321
|
+
summary: "First whole match of a regex (RE2 syntax), or '' when there is none.",
|
|
322
|
+
deterministic: true,
|
|
323
|
+
hostBacked: false,
|
|
324
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
325
|
+
const m = compileRe2("regexExtract", pattern, flags).matcher(s);
|
|
326
|
+
return m.find() ? (m.group() ?? "") : "";
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "regexExtractAll",
|
|
331
|
+
signature: "regexExtractAll(string, string, string?): list<string>",
|
|
332
|
+
category: "string",
|
|
333
|
+
summary: "Every whole match of a regex (RE2 syntax), in order.",
|
|
334
|
+
deterministic: true,
|
|
335
|
+
hostBacked: false,
|
|
336
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
337
|
+
const m = compileRe2("regexExtractAll", pattern, flags).matcher(s);
|
|
338
|
+
const out: string[] = [];
|
|
339
|
+
while (m.find()) out.push(m.group() ?? "");
|
|
340
|
+
return out;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: "regexGroups",
|
|
345
|
+
signature: "regexGroups(string, string, string?): list<string>",
|
|
346
|
+
category: "string",
|
|
347
|
+
summary:
|
|
348
|
+
"Capture groups of the first regex match (RE2 syntax); empty list when there is no match.",
|
|
349
|
+
deterministic: true,
|
|
350
|
+
hostBacked: false,
|
|
351
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
352
|
+
const m = compileRe2("regexGroups", pattern, flags).matcher(s);
|
|
353
|
+
if (!m.find()) return [];
|
|
354
|
+
return Array.from({ length: m.groupCount() }, (_unused, i) => m.group(i + 1) ?? "");
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "trimPrefix",
|
|
359
|
+
signature: "trimPrefix(string, string): string",
|
|
360
|
+
category: "string",
|
|
361
|
+
summary: "Strip a leading prefix if present.",
|
|
362
|
+
deterministic: true,
|
|
363
|
+
hostBacked: false,
|
|
364
|
+
build: () => (s: string, prefix: string) => (s.startsWith(prefix) ? s.slice(prefix.length) : s),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "trimSuffix",
|
|
368
|
+
signature: "trimSuffix(string, string): string",
|
|
369
|
+
category: "string",
|
|
370
|
+
summary: "Strip a trailing suffix if present.",
|
|
371
|
+
deterministic: true,
|
|
372
|
+
hostBacked: false,
|
|
373
|
+
build: () => (s: string, suffix: string) =>
|
|
374
|
+
suffix && s.endsWith(suffix) ? s.slice(0, s.length - suffix.length) : s,
|
|
375
|
+
},
|
|
244
376
|
// Math
|
|
245
377
|
{
|
|
246
378
|
name: "abs",
|