@telorun/cel 0.0.0-stage → 0.107.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/LICENSE +17 -0
- package/README.md +281 -2
- package/dist/activation.d.ts +27 -0
- package/dist/activation.d.ts.map +1 -0
- package/dist/activation.js +24 -0
- package/dist/backend-runtime.d.ts +146 -0
- package/dist/backend-runtime.d.ts.map +1 -0
- package/dist/backend-runtime.js +322 -0
- package/dist/bounded-cache.d.ts +21 -0
- package/dist/bounded-cache.d.ts.map +1 -0
- package/dist/bounded-cache.js +42 -0
- package/dist/catalog-runtime.d.ts +59 -0
- package/dist/catalog-runtime.d.ts.map +1 -0
- package/dist/catalog-runtime.js +785 -0
- package/dist/cel-expression.d.ts +32 -0
- package/dist/cel-expression.d.ts.map +1 -0
- package/dist/cel-expression.js +29 -0
- package/dist/cel-map-value.d.ts +34 -0
- package/dist/cel-map-value.d.ts.map +1 -0
- package/dist/cel-map-value.js +74 -0
- package/dist/cel-program.d.ts +44 -0
- package/dist/cel-program.d.ts.map +1 -0
- package/dist/cel-program.js +72 -0
- package/dist/cel-type.d.ts +131 -0
- package/dist/cel-type.d.ts.map +1 -0
- package/dist/cel-type.js +293 -0
- package/dist/cel-value.d.ts +159 -0
- package/dist/cel-value.d.ts.map +1 -0
- package/dist/cel-value.js +225 -0
- package/dist/check-diagnostic.d.ts +53 -0
- package/dist/check-diagnostic.d.ts.map +1 -0
- package/dist/check-diagnostic.js +66 -0
- package/dist/checker.d.ts +77 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +721 -0
- package/dist/closure-backend.d.ts +21 -0
- package/dist/closure-backend.d.ts.map +1 -0
- package/dist/closure-backend.js +436 -0
- package/dist/comprehension-bindings.d.ts +33 -0
- package/dist/comprehension-bindings.d.ts.map +1 -0
- package/dist/comprehension-bindings.js +50 -0
- package/dist/comprehension-runtime.d.ts +44 -0
- package/dist/comprehension-runtime.d.ts.map +1 -0
- package/dist/comprehension-runtime.js +137 -0
- package/dist/declared-chain.d.ts +35 -0
- package/dist/declared-chain.d.ts.map +1 -0
- package/dist/declared-chain.js +36 -0
- package/dist/duration-value.d.ts +59 -0
- package/dist/duration-value.d.ts.map +1 -0
- package/dist/duration-value.js +135 -0
- package/dist/emitted-module.d.ts +207 -0
- package/dist/emitted-module.d.ts.map +1 -0
- package/dist/emitted-module.js +359 -0
- package/dist/engine-version.d.ts +3 -0
- package/dist/engine-version.d.ts.map +1 -0
- package/dist/engine-version.js +8 -0
- package/dist/environment-digest.d.ts +44 -0
- package/dist/environment-digest.d.ts.map +1 -0
- package/dist/environment-digest.js +98 -0
- package/dist/environment.d.ts +283 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +459 -0
- package/dist/function-catalog.d.ts +66 -0
- package/dist/function-catalog.d.ts.map +1 -0
- package/dist/function-catalog.js +77 -0
- package/dist/function-registry.d.ts +78 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +189 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/integer-arithmetic.d.ts +27 -0
- package/dist/integer-arithmetic.d.ts.map +1 -0
- package/dist/integer-arithmetic.js +58 -0
- package/dist/js-emitter.d.ts +132 -0
- package/dist/js-emitter.d.ts.map +1 -0
- package/dist/js-emitter.js +562 -0
- package/dist/json-schema-type.d.ts +182 -0
- package/dist/json-schema-type.d.ts.map +1 -0
- package/dist/json-schema-type.js +487 -0
- package/dist/json-text-scan.d.ts +28 -0
- package/dist/json-text-scan.d.ts.map +1 -0
- package/dist/json-text-scan.js +159 -0
- package/dist/lexer.d.ts +103 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +458 -0
- package/dist/macro-check.d.ts +33 -0
- package/dist/macro-check.d.ts.map +1 -0
- package/dist/macro-check.js +162 -0
- package/dist/macro-shape.d.ts +24 -0
- package/dist/macro-shape.d.ts.map +1 -0
- package/dist/macro-shape.js +55 -0
- package/dist/member-read.d.ts +52 -0
- package/dist/member-read.d.ts.map +1 -0
- package/dist/member-read.js +125 -0
- package/dist/namespace-resolution.d.ts +35 -0
- package/dist/namespace-resolution.d.ts.map +1 -0
- package/dist/namespace-resolution.js +160 -0
- package/dist/nominal-type.d.ts +63 -0
- package/dist/nominal-type.d.ts.map +1 -0
- package/dist/nominal-type.js +98 -0
- package/dist/nullable-access.d.ts +38 -0
- package/dist/nullable-access.d.ts.map +1 -0
- package/dist/nullable-access.js +93 -0
- package/dist/parse-limits.d.ts +26 -0
- package/dist/parse-limits.d.ts.map +1 -0
- package/dist/parse-limits.js +21 -0
- package/dist/parser.d.ts +48 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +503 -0
- package/dist/qualified-calls.d.ts +22 -0
- package/dist/qualified-calls.d.ts.map +1 -0
- package/dist/qualified-calls.js +27 -0
- package/dist/regular-expression.d.ts +48 -0
- package/dist/regular-expression.d.ts.map +1 -0
- package/dist/regular-expression.js +77 -0
- package/dist/reserved-words.d.ts +52 -0
- package/dist/reserved-words.d.ts.map +1 -0
- package/dist/reserved-words.js +77 -0
- package/dist/resolved-call.d.ts +33 -0
- package/dist/resolved-call.d.ts.map +1 -0
- package/dist/resolved-call.js +15 -0
- package/dist/root-references.d.ts +23 -0
- package/dist/root-references.d.ts.map +1 -0
- package/dist/root-references.js +120 -0
- package/dist/runtime-library.d.ts +56 -0
- package/dist/runtime-library.d.ts.map +1 -0
- package/dist/runtime-library.js +545 -0
- package/dist/serializer.d.ts +24 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +240 -0
- package/dist/sha256.d.ts +20 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +103 -0
- package/dist/signature.d.ts +72 -0
- package/dist/signature.d.ts.map +1 -0
- package/dist/signature.js +61 -0
- package/dist/signatures/function-catalog.json +788 -0
- package/dist/signatures/standard-library.json +229 -0
- package/dist/standard-library.d.ts +41 -0
- package/dist/standard-library.d.ts.map +1 -0
- package/dist/standard-library.js +85 -0
- package/dist/syntax-diagnostic.d.ts +61 -0
- package/dist/syntax-diagnostic.d.ts.map +1 -0
- package/dist/syntax-diagnostic.js +28 -0
- package/dist/syntax-tree.d.ts +160 -0
- package/dist/syntax-tree.d.ts.map +1 -0
- package/dist/syntax-tree.js +58 -0
- package/dist/timestamp-value.d.ts +53 -0
- package/dist/timestamp-value.d.ts.map +1 -0
- package/dist/timestamp-value.js +223 -0
- package/dist/tree-equality.d.ts +15 -0
- package/dist/tree-equality.d.ts.map +1 -0
- package/dist/tree-equality.js +105 -0
- package/dist/type-expression.d.ts +26 -0
- package/dist/type-expression.d.ts.map +1 -0
- package/dist/type-expression.js +134 -0
- package/dist/value-equality.d.ts +38 -0
- package/dist/value-equality.d.ts.map +1 -0
- package/dist/value-equality.js +196 -0
- package/dist/value-text.d.ts +25 -0
- package/dist/value-text.d.ts.map +1 -0
- package/dist/value-text.js +44 -0
- package/dist/zoned-calendar.d.ts +61 -0
- package/dist/zoned-calendar.d.ts.map +1 -0
- package/dist/zoned-calendar.js +143 -0
- package/package.json +56 -3
- package/src/activation.ts +32 -0
- package/src/backend-runtime.ts +454 -0
- package/src/bounded-cache.ts +45 -0
- package/src/catalog-runtime.ts +921 -0
- package/src/cel-expression.ts +53 -0
- package/src/cel-map-value.ts +86 -0
- package/src/cel-program.ts +103 -0
- package/src/cel-type.ts +359 -0
- package/src/cel-value.ts +353 -0
- package/src/check-diagnostic.ts +102 -0
- package/src/checker.ts +932 -0
- package/src/closure-backend.ts +502 -0
- package/src/comprehension-bindings.ts +66 -0
- package/src/comprehension-runtime.ts +157 -0
- package/src/declared-chain.ts +45 -0
- package/src/duration-value.ts +145 -0
- package/src/emitted-module.ts +494 -0
- package/src/engine-version.ts +9 -0
- package/src/environment-digest.ts +111 -0
- package/src/environment.ts +740 -0
- package/src/function-catalog.ts +140 -0
- package/src/function-registry.ts +229 -0
- package/src/index.ts +386 -0
- package/src/integer-arithmetic.ts +64 -0
- package/src/js-emitter.ts +713 -0
- package/src/json-schema-type.ts +664 -0
- package/src/json-text-scan.ts +163 -0
- package/src/lexer.ts +562 -0
- package/src/macro-check.ts +191 -0
- package/src/macro-shape.ts +66 -0
- package/src/member-read.ts +126 -0
- package/src/namespace-resolution.ts +167 -0
- package/src/nominal-type.ts +149 -0
- package/src/nullable-access.ts +95 -0
- package/src/parse-limits.ts +36 -0
- package/src/parser.ts +554 -0
- package/src/qualified-calls.ts +39 -0
- package/src/regular-expression.ts +101 -0
- package/src/reserved-words.ts +94 -0
- package/src/resolved-call.ts +34 -0
- package/src/root-references.ts +126 -0
- package/src/runtime-library.ts +615 -0
- package/src/serializer.ts +246 -0
- package/src/sha256.ts +112 -0
- package/src/signature.ts +127 -0
- package/src/signatures/function-catalog.json +788 -0
- package/src/signatures/standard-library.json +235 -0
- package/src/standard-library.ts +149 -0
- package/src/syntax-diagnostic.ts +72 -0
- package/src/syntax-tree.ts +229 -0
- package/src/timestamp-value.ts +294 -0
- package/src/tree-equality.ts +130 -0
- package/src/type-expression.ts +160 -0
- package/src/value-equality.ts +201 -0
- package/src/value-text.ts +45 -0
- package/src/zoned-calendar.ts +178 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every function of the catalog **does** — once, for both backends.
|
|
3
|
+
*
|
|
4
|
+
* The declarations are data (`signatures/function-catalog.json`); this is the behaviour
|
|
5
|
+
* behind them, keyed by the same dispatch key the registry resolves on, exactly as
|
|
6
|
+
* `runtime-library.ts` is for CEL's own library. Both backends reach it through the
|
|
7
|
+
* registration's `implementation`, so there is one answer per operation and no second
|
|
8
|
+
* copy to drift.
|
|
9
|
+
*
|
|
10
|
+
* **Nine functions are the HOST's**, not this engine's: the four hashes, `hmac`, the two
|
|
11
|
+
* base64 string conversions, `json` and `joinPath`. Each needs a platform facility this
|
|
12
|
+
* package may not reach — a hash, a byte buffer, the host's own path separator — so the
|
|
13
|
+
* host supplies it through `CelCatalogHandlers` and a registration made without one
|
|
14
|
+
* answers an `unbound_function` error **naming the function**. Never a null, and never a
|
|
15
|
+
* silent empty string: a hash that answers nothing looks like a value and ends up in a
|
|
16
|
+
* cache key.
|
|
17
|
+
*
|
|
18
|
+
* **A refusal is worded by the catalog, in one voice** — `<function>: <what is wrong>` —
|
|
19
|
+
* and never by a library or by the host language's runtime. It is thrown as
|
|
20
|
+
* `CatalogRefusal` by the guard that meets it and turned into a CEL **error value** at
|
|
21
|
+
* exactly one place (`refusing`), so the short-circuit rules still hold and the static
|
|
22
|
+
* guard below reports the same sentence the evaluation would. Anything else thrown while
|
|
23
|
+
* an implementation runs is this engine failing, not the argument, and propagates.
|
|
24
|
+
*/
|
|
25
|
+
import { BoundedCache } from "./bounded-cache.js";
|
|
26
|
+
import { celMapOf, mapKeyIdentity } from "./cel-map-value.js";
|
|
27
|
+
import { base64Text, jsonAsCelValue } from "./runtime-library.js";
|
|
28
|
+
import { celError, celTypeNameOf, isCelBytes, isCelDuration, isCelMap, isCelRecord, isCelTimestamp, isCelUint, } from "./cel-value.js";
|
|
29
|
+
import { formatDuration } from "./duration-value.js";
|
|
30
|
+
import { scanJsonPrefix } from "./json-text-scan.js";
|
|
31
|
+
import { re2Pattern, RE2_FLAG_LETTERS } from "./regular-expression.js";
|
|
32
|
+
import { celTimestamp, formatTimestamp } from "./timestamp-value.js";
|
|
33
|
+
import { celEqual } from "./value-equality.js";
|
|
34
|
+
import { bytesToText, doubleText } from "./value-text.js";
|
|
35
|
+
import { civilTimeIn, dateTextIn, daysInMonth, instantOfCivilTime, isoTextIn, knownTimeZone, } from "./zoned-calendar.js";
|
|
36
|
+
import { formatLocale } from "d3-format";
|
|
37
|
+
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
38
|
+
/**
|
|
39
|
+
* A refusal a catalog function makes of its arguments. It carries the evaluation code the
|
|
40
|
+
* refusal answers with, so a pattern RE2 cannot parse is not reported as the same kind of
|
|
41
|
+
* mistake as a decimal count out of range.
|
|
42
|
+
*/
|
|
43
|
+
class CatalogRefusal extends Error {
|
|
44
|
+
code;
|
|
45
|
+
constructor(message, code = "invalid_argument") {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "CatalogRefusal";
|
|
48
|
+
this.code = code;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The one place a refusal becomes a value. Anything else thrown is a defect here. */
|
|
52
|
+
function refusing(run) {
|
|
53
|
+
return (args, ctx) => {
|
|
54
|
+
try {
|
|
55
|
+
return run(args, ctx);
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
if (cause instanceof CatalogRefusal)
|
|
59
|
+
return celError(cause.code, cause.message, ctx.range);
|
|
60
|
+
throw cause;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** An implementation that needs nothing of the host. */
|
|
65
|
+
const own = (run) => () => refusing(run);
|
|
66
|
+
/**
|
|
67
|
+
* An implementation the host supplies. With no handler the call answers an
|
|
68
|
+
* `unbound_function` error naming the function — the registration is still made, so the
|
|
69
|
+
* expression still type-checks, which is what an analyzer that never evaluates needs.
|
|
70
|
+
*/
|
|
71
|
+
const hosted = (name, build) => (handlers) => {
|
|
72
|
+
const handler = handlers[name];
|
|
73
|
+
if (handler === undefined) {
|
|
74
|
+
return (_args, ctx) => celError("unbound_function", `${name}() is supplied by the host, and this environment was given no implementation for it`, ctx.range);
|
|
75
|
+
}
|
|
76
|
+
return refusing(build(handler));
|
|
77
|
+
};
|
|
78
|
+
// --- reading arguments ------------------------------------------------------
|
|
79
|
+
const text = (value) => value;
|
|
80
|
+
const bytes = (value) => value;
|
|
81
|
+
/**
|
|
82
|
+
* A list argument. Every list-taking entry reads one through here, because a `dyn` slot can
|
|
83
|
+
* carry anything: the engine being replaced answered `0` for `sum` of a non-list and failed
|
|
84
|
+
* with its host language's own `TypeError` for the rest, and neither is an answer about the
|
|
85
|
+
* argument. Named in the catalog's voice instead, as an instant and a map already are.
|
|
86
|
+
*/
|
|
87
|
+
function list(what, value) {
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return value;
|
|
90
|
+
throw new CatalogRefusal(`${what}: expected a list, got ${celTypeNameOf(value) ?? "no CEL value"}`);
|
|
91
|
+
}
|
|
92
|
+
/** A value as a number, the way every arithmetic and formatting entry reads one. */
|
|
93
|
+
function numberOf(value) {
|
|
94
|
+
if (typeof value === "number")
|
|
95
|
+
return value;
|
|
96
|
+
if (typeof value === "bigint")
|
|
97
|
+
return Number(value);
|
|
98
|
+
if (isCelUint(value))
|
|
99
|
+
return Number(value.value);
|
|
100
|
+
if (typeof value === "boolean")
|
|
101
|
+
return value ? 1 : 0;
|
|
102
|
+
if (value === null)
|
|
103
|
+
return 0;
|
|
104
|
+
if (typeof value === "string")
|
|
105
|
+
return value.trim() === "" ? 0 : Number(value);
|
|
106
|
+
return Number.NaN;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A value as text, for `join` and for ordering a list of non-numbers. Every type whose
|
|
110
|
+
* single written form the engine defines is written that way — the same text `string()`
|
|
111
|
+
* answers. A container has no such form, and is **named rather than coerced**: the
|
|
112
|
+
* engine being replaced wrote a map as `[object Object]`, which is a value no author
|
|
113
|
+
* ever wrote and which prints into a document.
|
|
114
|
+
*/
|
|
115
|
+
function textOf(what, value) {
|
|
116
|
+
if (typeof value === "string")
|
|
117
|
+
return value;
|
|
118
|
+
if (typeof value === "bigint")
|
|
119
|
+
return String(value);
|
|
120
|
+
if (isCelUint(value))
|
|
121
|
+
return String(value.value);
|
|
122
|
+
if (typeof value === "number")
|
|
123
|
+
return doubleText(value);
|
|
124
|
+
if (typeof value === "boolean")
|
|
125
|
+
return String(value);
|
|
126
|
+
if (value === null)
|
|
127
|
+
return "null";
|
|
128
|
+
if (isCelTimestamp(value))
|
|
129
|
+
return formatTimestamp(value);
|
|
130
|
+
if (isCelDuration(value))
|
|
131
|
+
return formatDuration(value);
|
|
132
|
+
if (isCelBytes(value)) {
|
|
133
|
+
const decoded = bytesToText(value);
|
|
134
|
+
if (typeof decoded === "string")
|
|
135
|
+
return decoded;
|
|
136
|
+
throw new CatalogRefusal(`${what}: the bytes are not UTF-8 text`, "invalid_conversion");
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value))
|
|
139
|
+
return value.map((held) => textOf(what, held)).join(",");
|
|
140
|
+
throw new CatalogRefusal(`${what}: a value of type ${celTypeNameOf(value) ?? "none"} has no text`, "invalid_conversion");
|
|
141
|
+
}
|
|
142
|
+
/** An instant argument. A `dyn` slot can carry anything, so it is named, never coerced. */
|
|
143
|
+
function instantArg(what, value) {
|
|
144
|
+
if (isCelTimestamp(value))
|
|
145
|
+
return value;
|
|
146
|
+
throw new CatalogRefusal(`${what}: expected a timestamp`);
|
|
147
|
+
}
|
|
148
|
+
/** An instant as host milliseconds, which is the granularity the calendar works at. */
|
|
149
|
+
function millisOf(value) {
|
|
150
|
+
return Number(value.seconds) * 1000 + Math.floor(value.nanos / 1_000_000);
|
|
151
|
+
}
|
|
152
|
+
/** An instant from host milliseconds; out of CEL's range it is the domain's own error. */
|
|
153
|
+
function instantOfMillis(what, millis) {
|
|
154
|
+
const whole = Math.floor(millis / 1000);
|
|
155
|
+
const rest = millis - whole * 1000;
|
|
156
|
+
const built = celTimestamp(BigInt(whole), BigInt(rest) * 1000000n);
|
|
157
|
+
if (isCelTimestamp(built))
|
|
158
|
+
return built;
|
|
159
|
+
throw new CatalogRefusal(`${what}: the result is outside the range of a timestamp`, "invalid_conversion");
|
|
160
|
+
}
|
|
161
|
+
/** A map's entries, whichever container holds them. A list is named rather than joined. */
|
|
162
|
+
function mapEntries(what, value) {
|
|
163
|
+
if (isCelMap(value))
|
|
164
|
+
return [...value.entries.values()].map((entry) => [entry.key, entry.value]);
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
throw new CatalogRefusal(`${what}: expected a map, got a list — use '+' to join lists`);
|
|
167
|
+
}
|
|
168
|
+
if (isCelRecord(value)) {
|
|
169
|
+
return Object.keys(value).map((key) => [key, value[key]]);
|
|
170
|
+
}
|
|
171
|
+
throw new CatalogRefusal(`${what}: expected a map, got ${celTypeNameOf(value) ?? "no CEL value"}`);
|
|
172
|
+
}
|
|
173
|
+
/** A map of the same container the entries came out of, so what comes back is what went in. */
|
|
174
|
+
function mapOfEntries(like, pairs) {
|
|
175
|
+
if (isCelMap(like)) {
|
|
176
|
+
const entries = new Map();
|
|
177
|
+
for (const [key, value] of pairs) {
|
|
178
|
+
const identity = mapKeyIdentity(key);
|
|
179
|
+
if (identity === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
entries.set(identity, { key, value });
|
|
182
|
+
}
|
|
183
|
+
return celMapOf(entries);
|
|
184
|
+
}
|
|
185
|
+
const out = Object.create(null);
|
|
186
|
+
for (const [key, value] of pairs)
|
|
187
|
+
out[key] = value;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
// --- formatting -------------------------------------------------------------
|
|
191
|
+
/**
|
|
192
|
+
* The number-formatting locale, pinned rather than defaulted.
|
|
193
|
+
*
|
|
194
|
+
* d3-format's default locale renders a negative with U+2212 MINUS SIGN, so
|
|
195
|
+
* `format(-1.5, '.2f')` would be `"-1.50"` with a character no downstream parser,
|
|
196
|
+
* comparison or diff treats as the number it looks like. Every field is fixed to its
|
|
197
|
+
* ASCII form for the same reason the layer is locale-free at all: the same manifest must
|
|
198
|
+
* render the same bytes on every runtime, and a second engine implementing the specifier
|
|
199
|
+
* grammar has to be able to reproduce these exactly.
|
|
200
|
+
*/
|
|
201
|
+
const FORMAT_LOCALE = formatLocale({
|
|
202
|
+
decimal: ".",
|
|
203
|
+
thousands: ",",
|
|
204
|
+
grouping: [3],
|
|
205
|
+
currency: ["$", ""],
|
|
206
|
+
minus: "-",
|
|
207
|
+
percent: "%",
|
|
208
|
+
nan: "NaN",
|
|
209
|
+
});
|
|
210
|
+
/** Largest integer a double represents exactly. */
|
|
211
|
+
const MAX_EXACT_INT = 9007199254740991n;
|
|
212
|
+
/**
|
|
213
|
+
* A number d3 can format. A CEL int is a `bigint` and d3 throws on one outright, so it is
|
|
214
|
+
* converted here — and past 2^53 a double stops representing every integer, so emitting a
|
|
215
|
+
* number that is not the one the author computed is refused rather than printed.
|
|
216
|
+
*/
|
|
217
|
+
function formattable(what, value) {
|
|
218
|
+
if (typeof value === "bigint" || isCelUint(value)) {
|
|
219
|
+
const whole = typeof value === "bigint" ? value : value.value;
|
|
220
|
+
if (whole > MAX_EXACT_INT || whole < -MAX_EXACT_INT) {
|
|
221
|
+
throw new CatalogRefusal(`${what}: integer ${whole} exceeds 2^53-1 and cannot be formatted exactly as a double`);
|
|
222
|
+
}
|
|
223
|
+
return Number(whole);
|
|
224
|
+
}
|
|
225
|
+
const number = numberOf(value);
|
|
226
|
+
// The runtime backstop behind the typed registrations. A value that is not a number
|
|
227
|
+
// formats as the string "NaN", which is the failure this family exists to remove: it
|
|
228
|
+
// looks like an answer and prints into a document.
|
|
229
|
+
if (!Number.isFinite(number)) {
|
|
230
|
+
throw new CatalogRefusal(`${what}: expected a finite number, got ${celTypeNameOf(value) ?? "no CEL value"}`);
|
|
231
|
+
}
|
|
232
|
+
return number;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Specifier type characters d3 implements. An unknown one PARSES — `.2q` yields `"1"`
|
|
236
|
+
* rather than throwing — so a typo would silently format against the default type.
|
|
237
|
+
*/
|
|
238
|
+
const FORMAT_TYPES = new Set([..."efgrs%pbodxXcn"]);
|
|
239
|
+
/**
|
|
240
|
+
* Decimal places, bounded. The ceiling is well below what `toFixed` accepts because past
|
|
241
|
+
* it the digits are an artefact of the binary representation rather than of the value.
|
|
242
|
+
*
|
|
243
|
+
* ONE rule, enforced wherever a precision is written: `formatter` applies it to a
|
|
244
|
+
* specifier's `.precision` group too. Bounding only `fixed` let an author route around
|
|
245
|
+
* the guard by writing `format(x, '.11f')` — the family giving two answers to one
|
|
246
|
+
* question. Every specifier type and flag stays available; only the digit count is capped.
|
|
247
|
+
*/
|
|
248
|
+
const MAX_DECIMALS = 10;
|
|
249
|
+
function digitCount(what, digits) {
|
|
250
|
+
const count = numberOf(digits);
|
|
251
|
+
if (!Number.isInteger(count) || count < 0 || count > MAX_DECIMALS) {
|
|
252
|
+
throw new CatalogRefusal(`${what}: decimal places must be an integer 0-${MAX_DECIMALS}, got ${textOf(what, digits)}`);
|
|
253
|
+
}
|
|
254
|
+
return count;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A specifier is a CEL value, so it can be request-derived — a server evaluating
|
|
258
|
+
* `format(x, request.query.spec)` would otherwise grow this map for the life of the
|
|
259
|
+
* process. Bounded, with its capacity declared, like every cache in this package.
|
|
260
|
+
*/
|
|
261
|
+
const FORMATTER_CACHE_CAPACITY = 256;
|
|
262
|
+
const formatters = new BoundedCache(FORMATTER_CACHE_CAPACITY);
|
|
263
|
+
function formatter(what, specifier) {
|
|
264
|
+
const written = textOf(what, specifier);
|
|
265
|
+
const held = formatters.get(written);
|
|
266
|
+
if (held)
|
|
267
|
+
return held;
|
|
268
|
+
const type = written.slice(-1);
|
|
269
|
+
if (written !== "" && /[a-zA-Z%]/.test(type) && !FORMAT_TYPES.has(type)) {
|
|
270
|
+
throw new CatalogRefusal(`${what}: unknown format type '${type}' (one of ${[...FORMAT_TYPES].join("")})`);
|
|
271
|
+
}
|
|
272
|
+
// The `.precision` group — width is the digits BEFORE the dot, so this is the only
|
|
273
|
+
// `.`-digits sequence the grammar admits.
|
|
274
|
+
const precision = /\.(\d+)/.exec(written);
|
|
275
|
+
if (precision)
|
|
276
|
+
digitCount(what, precision[1]);
|
|
277
|
+
let built;
|
|
278
|
+
try {
|
|
279
|
+
built = FORMAT_LOCALE.format(written);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new CatalogRefusal(`${what}: invalid format specifier ${JSON.stringify(written)}`);
|
|
283
|
+
}
|
|
284
|
+
formatters.set(written, built);
|
|
285
|
+
return built;
|
|
286
|
+
}
|
|
287
|
+
/** Render a minute count against a declared day length — a policy, never an assumption. */
|
|
288
|
+
function durationText(minutes, minutesPerDay) {
|
|
289
|
+
const perDay = Math.round(formattable("formatDuration", minutesPerDay));
|
|
290
|
+
if (!Number.isFinite(perDay) || perDay <= 0) {
|
|
291
|
+
throw new CatalogRefusal(`formatDuration: minutesPerDay must be a positive number, got ${perDay}`);
|
|
292
|
+
}
|
|
293
|
+
const total = Math.round(formattable("formatDuration", minutes));
|
|
294
|
+
const magnitude = Math.abs(total);
|
|
295
|
+
const days = Math.floor(magnitude / perDay);
|
|
296
|
+
const withinDay = magnitude % perDay;
|
|
297
|
+
const hours = Math.floor(withinDay / 60);
|
|
298
|
+
const minutesLeft = withinDay % 60;
|
|
299
|
+
const parts = [];
|
|
300
|
+
if (days)
|
|
301
|
+
parts.push(`${days}d`);
|
|
302
|
+
if (hours)
|
|
303
|
+
parts.push(`${hours}h`);
|
|
304
|
+
if (minutesLeft)
|
|
305
|
+
parts.push(`${minutesLeft}m`);
|
|
306
|
+
if (parts.length === 0)
|
|
307
|
+
parts.push("0m");
|
|
308
|
+
return `${total < 0 ? "-" : ""}${parts.join(" ")}`;
|
|
309
|
+
}
|
|
310
|
+
// --- time zones -------------------------------------------------------------
|
|
311
|
+
/** Refuse an unknown zone in the catalog's own voice, never as a host `RangeError`. */
|
|
312
|
+
function zoneArg(what, value) {
|
|
313
|
+
const zone = value === undefined ? "UTC" : textOf(what, value);
|
|
314
|
+
if (!knownTimeZone(zone)) {
|
|
315
|
+
throw new CatalogRefusal(`${what}: unknown IANA time zone ${JSON.stringify(zone)}`);
|
|
316
|
+
}
|
|
317
|
+
return zone;
|
|
318
|
+
}
|
|
319
|
+
// --- regular expressions ----------------------------------------------------
|
|
320
|
+
/** The flag letters the catalog's regex family reads. `g` is accepted and means nothing. */
|
|
321
|
+
function re2Flags(what, flags) {
|
|
322
|
+
let bits = 0;
|
|
323
|
+
for (const letter of flags === undefined ? "" : textOf(what, flags)) {
|
|
324
|
+
if (letter === "g")
|
|
325
|
+
continue;
|
|
326
|
+
const bit = RE2_FLAG_LETTERS[letter];
|
|
327
|
+
if (bit === undefined) {
|
|
328
|
+
throw new CatalogRefusal(`${what}: unknown regex flag '${letter}' (supported: ${Object.keys(RE2_FLAG_LETTERS).join(", ")})`);
|
|
329
|
+
}
|
|
330
|
+
bits |= bit;
|
|
331
|
+
}
|
|
332
|
+
return bits;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* A compiled pattern, or the catalog's refusal naming RE2's own parse-error kind and
|
|
336
|
+
* nothing after it — the library's prefix and the fragment it quotes never reach an
|
|
337
|
+
* author. A parse failure outside that closed vocabulary is a defect here, raised as one
|
|
338
|
+
* with the library's message rather than printed as though it were a kind.
|
|
339
|
+
*/
|
|
340
|
+
function pattern(what, source, flags) {
|
|
341
|
+
const bits = re2Flags(what, flags);
|
|
342
|
+
const written = textOf(what, source);
|
|
343
|
+
return patternOrRefusal(what, written, bits);
|
|
344
|
+
}
|
|
345
|
+
function patternOrRefusal(what, written, bits) {
|
|
346
|
+
const compiled = re2Pattern(written, bits);
|
|
347
|
+
if (!("refused" in compiled))
|
|
348
|
+
return compiled.pattern;
|
|
349
|
+
const { kind, message } = compiled.refused;
|
|
350
|
+
if (kind === undefined) {
|
|
351
|
+
throw new Error(`${what}: the RE2 compiler failed on pattern ${JSON.stringify(written)} with an error outside the closed parse-error vocabulary: ${message}`);
|
|
352
|
+
}
|
|
353
|
+
throw new CatalogRefusal(`${what}: invalid RE2 pattern ${JSON.stringify(written)}: ${kind}`, "invalid_regular_expression");
|
|
354
|
+
}
|
|
355
|
+
// --- base64 ----------------------------------------------------------------
|
|
356
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
357
|
+
/**
|
|
358
|
+
* Base64 to bytes, written out rather than delegated: the host's `atob` round-trips
|
|
359
|
+
* through a string, which is the corruption this pair exists to avoid. The URL-safe
|
|
360
|
+
* alphabet and missing padding are both accepted, since both appear in real API payloads;
|
|
361
|
+
* a character outside the alphabet is refused rather than silently dropped.
|
|
362
|
+
*/
|
|
363
|
+
function bytesFromBase64(input) {
|
|
364
|
+
const clean = input
|
|
365
|
+
.replace(/[\r\n\t ]/g, "")
|
|
366
|
+
.replace(/-/g, "+")
|
|
367
|
+
.replace(/_/g, "/")
|
|
368
|
+
.replace(/=+$/, "");
|
|
369
|
+
// A remainder of 1 cannot come from any byte sequence: base64 writes 3 bytes as 4
|
|
370
|
+
// characters, so the valid remainders are 0, 2 and 3. Left to the loop it would leave
|
|
371
|
+
// six bits unwritten and answer a SHORT buffer — the silently dropped byte this
|
|
372
|
+
// function refuses a bad character for.
|
|
373
|
+
if (clean.length % 4 === 1) {
|
|
374
|
+
throw new CatalogRefusal("bytesFromBase64: input length is not valid base64");
|
|
375
|
+
}
|
|
376
|
+
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
|
|
377
|
+
let bits = 0;
|
|
378
|
+
let accumulated = 0;
|
|
379
|
+
let written = 0;
|
|
380
|
+
for (const character of clean) {
|
|
381
|
+
const value = BASE64_ALPHABET.indexOf(character);
|
|
382
|
+
if (value < 0) {
|
|
383
|
+
throw new CatalogRefusal(`bytesFromBase64: '${character}' is not a base64 character`);
|
|
384
|
+
}
|
|
385
|
+
accumulated = (accumulated << 6) | value;
|
|
386
|
+
bits += 6;
|
|
387
|
+
if (bits >= 8) {
|
|
388
|
+
bits -= 8;
|
|
389
|
+
out[written++] = (accumulated >> bits) & 0xff;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return out.subarray(0, written);
|
|
393
|
+
}
|
|
394
|
+
// --- JSON ------------------------------------------------------------------
|
|
395
|
+
/**
|
|
396
|
+
* The host parser's verdict stands; the scan beside it only words the refusal. A text the
|
|
397
|
+
* host refused and the scan reads as valid JSON is a defect here, raised as one with the
|
|
398
|
+
* host's error as its cause.
|
|
399
|
+
*/
|
|
400
|
+
function parseJsonText(written) {
|
|
401
|
+
try {
|
|
402
|
+
return jsonAsCelValue(JSON.parse(written));
|
|
403
|
+
}
|
|
404
|
+
catch (hostRefusal) {
|
|
405
|
+
const refusal = scanJsonPrefix(written);
|
|
406
|
+
if (refusal === undefined) {
|
|
407
|
+
throw new Error("parseJson: the host JSON parser refused a text the catalog's scan reads as valid JSON", {
|
|
408
|
+
cause: hostRefusal,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
throw new CatalogRefusal(`parseJson: invalid JSON at offset ${refusal.offset}${refusal.endOfInput ? " (unexpected end of input)" : ""}`, "invalid_conversion");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// --- collections ------------------------------------------------------------
|
|
415
|
+
function smallest(values, wantSmallest) {
|
|
416
|
+
if (values.length === 0)
|
|
417
|
+
return null;
|
|
418
|
+
let best = values[0];
|
|
419
|
+
let bestNumber = numberOf(best);
|
|
420
|
+
for (const held of values) {
|
|
421
|
+
const number = numberOf(held);
|
|
422
|
+
if (wantSmallest ? number < bestNumber : number > bestNumber) {
|
|
423
|
+
best = held;
|
|
424
|
+
bestNumber = number;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return best;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* A sorted copy: numerically where the first element is a number, else by text. One rule
|
|
431
|
+
* read off the first element, as the engine being replaced read it — a list CEL would
|
|
432
|
+
* refuse to order is not what this function is for.
|
|
433
|
+
*/
|
|
434
|
+
function sorted(what, values) {
|
|
435
|
+
const first = values[0];
|
|
436
|
+
const numeric = typeof first === "number" || typeof first === "bigint" || isCelUint(first);
|
|
437
|
+
return [...values].sort((left, right) => {
|
|
438
|
+
if (numeric) {
|
|
439
|
+
const difference = numberOf(left) - numberOf(right);
|
|
440
|
+
return difference < 0 ? -1 : difference > 0 ? 1 : 0;
|
|
441
|
+
}
|
|
442
|
+
const a = textOf(what, left);
|
|
443
|
+
const b = textOf(what, right);
|
|
444
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Duplicates removed by **CEL equality**, which is the only identity this value domain
|
|
449
|
+
* has: a uint, an instant and a map are objects here, so a host-level identity would call
|
|
450
|
+
* two equal values different and answer a list the expression never asked for.
|
|
451
|
+
*/
|
|
452
|
+
function distinct(values) {
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const held of values) {
|
|
455
|
+
if (!out.some((kept) => celEqual(kept, held)))
|
|
456
|
+
out.push(held);
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
/** Drop entries whose value is null or the empty string. Nothing else. */
|
|
461
|
+
function compacted(value) {
|
|
462
|
+
const keep = (held) => held !== null && held !== "";
|
|
463
|
+
if (Array.isArray(value))
|
|
464
|
+
return value.filter(keep);
|
|
465
|
+
return mapOfEntries(value, mapEntries("compact", value).filter(([, held]) => keep(held)));
|
|
466
|
+
}
|
|
467
|
+
// --- the table -------------------------------------------------------------
|
|
468
|
+
const IMPLEMENTATIONS = new Map([
|
|
469
|
+
// collections
|
|
470
|
+
["join(list<dyn>, string)", own((args) => list("join", args[0]).map((held) => textOf("join", held)).join(text(args[1])))],
|
|
471
|
+
["keys(map<dyn, dyn>)", own((args) => mapEntries("keys", args[0]).map(([key]) => key))],
|
|
472
|
+
["values(map<dyn, dyn>)", own((args) => mapEntries("values", args[0]).map(([, value]) => value))],
|
|
473
|
+
["distinct(list<dyn>)", own((args) => distinct(list("distinct", args[0])))],
|
|
474
|
+
["reverse(list<dyn>)", own((args) => [...list("reverse", args[0])].reverse())],
|
|
475
|
+
["flatten(list<dyn>)", own((args) => list("flatten", args[0]).flat())],
|
|
476
|
+
["sort(list<dyn>)", own((args) => sorted("sort", list("sort", args[0])))],
|
|
477
|
+
[
|
|
478
|
+
"range(int)",
|
|
479
|
+
own((args) => Array.from({ length: Math.max(0, Math.trunc(numberOf(args[0]))) }, (_unused, at) => BigInt(at))),
|
|
480
|
+
],
|
|
481
|
+
[
|
|
482
|
+
"enumerate(list<dyn>)",
|
|
483
|
+
own((args) => list("enumerate", args[0]).map((value, at) => {
|
|
484
|
+
const entry = Object.create(null);
|
|
485
|
+
entry.index = BigInt(at);
|
|
486
|
+
entry.value = value;
|
|
487
|
+
return entry;
|
|
488
|
+
})),
|
|
489
|
+
],
|
|
490
|
+
["compact(list<dyn>)", own((args) => compacted(args[0]))],
|
|
491
|
+
["compact(map<dyn, dyn>)", own((args) => compacted(args[0]))],
|
|
492
|
+
[
|
|
493
|
+
"merge(map<dyn, dyn>, map<dyn, dyn>)",
|
|
494
|
+
own((args) => mapOfEntries(args[0], [...mapEntries("merge", args[0]), ...mapEntries("merge", args[1])])),
|
|
495
|
+
],
|
|
496
|
+
// One body over the three: the overloads exist so that a statically wrong receiver is a
|
|
497
|
+
// type error, and at runtime a `dyn` receiver reaches whichever of them resolved first, so
|
|
498
|
+
// the VALUE decides what is sliced.
|
|
499
|
+
["slice(bytes, int, int)", own(sliced)],
|
|
500
|
+
["slice(string, int, int)", own(sliced)],
|
|
501
|
+
["slice(list<dyn>, int, int)", own(sliced)],
|
|
502
|
+
// strings
|
|
503
|
+
["lower(string)", own((args) => text(args[0]).toLowerCase())],
|
|
504
|
+
["upper(string)", own((args) => text(args[0]).toUpperCase())],
|
|
505
|
+
["trim(string)", own((args) => text(args[0]).trim())],
|
|
506
|
+
["replace(string, string, string)", own((args) => text(args[0]).split(text(args[1])).join(text(args[2])))],
|
|
507
|
+
["split(string, string)", own((args) => text(args[0]).split(text(args[1])))],
|
|
508
|
+
[
|
|
509
|
+
"trimPrefix(string, string)",
|
|
510
|
+
own((args) => {
|
|
511
|
+
const prefix = text(args[1]);
|
|
512
|
+
return text(args[0]).startsWith(prefix) ? text(args[0]).slice(prefix.length) : args[0];
|
|
513
|
+
}),
|
|
514
|
+
],
|
|
515
|
+
[
|
|
516
|
+
"trimSuffix(string, string)",
|
|
517
|
+
own((args) => {
|
|
518
|
+
const suffix = text(args[1]);
|
|
519
|
+
const value = text(args[0]);
|
|
520
|
+
return suffix !== "" && value.endsWith(suffix) ? value.slice(0, value.length - suffix.length) : value;
|
|
521
|
+
}),
|
|
522
|
+
],
|
|
523
|
+
[
|
|
524
|
+
"regexReplace(string, string, string)",
|
|
525
|
+
own((args) => pattern("regexReplace", args[1], undefined).matcher(text(args[0])).replaceAll(text(args[2]))),
|
|
526
|
+
],
|
|
527
|
+
[
|
|
528
|
+
"regexReplace(string, string, string, string)",
|
|
529
|
+
own((args) => pattern("regexReplace", args[1], args[3]).matcher(text(args[0])).replaceAll(text(args[2]))),
|
|
530
|
+
],
|
|
531
|
+
["regexExtract(string, string)", own((args) => firstMatch("regexExtract", args, undefined))],
|
|
532
|
+
["regexExtract(string, string, string)", own((args) => firstMatch("regexExtract", args, args[2]))],
|
|
533
|
+
["regexExtractAll(string, string)", own((args) => everyMatch("regexExtractAll", args, undefined))],
|
|
534
|
+
["regexExtractAll(string, string, string)", own((args) => everyMatch("regexExtractAll", args, args[2]))],
|
|
535
|
+
["regexGroups(string, string)", own((args) => matchGroups("regexGroups", args, undefined))],
|
|
536
|
+
["regexGroups(string, string, string)", own((args) => matchGroups("regexGroups", args, args[2]))],
|
|
537
|
+
// math
|
|
538
|
+
["abs(dyn)", own((args) => Math.abs(numberOf(args[0])))],
|
|
539
|
+
["floor(dyn)", own((args) => Math.floor(numberOf(args[0])))],
|
|
540
|
+
["ceil(dyn)", own((args) => Math.ceil(numberOf(args[0])))],
|
|
541
|
+
// Both arities go through `formattable`, so the 2^53 refusal does not depend on which
|
|
542
|
+
// one the author wrote: guarding only the two-argument form left `round(x)` silently
|
|
543
|
+
// answering with a neighbouring integer, reachable by writing one fewer argument.
|
|
544
|
+
["round(double)", own((args) => Math.round(formattable("round", args[0])))],
|
|
545
|
+
["round(int)", own((args) => Math.round(formattable("round", args[0])))],
|
|
546
|
+
["round(double, int)", own((args) => roundedTo(args))],
|
|
547
|
+
["round(int, int)", own((args) => roundedTo(args))],
|
|
548
|
+
["min(list<dyn>)", own((args) => smallest(list("min", args[0]), true))],
|
|
549
|
+
["max(list<dyn>)", own((args) => smallest(list("max", args[0]), false))],
|
|
550
|
+
[
|
|
551
|
+
"sum(list<dyn>)",
|
|
552
|
+
own((args) => list("sum", args[0]).reduce((total, held) => total + numberOf(held), 0)),
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
"avg(list<dyn>)",
|
|
556
|
+
own((args) => {
|
|
557
|
+
const values = list("avg", args[0]);
|
|
558
|
+
if (values.length === 0)
|
|
559
|
+
return null;
|
|
560
|
+
return values.reduce((total, held) => total + numberOf(held), 0) / values.length;
|
|
561
|
+
}),
|
|
562
|
+
],
|
|
563
|
+
// JSON
|
|
564
|
+
["json(dyn)", hosted("json", (handler) => (args) => handler(args[0]))],
|
|
565
|
+
["parseJson(string)", own((args) => parseJsonText(text(args[0])))],
|
|
566
|
+
// paths
|
|
567
|
+
[
|
|
568
|
+
"string.joinPath(string)",
|
|
569
|
+
hosted("joinPath", (handler) => (args) => handler(text(args[0]), text(args[1]))),
|
|
570
|
+
],
|
|
571
|
+
// encoding
|
|
572
|
+
["base64Encode(string)", hosted("base64Encode", (handler) => (args) => handler(text(args[0])))],
|
|
573
|
+
["base64Decode(string)", hosted("base64Decode", (handler) => (args) => handler(text(args[0])))],
|
|
574
|
+
["bytesFromBase64(string)", own((args) => bytesFromBase64(text(args[0])))],
|
|
575
|
+
["bytesToBase64(bytes)", own((args) => base64Text(bytes(args[0])))],
|
|
576
|
+
["urlEncode(string)", own((args) => encodeURIComponent(text(args[0])))],
|
|
577
|
+
["urlDecode(string)", own((args) => decodeURIComponent(text(args[0])))],
|
|
578
|
+
// hashing
|
|
579
|
+
["sha256(string)", hosted("sha256", (handler) => (args) => handler(text(args[0])))],
|
|
580
|
+
["md5(string)", hosted("md5", (handler) => (args) => handler(text(args[0])))],
|
|
581
|
+
["sha1(string)", hosted("sha1", (handler) => (args) => handler(text(args[0])))],
|
|
582
|
+
["sha512(string)", hosted("sha512", (handler) => (args) => handler(text(args[0])))],
|
|
583
|
+
[
|
|
584
|
+
"hmac(string, string, string)",
|
|
585
|
+
hosted("hmac", (handler) => (args) => handler(text(args[0]), text(args[1]), text(args[2]))),
|
|
586
|
+
],
|
|
587
|
+
// null handling
|
|
588
|
+
["default(dyn, dyn)", own((args) => (args[0] === null ? args[1] : args[0]))],
|
|
589
|
+
[
|
|
590
|
+
"coalesce(list<dyn>)",
|
|
591
|
+
own((args) => {
|
|
592
|
+
const found = list("coalesce", args[0]).find((held) => held !== null);
|
|
593
|
+
return found === undefined ? null : found;
|
|
594
|
+
}),
|
|
595
|
+
],
|
|
596
|
+
// time
|
|
597
|
+
["now()", own(() => instantOfMillis("now", Date.now()))],
|
|
598
|
+
["nowIso()", own(() => isoTextIn(Date.now(), "UTC"))],
|
|
599
|
+
["nowIso(string)", own((args) => isoTextIn(Date.now(), zoneArg("nowIso", args[0])))],
|
|
600
|
+
["today()", own(() => dateTextIn(Date.now(), "UTC"))],
|
|
601
|
+
["today(string)", own((args) => dateTextIn(Date.now(), zoneArg("today", args[0])))],
|
|
602
|
+
["nowMillis()", own(() => BigInt(Date.now()))],
|
|
603
|
+
["nowSeconds()", own(() => BigInt(Math.floor(Date.now() / 1000)))],
|
|
604
|
+
["dateIn(google.protobuf.Timestamp)", own((args) => dateIn(args, undefined))],
|
|
605
|
+
["dateIn(google.protobuf.Timestamp, string)", own((args) => dateIn(args, args[1]))],
|
|
606
|
+
["isoIn(google.protobuf.Timestamp)", own((args) => isoIn(args, undefined))],
|
|
607
|
+
["isoIn(google.protobuf.Timestamp, string)", own((args) => isoIn(args, args[1]))],
|
|
608
|
+
["startOfMonth(google.protobuf.Timestamp)", own((args) => startOfMonth(args, undefined))],
|
|
609
|
+
["startOfMonth(google.protobuf.Timestamp, string)", own((args) => startOfMonth(args, args[1]))],
|
|
610
|
+
["addMonths(google.protobuf.Timestamp, int)", own((args) => addMonths(args, undefined))],
|
|
611
|
+
["addMonths(google.protobuf.Timestamp, int, string)", own((args) => addMonths(args, args[2]))],
|
|
612
|
+
// formatting
|
|
613
|
+
["format(double, string)", own((args) => formatter("format", args[1])(formattable("format", args[0])))],
|
|
614
|
+
["format(int, string)", own((args) => formatter("format", args[1])(formattable("format", args[0])))],
|
|
615
|
+
["fixed(double, int)", own((args) => fixedText(args))],
|
|
616
|
+
["fixed(int, int)", own((args) => fixedText(args))],
|
|
617
|
+
["formatDuration(double, int)", own((args) => durationText(args[0], args[1]))],
|
|
618
|
+
["formatDuration(int, int)", own((args) => durationText(args[0], args[1]))],
|
|
619
|
+
// uuid
|
|
620
|
+
["uuidv1()", own(() => v1())],
|
|
621
|
+
["uuidv4()", own(() => v4())],
|
|
622
|
+
["uuidv6()", own(() => v6())],
|
|
623
|
+
["uuidv7()", own(() => v7())],
|
|
624
|
+
["uuidv3(string, string)", own((args) => v3(text(args[0]), text(args[1])))],
|
|
625
|
+
["uuidv5(string, string)", own((args) => v5(text(args[0]), text(args[1])))],
|
|
626
|
+
["uuidValidate(string)", own((args) => uuidValidate(text(args[0])))],
|
|
627
|
+
["uuidVersion(string)", own((args) => BigInt(uuidVersion(text(args[0]))))],
|
|
628
|
+
]);
|
|
629
|
+
/**
|
|
630
|
+
* The half-open range of a string, bytes or a list. Indices arrive as CEL ints (`bigint`),
|
|
631
|
+
* which neither `String.slice` nor `TypedArray.subarray` accepts. `subarray` rather than
|
|
632
|
+
* `slice` for bytes: a view costs no copy, and every consumer treats the result as read-only.
|
|
633
|
+
*/
|
|
634
|
+
function sliced(args) {
|
|
635
|
+
const from = Number(args[1]);
|
|
636
|
+
const to = Number(args[2]);
|
|
637
|
+
const value = args[0];
|
|
638
|
+
if (typeof value === "string")
|
|
639
|
+
return value.slice(from, to);
|
|
640
|
+
if (isCelBytes(value))
|
|
641
|
+
return value.subarray(from, to);
|
|
642
|
+
if (Array.isArray(value))
|
|
643
|
+
return value.slice(from, to);
|
|
644
|
+
throw new CatalogRefusal(`slice: expected a string, bytes or a list, got ${celTypeNameOf(value) ?? "no CEL value"}`);
|
|
645
|
+
}
|
|
646
|
+
function roundedTo(args) {
|
|
647
|
+
return Number(formattable("round", args[0]).toFixed(digitCount("round", args[1])));
|
|
648
|
+
}
|
|
649
|
+
function fixedText(args) {
|
|
650
|
+
return formatter("fixed", `.${digitCount("fixed", args[1])}f`)(formattable("fixed", args[0]));
|
|
651
|
+
}
|
|
652
|
+
function firstMatch(what, args, flags) {
|
|
653
|
+
const matcher = pattern(what, args[1], flags).matcher(text(args[0]));
|
|
654
|
+
return matcher.find() ? (matcher.group() ?? "") : "";
|
|
655
|
+
}
|
|
656
|
+
function everyMatch(what, args, flags) {
|
|
657
|
+
const matcher = pattern(what, args[1], flags).matcher(text(args[0]));
|
|
658
|
+
const out = [];
|
|
659
|
+
while (matcher.find())
|
|
660
|
+
out.push(matcher.group() ?? "");
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
function matchGroups(what, args, flags) {
|
|
664
|
+
const matcher = pattern(what, args[1], flags).matcher(text(args[0]));
|
|
665
|
+
if (!matcher.find())
|
|
666
|
+
return [];
|
|
667
|
+
return Array.from({ length: matcher.groupCount() }, (_unused, at) => matcher.group(at + 1) ?? "");
|
|
668
|
+
}
|
|
669
|
+
function dateIn(args, zone) {
|
|
670
|
+
return dateTextIn(millisOf(instantArg("dateIn", args[0])), zoneArg("dateIn", zone));
|
|
671
|
+
}
|
|
672
|
+
function isoIn(args, zone) {
|
|
673
|
+
return isoTextIn(millisOf(instantArg("isoIn", args[0])), zoneArg("isoIn", zone));
|
|
674
|
+
}
|
|
675
|
+
function startOfMonth(args, zone) {
|
|
676
|
+
const named = zoneArg("startOfMonth", zone);
|
|
677
|
+
const fields = civilTimeIn(millisOf(instantArg("startOfMonth", args[0])), named);
|
|
678
|
+
return instantOfMillis("startOfMonth", instantOfCivilTime({ ...fields, day: 1, hour: 0, minute: 0, second: 0 }, named));
|
|
679
|
+
}
|
|
680
|
+
function addMonths(args, zone) {
|
|
681
|
+
const named = zoneArg("addMonths", zone);
|
|
682
|
+
const fields = civilTimeIn(millisOf(instantArg("addMonths", args[0])), named);
|
|
683
|
+
const shifted = fields.year * 12 + (fields.month - 1) + Number(args[1]);
|
|
684
|
+
// `%` takes the dividend's sign in JS, so a negative total would yield month 0.
|
|
685
|
+
const year = Math.floor(shifted / 12);
|
|
686
|
+
const month = (((shifted % 12) + 12) % 12) + 1;
|
|
687
|
+
return instantOfMillis("addMonths", instantOfCivilTime({ ...fields, year, month, day: Math.min(fields.day, daysInMonth(year, month)) }, named));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The implementation of a catalog signature, given the host's handlers, or nothing where
|
|
691
|
+
* the catalog declares no such call. A declaration with no behaviour here is refused at
|
|
692
|
+
* registration rather than at evaluation.
|
|
693
|
+
*/
|
|
694
|
+
export function catalogImplementation(key, handlers) {
|
|
695
|
+
return IMPLEMENTATIONS.get(key)?.(handlers);
|
|
696
|
+
}
|
|
697
|
+
/** Every dispatch key this file answers for, for a completeness check. */
|
|
698
|
+
export function catalogImplementedKeys() {
|
|
699
|
+
return [...IMPLEMENTATIONS.keys()];
|
|
700
|
+
}
|
|
701
|
+
// --- the literal guards -----------------------------------------------------
|
|
702
|
+
/**
|
|
703
|
+
* The guards, over the arguments a call wrote as literals. The contract — which positions
|
|
704
|
+
* a guard is handed, and that it must skip an `undefined` — is `LiteralArgumentCheck`'s,
|
|
705
|
+
* in `signature.ts`, where the checker reads it. What is written here is only which
|
|
706
|
+
* function refuses what, and each guard runs the very code the evaluation runs.
|
|
707
|
+
*/
|
|
708
|
+
/** Runs a guard for its refusal alone. Anything else it throws is this engine failing. */
|
|
709
|
+
function refusalOf(run) {
|
|
710
|
+
try {
|
|
711
|
+
run();
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
catch (cause) {
|
|
715
|
+
if (cause instanceof CatalogRefusal)
|
|
716
|
+
return cause.message;
|
|
717
|
+
throw cause;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** A regex function's guard: the flags and the pattern, each judged only as a literal. */
|
|
721
|
+
const regexGuard = (what, patternAt, flagsAt) => (literals) => refusalOf(() => {
|
|
722
|
+
const flags = literals[flagsAt];
|
|
723
|
+
const bits = typeof flags === "string" ? re2Flags(what, flags) : 0;
|
|
724
|
+
const written = literals[patternAt];
|
|
725
|
+
if (typeof written === "string")
|
|
726
|
+
patternOrRefusal(what, written, bits);
|
|
727
|
+
});
|
|
728
|
+
const LITERAL_CHECKS = new Map([
|
|
729
|
+
["regexReplace", regexGuard("regexReplace", 1, 3)],
|
|
730
|
+
["regexExtract", regexGuard("regexExtract", 1, 2)],
|
|
731
|
+
["regexExtractAll", regexGuard("regexExtractAll", 1, 2)],
|
|
732
|
+
["regexGroups", regexGuard("regexGroups", 1, 2)],
|
|
733
|
+
[
|
|
734
|
+
"round",
|
|
735
|
+
(literals) => refusalOf(() => {
|
|
736
|
+
if (literals[1] !== undefined)
|
|
737
|
+
digitCount("round", literals[1]);
|
|
738
|
+
if (typeof literals[0] === "bigint")
|
|
739
|
+
formattable("round", literals[0]);
|
|
740
|
+
}),
|
|
741
|
+
],
|
|
742
|
+
[
|
|
743
|
+
"format",
|
|
744
|
+
(literals) => refusalOf(() => {
|
|
745
|
+
if (literals[1] !== undefined)
|
|
746
|
+
formatter("format", literals[1]);
|
|
747
|
+
if (typeof literals[0] === "bigint")
|
|
748
|
+
formattable("format", literals[0]);
|
|
749
|
+
}),
|
|
750
|
+
],
|
|
751
|
+
[
|
|
752
|
+
"fixed",
|
|
753
|
+
(literals) => refusalOf(() => {
|
|
754
|
+
if (literals[1] !== undefined)
|
|
755
|
+
digitCount("fixed", literals[1]);
|
|
756
|
+
if (typeof literals[0] === "bigint")
|
|
757
|
+
formattable("fixed", literals[0]);
|
|
758
|
+
}),
|
|
759
|
+
],
|
|
760
|
+
[
|
|
761
|
+
"formatDuration",
|
|
762
|
+
(literals) => refusalOf(() => {
|
|
763
|
+
if (literals[1] !== undefined)
|
|
764
|
+
durationText(literals[0] ?? 0n, literals[1]);
|
|
765
|
+
}),
|
|
766
|
+
],
|
|
767
|
+
["dateIn", zoneGuard("dateIn", 1)],
|
|
768
|
+
["isoIn", zoneGuard("isoIn", 1)],
|
|
769
|
+
["startOfMonth", zoneGuard("startOfMonth", 1)],
|
|
770
|
+
["addMonths", zoneGuard("addMonths", 2)],
|
|
771
|
+
]);
|
|
772
|
+
function zoneGuard(what, at) {
|
|
773
|
+
return (literals) => refusalOf(() => {
|
|
774
|
+
if (typeof literals[at] === "string")
|
|
775
|
+
zoneArg(what, literals[at]);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/** The literal guard of a catalog function, where it has one. */
|
|
779
|
+
export function catalogLiteralCheck(name) {
|
|
780
|
+
return LITERAL_CHECKS.get(name);
|
|
781
|
+
}
|
|
782
|
+
/** Every function name a literal guard answers for, for a completeness check. */
|
|
783
|
+
export function catalogGuardedNames() {
|
|
784
|
+
return [...LITERAL_CHECKS.keys()];
|
|
785
|
+
}
|