@jarenjs/json 0.9.2
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/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
package/src/path.js
ADDED
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
//#region JSONPath (RFC 9535)
|
|
2
|
+
// JSONPath: Query Expressions for JSON
|
|
3
|
+
// https://www.rfc-editor.org/rfc/rfc9535.html
|
|
4
|
+
//
|
|
5
|
+
// This module implements RFC 9535 as a two-stage compiler:
|
|
6
|
+
//
|
|
7
|
+
// 1. `parseJSONPath` - a strict, single-pass recursive-descent parser that
|
|
8
|
+
// turns a query string into an AST, enforcing the complete RFC grammar
|
|
9
|
+
// including the well-typedness rules of function expressions (2.4.3).
|
|
10
|
+
// 2. `compileJSONPath` - compiles the AST into a chain of specialized
|
|
11
|
+
// closures. All decisions (selector kind, sign of an index, literal
|
|
12
|
+
// regexps, singular-query detection) are taken at compile time so the
|
|
13
|
+
// returned query function does no interpretation at runtime.
|
|
14
|
+
//
|
|
15
|
+
// Runtime fast paths:
|
|
16
|
+
// - Singular queries (`$.a.b[3]`) compile to a direct property walk with
|
|
17
|
+
// no intermediate arrays.
|
|
18
|
+
// - Filter comparables compile to sentinel-returning getters; existence
|
|
19
|
+
// tests on singular queries never materialize nodelists.
|
|
20
|
+
// - `match()`/`search()` with a literal pattern precompile their RegExp;
|
|
21
|
+
// dynamic patterns use a per-callsite monomorphic cache.
|
|
22
|
+
// - Normalized-path production (RFC 9535 section 2.7) is compiled lazily,
|
|
23
|
+
// so value-only queries never pay for path-string building.
|
|
24
|
+
|
|
25
|
+
//#region constants & shared helpers
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
NOTHING,
|
|
29
|
+
isSingularSegments,
|
|
30
|
+
compileSingularGetter,
|
|
31
|
+
compileSegmentV,
|
|
32
|
+
runSegmentsV,
|
|
33
|
+
compileSegmentP,
|
|
34
|
+
runSegmentsP,
|
|
35
|
+
} from './segments.js';
|
|
36
|
+
import {
|
|
37
|
+
CC_TAB,
|
|
38
|
+
CC_LF,
|
|
39
|
+
CC_CR,
|
|
40
|
+
CC_SPACE,
|
|
41
|
+
CC_BANG,
|
|
42
|
+
CC_DQUOTE,
|
|
43
|
+
CC_DOLLAR,
|
|
44
|
+
CC_AMP,
|
|
45
|
+
CC_SQUOTE,
|
|
46
|
+
CC_LPAREN,
|
|
47
|
+
CC_RPAREN,
|
|
48
|
+
CC_STAR,
|
|
49
|
+
CC_COMMA,
|
|
50
|
+
CC_MINUS,
|
|
51
|
+
CC_DOT,
|
|
52
|
+
CC_SLASH,
|
|
53
|
+
CC_0,
|
|
54
|
+
CC_9,
|
|
55
|
+
CC_COLON,
|
|
56
|
+
CC_LT,
|
|
57
|
+
CC_EQ,
|
|
58
|
+
CC_GT,
|
|
59
|
+
CC_QUESTION,
|
|
60
|
+
CC_AT,
|
|
61
|
+
CC_LBRACKET,
|
|
62
|
+
CC_BACKSLASH,
|
|
63
|
+
CC_RBRACKET,
|
|
64
|
+
CC_UNDERSCORE,
|
|
65
|
+
CC_PIPE,
|
|
66
|
+
isDigitCode,
|
|
67
|
+
} from '@jarenjs/core/scan';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sentinel for the absence of a value ("Nothing" in RFC 9535 terms), as
|
|
71
|
+
* distinct from the JSON value `null`.
|
|
72
|
+
*/
|
|
73
|
+
export const JSONPATH_NOTHING = NOTHING;
|
|
74
|
+
|
|
75
|
+
function deepFreeze(value) {
|
|
76
|
+
if (typeof value !== 'object' || value === null)
|
|
77
|
+
return value;
|
|
78
|
+
const keys = Object.keys(value);
|
|
79
|
+
for (let i = 0; i < keys.length; i++)
|
|
80
|
+
deepFreeze(value[keys[i]]);
|
|
81
|
+
return Object.freeze(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Error thrown when a JSONPath query is not valid RFC 9535 syntax
|
|
86
|
+
* (including queries that are not well-typed per section 2.4.3).
|
|
87
|
+
*/
|
|
88
|
+
export class JSONPathSyntaxError extends SyntaxError {
|
|
89
|
+
constructor(message, source, position) {
|
|
90
|
+
super(`Invalid JSONPath: ${message} at position ${position} in '${source}'`);
|
|
91
|
+
this.name = 'JSONPathSyntaxError';
|
|
92
|
+
this.source = source;
|
|
93
|
+
this.position = position;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Built-in function extensions (RFC 9535 section 2.4).
|
|
98
|
+
// Parameter/return types: 'value' = ValueType, 'nodes' = NodesType,
|
|
99
|
+
// 'logical' = LogicalType.
|
|
100
|
+
const FUNCTIONS = {
|
|
101
|
+
length: { params: ['value'], returns: 'value' },
|
|
102
|
+
count: { params: ['nodes'], returns: 'value' },
|
|
103
|
+
match: { params: ['value', 'value'], returns: 'logical' },
|
|
104
|
+
search: { params: ['value', 'value'], returns: 'logical' },
|
|
105
|
+
value: { params: ['nodes'], returns: 'value' },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
|
|
110
|
+
//#region parser
|
|
111
|
+
|
|
112
|
+
function isNameFirstCode(c) {
|
|
113
|
+
return (c >= 0x41 && c <= 0x5A) // A-Z
|
|
114
|
+
|| (c >= 0x61 && c <= 0x7A) // a-z
|
|
115
|
+
|| c === CC_UNDERSCORE
|
|
116
|
+
|| c >= 0x80; // any non-ASCII code unit
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isNameCharCode(c) {
|
|
120
|
+
return isNameFirstCode(c) || isDigitCode(c);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Selects a named object member (RFC 9535 name selector).
|
|
125
|
+
* @typedef {Object} JSONPathNameSelector
|
|
126
|
+
* @property {'name'} kind - Discriminator
|
|
127
|
+
* @property {string} name - The member name to select
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Selects all members of an object / all elements of an array
|
|
132
|
+
* (RFC 9535 wildcard selector).
|
|
133
|
+
* @typedef {Object} JSONPathWildcardSelector
|
|
134
|
+
* @property {'wildcard'} kind - Discriminator
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Selects an array element by index; negative indexes count from the end
|
|
139
|
+
* (RFC 9535 index selector).
|
|
140
|
+
* @typedef {Object} JSONPathIndexSelector
|
|
141
|
+
* @property {'index'} kind - Discriminator
|
|
142
|
+
* @property {number} index - The array index to select
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Selects a range of array elements (RFC 9535 array slice selector).
|
|
147
|
+
* `null` means the bound was omitted in the query.
|
|
148
|
+
* @typedef {Object} JSONPathSliceSelector
|
|
149
|
+
* @property {'slice'} kind - Discriminator
|
|
150
|
+
* @property {number | null} start - Slice start, or null when omitted
|
|
151
|
+
* @property {number | null} end - Slice end (exclusive), or null when omitted
|
|
152
|
+
* @property {number | null} step - Slice step, or null when omitted
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Selects children for which a filter expression yields a truthy result
|
|
157
|
+
* (RFC 9535 filter selector).
|
|
158
|
+
* @typedef {Object} JSONPathFilterSelector
|
|
159
|
+
* @property {'filter'} kind - Discriminator
|
|
160
|
+
* @property {object} expr - The parsed filter expression tree
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Any RFC 9535 selector, discriminated by its `kind` property.
|
|
165
|
+
* @typedef {JSONPathNameSelector | JSONPathWildcardSelector | JSONPathIndexSelector | JSONPathSliceSelector | JSONPathFilterSelector} JSONPathSelector
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One segment of a JSONPath query: a child (`.` / `[...]`) or descendant
|
|
170
|
+
* (`..`) step holding one or more selectors.
|
|
171
|
+
* @typedef {Object} JSONPathSegment
|
|
172
|
+
* @property {boolean} descendant - True for a descendant (`..`) segment
|
|
173
|
+
* @property {JSONPathSelector[]} selectors - The segment's selectors
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The parsed AST of a JSONPath query.
|
|
178
|
+
* @typedef {Object} JSONPathAst
|
|
179
|
+
* @property {boolean} relative - True for a relative query (`@`), false for a root query (`$`)
|
|
180
|
+
* @property {JSONPathSegment[]} segments - The query's segments in order
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One result of a JSONPath query in nodes mode: the matched value together
|
|
185
|
+
* with its normalized path (RFC 9535 section 2.7).
|
|
186
|
+
* @typedef {Object} JSONPathNode
|
|
187
|
+
* @property {string} path - The normalized path (e.g. `$['store']['book'][0]`)
|
|
188
|
+
* @property {any} value - The matched value
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A compiled JSONPath query. Calling it returns the matched values; the
|
|
193
|
+
* attached methods expose the other result modes, and `source`/`ast`
|
|
194
|
+
* expose the original query string and its parsed (deeply frozen) AST.
|
|
195
|
+
* @typedef {((data: any) => any[]) & {
|
|
196
|
+
* values: (data: any) => any[],
|
|
197
|
+
* first: (data: any) => any,
|
|
198
|
+
* exists: (data: any) => boolean,
|
|
199
|
+
* nodes: (data: any) => JSONPathNode[],
|
|
200
|
+
* paths: (data: any) => string[],
|
|
201
|
+
* source: string,
|
|
202
|
+
* ast: JSONPathAst,
|
|
203
|
+
* }} JSONPathQuery
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse a JSONPath query string into an AST.
|
|
208
|
+
* @param {string} source - The JSONPath expression (e.g. `$.store.book[?@.price < 10].title`)
|
|
209
|
+
* @returns {JSONPathAst} The parsed query AST
|
|
210
|
+
* @throws {JSONPathSyntaxError} When the query violates the RFC 9535 grammar
|
|
211
|
+
*/
|
|
212
|
+
export function parseJSONPath(source) {
|
|
213
|
+
if (typeof source !== 'string')
|
|
214
|
+
throw new JSONPathSyntaxError('query must be a string', String(source), 0);
|
|
215
|
+
|
|
216
|
+
const len = source.length;
|
|
217
|
+
let pos = 0;
|
|
218
|
+
|
|
219
|
+
function fail(message, at = pos) {
|
|
220
|
+
throw new JSONPathSyntaxError(message, source, at);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cc(at) {
|
|
224
|
+
return at < len ? source.charCodeAt(at) : -1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function skipWS() {
|
|
228
|
+
while (pos < len) {
|
|
229
|
+
const c = source.charCodeAt(pos);
|
|
230
|
+
if (c !== CC_SPACE && c !== CC_TAB && c !== CC_LF && c !== CC_CR)
|
|
231
|
+
break;
|
|
232
|
+
pos++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#region scalar tokens
|
|
237
|
+
|
|
238
|
+
function parseIdentifier() {
|
|
239
|
+
const start = pos;
|
|
240
|
+
let c = cc(pos);
|
|
241
|
+
while ((c >= 0x61 && c <= 0x7A) || c === CC_UNDERSCORE || isDigitCode(c)) {
|
|
242
|
+
pos++;
|
|
243
|
+
c = cc(pos);
|
|
244
|
+
}
|
|
245
|
+
return source.slice(start, pos);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseMemberNameShorthand() {
|
|
249
|
+
const start = pos;
|
|
250
|
+
if (!isNameFirstCode(cc(pos)))
|
|
251
|
+
fail('expected member name');
|
|
252
|
+
while (pos < len) {
|
|
253
|
+
const c = source.charCodeAt(pos);
|
|
254
|
+
if (c >= 0xD800 && c <= 0xDFFF) {
|
|
255
|
+
// queries are sequences of Unicode scalar values (RFC 9535 2.1);
|
|
256
|
+
// raw surrogates must form a well-formed pair
|
|
257
|
+
const d = cc(pos + 1);
|
|
258
|
+
if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
|
|
259
|
+
fail('lone surrogate in member name');
|
|
260
|
+
pos += 2;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!isNameCharCode(c))
|
|
264
|
+
break;
|
|
265
|
+
pos++;
|
|
266
|
+
}
|
|
267
|
+
return source.slice(start, pos);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hex4() {
|
|
271
|
+
let value = 0;
|
|
272
|
+
for (let i = 0; i < 4; i++) {
|
|
273
|
+
const c = cc(pos);
|
|
274
|
+
let d;
|
|
275
|
+
if (c >= CC_0 && c <= CC_9) d = c - CC_0;
|
|
276
|
+
else if (c >= 0x41 && c <= 0x46) d = c - 0x37; // A-F
|
|
277
|
+
else if (c >= 0x61 && c <= 0x66) d = c - 0x57; // a-f
|
|
278
|
+
else return fail('invalid unicode escape');
|
|
279
|
+
value = (value << 4) | d;
|
|
280
|
+
pos++;
|
|
281
|
+
}
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseUnicodeEscape() {
|
|
286
|
+
const hi = hex4();
|
|
287
|
+
if (hi >= 0xD800 && hi <= 0xDBFF) {
|
|
288
|
+
// high surrogate must be directly followed by a low surrogate escape
|
|
289
|
+
if (cc(pos) !== CC_BACKSLASH || cc(pos + 1) !== 0x75 /* u */)
|
|
290
|
+
fail('lone surrogate in unicode escape');
|
|
291
|
+
pos += 2;
|
|
292
|
+
const lo = hex4();
|
|
293
|
+
if (lo < 0xDC00 || lo > 0xDFFF)
|
|
294
|
+
fail('invalid low surrogate in unicode escape');
|
|
295
|
+
return String.fromCharCode(hi, lo);
|
|
296
|
+
}
|
|
297
|
+
if (hi >= 0xDC00 && hi <= 0xDFFF)
|
|
298
|
+
fail('lone surrogate in unicode escape');
|
|
299
|
+
return String.fromCharCode(hi);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseEscape(quote) {
|
|
303
|
+
const c = cc(pos);
|
|
304
|
+
pos++;
|
|
305
|
+
switch (c) {
|
|
306
|
+
case 0x62: return '\b'; // \b
|
|
307
|
+
case 0x66: return '\f'; // \f
|
|
308
|
+
case 0x6E: return '\n'; // \n
|
|
309
|
+
case 0x72: return '\r'; // \r
|
|
310
|
+
case 0x74: return '\t'; // \t
|
|
311
|
+
case CC_SLASH: return '/';
|
|
312
|
+
case CC_BACKSLASH: return '\\';
|
|
313
|
+
case 0x75: return parseUnicodeEscape(); // \uXXXX
|
|
314
|
+
default:
|
|
315
|
+
if (c === quote)
|
|
316
|
+
return String.fromCharCode(quote);
|
|
317
|
+
return fail('invalid escape sequence', pos - 1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseStringLiteral() {
|
|
322
|
+
const quote = source.charCodeAt(pos);
|
|
323
|
+
pos++;
|
|
324
|
+
let out = '';
|
|
325
|
+
let chunk = pos;
|
|
326
|
+
while (pos < len) {
|
|
327
|
+
const c = source.charCodeAt(pos);
|
|
328
|
+
if (c === quote) {
|
|
329
|
+
out += source.slice(chunk, pos);
|
|
330
|
+
pos++;
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
if (c === CC_BACKSLASH) {
|
|
334
|
+
out += source.slice(chunk, pos);
|
|
335
|
+
pos++;
|
|
336
|
+
out += parseEscape(quote);
|
|
337
|
+
chunk = pos;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (c < CC_SPACE)
|
|
341
|
+
fail('unescaped control character in string literal');
|
|
342
|
+
if (c >= 0xD800 && c <= 0xDFFF) {
|
|
343
|
+
// raw surrogates must form a well-formed pair (RFC 9535 2.1)
|
|
344
|
+
const d = cc(pos + 1);
|
|
345
|
+
if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
|
|
346
|
+
fail('lone surrogate in string literal');
|
|
347
|
+
pos += 2;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
pos++;
|
|
351
|
+
}
|
|
352
|
+
return fail('unterminated string literal');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// int per RFC: "0" or ["-"] 1-9 *DIGIT; no leading zeros, no -0
|
|
356
|
+
function parseIntToken() {
|
|
357
|
+
const start = pos;
|
|
358
|
+
let neg = false;
|
|
359
|
+
if (cc(pos) === CC_MINUS) {
|
|
360
|
+
neg = true;
|
|
361
|
+
pos++;
|
|
362
|
+
}
|
|
363
|
+
const first = cc(pos);
|
|
364
|
+
if (!isDigitCode(first))
|
|
365
|
+
fail('expected integer');
|
|
366
|
+
pos++;
|
|
367
|
+
if (first === CC_0) {
|
|
368
|
+
if (isDigitCode(cc(pos)))
|
|
369
|
+
fail('leading zeros are not allowed', start);
|
|
370
|
+
if (neg)
|
|
371
|
+
fail("'-0' is not allowed as an index", start);
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
while (isDigitCode(cc(pos)))
|
|
375
|
+
pos++;
|
|
376
|
+
const value = Number(source.slice(start, pos));
|
|
377
|
+
if (!Number.isSafeInteger(value))
|
|
378
|
+
fail('integer out of interoperable range', start);
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// number per RFC: (int / "-0") [frac] [exp]
|
|
383
|
+
function parseNumberLiteral() {
|
|
384
|
+
const start = pos;
|
|
385
|
+
if (cc(pos) === CC_MINUS)
|
|
386
|
+
pos++;
|
|
387
|
+
if (!isDigitCode(cc(pos)))
|
|
388
|
+
fail('expected digit in number literal');
|
|
389
|
+
if (cc(pos) === CC_0) {
|
|
390
|
+
pos++;
|
|
391
|
+
if (isDigitCode(cc(pos)))
|
|
392
|
+
fail('leading zeros are not allowed', start);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
while (isDigitCode(cc(pos)))
|
|
396
|
+
pos++;
|
|
397
|
+
}
|
|
398
|
+
if (cc(pos) === CC_DOT) {
|
|
399
|
+
pos++;
|
|
400
|
+
if (!isDigitCode(cc(pos)))
|
|
401
|
+
fail('expected digit after decimal point');
|
|
402
|
+
while (isDigitCode(cc(pos)))
|
|
403
|
+
pos++;
|
|
404
|
+
}
|
|
405
|
+
const e = cc(pos);
|
|
406
|
+
if (e === 0x65 || e === 0x45) { // e | E
|
|
407
|
+
pos++;
|
|
408
|
+
const s = cc(pos);
|
|
409
|
+
if (s === 0x2B || s === CC_MINUS)
|
|
410
|
+
pos++;
|
|
411
|
+
if (!isDigitCode(cc(pos)))
|
|
412
|
+
fail('expected digit in exponent');
|
|
413
|
+
while (isDigitCode(cc(pos)))
|
|
414
|
+
pos++;
|
|
415
|
+
}
|
|
416
|
+
return Number(source.slice(start, pos));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
|
|
421
|
+
//#region segments & selectors
|
|
422
|
+
|
|
423
|
+
function parseSegments() {
|
|
424
|
+
const segments = [];
|
|
425
|
+
for (;;) {
|
|
426
|
+
const save = pos;
|
|
427
|
+
skipWS();
|
|
428
|
+
const c = cc(pos);
|
|
429
|
+
if (c === CC_LBRACKET) {
|
|
430
|
+
segments.push(parseBracketed(false));
|
|
431
|
+
}
|
|
432
|
+
else if (c === CC_DOT) {
|
|
433
|
+
if (cc(pos + 1) === CC_DOT) {
|
|
434
|
+
pos += 2;
|
|
435
|
+
segments.push(parseDescendant());
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
pos += 1;
|
|
439
|
+
segments.push(parseChildShorthand());
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
pos = save;
|
|
444
|
+
return segments;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parseChildShorthand() {
|
|
450
|
+
// no whitespace allowed after '.'
|
|
451
|
+
if (cc(pos) === CC_STAR) {
|
|
452
|
+
pos++;
|
|
453
|
+
return { descendant: false, selectors: [{ kind: 'wildcard' }] };
|
|
454
|
+
}
|
|
455
|
+
return { descendant: false, selectors: [{ kind: 'name', name: parseMemberNameShorthand() }] };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function parseDescendant() {
|
|
459
|
+
// no whitespace allowed after '..'
|
|
460
|
+
const c = cc(pos);
|
|
461
|
+
if (c === CC_LBRACKET)
|
|
462
|
+
return parseBracketed(true);
|
|
463
|
+
if (c === CC_STAR) {
|
|
464
|
+
pos++;
|
|
465
|
+
return { descendant: true, selectors: [{ kind: 'wildcard' }] };
|
|
466
|
+
}
|
|
467
|
+
return { descendant: true, selectors: [{ kind: 'name', name: parseMemberNameShorthand() }] };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseBracketed(descendant) {
|
|
471
|
+
pos++; // consume '['
|
|
472
|
+
skipWS();
|
|
473
|
+
const selectors = [];
|
|
474
|
+
for (;;) {
|
|
475
|
+
selectors.push(parseSelector());
|
|
476
|
+
skipWS();
|
|
477
|
+
const c = cc(pos);
|
|
478
|
+
if (c === CC_COMMA) {
|
|
479
|
+
pos++;
|
|
480
|
+
skipWS();
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (c === CC_RBRACKET) {
|
|
484
|
+
pos++;
|
|
485
|
+
return { descendant, selectors };
|
|
486
|
+
}
|
|
487
|
+
fail("expected ',' or ']'");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseSelector() {
|
|
492
|
+
const c = cc(pos);
|
|
493
|
+
if (c === CC_SQUOTE || c === CC_DQUOTE)
|
|
494
|
+
return { kind: 'name', name: parseStringLiteral() };
|
|
495
|
+
if (c === CC_STAR) {
|
|
496
|
+
pos++;
|
|
497
|
+
return { kind: 'wildcard' };
|
|
498
|
+
}
|
|
499
|
+
if (c === CC_QUESTION) {
|
|
500
|
+
pos++;
|
|
501
|
+
skipWS();
|
|
502
|
+
return { kind: 'filter', expr: parseLogicalOr() };
|
|
503
|
+
}
|
|
504
|
+
if (c === CC_COLON || c === CC_MINUS || isDigitCode(c))
|
|
505
|
+
return parseIndexOrSlice();
|
|
506
|
+
return fail('expected a selector');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function parseIndexOrSlice() {
|
|
510
|
+
let start = null;
|
|
511
|
+
if (cc(pos) !== CC_COLON)
|
|
512
|
+
start = parseIntToken();
|
|
513
|
+
let save = pos;
|
|
514
|
+
skipWS();
|
|
515
|
+
if (cc(pos) !== CC_COLON) {
|
|
516
|
+
pos = save;
|
|
517
|
+
return { kind: 'index', index: start };
|
|
518
|
+
}
|
|
519
|
+
pos++; // consume ':'
|
|
520
|
+
skipWS();
|
|
521
|
+
let end = null;
|
|
522
|
+
let c = cc(pos);
|
|
523
|
+
if (c === CC_MINUS || isDigitCode(c))
|
|
524
|
+
end = parseIntToken();
|
|
525
|
+
save = pos;
|
|
526
|
+
skipWS();
|
|
527
|
+
let step = null;
|
|
528
|
+
if (cc(pos) === CC_COLON) {
|
|
529
|
+
pos++;
|
|
530
|
+
skipWS();
|
|
531
|
+
c = cc(pos);
|
|
532
|
+
if (c === CC_MINUS || isDigitCode(c))
|
|
533
|
+
step = parseIntToken();
|
|
534
|
+
// no step after ':' is fine; trailing WS belongs to the bracket
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
pos = save;
|
|
538
|
+
}
|
|
539
|
+
return { kind: 'slice', start, end, step };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
|
|
544
|
+
//#region filter expressions
|
|
545
|
+
|
|
546
|
+
function parseLogicalOr() {
|
|
547
|
+
let expr = parseLogicalAnd();
|
|
548
|
+
for (;;) {
|
|
549
|
+
const save = pos;
|
|
550
|
+
skipWS();
|
|
551
|
+
if (cc(pos) === CC_PIPE && cc(pos + 1) === CC_PIPE) {
|
|
552
|
+
pos += 2;
|
|
553
|
+
skipWS();
|
|
554
|
+
const right = parseLogicalAnd();
|
|
555
|
+
expr = expr.kind === 'or'
|
|
556
|
+
? { kind: 'or', operands: [...expr.operands, right] }
|
|
557
|
+
: { kind: 'or', operands: [expr, right] };
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
pos = save;
|
|
561
|
+
return expr;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function parseLogicalAnd() {
|
|
567
|
+
let expr = parseBasicExpr();
|
|
568
|
+
for (;;) {
|
|
569
|
+
const save = pos;
|
|
570
|
+
skipWS();
|
|
571
|
+
if (cc(pos) === CC_AMP && cc(pos + 1) === CC_AMP) {
|
|
572
|
+
pos += 2;
|
|
573
|
+
skipWS();
|
|
574
|
+
const right = parseBasicExpr();
|
|
575
|
+
expr = expr.kind === 'and'
|
|
576
|
+
? { kind: 'and', operands: [...expr.operands, right] }
|
|
577
|
+
: { kind: 'and', operands: [expr, right] };
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
pos = save;
|
|
581
|
+
return expr;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function parseBasicExpr() {
|
|
587
|
+
const c = cc(pos);
|
|
588
|
+
if (c === CC_BANG) {
|
|
589
|
+
pos++;
|
|
590
|
+
skipWS();
|
|
591
|
+
return { kind: 'not', operand: parseNegatable() };
|
|
592
|
+
}
|
|
593
|
+
if (c === CC_LPAREN)
|
|
594
|
+
return parseParen();
|
|
595
|
+
return parseComparisonOrTest();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function parseNegatable() {
|
|
599
|
+
const c = cc(pos);
|
|
600
|
+
if (c === CC_LPAREN)
|
|
601
|
+
return parseParen();
|
|
602
|
+
if (c === CC_AT || c === CC_DOLLAR)
|
|
603
|
+
return { kind: 'exists', query: parseFilterQuery() };
|
|
604
|
+
if (c >= 0x61 && c <= 0x7A) {
|
|
605
|
+
const at = pos;
|
|
606
|
+
const ident = parseIdentifier();
|
|
607
|
+
if (cc(pos) !== CC_LPAREN)
|
|
608
|
+
fail('expected a test expression after !', at);
|
|
609
|
+
const func = parseFunctionExpr(ident, at);
|
|
610
|
+
if (func.returns === 'value')
|
|
611
|
+
fail('a function of type ValueType cannot be used as a test expression', at);
|
|
612
|
+
return { kind: 'ftest', func };
|
|
613
|
+
}
|
|
614
|
+
return fail('expected a test expression after !');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function parseParen() {
|
|
618
|
+
pos++; // consume '('
|
|
619
|
+
skipWS();
|
|
620
|
+
const expr = parseLogicalOr();
|
|
621
|
+
skipWS();
|
|
622
|
+
if (cc(pos) !== CC_RPAREN)
|
|
623
|
+
fail("expected ')'");
|
|
624
|
+
pos++;
|
|
625
|
+
return expr;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function tryParseCompOp() {
|
|
629
|
+
const c = cc(pos);
|
|
630
|
+
if (c === CC_EQ) {
|
|
631
|
+
if (cc(pos + 1) !== CC_EQ)
|
|
632
|
+
fail("expected '=='");
|
|
633
|
+
pos += 2;
|
|
634
|
+
return '==';
|
|
635
|
+
}
|
|
636
|
+
if (c === CC_BANG) {
|
|
637
|
+
if (cc(pos + 1) !== CC_EQ)
|
|
638
|
+
return null;
|
|
639
|
+
pos += 2;
|
|
640
|
+
return '!=';
|
|
641
|
+
}
|
|
642
|
+
if (c === CC_LT) {
|
|
643
|
+
if (cc(pos + 1) === CC_EQ) {
|
|
644
|
+
pos += 2;
|
|
645
|
+
return '<=';
|
|
646
|
+
}
|
|
647
|
+
pos += 1;
|
|
648
|
+
return '<';
|
|
649
|
+
}
|
|
650
|
+
if (c === CC_GT) {
|
|
651
|
+
if (cc(pos + 1) === CC_EQ) {
|
|
652
|
+
pos += 2;
|
|
653
|
+
return '>=';
|
|
654
|
+
}
|
|
655
|
+
pos += 1;
|
|
656
|
+
return '>';
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function parseFilterQuery() {
|
|
662
|
+
const relative = cc(pos) === CC_AT;
|
|
663
|
+
pos++; // consume '@' or '$'
|
|
664
|
+
return { relative, segments: parseSegments() };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function requireSingular(query, at, what) {
|
|
668
|
+
if (!isSingularSegments(query.segments))
|
|
669
|
+
fail(`${what} requires a singular query`, at);
|
|
670
|
+
return query;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function parseComparisonOrTest() {
|
|
674
|
+
const at = pos;
|
|
675
|
+
const c = cc(pos);
|
|
676
|
+
if (c === CC_AT || c === CC_DOLLAR) {
|
|
677
|
+
const query = parseFilterQuery();
|
|
678
|
+
const save = pos;
|
|
679
|
+
skipWS();
|
|
680
|
+
const op = tryParseCompOp();
|
|
681
|
+
if (op === null) {
|
|
682
|
+
pos = save;
|
|
683
|
+
return { kind: 'exists', query };
|
|
684
|
+
}
|
|
685
|
+
requireSingular(query, at, 'a comparison');
|
|
686
|
+
skipWS();
|
|
687
|
+
return { kind: 'cmp', op, left: { kind: 'query', query }, right: parseComparable() };
|
|
688
|
+
}
|
|
689
|
+
if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c)) {
|
|
690
|
+
const left = parseLiteralComparable();
|
|
691
|
+
skipWS();
|
|
692
|
+
const op = tryParseCompOp();
|
|
693
|
+
if (op === null)
|
|
694
|
+
fail('a literal must be part of a comparison', at);
|
|
695
|
+
skipWS();
|
|
696
|
+
return { kind: 'cmp', op, left, right: parseComparable() };
|
|
697
|
+
}
|
|
698
|
+
if (c >= 0x61 && c <= 0x7A) {
|
|
699
|
+
const ident = parseIdentifier();
|
|
700
|
+
if (cc(pos) === CC_LPAREN) {
|
|
701
|
+
const func = parseFunctionExpr(ident, at);
|
|
702
|
+
const save = pos;
|
|
703
|
+
skipWS();
|
|
704
|
+
const op = tryParseCompOp();
|
|
705
|
+
if (op === null) {
|
|
706
|
+
pos = save;
|
|
707
|
+
if (func.returns === 'value')
|
|
708
|
+
fail('a function of type ValueType cannot be used as a test expression', at);
|
|
709
|
+
return { kind: 'ftest', func };
|
|
710
|
+
}
|
|
711
|
+
if (func.returns !== 'value')
|
|
712
|
+
fail('only functions of type ValueType can be compared', at);
|
|
713
|
+
skipWS();
|
|
714
|
+
return { kind: 'cmp', op, left: func, right: parseComparable() };
|
|
715
|
+
}
|
|
716
|
+
if (ident === 'true' || ident === 'false' || ident === 'null') {
|
|
717
|
+
skipWS();
|
|
718
|
+
const op = tryParseCompOp();
|
|
719
|
+
if (op === null)
|
|
720
|
+
fail('a literal must be part of a comparison', at);
|
|
721
|
+
skipWS();
|
|
722
|
+
const value = ident === 'true' ? true : ident === 'false' ? false : null;
|
|
723
|
+
return { kind: 'cmp', op, left: { kind: 'literal', value }, right: parseComparable() };
|
|
724
|
+
}
|
|
725
|
+
return fail(`unexpected identifier '${ident}'`, at);
|
|
726
|
+
}
|
|
727
|
+
return fail('expected a filter expression');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function parseLiteralComparable() {
|
|
731
|
+
const c = cc(pos);
|
|
732
|
+
if (c === CC_SQUOTE || c === CC_DQUOTE)
|
|
733
|
+
return { kind: 'literal', value: parseStringLiteral() };
|
|
734
|
+
return { kind: 'literal', value: parseNumberLiteral() };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parseComparable() {
|
|
738
|
+
const at = pos;
|
|
739
|
+
const c = cc(pos);
|
|
740
|
+
if (c === CC_AT || c === CC_DOLLAR) {
|
|
741
|
+
const query = requireSingular(parseFilterQuery(), at, 'a comparison');
|
|
742
|
+
return { kind: 'query', query };
|
|
743
|
+
}
|
|
744
|
+
if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c))
|
|
745
|
+
return parseLiteralComparable();
|
|
746
|
+
if (c >= 0x61 && c <= 0x7A) {
|
|
747
|
+
const ident = parseIdentifier();
|
|
748
|
+
if (cc(pos) === CC_LPAREN) {
|
|
749
|
+
const func = parseFunctionExpr(ident, at);
|
|
750
|
+
if (func.returns !== 'value')
|
|
751
|
+
fail('only functions of type ValueType can be compared', at);
|
|
752
|
+
return func;
|
|
753
|
+
}
|
|
754
|
+
if (ident === 'true') return { kind: 'literal', value: true };
|
|
755
|
+
if (ident === 'false') return { kind: 'literal', value: false };
|
|
756
|
+
if (ident === 'null') return { kind: 'literal', value: null };
|
|
757
|
+
return fail(`unexpected identifier '${ident}'`, at);
|
|
758
|
+
}
|
|
759
|
+
return fail('expected a comparable expression');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function parseFunctionArg() {
|
|
763
|
+
const at = pos;
|
|
764
|
+
const c = cc(pos);
|
|
765
|
+
if (c === CC_AT || c === CC_DOLLAR)
|
|
766
|
+
return { kind: 'query', query: parseFilterQuery() };
|
|
767
|
+
if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c))
|
|
768
|
+
return parseLiteralComparable();
|
|
769
|
+
if (c >= 0x61 && c <= 0x7A) {
|
|
770
|
+
const ident = parseIdentifier();
|
|
771
|
+
if (cc(pos) === CC_LPAREN)
|
|
772
|
+
return parseFunctionExpr(ident, at);
|
|
773
|
+
if (ident === 'true') return { kind: 'literal', value: true };
|
|
774
|
+
if (ident === 'false') return { kind: 'literal', value: false };
|
|
775
|
+
if (ident === 'null') return { kind: 'literal', value: null };
|
|
776
|
+
return fail(`unexpected identifier '${ident}'`, at);
|
|
777
|
+
}
|
|
778
|
+
return fail('expected a function argument');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function parseFunctionExpr(name, at) {
|
|
782
|
+
const def = FUNCTIONS[name];
|
|
783
|
+
if (def === undefined)
|
|
784
|
+
fail(`unknown function '${name}'`, at);
|
|
785
|
+
pos++; // consume '('
|
|
786
|
+
skipWS();
|
|
787
|
+
const args = [];
|
|
788
|
+
if (cc(pos) !== CC_RPAREN) {
|
|
789
|
+
for (;;) {
|
|
790
|
+
args.push(parseFunctionArg());
|
|
791
|
+
skipWS();
|
|
792
|
+
if (cc(pos) === CC_COMMA) {
|
|
793
|
+
pos++;
|
|
794
|
+
skipWS();
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (cc(pos) !== CC_RPAREN)
|
|
801
|
+
fail("expected ')'");
|
|
802
|
+
pos++;
|
|
803
|
+
if (args.length !== def.params.length)
|
|
804
|
+
fail(`function '${name}' expects ${def.params.length} argument(s), got ${args.length}`, at);
|
|
805
|
+
for (let i = 0; i < args.length; i++) {
|
|
806
|
+
const param = def.params[i];
|
|
807
|
+
const arg = args[i];
|
|
808
|
+
if (param === 'value') {
|
|
809
|
+
if (arg.kind === 'literal')
|
|
810
|
+
continue;
|
|
811
|
+
if (arg.kind === 'query') {
|
|
812
|
+
requireSingular(arg.query, at, `argument ${i + 1} of '${name}'`);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (arg.kind === 'func' && arg.returns === 'value')
|
|
816
|
+
continue;
|
|
817
|
+
fail(`argument ${i + 1} of '${name}' must be of type ValueType`, at);
|
|
818
|
+
}
|
|
819
|
+
else { // 'nodes'
|
|
820
|
+
if (arg.kind === 'query')
|
|
821
|
+
continue;
|
|
822
|
+
if (arg.kind === 'func' && arg.returns === 'nodes')
|
|
823
|
+
continue;
|
|
824
|
+
fail(`argument ${i + 1} of '${name}' must be a query (NodesType)`, at);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return { kind: 'func', name, args, returns: def.returns };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
//#endregion
|
|
831
|
+
|
|
832
|
+
// jsonpath-query = root-identifier segments
|
|
833
|
+
if (len === 0)
|
|
834
|
+
fail('empty query', 0);
|
|
835
|
+
if (source.charCodeAt(0) !== CC_DOLLAR)
|
|
836
|
+
fail("query must start with '$'", 0);
|
|
837
|
+
pos = 1;
|
|
838
|
+
const segments = parseSegments();
|
|
839
|
+
if (pos !== len)
|
|
840
|
+
fail('unexpected token');
|
|
841
|
+
return { relative: false, segments };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
//#endregion
|
|
845
|
+
|
|
846
|
+
// The nodes-mode segment compilers (normalized paths, RFC 9535 section
|
|
847
|
+
// 2.7) live in segments.js next to the values-mode ones; nodes mode is
|
|
848
|
+
// still compiled lazily here (see compileJSONPath), so value-only
|
|
849
|
+
// queries never pay for path-string building.
|
|
850
|
+
|
|
851
|
+
//#region public API
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Compile a JSONPath query (RFC 9535) into a reusable query function.
|
|
855
|
+
*
|
|
856
|
+
* The returned function applies the query to a JSON value and returns
|
|
857
|
+
* the resulting nodelist as an array of values. It also carries helper
|
|
858
|
+
* methods:
|
|
859
|
+
*
|
|
860
|
+
* - `query(data)` / `query.values(data)` - array of matched values
|
|
861
|
+
* - `query.first(data)` - first matched value, or `undefined`
|
|
862
|
+
* - `query.exists(data)` - true when the query selects at least one node
|
|
863
|
+
* - `query.nodes(data)` - array of `{ path, value }` with normalized paths
|
|
864
|
+
* - `query.paths(data)` - array of normalized paths (RFC 9535 section 2.7)
|
|
865
|
+
* - `query.source` - the original query string
|
|
866
|
+
* - `query.ast` - the parsed query AST (deeply frozen; the lazily
|
|
867
|
+
* compiled path mode must agree with the eagerly compiled value mode)
|
|
868
|
+
*
|
|
869
|
+
* @param {string} source - The JSONPath expression
|
|
870
|
+
* @returns {JSONPathQuery} The compiled query function
|
|
871
|
+
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
872
|
+
* @example
|
|
873
|
+
* const q = compileJSONPath('$.store.book[?@.price < 10].title');
|
|
874
|
+
* q(data); // ['Sayings of the Century', 'Moby Dick']
|
|
875
|
+
* q.paths(data); // ["$['store']['book'][0]['title']", ...]
|
|
876
|
+
*/
|
|
877
|
+
export function compileJSONPath(source) {
|
|
878
|
+
const ast = deepFreeze(parseJSONPath(source));
|
|
879
|
+
const segments = ast.segments;
|
|
880
|
+
|
|
881
|
+
let values, first, exists;
|
|
882
|
+
if (isSingularSegments(segments)) {
|
|
883
|
+
const getter = compileSingularGetter(segments, false);
|
|
884
|
+
values = (data) => {
|
|
885
|
+
const v = getter(data, data);
|
|
886
|
+
return v === NOTHING ? [] : [v];
|
|
887
|
+
};
|
|
888
|
+
first = (data) => {
|
|
889
|
+
const v = getter(data, data);
|
|
890
|
+
return v === NOTHING ? undefined : v;
|
|
891
|
+
};
|
|
892
|
+
exists = (data) => getter(data, data) !== NOTHING;
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
const segs = segments.map(compileSegmentV);
|
|
896
|
+
values = (data) => runSegmentsV(segs, data, data);
|
|
897
|
+
first = (data) => {
|
|
898
|
+
const result = runSegmentsV(segs, data, data);
|
|
899
|
+
return result.length !== 0 ? result[0] : undefined;
|
|
900
|
+
};
|
|
901
|
+
exists = (data) => runSegmentsV(segs, data, data).length !== 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// nodes mode is compiled lazily; value-only queries never pay for it
|
|
905
|
+
let segsP = null;
|
|
906
|
+
function runNodes(data) {
|
|
907
|
+
if (segsP === null)
|
|
908
|
+
segsP = segments.map(compileSegmentP);
|
|
909
|
+
return runSegmentsP(segsP, data, '$', data);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const query = (data) => values(data);
|
|
913
|
+
query.values = values;
|
|
914
|
+
query.first = first;
|
|
915
|
+
query.exists = exists;
|
|
916
|
+
query.nodes = (data) => {
|
|
917
|
+
const { vals, paths } = runNodes(data);
|
|
918
|
+
const nodes = new Array(vals.length);
|
|
919
|
+
for (let i = 0; i < vals.length; i++)
|
|
920
|
+
nodes[i] = { path: paths[i], value: vals[i] };
|
|
921
|
+
return nodes;
|
|
922
|
+
};
|
|
923
|
+
query.paths = (data) => runNodes(data).paths;
|
|
924
|
+
query.source = source;
|
|
925
|
+
query.ast = ast;
|
|
926
|
+
return query;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const QUERY_CACHE = new Map();
|
|
930
|
+
const QUERY_CACHE_LIMIT = 512;
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Apply a JSONPath query to a JSON value in one call. Compiled queries
|
|
934
|
+
* are cached (FIFO, 512 entries), so repeated calls with the same query
|
|
935
|
+
* string reuse the compiled function.
|
|
936
|
+
* @param {string} source - The JSONPath expression
|
|
937
|
+
* @param {any} data - The JSON value to query
|
|
938
|
+
* @returns {any[]} Array of matched values
|
|
939
|
+
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
940
|
+
*/
|
|
941
|
+
export function queryJSONPath(source, data) {
|
|
942
|
+
let query = QUERY_CACHE.get(source);
|
|
943
|
+
if (query === undefined) {
|
|
944
|
+
query = compileJSONPath(source);
|
|
945
|
+
if (QUERY_CACHE.size >= QUERY_CACHE_LIMIT)
|
|
946
|
+
QUERY_CACHE.delete(QUERY_CACHE.keys().next().value);
|
|
947
|
+
QUERY_CACHE.set(source, query);
|
|
948
|
+
}
|
|
949
|
+
return query(data);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Validates a JSONPath expression strictly against the RFC 9535 grammar,
|
|
954
|
+
* including well-typedness of function expressions. Unlike the heuristic
|
|
955
|
+
* `isValidJSONPath` in basic.js, this uses the full parser.
|
|
956
|
+
* @param {string} str - The JSONPath expression to validate
|
|
957
|
+
* @returns {boolean} True when the string is a valid RFC 9535 query
|
|
958
|
+
* @example
|
|
959
|
+
* isValidJSONPathStrict('$.store.book[?@.price < 10]'); // true
|
|
960
|
+
* isValidJSONPathStrict('$.store.book[0 5]'); // false
|
|
961
|
+
* isValidJSONPathStrict('@.name'); // false (queries start at $)
|
|
962
|
+
*/
|
|
963
|
+
export function isValidJSONPathStrict(str) {
|
|
964
|
+
if (typeof str !== 'string')
|
|
965
|
+
return false;
|
|
966
|
+
try {
|
|
967
|
+
parseJSONPath(str);
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
//#endregion
|
|
976
|
+
|
|
977
|
+
//#endregion
|