@laserdata/laser-sdk 0.4.0 → 0.5.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 +49 -3
- package/dist/agent/reliable-consumer.d.ts.map +1 -1
- package/dist/agent/reliable-consumer.js +5 -1
- package/dist/agent/reliable-consumer.js.map +1 -1
- package/dist/bridges/a2a.d.ts +1 -1
- package/dist/bridges/a2a.js +1 -1
- package/dist/client/capabilities.d.ts +15 -2
- package/dist/client/capabilities.d.ts.map +1 -1
- package/dist/client/capabilities.js +22 -1
- package/dist/client/capabilities.js.map +1 -1
- package/dist/client/connect-options.d.ts +5 -0
- package/dist/client/connect-options.d.ts.map +1 -0
- package/dist/client/connect-options.js +18 -0
- package/dist/client/connect-options.js.map +1 -0
- package/dist/client/errors.d.ts +23 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +34 -0
- package/dist/client/errors.js.map +1 -1
- package/dist/client/laser.d.ts +17 -1
- package/dist/client/laser.d.ts.map +1 -1
- package/dist/client/laser.js +96 -26
- package/dist/client/laser.js.map +1 -1
- package/dist/client/managed.d.ts +1 -0
- package/dist/client/managed.d.ts.map +1 -1
- package/dist/client/managed.js +15 -2
- package/dist/client/managed.js.map +1 -1
- package/dist/iggy/apache-iggy.d.ts +69 -4
- package/dist/iggy/apache-iggy.d.ts.map +1 -1
- package/dist/iggy/apache-iggy.js +416 -23
- package/dist/iggy/apache-iggy.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/managed/filters.d.ts +358 -0
- package/dist/managed/filters.d.ts.map +1 -0
- package/dist/managed/filters.js +1913 -0
- package/dist/managed/filters.js.map +1 -0
- package/dist/stream/consumer.d.ts +11 -1
- package/dist/stream/consumer.d.ts.map +1 -1
- package/dist/stream/consumer.js +82 -9
- package/dist/stream/consumer.js.map +1 -1
- package/dist/stream/stream.d.ts +4 -1
- package/dist/stream/stream.d.ts.map +1 -1
- package/dist/stream/stream.js +11 -1
- package/dist/stream/stream.js.map +1 -1
- package/dist/stream/topic.d.ts +2 -0
- package/dist/stream/topic.d.ts.map +1 -1
- package/dist/stream/topic.js +6 -0
- package/dist/stream/topic.js.map +1 -1
- package/dist/wire/authz.d.ts +1 -1
- package/dist/wire/authz.d.ts.map +1 -1
- package/dist/wire/authz.js +12 -3
- package/dist/wire/authz.js.map +1 -1
- package/dist/wire/codes.d.ts +17 -0
- package/dist/wire/codes.d.ts.map +1 -1
- package/dist/wire/codes.js +17 -0
- package/dist/wire/codes.js.map +1 -1
- package/dist/wire/commands.d.ts +12 -1
- package/dist/wire/commands.d.ts.map +1 -1
- package/dist/wire/commands.js +13 -1
- package/dist/wire/commands.js.map +1 -1
- package/dist/wire/control.d.ts +4 -0
- package/dist/wire/control.d.ts.map +1 -1
- package/dist/wire/control.js +5 -0
- package/dist/wire/control.js.map +1 -1
- package/dist/wire/filter-codecs.d.ts +16 -0
- package/dist/wire/filter-codecs.d.ts.map +1 -0
- package/dist/wire/filter-codecs.js +519 -0
- package/dist/wire/filter-codecs.js.map +1 -0
- package/dist/wire/filter-eval.d.ts +75 -0
- package/dist/wire/filter-eval.d.ts.map +1 -0
- package/dist/wire/filter-eval.js +899 -0
- package/dist/wire/filter-eval.js.map +1 -0
- package/dist/wire/filter.d.ts +772 -0
- package/dist/wire/filter.d.ts.map +1 -0
- package/dist/wire/filter.js +2869 -0
- package/dist/wire/filter.js.map +1 -0
- package/dist/wire/hello.d.ts +18 -0
- package/dist/wire/hello.d.ts.map +1 -1
- package/dist/wire/hello.js +29 -3
- package/dist/wire/hello.js.map +1 -1
- package/dist/wire/http.d.ts +22 -0
- package/dist/wire/http.d.ts.map +1 -1
- package/dist/wire/http.js +54 -47
- package/dist/wire/http.js.map +1 -1
- package/dist/wire/limits.d.ts +25 -0
- package/dist/wire/limits.d.ts.map +1 -1
- package/dist/wire/limits.js +25 -0
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/result.d.ts +4 -0
- package/dist/wire/result.d.ts.map +1 -1
- package/dist/wire/result.js +50 -0
- package/dist/wire/result.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { MAX_FILTER_PARSE_DEPTH } from "./limits.js";
|
|
2
|
+
import { PayloadDecoders, PayloadFault } from "./filter-codecs.js";
|
|
3
|
+
import { InvalidError } from "../client/errors.js";
|
|
4
|
+
import { ExactDecimal, FieldPath, consumerFilterDigest, globTokens, parseCanonicalJson, timestampFromInteger, timestampFromText, validateConsumerFilter } from "./filter.js";
|
|
5
|
+
const UTF8_STRICT = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
6
|
+
const CONTENT_TYPE_HEADER = "agdx.ct";
|
|
7
|
+
// agdx.ct codes of the payload codecs a filter decodes, and `any`, which never makes a record foreign.
|
|
8
|
+
const CODEC_CONTENT_TYPES = {
|
|
9
|
+
json: 1,
|
|
10
|
+
cbor: 3,
|
|
11
|
+
avro: 5,
|
|
12
|
+
protobuf: 6
|
|
13
|
+
};
|
|
14
|
+
const ANY_CONTENT_TYPE = 255;
|
|
15
|
+
const KNOWN_CONTENT_TYPES = new Set([
|
|
16
|
+
0,
|
|
17
|
+
1,
|
|
18
|
+
2,
|
|
19
|
+
3,
|
|
20
|
+
4,
|
|
21
|
+
5,
|
|
22
|
+
6,
|
|
23
|
+
7,
|
|
24
|
+
8,
|
|
25
|
+
ANY_CONTENT_TYPE
|
|
26
|
+
]);
|
|
27
|
+
const FOREIGN_REASONS = new Set([
|
|
28
|
+
"foreign_codec",
|
|
29
|
+
"missing_schema",
|
|
30
|
+
"schema_not_allowed",
|
|
31
|
+
"schema_mismatch"
|
|
32
|
+
]);
|
|
33
|
+
export const DEFAULT_DECODE_LIMITS = {
|
|
34
|
+
maxPayloadBytes: 1024 * 1024,
|
|
35
|
+
maxDepth: 64
|
|
36
|
+
};
|
|
37
|
+
/** A validated filter compiled for repeated evaluation. Compile once, evaluate per record. */
|
|
38
|
+
export class CompiledFilter {
|
|
39
|
+
filter;
|
|
40
|
+
digest;
|
|
41
|
+
root;
|
|
42
|
+
decoders;
|
|
43
|
+
constructor(filter, digest, root, decoders) {
|
|
44
|
+
this.filter = filter;
|
|
45
|
+
this.digest = digest;
|
|
46
|
+
this.root = root;
|
|
47
|
+
this.decoders = decoders;
|
|
48
|
+
}
|
|
49
|
+
get readsHeaders() {
|
|
50
|
+
return this.root.readsHeaders || this.filter.schemaRefs.length > 0;
|
|
51
|
+
}
|
|
52
|
+
/** Whether a caller must pass the record headers: a payload filter reads agdx.ct first. */
|
|
53
|
+
get needsHeaders() {
|
|
54
|
+
return this.readsHeaders || this.root.readsPayload;
|
|
55
|
+
}
|
|
56
|
+
static compile(filter, schemas = []) {
|
|
57
|
+
validateConsumerFilter(filter);
|
|
58
|
+
return new CompiledFilter(filter, consumerFilterDigest(filter), compileNode(filter.expr), new PayloadDecoders(filter, schemas));
|
|
59
|
+
}
|
|
60
|
+
get faultPolicy() {
|
|
61
|
+
return this.filter.faultPolicy;
|
|
62
|
+
}
|
|
63
|
+
get readsPayload() {
|
|
64
|
+
return this.root.readsPayload;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the record is selected, rejected, or could not be decoded. Header
|
|
68
|
+
* predicates run before the payload is touched, and the payload is decoded at
|
|
69
|
+
* most once.
|
|
70
|
+
*/
|
|
71
|
+
evaluate(record, limits = DEFAULT_DECODE_LIMITS) {
|
|
72
|
+
return this.outcome(this.context(record, limits)).verdict;
|
|
73
|
+
}
|
|
74
|
+
/** The verdict and, for a fault or a policy rejection, its reason, from one decode. */
|
|
75
|
+
outcomeOf(record, limits = DEFAULT_DECODE_LIMITS) {
|
|
76
|
+
return this.outcome(this.context(record, limits));
|
|
77
|
+
}
|
|
78
|
+
/** The fault reason of a record `evaluate` judged a fault, `undefined` otherwise. */
|
|
79
|
+
faultReason(record, limits = DEFAULT_DECODE_LIMITS) {
|
|
80
|
+
return this.outcome(this.context(record, limits)).fault;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The policy a record that faulted for `reason` follows: a record in another
|
|
84
|
+
* format the foreign policy, a type mismatch the mismatch policy, a broken
|
|
85
|
+
* payload the fault policy. A `reject` record policy maps to `drop`.
|
|
86
|
+
*/
|
|
87
|
+
policyFor(reason) {
|
|
88
|
+
let policy;
|
|
89
|
+
if (FOREIGN_REASONS.has(reason))
|
|
90
|
+
policy = this.filter.foreignPolicy ?? "reject";
|
|
91
|
+
else if (reason === "type_mismatch")
|
|
92
|
+
policy = this.filter.mismatchPolicy ?? "reject";
|
|
93
|
+
else
|
|
94
|
+
return this.filter.faultPolicy;
|
|
95
|
+
return policy === "pass" ? "pass" : "drop";
|
|
96
|
+
}
|
|
97
|
+
/** Each node's truth, for sample tests. The verdict is the delivery verdict of `evaluate`. */
|
|
98
|
+
explain(record, limits = DEFAULT_DECODE_LIMITS) {
|
|
99
|
+
const context = this.context(record, limits);
|
|
100
|
+
const { verdict, fault } = this.outcome(context);
|
|
101
|
+
const root = context.explain(this.root);
|
|
102
|
+
return { verdict, ...(fault !== undefined ? { fault } : {}), root };
|
|
103
|
+
}
|
|
104
|
+
context(record, limits) {
|
|
105
|
+
return new Context(record, { ...limits, maxDepth: Math.min(limits.maxDepth, MAX_FILTER_PARSE_DEPTH) }, this.filter.codec, this.decoders);
|
|
106
|
+
}
|
|
107
|
+
// A `reject` record policy decides here, so the record is rejected and never
|
|
108
|
+
// a fault. `pass` surfaces as a fault with its reason.
|
|
109
|
+
outcome(context) {
|
|
110
|
+
let reason;
|
|
111
|
+
try {
|
|
112
|
+
const truth = context.truth(this.root);
|
|
113
|
+
if (truth === "match")
|
|
114
|
+
return { verdict: "selected" };
|
|
115
|
+
if (truth === "no_match" || !context.mismatched)
|
|
116
|
+
return { verdict: "rejected" };
|
|
117
|
+
reason = "type_mismatch";
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!(error instanceof PayloadFault))
|
|
121
|
+
throw error;
|
|
122
|
+
reason = error.reason;
|
|
123
|
+
}
|
|
124
|
+
const recordPolicy = FOREIGN_REASONS.has(reason) || reason === "type_mismatch";
|
|
125
|
+
if (recordPolicy && this.policyFor(reason) === "drop")
|
|
126
|
+
return { verdict: "rejected", fault: reason };
|
|
127
|
+
return { verdict: "fault", fault: reason };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function usesRegex(expr) {
|
|
131
|
+
switch (expr.kind) {
|
|
132
|
+
case "all":
|
|
133
|
+
case "any":
|
|
134
|
+
return expr.children.some(usesRegex);
|
|
135
|
+
case "not":
|
|
136
|
+
return usesRegex(expr.child);
|
|
137
|
+
case "text":
|
|
138
|
+
case "header_text":
|
|
139
|
+
return expr.predicate.kind === "regex";
|
|
140
|
+
case "pred":
|
|
141
|
+
case "pred_as":
|
|
142
|
+
case "present":
|
|
143
|
+
case "absent":
|
|
144
|
+
case "header":
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A validated text predicate ready to run per record. */
|
|
149
|
+
export class TextMatcher {
|
|
150
|
+
kind;
|
|
151
|
+
source;
|
|
152
|
+
caseInsensitive;
|
|
153
|
+
pattern;
|
|
154
|
+
glob;
|
|
155
|
+
// Regex predicates run with the server's Rust engine, whose syntax is the
|
|
156
|
+
// contract. JavaScript regexes differ, so a local evaluation refuses them
|
|
157
|
+
// instead of guessing.
|
|
158
|
+
constructor(kind, source, caseInsensitive) {
|
|
159
|
+
this.kind = kind;
|
|
160
|
+
this.source = source;
|
|
161
|
+
this.caseInsensitive = caseInsensitive;
|
|
162
|
+
if (kind === "regex") {
|
|
163
|
+
throw new InvalidError("regex predicates cannot be evaluated in TypeScript, the server runs them");
|
|
164
|
+
}
|
|
165
|
+
this.pattern = caseInsensitive ? source.toLowerCase() : source;
|
|
166
|
+
if (kind === "glob") {
|
|
167
|
+
const tokens = globTokens(source);
|
|
168
|
+
if (tokens === undefined)
|
|
169
|
+
throw new InvalidError("the glob has a trailing escape");
|
|
170
|
+
this.glob = new GlobProgram(tokens.flatMap((token) => token.kind === "literal" && caseInsensitive
|
|
171
|
+
? Array.from(token.value.toLowerCase(), (value) => ({
|
|
172
|
+
kind: "literal",
|
|
173
|
+
value
|
|
174
|
+
}))
|
|
175
|
+
: [token]));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
static of(predicate) {
|
|
179
|
+
return new TextMatcher(predicate.kind, predicate.pattern, predicate.caseInsensitive === true);
|
|
180
|
+
}
|
|
181
|
+
matches(text) {
|
|
182
|
+
const value = this.caseInsensitive ? text.toLowerCase() : text;
|
|
183
|
+
switch (this.kind) {
|
|
184
|
+
case "equals":
|
|
185
|
+
return value === this.pattern;
|
|
186
|
+
case "prefix":
|
|
187
|
+
return value.startsWith(this.pattern);
|
|
188
|
+
case "suffix":
|
|
189
|
+
return value.endsWith(this.pattern);
|
|
190
|
+
case "contains":
|
|
191
|
+
return value.includes(this.pattern);
|
|
192
|
+
case "glob":
|
|
193
|
+
return this.glob?.matches(value) === true;
|
|
194
|
+
case "regex":
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
label() {
|
|
199
|
+
const pattern = JSON.stringify(this.source);
|
|
200
|
+
return this.caseInsensitive ? `${pattern} ignoring case` : pattern;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// A bitset holds all active glob states. Each character advances them once,
|
|
204
|
+
// without rescanning an already matched literal prefix after a star.
|
|
205
|
+
class GlobProgram {
|
|
206
|
+
runs = 0n;
|
|
207
|
+
ones = 0n;
|
|
208
|
+
literals = new Map();
|
|
209
|
+
accept;
|
|
210
|
+
constructor(tokens) {
|
|
211
|
+
let count = 0n;
|
|
212
|
+
let previousRun = false;
|
|
213
|
+
for (const token of tokens) {
|
|
214
|
+
if (token.kind === "run" && previousRun)
|
|
215
|
+
continue;
|
|
216
|
+
const state = 1n << count++;
|
|
217
|
+
previousRun = token.kind === "run";
|
|
218
|
+
if (token.kind === "run")
|
|
219
|
+
this.runs |= state;
|
|
220
|
+
else if (token.kind === "one")
|
|
221
|
+
this.ones |= state;
|
|
222
|
+
else
|
|
223
|
+
this.literals.set(token.value, (this.literals.get(token.value) ?? 0n) | state);
|
|
224
|
+
}
|
|
225
|
+
this.accept = 1n << count;
|
|
226
|
+
}
|
|
227
|
+
matches(value) {
|
|
228
|
+
let active = 1n | ((1n & this.runs) << 1n);
|
|
229
|
+
for (const character of value) {
|
|
230
|
+
active =
|
|
231
|
+
((active & (this.ones | (this.literals.get(character) ?? 0n))) << 1n) | (active & this.runs);
|
|
232
|
+
active |= (active & this.runs) << 1n;
|
|
233
|
+
if (active === 0n)
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return (active & this.accept) !== 0n;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Decode a JSON payload within `limits`. The depth is checked by a scan over
|
|
241
|
+
* the raw bytes before any value is built.
|
|
242
|
+
*/
|
|
243
|
+
export function decodeJsonPayload(payload, limits) {
|
|
244
|
+
if (payload.byteLength > limits.maxPayloadBytes)
|
|
245
|
+
return new PayloadFault("too_large");
|
|
246
|
+
if (!jsonDepthWithin(payload, limits.maxDepth))
|
|
247
|
+
return new PayloadFault("too_deep");
|
|
248
|
+
try {
|
|
249
|
+
return parseCanonicalJson(UTF8_STRICT.decode(payload), "payload");
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return new PayloadFault("malformed");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function jsonDepthWithin(payload, maxDepth) {
|
|
256
|
+
let depth = 0;
|
|
257
|
+
let inString = false;
|
|
258
|
+
let escaped = false;
|
|
259
|
+
for (const byte of payload) {
|
|
260
|
+
if (inString) {
|
|
261
|
+
if (escaped)
|
|
262
|
+
escaped = false;
|
|
263
|
+
else if (byte === 0x5c)
|
|
264
|
+
escaped = true;
|
|
265
|
+
else if (byte === 0x22)
|
|
266
|
+
inString = false;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (byte === 0x22)
|
|
270
|
+
inString = true;
|
|
271
|
+
else if (byte === 0x5b || byte === 0x7b) {
|
|
272
|
+
depth += 1;
|
|
273
|
+
if (depth > maxDepth)
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
else if (byte === 0x5d || byte === 0x7d) {
|
|
277
|
+
depth = Math.max(0, depth - 1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
// Built bottom up, so what a node reads is known from its children and no
|
|
283
|
+
// subtree is walked twice.
|
|
284
|
+
function compileNode(expr) {
|
|
285
|
+
switch (expr.kind) {
|
|
286
|
+
case "all":
|
|
287
|
+
case "any": {
|
|
288
|
+
const children = expr.children.map(compileNode);
|
|
289
|
+
return {
|
|
290
|
+
node: {
|
|
291
|
+
kind: expr.kind,
|
|
292
|
+
children,
|
|
293
|
+
ordered: [
|
|
294
|
+
...children.filter((child) => !child.readsPayload),
|
|
295
|
+
...children.filter((child) => child.readsPayload)
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
readsPayload: children.some((child) => child.readsPayload),
|
|
299
|
+
readsHeaders: children.some((child) => child.readsHeaders)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
case "not": {
|
|
303
|
+
const child = compileNode(expr.child);
|
|
304
|
+
return {
|
|
305
|
+
node: { kind: "not", child },
|
|
306
|
+
readsPayload: child.readsPayload,
|
|
307
|
+
readsHeaders: child.readsHeaders
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "pred":
|
|
311
|
+
return {
|
|
312
|
+
node: {
|
|
313
|
+
kind: "compare",
|
|
314
|
+
path: FieldPath.parse(expr.predicate.field),
|
|
315
|
+
op: expr.predicate.op,
|
|
316
|
+
literal: compileLiteral(expr.predicate.value)
|
|
317
|
+
},
|
|
318
|
+
readsPayload: true,
|
|
319
|
+
readsHeaders: false
|
|
320
|
+
};
|
|
321
|
+
case "pred_as":
|
|
322
|
+
return {
|
|
323
|
+
node: {
|
|
324
|
+
kind: "coerced",
|
|
325
|
+
path: FieldPath.parse(expr.predicate.pred.field),
|
|
326
|
+
op: expr.predicate.pred.op,
|
|
327
|
+
coerce: expr.predicate.coerce,
|
|
328
|
+
literal: compileCoercedLiteral(expr.predicate.coerce, expr.predicate.pred.value)
|
|
329
|
+
},
|
|
330
|
+
readsPayload: true,
|
|
331
|
+
readsHeaders: false
|
|
332
|
+
};
|
|
333
|
+
case "present":
|
|
334
|
+
case "absent":
|
|
335
|
+
return {
|
|
336
|
+
node: { kind: expr.kind, path: expr.path },
|
|
337
|
+
readsPayload: true,
|
|
338
|
+
readsHeaders: false
|
|
339
|
+
};
|
|
340
|
+
case "header":
|
|
341
|
+
return {
|
|
342
|
+
node: {
|
|
343
|
+
kind: "header",
|
|
344
|
+
key: expr.predicate.key,
|
|
345
|
+
op: expr.predicate.op,
|
|
346
|
+
literal: compileLiteral(expr.predicate.value)
|
|
347
|
+
},
|
|
348
|
+
readsPayload: false,
|
|
349
|
+
readsHeaders: true
|
|
350
|
+
};
|
|
351
|
+
case "text":
|
|
352
|
+
return {
|
|
353
|
+
node: {
|
|
354
|
+
kind: "text",
|
|
355
|
+
path: FieldPath.parse(expr.predicate.field),
|
|
356
|
+
matcher: TextMatcher.of(expr.predicate)
|
|
357
|
+
},
|
|
358
|
+
readsPayload: true,
|
|
359
|
+
readsHeaders: false
|
|
360
|
+
};
|
|
361
|
+
case "header_text":
|
|
362
|
+
return {
|
|
363
|
+
node: {
|
|
364
|
+
kind: "header_text",
|
|
365
|
+
key: expr.predicate.field,
|
|
366
|
+
matcher: TextMatcher.of(expr.predicate)
|
|
367
|
+
},
|
|
368
|
+
readsPayload: false,
|
|
369
|
+
readsHeaders: true
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function compileLiteral(value) {
|
|
374
|
+
switch (value.kind) {
|
|
375
|
+
case "null":
|
|
376
|
+
return { kind: "null" };
|
|
377
|
+
case "boolean":
|
|
378
|
+
return { kind: "bool", value: value.value };
|
|
379
|
+
case "int":
|
|
380
|
+
return { kind: "int", value: BigInt(value.value) };
|
|
381
|
+
case "long":
|
|
382
|
+
return { kind: "int", value: value.value };
|
|
383
|
+
case "double":
|
|
384
|
+
return { kind: "float", value: value.value };
|
|
385
|
+
case "string":
|
|
386
|
+
return { kind: "text", value: value.value };
|
|
387
|
+
case "list":
|
|
388
|
+
return { kind: "list", items: value.value.map(compileLiteral) };
|
|
389
|
+
case "float":
|
|
390
|
+
case "time_micros":
|
|
391
|
+
case "timestamp_micros":
|
|
392
|
+
case "timestamp_tz_micros":
|
|
393
|
+
case "date":
|
|
394
|
+
case "decimal":
|
|
395
|
+
case "uuid":
|
|
396
|
+
case "fixed":
|
|
397
|
+
case "binary":
|
|
398
|
+
case "struct":
|
|
399
|
+
case "map":
|
|
400
|
+
throw new InvalidError(`unsupported filter literal ${value.kind}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function compileCoercedLiteral(coerce, value) {
|
|
404
|
+
if (value.kind === "list") {
|
|
405
|
+
return { kind: "list", items: value.value.map((item) => compileCoercedLiteral(coerce, item)) };
|
|
406
|
+
}
|
|
407
|
+
let comparable;
|
|
408
|
+
if (coerce.kind === "timestamp") {
|
|
409
|
+
const micros = value.kind === "string"
|
|
410
|
+
? timestampFromText(coerce.format, value.value)
|
|
411
|
+
: value.kind === "int" || value.kind === "long"
|
|
412
|
+
? timestampFromInteger(coerce.format, BigInt(value.value))
|
|
413
|
+
: undefined;
|
|
414
|
+
comparable = micros === undefined ? undefined : { kind: "instant", micros };
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const decimal = value.kind === "string"
|
|
418
|
+
? ExactDecimal.parse(value.value)
|
|
419
|
+
: value.kind === "int" || value.kind === "long"
|
|
420
|
+
? ExactDecimal.fromInteger(BigInt(value.value))
|
|
421
|
+
: undefined;
|
|
422
|
+
comparable = decimal === undefined ? undefined : { kind: "decimal", decimal };
|
|
423
|
+
}
|
|
424
|
+
if (comparable === undefined) {
|
|
425
|
+
throw new InvalidError(`literal ${value.kind} does not parse under its coercion`);
|
|
426
|
+
}
|
|
427
|
+
return comparable;
|
|
428
|
+
}
|
|
429
|
+
class Context {
|
|
430
|
+
record;
|
|
431
|
+
limits;
|
|
432
|
+
codec;
|
|
433
|
+
decoders;
|
|
434
|
+
payload;
|
|
435
|
+
/** A predicate saw a value of a type it cannot compare. */
|
|
436
|
+
mismatched = false;
|
|
437
|
+
constructor(record, limits, codec, decoders) {
|
|
438
|
+
this.record = record;
|
|
439
|
+
this.limits = limits;
|
|
440
|
+
this.codec = codec;
|
|
441
|
+
this.decoders = decoders;
|
|
442
|
+
}
|
|
443
|
+
decoded() {
|
|
444
|
+
if (this.payload === undefined) {
|
|
445
|
+
this.payload = this.declaresForeignCodec()
|
|
446
|
+
? new PayloadFault("foreign_codec")
|
|
447
|
+
: this.codec === "json"
|
|
448
|
+
? decodeJsonPayload(this.record.payload, this.limits)
|
|
449
|
+
: this.decoders.decode(this.record, this.limits);
|
|
450
|
+
}
|
|
451
|
+
if (this.payload instanceof PayloadFault)
|
|
452
|
+
throw this.payload;
|
|
453
|
+
return this.payload;
|
|
454
|
+
}
|
|
455
|
+
// `any` and codes this build does not know decode as usual.
|
|
456
|
+
declaresForeignCodec() {
|
|
457
|
+
const header = this.record.headers.findLast((candidate) => candidate.key === CONTENT_TYPE_HEADER);
|
|
458
|
+
const expected = CODEC_CONTENT_TYPES[this.codec];
|
|
459
|
+
if (header === undefined || expected === undefined)
|
|
460
|
+
return false;
|
|
461
|
+
if (header.value.kind !== "uint" && header.value.kind !== "int")
|
|
462
|
+
return false;
|
|
463
|
+
const code = Number(header.value.value);
|
|
464
|
+
if (!KNOWN_CONTENT_TYPES.has(code) || code === ANY_CONTENT_TYPE)
|
|
465
|
+
return false;
|
|
466
|
+
return code !== expected;
|
|
467
|
+
}
|
|
468
|
+
payloadFault() {
|
|
469
|
+
return this.payload instanceof PayloadFault ? this.payload.reason : undefined;
|
|
470
|
+
}
|
|
471
|
+
truth(node) {
|
|
472
|
+
const kind = node.node;
|
|
473
|
+
switch (kind.kind) {
|
|
474
|
+
case "all":
|
|
475
|
+
return this.combine(kind.ordered, "no_match", "match");
|
|
476
|
+
case "any":
|
|
477
|
+
return this.combine(kind.ordered, "match", "no_match");
|
|
478
|
+
case "not":
|
|
479
|
+
return negate(this.truth(kind.child));
|
|
480
|
+
case "header":
|
|
481
|
+
return this.noted(this.headerTruth(kind.key, kind.op, kind.literal));
|
|
482
|
+
case "header_text":
|
|
483
|
+
return this.noted(this.headerTextTruth(kind.key, kind.matcher));
|
|
484
|
+
case "compare":
|
|
485
|
+
case "coerced":
|
|
486
|
+
case "present":
|
|
487
|
+
case "absent":
|
|
488
|
+
case "text":
|
|
489
|
+
return this.noted(payloadTruth(kind, this.decoded()));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
noted([truth, mismatched]) {
|
|
493
|
+
if (mismatched)
|
|
494
|
+
this.mismatched = true;
|
|
495
|
+
return truth;
|
|
496
|
+
}
|
|
497
|
+
// `ordered` runs the children that read no payload first, so headers can
|
|
498
|
+
// decide the node before the payload is ever decoded.
|
|
499
|
+
combine(ordered, decisive, otherwise) {
|
|
500
|
+
let unknown = false;
|
|
501
|
+
for (const child of ordered) {
|
|
502
|
+
const truth = this.truth(child);
|
|
503
|
+
if (truth === decisive)
|
|
504
|
+
return decisive;
|
|
505
|
+
if (truth === "unknown")
|
|
506
|
+
unknown = true;
|
|
507
|
+
}
|
|
508
|
+
return unknown ? "unknown" : otherwise;
|
|
509
|
+
}
|
|
510
|
+
// The truth, and whether the header exists with a type the predicate cannot compare.
|
|
511
|
+
headerTruth(key, op, literal) {
|
|
512
|
+
const header = this.record.headers.findLast((candidate) => candidate.key === key);
|
|
513
|
+
if (header === undefined)
|
|
514
|
+
return ["unknown", false];
|
|
515
|
+
const scalar = headerScalar(header);
|
|
516
|
+
const truth = compareScalar(scalar, undefined, op, literal);
|
|
517
|
+
return [truth, truth === "unknown" && !comparable(scalar, undefined, op, literal)];
|
|
518
|
+
}
|
|
519
|
+
headerTextTruth(key, matcher) {
|
|
520
|
+
const header = this.record.headers.findLast((candidate) => candidate.key === key);
|
|
521
|
+
if (header === undefined)
|
|
522
|
+
return ["unknown", false];
|
|
523
|
+
const scalar = headerScalar(header);
|
|
524
|
+
return scalar.kind === "text"
|
|
525
|
+
? [truthOf(matcher.matches(scalar.value)), false]
|
|
526
|
+
: ["unknown", true];
|
|
527
|
+
}
|
|
528
|
+
explain(node) {
|
|
529
|
+
const kind = node.node;
|
|
530
|
+
if (kind.kind === "all" || kind.kind === "any" || kind.kind === "not") {
|
|
531
|
+
const children = kind.kind === "not" ? [kind.child] : kind.children;
|
|
532
|
+
const explained = children.map((child) => this.explain(child));
|
|
533
|
+
const truths = explained.flatMap((child) => (child.truth === undefined ? [] : [child.truth]));
|
|
534
|
+
const complete = truths.length === explained.length;
|
|
535
|
+
const first = truths[0] ?? "unknown";
|
|
536
|
+
const truth = !complete
|
|
537
|
+
? undefined
|
|
538
|
+
: kind.kind === "all"
|
|
539
|
+
? fold(truths, "no_match", "match")
|
|
540
|
+
: kind.kind === "any"
|
|
541
|
+
? fold(truths, "match", "no_match")
|
|
542
|
+
: negate(first);
|
|
543
|
+
return { label: kind.kind, ...(truth !== undefined ? { truth } : {}), children: explained };
|
|
544
|
+
}
|
|
545
|
+
let truth;
|
|
546
|
+
if (kind.kind === "header") {
|
|
547
|
+
truth = this.headerTruth(kind.key, kind.op, kind.literal)[0];
|
|
548
|
+
}
|
|
549
|
+
else if (kind.kind === "header_text") {
|
|
550
|
+
truth = this.headerTextTruth(kind.key, kind.matcher)[0];
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
try {
|
|
554
|
+
truth = payloadTruth(kind, this.decoded())[0];
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
if (!(error instanceof PayloadFault))
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { label: leafLabel(kind), ...(truth !== undefined ? { truth } : {}), children: [] };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function headerScalar(header) {
|
|
565
|
+
switch (header.value.kind) {
|
|
566
|
+
case "bool":
|
|
567
|
+
return { kind: "bool", value: header.value.value };
|
|
568
|
+
case "int":
|
|
569
|
+
case "uint":
|
|
570
|
+
return { kind: "int", value: header.value.value };
|
|
571
|
+
case "float":
|
|
572
|
+
return { kind: "float", value: header.value.value };
|
|
573
|
+
case "string":
|
|
574
|
+
return { kind: "text", value: header.value.value };
|
|
575
|
+
case "raw":
|
|
576
|
+
try {
|
|
577
|
+
return { kind: "text", value: UTF8_STRICT.decode(header.value.value) };
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
return { kind: "composite" };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function fold(truths, decisive, otherwise) {
|
|
585
|
+
if (truths.includes(decisive))
|
|
586
|
+
return decisive;
|
|
587
|
+
if (truths.includes("unknown"))
|
|
588
|
+
return "unknown";
|
|
589
|
+
return otherwise;
|
|
590
|
+
}
|
|
591
|
+
function negate(truth) {
|
|
592
|
+
return truth === "match" ? "no_match" : truth === "no_match" ? "match" : "unknown";
|
|
593
|
+
}
|
|
594
|
+
function truthOf(matched) {
|
|
595
|
+
return matched ? "match" : "no_match";
|
|
596
|
+
}
|
|
597
|
+
// The truth, and whether the field exists, is not null, and has a type the
|
|
598
|
+
// predicate cannot compare. A missing or null field is plain unknown.
|
|
599
|
+
function payloadTruth(kind, value) {
|
|
600
|
+
switch (kind.kind) {
|
|
601
|
+
case "compare": {
|
|
602
|
+
const found = resolve(value, kind.path);
|
|
603
|
+
if (found === undefined)
|
|
604
|
+
return [missingTruth(kind.op, kind.literal), false];
|
|
605
|
+
const scalar = scalarOf(found);
|
|
606
|
+
const truth = compareScalar(scalar, found, kind.op, kind.literal);
|
|
607
|
+
return [truth, truth === "unknown" && !comparable(scalar, found, kind.op, kind.literal)];
|
|
608
|
+
}
|
|
609
|
+
case "coerced": {
|
|
610
|
+
const found = resolve(value, kind.path);
|
|
611
|
+
if (found === undefined)
|
|
612
|
+
return ["unknown", false];
|
|
613
|
+
const comparable = coerceValue(kind.coerce, found);
|
|
614
|
+
return comparable === undefined
|
|
615
|
+
? ["unknown", found !== null]
|
|
616
|
+
: [compareCoerced(comparable, kind.op, kind.literal), false];
|
|
617
|
+
}
|
|
618
|
+
case "text": {
|
|
619
|
+
const found = resolve(value, kind.path);
|
|
620
|
+
if (found === undefined || found === null)
|
|
621
|
+
return ["unknown", false];
|
|
622
|
+
return typeof found === "string"
|
|
623
|
+
? [truthOf(kind.matcher.matches(found)), false]
|
|
624
|
+
: ["unknown", true];
|
|
625
|
+
}
|
|
626
|
+
case "present":
|
|
627
|
+
return [truthOf(resolve(value, kind.path) !== undefined), false];
|
|
628
|
+
case "absent":
|
|
629
|
+
return [truthOf(resolve(value, kind.path) === undefined), false];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// A missing field is unknown, except for the null tests: `eq null` matches a
|
|
633
|
+
// missing field and `ne null` does not.
|
|
634
|
+
function missingTruth(op, literal) {
|
|
635
|
+
if (literal.kind === "null" && op === "eq")
|
|
636
|
+
return "match";
|
|
637
|
+
if (literal.kind === "null" && op === "ne")
|
|
638
|
+
return "no_match";
|
|
639
|
+
return "unknown";
|
|
640
|
+
}
|
|
641
|
+
function comparable(scalar, found, op, literal) {
|
|
642
|
+
if (scalar.kind === "null" || literal.kind === "null")
|
|
643
|
+
return true;
|
|
644
|
+
if ((scalar.kind === "int" || scalar.kind === "float") &&
|
|
645
|
+
(literal.kind === "int" || literal.kind === "float"))
|
|
646
|
+
return true;
|
|
647
|
+
if ((scalar.kind === "text" && literal.kind === "text") ||
|
|
648
|
+
(scalar.kind === "bool" && literal.kind === "bool"))
|
|
649
|
+
return true;
|
|
650
|
+
if (literal.kind === "list")
|
|
651
|
+
return literal.items.some((item) => comparable(scalar, found, op, item));
|
|
652
|
+
return scalar.kind === "composite" && op === "contains" && Array.isArray(found);
|
|
653
|
+
}
|
|
654
|
+
function compareScalar(scalar, found, op, literal) {
|
|
655
|
+
if (scalar.kind === "null")
|
|
656
|
+
return literal.kind === "null" ? truthOf(op === "eq") : "unknown";
|
|
657
|
+
if (literal.kind === "null")
|
|
658
|
+
return truthOf(op === "ne");
|
|
659
|
+
switch (op) {
|
|
660
|
+
case "eq": {
|
|
661
|
+
const equal = scalarEq(scalar, literal);
|
|
662
|
+
return equal === undefined ? "unknown" : truthOf(equal);
|
|
663
|
+
}
|
|
664
|
+
case "ne": {
|
|
665
|
+
const equal = scalarEq(scalar, literal);
|
|
666
|
+
return equal === undefined ? "unknown" : truthOf(!equal);
|
|
667
|
+
}
|
|
668
|
+
case "lt":
|
|
669
|
+
case "lte":
|
|
670
|
+
case "gt":
|
|
671
|
+
case "gte": {
|
|
672
|
+
const ordering = scalarCmp(scalar, literal);
|
|
673
|
+
if (ordering === undefined)
|
|
674
|
+
return "unknown";
|
|
675
|
+
return truthOf(op === "lt"
|
|
676
|
+
? ordering < 0
|
|
677
|
+
: op === "lte"
|
|
678
|
+
? ordering <= 0
|
|
679
|
+
: op === "gt"
|
|
680
|
+
? ordering > 0
|
|
681
|
+
: ordering >= 0);
|
|
682
|
+
}
|
|
683
|
+
case "in": {
|
|
684
|
+
if (literal.kind !== "list")
|
|
685
|
+
return "unknown";
|
|
686
|
+
let unknown = false;
|
|
687
|
+
for (const item of literal.items) {
|
|
688
|
+
const equal = scalarEq(scalar, item);
|
|
689
|
+
if (equal === true)
|
|
690
|
+
return "match";
|
|
691
|
+
if (equal === undefined)
|
|
692
|
+
unknown = true;
|
|
693
|
+
}
|
|
694
|
+
return unknown ? "unknown" : "no_match";
|
|
695
|
+
}
|
|
696
|
+
case "contains":
|
|
697
|
+
if (scalar.kind === "text" && literal.kind === "text") {
|
|
698
|
+
return truthOf(scalar.value.includes(literal.value));
|
|
699
|
+
}
|
|
700
|
+
if (scalar.kind === "composite" && Array.isArray(found)) {
|
|
701
|
+
return truthOf(found.some((element) => scalarEq(scalarOf(element), literal) === true));
|
|
702
|
+
}
|
|
703
|
+
return "unknown";
|
|
704
|
+
case "prefix":
|
|
705
|
+
return scalar.kind === "text" && literal.kind === "text"
|
|
706
|
+
? truthOf(scalar.value.startsWith(literal.value))
|
|
707
|
+
: "unknown";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function scalarEq(scalar, literal) {
|
|
711
|
+
if (scalar.kind === "bool" && literal.kind === "bool")
|
|
712
|
+
return scalar.value === literal.value;
|
|
713
|
+
if (scalar.kind === "text" && literal.kind === "text")
|
|
714
|
+
return scalar.value === literal.value;
|
|
715
|
+
const ordering = scalarCmp(scalar, literal);
|
|
716
|
+
return ordering === undefined ? undefined : ordering === 0;
|
|
717
|
+
}
|
|
718
|
+
function scalarCmp(scalar, literal) {
|
|
719
|
+
if (scalar.kind === "int" && literal.kind === "int")
|
|
720
|
+
return compareBigInt(scalar.value, literal.value);
|
|
721
|
+
if (scalar.kind === "float" && literal.kind === "float")
|
|
722
|
+
return compareDouble(scalar.value, literal.value);
|
|
723
|
+
if (scalar.kind === "int" && literal.kind === "float") {
|
|
724
|
+
const left = exactDouble(scalar.value);
|
|
725
|
+
return left === undefined ? undefined : compareDouble(left, literal.value);
|
|
726
|
+
}
|
|
727
|
+
if (scalar.kind === "float" && literal.kind === "int") {
|
|
728
|
+
const right = exactDouble(literal.value);
|
|
729
|
+
return right === undefined ? undefined : compareDouble(scalar.value, right);
|
|
730
|
+
}
|
|
731
|
+
if (scalar.kind === "text" && literal.kind === "text")
|
|
732
|
+
return compareCodePoints(scalar.value, literal.value);
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
function compareBigInt(left, right) {
|
|
736
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
737
|
+
}
|
|
738
|
+
function compareDouble(left, right) {
|
|
739
|
+
if (Number.isNaN(left) || Number.isNaN(right))
|
|
740
|
+
return undefined;
|
|
741
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
742
|
+
}
|
|
743
|
+
function exactDouble(value) {
|
|
744
|
+
const converted = Number(value);
|
|
745
|
+
return Number.isFinite(converted) && BigInt(converted) === value ? converted : undefined;
|
|
746
|
+
}
|
|
747
|
+
// Code-point order equals the UTF-8 byte order the server compares in.
|
|
748
|
+
function compareCodePoints(left, right) {
|
|
749
|
+
let leftIndex = 0;
|
|
750
|
+
let rightIndex = 0;
|
|
751
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
752
|
+
const leftPoint = left.codePointAt(leftIndex) ?? 0;
|
|
753
|
+
const rightPoint = right.codePointAt(rightIndex) ?? 0;
|
|
754
|
+
if (leftPoint !== rightPoint)
|
|
755
|
+
return leftPoint < rightPoint ? -1 : 1;
|
|
756
|
+
leftIndex += leftPoint > 0xffff ? 2 : 1;
|
|
757
|
+
rightIndex += rightPoint > 0xffff ? 2 : 1;
|
|
758
|
+
}
|
|
759
|
+
const leftDone = leftIndex >= left.length;
|
|
760
|
+
const rightDone = rightIndex >= right.length;
|
|
761
|
+
return leftDone && rightDone ? 0 : leftDone ? -1 : 1;
|
|
762
|
+
}
|
|
763
|
+
function compareCoerced(comparable, op, literal) {
|
|
764
|
+
const ordering = (target) => {
|
|
765
|
+
if (comparable.kind === "instant" && target.kind === "instant") {
|
|
766
|
+
return compareBigInt(comparable.micros, target.micros);
|
|
767
|
+
}
|
|
768
|
+
if (comparable.kind === "decimal" && target.kind === "decimal") {
|
|
769
|
+
return comparable.decimal.compare(target.decimal);
|
|
770
|
+
}
|
|
771
|
+
return undefined;
|
|
772
|
+
};
|
|
773
|
+
if (op === "in" && literal.kind === "list") {
|
|
774
|
+
return truthOf(literal.items.some((item) => ordering(item) === 0));
|
|
775
|
+
}
|
|
776
|
+
const result = ordering(literal);
|
|
777
|
+
if (result === undefined)
|
|
778
|
+
return "unknown";
|
|
779
|
+
switch (op) {
|
|
780
|
+
case "eq":
|
|
781
|
+
return truthOf(result === 0);
|
|
782
|
+
case "ne":
|
|
783
|
+
return truthOf(result !== 0);
|
|
784
|
+
case "lt":
|
|
785
|
+
return truthOf(result < 0);
|
|
786
|
+
case "lte":
|
|
787
|
+
return truthOf(result <= 0);
|
|
788
|
+
case "gt":
|
|
789
|
+
return truthOf(result > 0);
|
|
790
|
+
case "gte":
|
|
791
|
+
return truthOf(result >= 0);
|
|
792
|
+
case "in":
|
|
793
|
+
case "contains":
|
|
794
|
+
case "prefix":
|
|
795
|
+
return "unknown";
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function coerceValue(coerce, value) {
|
|
799
|
+
if (coerce.kind === "timestamp") {
|
|
800
|
+
const micros = timestampOfValue(coerce.format, value);
|
|
801
|
+
return micros === undefined ? undefined : { kind: "instant", micros };
|
|
802
|
+
}
|
|
803
|
+
let decimal;
|
|
804
|
+
if (typeof value === "string")
|
|
805
|
+
decimal = ExactDecimal.parse(value);
|
|
806
|
+
else if (typeof value === "bigint")
|
|
807
|
+
decimal = ExactDecimal.fromInteger(value);
|
|
808
|
+
else if (typeof value === "number")
|
|
809
|
+
decimal = ExactDecimal.fromDouble(value);
|
|
810
|
+
return decimal === undefined ? undefined : { kind: "decimal", decimal };
|
|
811
|
+
}
|
|
812
|
+
function timestampOfValue(format, value) {
|
|
813
|
+
if (typeof value === "string")
|
|
814
|
+
return timestampFromText(format, value);
|
|
815
|
+
if (format === "rfc3339")
|
|
816
|
+
return undefined;
|
|
817
|
+
return typeof value === "bigint" ? timestampFromInteger(format, value) : undefined;
|
|
818
|
+
}
|
|
819
|
+
function scalarOf(value) {
|
|
820
|
+
if (value === null)
|
|
821
|
+
return { kind: "null" };
|
|
822
|
+
if (typeof value === "boolean")
|
|
823
|
+
return { kind: "bool", value };
|
|
824
|
+
if (typeof value === "bigint")
|
|
825
|
+
return { kind: "int", value };
|
|
826
|
+
if (typeof value === "number")
|
|
827
|
+
return { kind: "float", value };
|
|
828
|
+
if (typeof value === "string")
|
|
829
|
+
return { kind: "text", value };
|
|
830
|
+
return { kind: "composite" };
|
|
831
|
+
}
|
|
832
|
+
function resolve(value, path) {
|
|
833
|
+
let current = value;
|
|
834
|
+
for (const segment of path.segments) {
|
|
835
|
+
if (current === undefined)
|
|
836
|
+
return undefined;
|
|
837
|
+
if (segment.kind === "key") {
|
|
838
|
+
current =
|
|
839
|
+
current instanceof Map
|
|
840
|
+
? current.get(segment.key)
|
|
841
|
+
: undefined;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
current = Array.isArray(current)
|
|
845
|
+
? current[segment.index]
|
|
846
|
+
: undefined;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return current;
|
|
850
|
+
}
|
|
851
|
+
function leafLabel(kind) {
|
|
852
|
+
switch (kind.kind) {
|
|
853
|
+
case "compare":
|
|
854
|
+
return `${kind.path.toString()} ${kind.op} ${literalLabel(kind.literal)}`;
|
|
855
|
+
case "coerced": {
|
|
856
|
+
const coerce = kind.coerce.kind === "timestamp" ? `timestamp(${kind.coerce.format})` : "number";
|
|
857
|
+
return `${coerce}(${kind.path.toString()}) ${kind.op} ${coercedLabel(kind.literal)}`;
|
|
858
|
+
}
|
|
859
|
+
case "present":
|
|
860
|
+
return `${kind.path.toString()} is present`;
|
|
861
|
+
case "absent":
|
|
862
|
+
return `${kind.path.toString()} is absent`;
|
|
863
|
+
case "header":
|
|
864
|
+
return `header ${kind.key} ${kind.op} ${literalLabel(kind.literal)}`;
|
|
865
|
+
case "text":
|
|
866
|
+
return `${kind.path.toString()} ${kind.matcher.kind} ${kind.matcher.label()}`;
|
|
867
|
+
case "header_text":
|
|
868
|
+
return `header ${kind.key} ${kind.matcher.kind} ${kind.matcher.label()}`;
|
|
869
|
+
case "all":
|
|
870
|
+
case "any":
|
|
871
|
+
case "not":
|
|
872
|
+
return kind.kind;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function literalLabel(literal) {
|
|
876
|
+
switch (literal.kind) {
|
|
877
|
+
case "null":
|
|
878
|
+
return "null";
|
|
879
|
+
case "bool":
|
|
880
|
+
case "int":
|
|
881
|
+
case "float":
|
|
882
|
+
return String(literal.value);
|
|
883
|
+
case "text":
|
|
884
|
+
return JSON.stringify(literal.value);
|
|
885
|
+
case "list":
|
|
886
|
+
return `[${literal.items.map(literalLabel).join(", ")}]`;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function coercedLabel(literal) {
|
|
890
|
+
switch (literal.kind) {
|
|
891
|
+
case "instant":
|
|
892
|
+
return `${literal.micros.toString()}us`;
|
|
893
|
+
case "decimal":
|
|
894
|
+
return literal.decimal.toString();
|
|
895
|
+
case "list":
|
|
896
|
+
return `[${literal.items.map(coercedLabel).join(", ")}]`;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
//# sourceMappingURL=filter-eval.js.map
|