@irvinebroque/http-rfc-utils 0.1.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 +21 -0
- package/README.md +222 -0
- package/dist/auth.d.ts +139 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +991 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache-status.d.ts +15 -0
- package/dist/cache-status.d.ts.map +1 -0
- package/dist/cache-status.js +152 -0
- package/dist/cache-status.js.map +1 -0
- package/dist/cache.d.ts +94 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +244 -0
- package/dist/cache.js.map +1 -0
- package/dist/client-hints.d.ts +23 -0
- package/dist/client-hints.d.ts.map +1 -0
- package/dist/client-hints.js +81 -0
- package/dist/client-hints.js.map +1 -0
- package/dist/conditional.d.ts +97 -0
- package/dist/conditional.d.ts.map +1 -0
- package/dist/conditional.js +300 -0
- package/dist/conditional.js.map +1 -0
- package/dist/content-disposition.d.ts +23 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +122 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/cookie.d.ts +43 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +472 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +53 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +170 -0
- package/dist/cors.js.map +1 -0
- package/dist/datetime.d.ts +53 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +205 -0
- package/dist/datetime.js.map +1 -0
- package/dist/digest.d.ts +220 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +355 -0
- package/dist/digest.js.map +1 -0
- package/dist/encoding.d.ts +14 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +86 -0
- package/dist/encoding.js.map +1 -0
- package/dist/etag.d.ts +55 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +182 -0
- package/dist/etag.js.map +1 -0
- package/dist/ext-value.d.ts +40 -0
- package/dist/ext-value.d.ts.map +1 -0
- package/dist/ext-value.js +119 -0
- package/dist/ext-value.js.map +1 -0
- package/dist/forwarded.d.ts +14 -0
- package/dist/forwarded.d.ts.map +1 -0
- package/dist/forwarded.js +93 -0
- package/dist/forwarded.js.map +1 -0
- package/dist/header-utils.d.ts +71 -0
- package/dist/header-utils.d.ts.map +1 -0
- package/dist/header-utils.js +143 -0
- package/dist/header-utils.js.map +1 -0
- package/dist/headers.d.ts +71 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +134 -0
- package/dist/headers.js.map +1 -0
- package/dist/hsts.d.ts +15 -0
- package/dist/hsts.d.ts.map +1 -0
- package/dist/hsts.js +106 -0
- package/dist/hsts.js.map +1 -0
- package/dist/http-signatures.d.ts +202 -0
- package/dist/http-signatures.d.ts.map +1 -0
- package/dist/http-signatures.js +720 -0
- package/dist/http-signatures.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/json-pointer.d.ts +97 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +278 -0
- package/dist/json-pointer.js.map +1 -0
- package/dist/jsonpath.d.ts +98 -0
- package/dist/jsonpath.d.ts.map +1 -0
- package/dist/jsonpath.js +1470 -0
- package/dist/jsonpath.js.map +1 -0
- package/dist/language.d.ts +14 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +95 -0
- package/dist/language.js.map +1 -0
- package/dist/link.d.ts +102 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +437 -0
- package/dist/link.js.map +1 -0
- package/dist/linkset.d.ts +111 -0
- package/dist/linkset.d.ts.map +1 -0
- package/dist/linkset.js +501 -0
- package/dist/linkset.js.map +1 -0
- package/dist/negotiate.d.ts +71 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/negotiate.js +357 -0
- package/dist/negotiate.js.map +1 -0
- package/dist/pagination.d.ts +80 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +188 -0
- package/dist/pagination.js.map +1 -0
- package/dist/prefer.d.ts +18 -0
- package/dist/prefer.d.ts.map +1 -0
- package/dist/prefer.js +93 -0
- package/dist/prefer.js.map +1 -0
- package/dist/problem.d.ts +54 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +104 -0
- package/dist/problem.js.map +1 -0
- package/dist/proxy-status.d.ts +28 -0
- package/dist/proxy-status.d.ts.map +1 -0
- package/dist/proxy-status.js +220 -0
- package/dist/proxy-status.js.map +1 -0
- package/dist/range.d.ts +28 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +243 -0
- package/dist/range.js.map +1 -0
- package/dist/response.d.ts +101 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +200 -0
- package/dist/response.js.map +1 -0
- package/dist/sorting.d.ts +66 -0
- package/dist/sorting.d.ts.map +1 -0
- package/dist/sorting.js +168 -0
- package/dist/sorting.js.map +1 -0
- package/dist/structured-fields.d.ts +30 -0
- package/dist/structured-fields.d.ts.map +1 -0
- package/dist/structured-fields.js +468 -0
- package/dist/structured-fields.js.map +1 -0
- package/dist/types.d.ts +772 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/uri-template.d.ts +48 -0
- package/dist/uri-template.d.ts.map +1 -0
- package/dist/uri-template.js +483 -0
- package/dist/uri-template.js.map +1 -0
- package/dist/uri.d.ts +80 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +423 -0
- package/dist/uri.js.map +1 -0
- package/package.json +66 -0
package/dist/jsonpath.js
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONPath query expressions per RFC 9535.
|
|
3
|
+
* RFC 9535 §§2.1-2.7.
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html
|
|
5
|
+
*
|
|
6
|
+
* Implements:
|
|
7
|
+
* - Root identifier ($) and current node identifier (@)
|
|
8
|
+
* - Name, wildcard, index, slice, and filter selectors
|
|
9
|
+
* - Child and descendant segments
|
|
10
|
+
* - Built-in functions: length(), count(), match(), search(), value()
|
|
11
|
+
* - Normalized path formatting
|
|
12
|
+
*
|
|
13
|
+
* Out of scope:
|
|
14
|
+
* - Custom function extensions (only built-in functions supported)
|
|
15
|
+
* - Full I-Regexp (RFC 9485) validation (uses JavaScript RegExp)
|
|
16
|
+
*/
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Constants
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// RFC 9535 §2.1: Integers MUST be within I-JSON range
|
|
21
|
+
const I_JSON_MIN = -(2 ** 53) + 1; // -9007199254740991
|
|
22
|
+
const I_JSON_MAX = 2 ** 53 - 1; // 9007199254740991
|
|
23
|
+
// Built-in function names (RFC 9535 §2.4)
|
|
24
|
+
const BUILTIN_FUNCTIONS = new Set([
|
|
25
|
+
'length', 'count', 'match', 'search', 'value'
|
|
26
|
+
]);
|
|
27
|
+
class Lexer {
|
|
28
|
+
input;
|
|
29
|
+
pos = 0;
|
|
30
|
+
tokens = [];
|
|
31
|
+
tokenIndex = 0;
|
|
32
|
+
constructor(input) {
|
|
33
|
+
this.input = input;
|
|
34
|
+
this.tokenize();
|
|
35
|
+
}
|
|
36
|
+
tokenize() {
|
|
37
|
+
while (this.pos < this.input.length) {
|
|
38
|
+
this.skipWhitespace();
|
|
39
|
+
if (this.pos >= this.input.length)
|
|
40
|
+
break;
|
|
41
|
+
const ch = this.input[this.pos];
|
|
42
|
+
const startPos = this.pos;
|
|
43
|
+
// Two-character tokens
|
|
44
|
+
if (ch === '.' && this.peek(1) === '.') {
|
|
45
|
+
this.pos += 2;
|
|
46
|
+
this.tokens.push({ type: 'DOTDOT', value: '..', pos: startPos });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === '&' && this.peek(1) === '&') {
|
|
50
|
+
this.pos += 2;
|
|
51
|
+
this.tokens.push({ type: 'AND', value: '&&', pos: startPos });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === '|' && this.peek(1) === '|') {
|
|
55
|
+
this.pos += 2;
|
|
56
|
+
this.tokens.push({ type: 'OR', value: '||', pos: startPos });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '=' && this.peek(1) === '=') {
|
|
60
|
+
this.pos += 2;
|
|
61
|
+
this.tokens.push({ type: 'EQ', value: '==', pos: startPos });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '!' && this.peek(1) === '=') {
|
|
65
|
+
this.pos += 2;
|
|
66
|
+
this.tokens.push({ type: 'NE', value: '!=', pos: startPos });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === '<' && this.peek(1) === '=') {
|
|
70
|
+
this.pos += 2;
|
|
71
|
+
this.tokens.push({ type: 'LE', value: '<=', pos: startPos });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === '>' && this.peek(1) === '=') {
|
|
75
|
+
this.pos += 2;
|
|
76
|
+
this.tokens.push({ type: 'GE', value: '>=', pos: startPos });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Single-character tokens
|
|
80
|
+
switch (ch) {
|
|
81
|
+
case '$':
|
|
82
|
+
this.pos++;
|
|
83
|
+
this.tokens.push({ type: 'ROOT', value: '$', pos: startPos });
|
|
84
|
+
continue;
|
|
85
|
+
case '@':
|
|
86
|
+
this.pos++;
|
|
87
|
+
this.tokens.push({ type: 'CURRENT', value: '@', pos: startPos });
|
|
88
|
+
continue;
|
|
89
|
+
case '.':
|
|
90
|
+
this.pos++;
|
|
91
|
+
this.tokens.push({ type: 'DOT', value: '.', pos: startPos });
|
|
92
|
+
continue;
|
|
93
|
+
case '[':
|
|
94
|
+
this.pos++;
|
|
95
|
+
this.tokens.push({ type: 'LBRACKET', value: '[', pos: startPos });
|
|
96
|
+
continue;
|
|
97
|
+
case ']':
|
|
98
|
+
this.pos++;
|
|
99
|
+
this.tokens.push({ type: 'RBRACKET', value: ']', pos: startPos });
|
|
100
|
+
continue;
|
|
101
|
+
case '(':
|
|
102
|
+
this.pos++;
|
|
103
|
+
this.tokens.push({ type: 'LPAREN', value: '(', pos: startPos });
|
|
104
|
+
continue;
|
|
105
|
+
case ')':
|
|
106
|
+
this.pos++;
|
|
107
|
+
this.tokens.push({ type: 'RPAREN', value: ')', pos: startPos });
|
|
108
|
+
continue;
|
|
109
|
+
case ':':
|
|
110
|
+
this.pos++;
|
|
111
|
+
this.tokens.push({ type: 'COLON', value: ':', pos: startPos });
|
|
112
|
+
continue;
|
|
113
|
+
case ',':
|
|
114
|
+
this.pos++;
|
|
115
|
+
this.tokens.push({ type: 'COMMA', value: ',', pos: startPos });
|
|
116
|
+
continue;
|
|
117
|
+
case '*':
|
|
118
|
+
this.pos++;
|
|
119
|
+
this.tokens.push({ type: 'WILDCARD', value: '*', pos: startPos });
|
|
120
|
+
continue;
|
|
121
|
+
case '?':
|
|
122
|
+
this.pos++;
|
|
123
|
+
this.tokens.push({ type: 'QUESTION', value: '?', pos: startPos });
|
|
124
|
+
continue;
|
|
125
|
+
case '!':
|
|
126
|
+
this.pos++;
|
|
127
|
+
this.tokens.push({ type: 'NOT', value: '!', pos: startPos });
|
|
128
|
+
continue;
|
|
129
|
+
case '<':
|
|
130
|
+
this.pos++;
|
|
131
|
+
this.tokens.push({ type: 'LT', value: '<', pos: startPos });
|
|
132
|
+
continue;
|
|
133
|
+
case '>':
|
|
134
|
+
this.pos++;
|
|
135
|
+
this.tokens.push({ type: 'GT', value: '>', pos: startPos });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// String literals
|
|
139
|
+
if (ch === '"' || ch === "'") {
|
|
140
|
+
const str = this.readString(ch);
|
|
141
|
+
if (str === null) {
|
|
142
|
+
throw new Error(`Invalid string at position ${startPos}`);
|
|
143
|
+
}
|
|
144
|
+
this.tokens.push({ type: 'STRING', value: str, pos: startPos });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Numbers (including negative)
|
|
148
|
+
if (ch === '-' || (ch >= '0' && ch <= '9')) {
|
|
149
|
+
const num = this.readNumber();
|
|
150
|
+
if (num === null) {
|
|
151
|
+
throw new Error(`Invalid number at position ${startPos}`);
|
|
152
|
+
}
|
|
153
|
+
this.tokens.push({ type: 'NUMBER', value: num, pos: startPos });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Keywords and names
|
|
157
|
+
if (this.isNameFirst(ch)) {
|
|
158
|
+
const name = this.readName();
|
|
159
|
+
if (name === 'true') {
|
|
160
|
+
this.tokens.push({ type: 'TRUE', value: true, pos: startPos });
|
|
161
|
+
}
|
|
162
|
+
else if (name === 'false') {
|
|
163
|
+
this.tokens.push({ type: 'FALSE', value: false, pos: startPos });
|
|
164
|
+
}
|
|
165
|
+
else if (name === 'null') {
|
|
166
|
+
this.tokens.push({ type: 'NULL', value: null, pos: startPos });
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
this.tokens.push({ type: 'NAME', value: name, pos: startPos });
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`Unexpected character '${ch}' at position ${this.pos}`);
|
|
174
|
+
}
|
|
175
|
+
this.tokens.push({ type: 'EOF', value: null, pos: this.pos });
|
|
176
|
+
}
|
|
177
|
+
peek(offset = 0) {
|
|
178
|
+
return this.input[this.pos + offset];
|
|
179
|
+
}
|
|
180
|
+
skipWhitespace() {
|
|
181
|
+
// RFC 9535 §2.1.1: B = %x20 / %x09 / %x0A / %x0D
|
|
182
|
+
while (this.pos < this.input.length) {
|
|
183
|
+
const ch = this.input[this.pos];
|
|
184
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
185
|
+
this.pos++;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// RFC 9535 §2.3.1.1: member-name-shorthand = name-first *name-char
|
|
193
|
+
isNameFirst(ch) {
|
|
194
|
+
const code = ch.codePointAt(0);
|
|
195
|
+
if (code === undefined)
|
|
196
|
+
return false;
|
|
197
|
+
// ALPHA / "_" / %x80-D7FF / %xE000-10FFFF
|
|
198
|
+
return ((code >= 0x41 && code <= 0x5A) || // A-Z
|
|
199
|
+
(code >= 0x61 && code <= 0x7A) || // a-z
|
|
200
|
+
code === 0x5F || // _
|
|
201
|
+
(code >= 0x80 && code <= 0xD7FF) ||
|
|
202
|
+
(code >= 0xE000 && code <= 0x10FFFF));
|
|
203
|
+
}
|
|
204
|
+
isNameChar(ch) {
|
|
205
|
+
const code = ch.codePointAt(0);
|
|
206
|
+
if (code === undefined)
|
|
207
|
+
return false;
|
|
208
|
+
// name-first / DIGIT
|
|
209
|
+
return this.isNameFirst(ch) || (code >= 0x30 && code <= 0x39);
|
|
210
|
+
}
|
|
211
|
+
readName() {
|
|
212
|
+
const start = this.pos;
|
|
213
|
+
// Handle multi-byte characters
|
|
214
|
+
while (this.pos < this.input.length) {
|
|
215
|
+
const ch = this.input[this.pos];
|
|
216
|
+
if (!this.isNameChar(ch))
|
|
217
|
+
break;
|
|
218
|
+
// Handle surrogate pairs
|
|
219
|
+
const code = ch.codePointAt(0);
|
|
220
|
+
if (code > 0xFFFF) {
|
|
221
|
+
this.pos += 2;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.pos++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return this.input.slice(start, this.pos);
|
|
228
|
+
}
|
|
229
|
+
// RFC 9535 §2.3.1.1: String literal parsing with escape sequences
|
|
230
|
+
readString(quote) {
|
|
231
|
+
this.pos++; // Skip opening quote
|
|
232
|
+
let result = '';
|
|
233
|
+
while (this.pos < this.input.length) {
|
|
234
|
+
const ch = this.input[this.pos];
|
|
235
|
+
if (ch === quote) {
|
|
236
|
+
this.pos++; // Skip closing quote
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
if (ch === '\\') {
|
|
240
|
+
this.pos++;
|
|
241
|
+
if (this.pos >= this.input.length)
|
|
242
|
+
return null;
|
|
243
|
+
const escaped = this.input[this.pos];
|
|
244
|
+
switch (escaped) {
|
|
245
|
+
case 'b':
|
|
246
|
+
result += '\b';
|
|
247
|
+
break;
|
|
248
|
+
case 'f':
|
|
249
|
+
result += '\f';
|
|
250
|
+
break;
|
|
251
|
+
case 'n':
|
|
252
|
+
result += '\n';
|
|
253
|
+
break;
|
|
254
|
+
case 'r':
|
|
255
|
+
result += '\r';
|
|
256
|
+
break;
|
|
257
|
+
case 't':
|
|
258
|
+
result += '\t';
|
|
259
|
+
break;
|
|
260
|
+
case '/':
|
|
261
|
+
result += '/';
|
|
262
|
+
break;
|
|
263
|
+
case '\\':
|
|
264
|
+
result += '\\';
|
|
265
|
+
break;
|
|
266
|
+
case '"':
|
|
267
|
+
result += '"';
|
|
268
|
+
break;
|
|
269
|
+
case "'":
|
|
270
|
+
result += "'";
|
|
271
|
+
break;
|
|
272
|
+
case 'u': {
|
|
273
|
+
// Unicode escape: \uXXXX or surrogate pair
|
|
274
|
+
this.pos++;
|
|
275
|
+
const hex = this.input.slice(this.pos, this.pos + 4);
|
|
276
|
+
if (!/^[0-9A-Fa-f]{4}$/.test(hex))
|
|
277
|
+
return null;
|
|
278
|
+
const codePoint = parseInt(hex, 16);
|
|
279
|
+
this.pos += 4;
|
|
280
|
+
// Check for high surrogate
|
|
281
|
+
if (codePoint >= 0xD800 && codePoint <= 0xDBFF) {
|
|
282
|
+
// Must be followed by \uXXXX low surrogate
|
|
283
|
+
if (this.input.slice(this.pos, this.pos + 2) !== '\\u')
|
|
284
|
+
return null;
|
|
285
|
+
this.pos += 2;
|
|
286
|
+
const hex2 = this.input.slice(this.pos, this.pos + 4);
|
|
287
|
+
if (!/^[0-9A-Fa-f]{4}$/.test(hex2))
|
|
288
|
+
return null;
|
|
289
|
+
const lowSurrogate = parseInt(hex2, 16);
|
|
290
|
+
if (lowSurrogate < 0xDC00 || lowSurrogate > 0xDFFF)
|
|
291
|
+
return null;
|
|
292
|
+
this.pos += 4;
|
|
293
|
+
// Decode surrogate pair
|
|
294
|
+
const combined = 0x10000 + ((codePoint - 0xD800) << 10) + (lowSurrogate - 0xDC00);
|
|
295
|
+
result += String.fromCodePoint(combined);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
result += String.fromCodePoint(codePoint);
|
|
299
|
+
}
|
|
300
|
+
continue; // Already advanced pos
|
|
301
|
+
}
|
|
302
|
+
default:
|
|
303
|
+
return null; // Invalid escape
|
|
304
|
+
}
|
|
305
|
+
this.pos++;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
result += ch;
|
|
309
|
+
this.pos++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null; // Unterminated string
|
|
313
|
+
}
|
|
314
|
+
// RFC 9535 §2.3.3.1: int = "0" / (["-"] DIGIT1 *DIGIT)
|
|
315
|
+
readNumber() {
|
|
316
|
+
const start = this.pos;
|
|
317
|
+
let hasSign = false;
|
|
318
|
+
if (this.input[this.pos] === '-') {
|
|
319
|
+
hasSign = true;
|
|
320
|
+
this.pos++;
|
|
321
|
+
}
|
|
322
|
+
if (this.pos >= this.input.length) {
|
|
323
|
+
this.pos = start;
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const firstDigit = this.input[this.pos];
|
|
327
|
+
// "0" by itself or leading zeros not allowed for non-zero integers
|
|
328
|
+
if (firstDigit === '0') {
|
|
329
|
+
this.pos++;
|
|
330
|
+
// Check if followed by more digits (invalid: 00, 01, etc.)
|
|
331
|
+
if (this.pos < this.input.length) {
|
|
332
|
+
const next = this.input[this.pos];
|
|
333
|
+
if (next >= '0' && next <= '9') {
|
|
334
|
+
// Could be a decimal
|
|
335
|
+
if (next !== '.') {
|
|
336
|
+
this.pos = start;
|
|
337
|
+
return null; // Leading zero in integer
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (firstDigit >= '1' && firstDigit <= '9') {
|
|
343
|
+
this.pos++;
|
|
344
|
+
while (this.pos < this.input.length) {
|
|
345
|
+
const ch = this.input[this.pos];
|
|
346
|
+
if (ch >= '0' && ch <= '9') {
|
|
347
|
+
this.pos++;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else if (hasSign) {
|
|
355
|
+
// - not followed by digit
|
|
356
|
+
this.pos = start;
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.pos = start;
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const numStr = this.input.slice(start, this.pos);
|
|
364
|
+
const num = parseInt(numStr, 10);
|
|
365
|
+
// RFC 9535 §2.1: Integers MUST be within I-JSON range
|
|
366
|
+
if (num < I_JSON_MIN || num > I_JSON_MAX) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return num;
|
|
370
|
+
}
|
|
371
|
+
// Token access methods
|
|
372
|
+
current() {
|
|
373
|
+
return this.tokens[this.tokenIndex] ?? this.tokens[this.tokens.length - 1];
|
|
374
|
+
}
|
|
375
|
+
advance() {
|
|
376
|
+
const token = this.current();
|
|
377
|
+
if (this.tokenIndex < this.tokens.length - 1) {
|
|
378
|
+
this.tokenIndex++;
|
|
379
|
+
}
|
|
380
|
+
return token;
|
|
381
|
+
}
|
|
382
|
+
check(type) {
|
|
383
|
+
return this.current().type === type;
|
|
384
|
+
}
|
|
385
|
+
match(...types) {
|
|
386
|
+
for (const type of types) {
|
|
387
|
+
if (this.check(type)) {
|
|
388
|
+
this.advance();
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
expect(type) {
|
|
395
|
+
if (!this.check(type)) {
|
|
396
|
+
throw new Error(`Expected ${type} but got ${this.current().type}`);
|
|
397
|
+
}
|
|
398
|
+
return this.advance();
|
|
399
|
+
}
|
|
400
|
+
isAtEnd() {
|
|
401
|
+
return this.check('EOF');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// =============================================================================
|
|
405
|
+
// Parser
|
|
406
|
+
// =============================================================================
|
|
407
|
+
/**
|
|
408
|
+
* Parse a JSONPath query string into an AST.
|
|
409
|
+
* Returns null if the query is not well-formed or valid.
|
|
410
|
+
*
|
|
411
|
+
* @param query - JSONPath query string (e.g., "$.store.book[*].author")
|
|
412
|
+
* @returns Parsed AST, or null on invalid syntax
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* parseJsonPath('$.store.book[*]') // { type: 'query', root: '$', segments: [...] }
|
|
416
|
+
* parseJsonPath('invalid') // null
|
|
417
|
+
*
|
|
418
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1
|
|
419
|
+
*/
|
|
420
|
+
export function parseJsonPath(query) {
|
|
421
|
+
try {
|
|
422
|
+
const lexer = new Lexer(query);
|
|
423
|
+
const ast = parseQuery(lexer, '$');
|
|
424
|
+
// Must consume entire input
|
|
425
|
+
if (!lexer.isAtEnd()) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return ast;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function parseQuery(lexer, expectedRoot) {
|
|
435
|
+
// RFC 9535 §2.2.1: jsonpath-query = root-identifier segments
|
|
436
|
+
const rootType = expectedRoot === '$' ? 'ROOT' : 'CURRENT';
|
|
437
|
+
if (!lexer.match(rootType)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const segments = parseSegments(lexer);
|
|
441
|
+
return {
|
|
442
|
+
type: 'query',
|
|
443
|
+
root: expectedRoot,
|
|
444
|
+
segments,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function parseSegments(lexer) {
|
|
448
|
+
// RFC 9535 §2.1.1: segments = *(S segment)
|
|
449
|
+
const segments = [];
|
|
450
|
+
while (!lexer.isAtEnd()) {
|
|
451
|
+
const segment = parseSegment(lexer);
|
|
452
|
+
if (segment === null)
|
|
453
|
+
break;
|
|
454
|
+
segments.push(segment);
|
|
455
|
+
}
|
|
456
|
+
return segments;
|
|
457
|
+
}
|
|
458
|
+
function parseSegment(lexer) {
|
|
459
|
+
// RFC 9535 §2.5: segment = child-segment / descendant-segment
|
|
460
|
+
// Descendant segment: ".." (bracketed-selection / wildcard-selector / member-name-shorthand)
|
|
461
|
+
if (lexer.match('DOTDOT')) {
|
|
462
|
+
return parseDescendantSegment(lexer);
|
|
463
|
+
}
|
|
464
|
+
// Child segment: "[" ... "]" or "." (wildcard-selector / member-name-shorthand)
|
|
465
|
+
if (lexer.check('LBRACKET')) {
|
|
466
|
+
return parseChildBracketSegment(lexer);
|
|
467
|
+
}
|
|
468
|
+
if (lexer.match('DOT')) {
|
|
469
|
+
return parseChildDotSegment(lexer);
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
function parseDescendantSegment(lexer) {
|
|
474
|
+
// RFC 9535 §2.5.2.1: descendant-segment = ".." (bracketed-selection / wildcard / member-name)
|
|
475
|
+
if (lexer.check('LBRACKET')) {
|
|
476
|
+
const selectors = parseBracketedSelection(lexer);
|
|
477
|
+
if (selectors === null)
|
|
478
|
+
return null;
|
|
479
|
+
return { type: 'descendant', selectors };
|
|
480
|
+
}
|
|
481
|
+
if (lexer.match('WILDCARD')) {
|
|
482
|
+
return { type: 'descendant', selectors: [{ type: 'wildcard' }] };
|
|
483
|
+
}
|
|
484
|
+
if (lexer.check('NAME')) {
|
|
485
|
+
const name = lexer.advance().value;
|
|
486
|
+
return { type: 'descendant', selectors: [{ type: 'name', name }] };
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
function parseChildBracketSegment(lexer) {
|
|
491
|
+
const selectors = parseBracketedSelection(lexer);
|
|
492
|
+
if (selectors === null)
|
|
493
|
+
return null;
|
|
494
|
+
return { type: 'child', selectors };
|
|
495
|
+
}
|
|
496
|
+
function parseChildDotSegment(lexer) {
|
|
497
|
+
// RFC 9535 §2.5.1.1: "." (wildcard-selector / member-name-shorthand)
|
|
498
|
+
if (lexer.match('WILDCARD')) {
|
|
499
|
+
return { type: 'child', selectors: [{ type: 'wildcard' }] };
|
|
500
|
+
}
|
|
501
|
+
if (lexer.check('NAME')) {
|
|
502
|
+
const name = lexer.advance().value;
|
|
503
|
+
return { type: 'child', selectors: [{ type: 'name', name }] };
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function parseBracketedSelection(lexer) {
|
|
508
|
+
// RFC 9535 §2.5.1.1: bracketed-selection = "[" S selector *( S "," S selector ) S "]"
|
|
509
|
+
if (!lexer.match('LBRACKET'))
|
|
510
|
+
return null;
|
|
511
|
+
const selectors = [];
|
|
512
|
+
const first = parseSelector(lexer);
|
|
513
|
+
if (first === null)
|
|
514
|
+
return null;
|
|
515
|
+
selectors.push(first);
|
|
516
|
+
while (lexer.match('COMMA')) {
|
|
517
|
+
const next = parseSelector(lexer);
|
|
518
|
+
if (next === null)
|
|
519
|
+
return null;
|
|
520
|
+
selectors.push(next);
|
|
521
|
+
}
|
|
522
|
+
if (!lexer.match('RBRACKET'))
|
|
523
|
+
return null;
|
|
524
|
+
return selectors;
|
|
525
|
+
}
|
|
526
|
+
function parseSelector(lexer) {
|
|
527
|
+
// RFC 9535 §2.3: selector = name-selector / wildcard / slice / index / filter
|
|
528
|
+
// Filter selector: "?" logical-expr
|
|
529
|
+
if (lexer.match('QUESTION')) {
|
|
530
|
+
const expr = parseLogicalExpr(lexer);
|
|
531
|
+
if (expr === null)
|
|
532
|
+
return null;
|
|
533
|
+
return { type: 'filter', expression: expr };
|
|
534
|
+
}
|
|
535
|
+
// Wildcard selector
|
|
536
|
+
if (lexer.match('WILDCARD')) {
|
|
537
|
+
return { type: 'wildcard' };
|
|
538
|
+
}
|
|
539
|
+
// Name selector (string literal)
|
|
540
|
+
if (lexer.check('STRING')) {
|
|
541
|
+
const name = lexer.advance().value;
|
|
542
|
+
return { type: 'name', name };
|
|
543
|
+
}
|
|
544
|
+
// Index or slice selector
|
|
545
|
+
if (lexer.check('NUMBER') || lexer.check('COLON')) {
|
|
546
|
+
return parseIndexOrSlice(lexer);
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
function parseIndexOrSlice(lexer) {
|
|
551
|
+
// RFC 9535 §2.3.3/§2.3.4: Determine if this is an index or slice
|
|
552
|
+
let start;
|
|
553
|
+
let end;
|
|
554
|
+
let step;
|
|
555
|
+
let isSlice = false;
|
|
556
|
+
// Start value
|
|
557
|
+
if (lexer.check('NUMBER')) {
|
|
558
|
+
start = lexer.advance().value;
|
|
559
|
+
}
|
|
560
|
+
// First colon (makes it a slice)
|
|
561
|
+
if (lexer.match('COLON')) {
|
|
562
|
+
isSlice = true;
|
|
563
|
+
// End value
|
|
564
|
+
if (lexer.check('NUMBER')) {
|
|
565
|
+
end = lexer.advance().value;
|
|
566
|
+
}
|
|
567
|
+
// Second colon and step
|
|
568
|
+
if (lexer.match('COLON')) {
|
|
569
|
+
if (lexer.check('NUMBER')) {
|
|
570
|
+
step = lexer.advance().value;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (isSlice) {
|
|
575
|
+
return { type: 'slice', start, end, step };
|
|
576
|
+
}
|
|
577
|
+
if (start !== undefined) {
|
|
578
|
+
return { type: 'index', index: start };
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
// =============================================================================
|
|
583
|
+
// Filter Expression Parser
|
|
584
|
+
// =============================================================================
|
|
585
|
+
function parseLogicalExpr(lexer) {
|
|
586
|
+
// RFC 9535 §2.3.5.1: logical-expr = logical-or-expr
|
|
587
|
+
return parseLogicalOrExpr(lexer);
|
|
588
|
+
}
|
|
589
|
+
function parseLogicalOrExpr(lexer) {
|
|
590
|
+
// RFC 9535 §2.3.5.1: logical-or-expr = logical-and-expr *( "||" S logical-and-expr )
|
|
591
|
+
let left = parseLogicalAndExpr(lexer);
|
|
592
|
+
if (left === null)
|
|
593
|
+
return null;
|
|
594
|
+
const operands = [left];
|
|
595
|
+
while (lexer.match('OR')) {
|
|
596
|
+
const right = parseLogicalAndExpr(lexer);
|
|
597
|
+
if (right === null)
|
|
598
|
+
return null;
|
|
599
|
+
operands.push(right);
|
|
600
|
+
}
|
|
601
|
+
if (operands.length === 1) {
|
|
602
|
+
return operands[0];
|
|
603
|
+
}
|
|
604
|
+
return { type: 'or', operands };
|
|
605
|
+
}
|
|
606
|
+
function parseLogicalAndExpr(lexer) {
|
|
607
|
+
// RFC 9535 §2.3.5.1: logical-and-expr = basic-expr *( "&&" S basic-expr )
|
|
608
|
+
let left = parseBasicExpr(lexer);
|
|
609
|
+
if (left === null)
|
|
610
|
+
return null;
|
|
611
|
+
const operands = [left];
|
|
612
|
+
while (lexer.match('AND')) {
|
|
613
|
+
const right = parseBasicExpr(lexer);
|
|
614
|
+
if (right === null)
|
|
615
|
+
return null;
|
|
616
|
+
operands.push(right);
|
|
617
|
+
}
|
|
618
|
+
if (operands.length === 1) {
|
|
619
|
+
return operands[0];
|
|
620
|
+
}
|
|
621
|
+
return { type: 'and', operands };
|
|
622
|
+
}
|
|
623
|
+
function parseBasicExpr(lexer) {
|
|
624
|
+
// RFC 9535 §2.3.5.1: basic-expr = paren-expr / comparison-expr / test-expr
|
|
625
|
+
// Parenthesized expression
|
|
626
|
+
if (lexer.match('LPAREN')) {
|
|
627
|
+
const expr = parseLogicalExpr(lexer);
|
|
628
|
+
if (expr === null)
|
|
629
|
+
return null;
|
|
630
|
+
if (!lexer.match('RPAREN'))
|
|
631
|
+
return null;
|
|
632
|
+
return expr;
|
|
633
|
+
}
|
|
634
|
+
// Negation
|
|
635
|
+
if (lexer.match('NOT')) {
|
|
636
|
+
const operand = parseBasicExpr(lexer);
|
|
637
|
+
if (operand === null)
|
|
638
|
+
return null;
|
|
639
|
+
return { type: 'not', operand };
|
|
640
|
+
}
|
|
641
|
+
// Try to parse a comparable (for comparison) or test expression
|
|
642
|
+
return parseComparisonOrTest(lexer);
|
|
643
|
+
}
|
|
644
|
+
function parseComparisonOrTest(lexer) {
|
|
645
|
+
// This handles both comparison expressions and test expressions
|
|
646
|
+
// We need to look ahead to determine which one
|
|
647
|
+
// Check for filter query (existence test): @... or $...
|
|
648
|
+
if (lexer.check('CURRENT') || lexer.check('ROOT')) {
|
|
649
|
+
const query = parseFilterQuery(lexer);
|
|
650
|
+
if (query === null)
|
|
651
|
+
return null;
|
|
652
|
+
// Check if followed by comparison operator
|
|
653
|
+
const op = tryParseComparisonOp(lexer);
|
|
654
|
+
if (op !== null) {
|
|
655
|
+
const right = parseComparable(lexer);
|
|
656
|
+
if (right === null)
|
|
657
|
+
return null;
|
|
658
|
+
// Convert query to singular-query comparable
|
|
659
|
+
const left = {
|
|
660
|
+
type: 'singular-query',
|
|
661
|
+
root: query.root,
|
|
662
|
+
segments: query.segments,
|
|
663
|
+
};
|
|
664
|
+
return { type: 'comparison', operator: op, left, right };
|
|
665
|
+
}
|
|
666
|
+
// It's a test expression
|
|
667
|
+
return { type: 'test', query };
|
|
668
|
+
}
|
|
669
|
+
// Check for function call
|
|
670
|
+
if (lexer.check('NAME')) {
|
|
671
|
+
const func = parseFunctionExpr(lexer);
|
|
672
|
+
if (func === null)
|
|
673
|
+
return null;
|
|
674
|
+
// Check if followed by comparison operator
|
|
675
|
+
const op = tryParseComparisonOp(lexer);
|
|
676
|
+
if (op !== null) {
|
|
677
|
+
const right = parseComparable(lexer);
|
|
678
|
+
if (right === null)
|
|
679
|
+
return null;
|
|
680
|
+
return { type: 'comparison', operator: op, left: func, right };
|
|
681
|
+
}
|
|
682
|
+
// It's a test expression (function returning LogicalType)
|
|
683
|
+
return func;
|
|
684
|
+
}
|
|
685
|
+
// Literal followed by comparison
|
|
686
|
+
const literal = parseLiteral(lexer);
|
|
687
|
+
if (literal !== null) {
|
|
688
|
+
const op = tryParseComparisonOp(lexer);
|
|
689
|
+
if (op !== null) {
|
|
690
|
+
const right = parseComparable(lexer);
|
|
691
|
+
if (right === null)
|
|
692
|
+
return null;
|
|
693
|
+
return { type: 'comparison', operator: op, left: literal, right };
|
|
694
|
+
}
|
|
695
|
+
// Bare literal is not valid as a test expression
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
function parseFilterQuery(lexer) {
|
|
701
|
+
// RFC 9535 §2.3.5.1: filter-query = rel-query / jsonpath-query
|
|
702
|
+
if (lexer.check('ROOT')) {
|
|
703
|
+
return parseQuery(lexer, '$');
|
|
704
|
+
}
|
|
705
|
+
if (lexer.check('CURRENT')) {
|
|
706
|
+
return parseQuery(lexer, '@');
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
function tryParseComparisonOp(lexer) {
|
|
711
|
+
if (lexer.match('EQ'))
|
|
712
|
+
return '==';
|
|
713
|
+
if (lexer.match('NE'))
|
|
714
|
+
return '!=';
|
|
715
|
+
if (lexer.match('LE'))
|
|
716
|
+
return '<=';
|
|
717
|
+
if (lexer.match('GE'))
|
|
718
|
+
return '>=';
|
|
719
|
+
if (lexer.match('LT'))
|
|
720
|
+
return '<';
|
|
721
|
+
if (lexer.match('GT'))
|
|
722
|
+
return '>';
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
function parseComparable(lexer) {
|
|
726
|
+
// RFC 9535 §2.3.5.1: comparable = literal / singular-query / function-expr
|
|
727
|
+
// Function
|
|
728
|
+
if (lexer.check('NAME')) {
|
|
729
|
+
return parseFunctionExpr(lexer);
|
|
730
|
+
}
|
|
731
|
+
// Singular query
|
|
732
|
+
if (lexer.check('ROOT') || lexer.check('CURRENT')) {
|
|
733
|
+
const query = parseFilterQuery(lexer);
|
|
734
|
+
if (query === null)
|
|
735
|
+
return null;
|
|
736
|
+
return {
|
|
737
|
+
type: 'singular-query',
|
|
738
|
+
root: query.root,
|
|
739
|
+
segments: query.segments,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// Literal
|
|
743
|
+
return parseLiteral(lexer);
|
|
744
|
+
}
|
|
745
|
+
function parseLiteral(lexer) {
|
|
746
|
+
if (lexer.check('STRING')) {
|
|
747
|
+
return { type: 'literal', value: lexer.advance().value };
|
|
748
|
+
}
|
|
749
|
+
if (lexer.check('NUMBER')) {
|
|
750
|
+
return { type: 'literal', value: lexer.advance().value };
|
|
751
|
+
}
|
|
752
|
+
if (lexer.match('TRUE')) {
|
|
753
|
+
return { type: 'literal', value: true };
|
|
754
|
+
}
|
|
755
|
+
if (lexer.match('FALSE')) {
|
|
756
|
+
return { type: 'literal', value: false };
|
|
757
|
+
}
|
|
758
|
+
if (lexer.match('NULL')) {
|
|
759
|
+
return { type: 'literal', value: null };
|
|
760
|
+
}
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
function parseFunctionExpr(lexer) {
|
|
764
|
+
// RFC 9535 §2.4: function-expr = function-name "(" [function-argument *( "," function-argument )] ")"
|
|
765
|
+
if (!lexer.check('NAME'))
|
|
766
|
+
return null;
|
|
767
|
+
const name = lexer.advance().value;
|
|
768
|
+
if (!BUILTIN_FUNCTIONS.has(name)) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
if (!lexer.match('LPAREN'))
|
|
772
|
+
return null;
|
|
773
|
+
const args = [];
|
|
774
|
+
if (!lexer.check('RPAREN')) {
|
|
775
|
+
const first = parseFunctionArg(lexer);
|
|
776
|
+
if (first === null)
|
|
777
|
+
return null;
|
|
778
|
+
args.push(first);
|
|
779
|
+
while (lexer.match('COMMA')) {
|
|
780
|
+
const next = parseFunctionArg(lexer);
|
|
781
|
+
if (next === null)
|
|
782
|
+
return null;
|
|
783
|
+
args.push(next);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!lexer.match('RPAREN'))
|
|
787
|
+
return null;
|
|
788
|
+
return {
|
|
789
|
+
type: 'function',
|
|
790
|
+
name: name,
|
|
791
|
+
args,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function parseFunctionArg(lexer) {
|
|
795
|
+
// RFC 9535 §2.4: function-argument = literal / filter-query / function-expr
|
|
796
|
+
// Function
|
|
797
|
+
if (lexer.check('NAME')) {
|
|
798
|
+
return parseFunctionExpr(lexer);
|
|
799
|
+
}
|
|
800
|
+
// Filter query
|
|
801
|
+
if (lexer.check('ROOT') || lexer.check('CURRENT')) {
|
|
802
|
+
return parseFilterQuery(lexer);
|
|
803
|
+
}
|
|
804
|
+
// Literal
|
|
805
|
+
return parseLiteral(lexer);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Execute a JSONPath query against a JSON document.
|
|
809
|
+
* Returns an array of matching values (nodelist).
|
|
810
|
+
*
|
|
811
|
+
* @param query - JSONPath query string
|
|
812
|
+
* @param document - JSON document to query
|
|
813
|
+
* @param options - Query options
|
|
814
|
+
* @returns Array of matching values, or null on invalid query
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* queryJsonPath('$.store.book[*].author', doc) // ['Author1', 'Author2']
|
|
818
|
+
* queryJsonPath('$..price', doc) // [8.95, 12.99, 399]
|
|
819
|
+
* queryJsonPath('$.store.book[?@.price<10]', doc) // [{ title: 'Book1', ... }]
|
|
820
|
+
*
|
|
821
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1.2
|
|
822
|
+
*/
|
|
823
|
+
export function queryJsonPath(query, document, options) {
|
|
824
|
+
const ast = parseJsonPath(query);
|
|
825
|
+
if (ast === null) {
|
|
826
|
+
if (options?.throwOnError) {
|
|
827
|
+
throw new Error(`Invalid JSONPath query: ${query}`);
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
const nodes = evaluateQuery(ast, document);
|
|
832
|
+
return nodes.map(n => n.value);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Execute a JSONPath query and return nodes with paths.
|
|
836
|
+
*
|
|
837
|
+
* @param query - JSONPath query string
|
|
838
|
+
* @param document - JSON document to query
|
|
839
|
+
* @returns Array of nodes with values and paths, or null on invalid query
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* queryJsonPathNodes('$.store.book[0].title', doc)
|
|
843
|
+
* // [{ value: 'Sayings of the Century', path: "$['store']['book'][0]['title']" }]
|
|
844
|
+
*
|
|
845
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7
|
|
846
|
+
*/
|
|
847
|
+
export function queryJsonPathNodes(query, document) {
|
|
848
|
+
const ast = parseJsonPath(query);
|
|
849
|
+
if (ast === null)
|
|
850
|
+
return null;
|
|
851
|
+
return evaluateQuery(ast, document);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Validate a JSONPath query string without parsing.
|
|
855
|
+
*
|
|
856
|
+
* @param query - String to validate
|
|
857
|
+
* @returns true if valid JSONPath syntax
|
|
858
|
+
*
|
|
859
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1
|
|
860
|
+
*/
|
|
861
|
+
export function isValidJsonPath(query) {
|
|
862
|
+
return parseJsonPath(query) !== null;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Format a normalized path from path segments.
|
|
866
|
+
*
|
|
867
|
+
* @param segments - Array of string keys or numeric indices
|
|
868
|
+
* @returns Normalized path string
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* formatNormalizedPath(['store', 'book', 0]) // "$['store']['book'][0]"
|
|
872
|
+
*
|
|
873
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7
|
|
874
|
+
*/
|
|
875
|
+
export function formatNormalizedPath(segments) {
|
|
876
|
+
// RFC 9535 §2.7: normalized-path = root-identifier *(normal-index-segment / normal-name-segment)
|
|
877
|
+
let path = '$';
|
|
878
|
+
for (const seg of segments) {
|
|
879
|
+
if (typeof seg === 'number') {
|
|
880
|
+
path += `[${seg}]`;
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
// Escape single quotes and backslashes
|
|
884
|
+
const escaped = seg
|
|
885
|
+
.replace(/\\/g, '\\\\')
|
|
886
|
+
.replace(/'/g, "\\'");
|
|
887
|
+
path += `['${escaped}']`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return path;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Compile a JSONPath query for repeated execution.
|
|
894
|
+
* More efficient when the same query is used multiple times.
|
|
895
|
+
*
|
|
896
|
+
* @param query - JSONPath query string
|
|
897
|
+
* @returns Compiled query function, or null on invalid query
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* const fn = compileJsonPath('$.store.book[*].author');
|
|
901
|
+
* fn(doc1) // ['Author1', ...]
|
|
902
|
+
* fn(doc2) // ['Author2', ...]
|
|
903
|
+
*/
|
|
904
|
+
export function compileJsonPath(query) {
|
|
905
|
+
const ast = parseJsonPath(query);
|
|
906
|
+
if (ast === null)
|
|
907
|
+
return null;
|
|
908
|
+
return (document) => {
|
|
909
|
+
const nodes = evaluateQuery(ast, document);
|
|
910
|
+
return nodes.map(n => n.value);
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function evaluateQuery(ast, document) {
|
|
914
|
+
// RFC 9535 §2.1.2: Start with root node
|
|
915
|
+
const ctx = {
|
|
916
|
+
root: document,
|
|
917
|
+
current: document,
|
|
918
|
+
currentPath: [],
|
|
919
|
+
};
|
|
920
|
+
let nodes = [{
|
|
921
|
+
value: document,
|
|
922
|
+
path: '$',
|
|
923
|
+
}];
|
|
924
|
+
// Apply each segment in sequence
|
|
925
|
+
for (const segment of ast.segments) {
|
|
926
|
+
nodes = evaluateSegment(segment, nodes, ctx);
|
|
927
|
+
}
|
|
928
|
+
return nodes;
|
|
929
|
+
}
|
|
930
|
+
function evaluateSegment(segment, nodes, ctx) {
|
|
931
|
+
if (segment.type === 'child') {
|
|
932
|
+
return evaluateChildSegment(segment, nodes, ctx);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
return evaluateDescendantSegment(segment, nodes, ctx);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function evaluateChildSegment(segment, nodes, ctx) {
|
|
939
|
+
// RFC 9535 §2.5.1.2: Apply selectors to each input node
|
|
940
|
+
const result = [];
|
|
941
|
+
for (const node of nodes) {
|
|
942
|
+
for (const selector of segment.selectors) {
|
|
943
|
+
const selected = evaluateSelector(selector, node, ctx);
|
|
944
|
+
result.push(...selected);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
function evaluateDescendantSegment(segment, nodes, ctx) {
|
|
950
|
+
// RFC 9535 §2.5.2.2: Select from node and all its descendants
|
|
951
|
+
const result = [];
|
|
952
|
+
for (const node of nodes) {
|
|
953
|
+
// Collect all descendants including the node itself
|
|
954
|
+
const allNodes = collectDescendants(node);
|
|
955
|
+
for (const descNode of allNodes) {
|
|
956
|
+
for (const selector of segment.selectors) {
|
|
957
|
+
const selected = evaluateSelector(selector, descNode, ctx);
|
|
958
|
+
result.push(...selected);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
|
+
function collectDescendants(node) {
|
|
965
|
+
const result = [node];
|
|
966
|
+
const value = node.value;
|
|
967
|
+
if (Array.isArray(value)) {
|
|
968
|
+
for (let i = 0; i < value.length; i++) {
|
|
969
|
+
const childPath = parseNormalizedPath(node.path);
|
|
970
|
+
childPath.push(i);
|
|
971
|
+
const childNode = {
|
|
972
|
+
value: value[i],
|
|
973
|
+
path: formatNormalizedPath(childPath),
|
|
974
|
+
};
|
|
975
|
+
result.push(...collectDescendants(childNode));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
else if (value !== null && typeof value === 'object') {
|
|
979
|
+
for (const key of Object.keys(value)) {
|
|
980
|
+
const childPath = parseNormalizedPath(node.path);
|
|
981
|
+
childPath.push(key);
|
|
982
|
+
const childNode = {
|
|
983
|
+
value: value[key],
|
|
984
|
+
path: formatNormalizedPath(childPath),
|
|
985
|
+
};
|
|
986
|
+
result.push(...collectDescendants(childNode));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
function parseNormalizedPath(path) {
|
|
992
|
+
// Parse a normalized path back into segments
|
|
993
|
+
const segments = [];
|
|
994
|
+
if (!path.startsWith('$'))
|
|
995
|
+
return segments;
|
|
996
|
+
let i = 1; // Skip $
|
|
997
|
+
while (i < path.length) {
|
|
998
|
+
if (path[i] === '[') {
|
|
999
|
+
i++; // Skip [
|
|
1000
|
+
if (path[i] === "'") {
|
|
1001
|
+
// String segment
|
|
1002
|
+
i++; // Skip opening quote
|
|
1003
|
+
let name = '';
|
|
1004
|
+
while (i < path.length && path[i] !== "'") {
|
|
1005
|
+
if (path[i] === '\\' && i + 1 < path.length) {
|
|
1006
|
+
i++;
|
|
1007
|
+
name += path[i];
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
name += path[i];
|
|
1011
|
+
}
|
|
1012
|
+
i++;
|
|
1013
|
+
}
|
|
1014
|
+
i++; // Skip closing quote
|
|
1015
|
+
segments.push(name);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
// Numeric segment
|
|
1019
|
+
let numStr = '';
|
|
1020
|
+
while (i < path.length && path[i] !== ']') {
|
|
1021
|
+
numStr += path[i];
|
|
1022
|
+
i++;
|
|
1023
|
+
}
|
|
1024
|
+
segments.push(parseInt(numStr, 10));
|
|
1025
|
+
}
|
|
1026
|
+
i++; // Skip ]
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return segments;
|
|
1033
|
+
}
|
|
1034
|
+
function evaluateSelector(selector, node, ctx) {
|
|
1035
|
+
switch (selector.type) {
|
|
1036
|
+
case 'name':
|
|
1037
|
+
return evaluateNameSelector(selector, node);
|
|
1038
|
+
case 'wildcard':
|
|
1039
|
+
return evaluateWildcardSelector(node);
|
|
1040
|
+
case 'index':
|
|
1041
|
+
return evaluateIndexSelector(selector, node);
|
|
1042
|
+
case 'slice':
|
|
1043
|
+
return evaluateSliceSelector(selector, node);
|
|
1044
|
+
case 'filter':
|
|
1045
|
+
return evaluateFilterSelector(selector, node, ctx);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function evaluateNameSelector(selector, node) {
|
|
1049
|
+
// RFC 9535 §2.3.1.2: Select member value by name
|
|
1050
|
+
const value = node.value;
|
|
1051
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
1052
|
+
return [];
|
|
1053
|
+
}
|
|
1054
|
+
const obj = value;
|
|
1055
|
+
if (!(selector.name in obj)) {
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1058
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1059
|
+
childPath.push(selector.name);
|
|
1060
|
+
return [{
|
|
1061
|
+
value: obj[selector.name],
|
|
1062
|
+
path: formatNormalizedPath(childPath),
|
|
1063
|
+
}];
|
|
1064
|
+
}
|
|
1065
|
+
function evaluateWildcardSelector(node) {
|
|
1066
|
+
// RFC 9535 §2.3.2.2: Select all children
|
|
1067
|
+
const value = node.value;
|
|
1068
|
+
const result = [];
|
|
1069
|
+
if (Array.isArray(value)) {
|
|
1070
|
+
for (let i = 0; i < value.length; i++) {
|
|
1071
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1072
|
+
childPath.push(i);
|
|
1073
|
+
result.push({
|
|
1074
|
+
value: value[i],
|
|
1075
|
+
path: formatNormalizedPath(childPath),
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
else if (value !== null && typeof value === 'object') {
|
|
1080
|
+
for (const key of Object.keys(value)) {
|
|
1081
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1082
|
+
childPath.push(key);
|
|
1083
|
+
result.push({
|
|
1084
|
+
value: value[key],
|
|
1085
|
+
path: formatNormalizedPath(childPath),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
function evaluateIndexSelector(selector, node) {
|
|
1092
|
+
// RFC 9535 §2.3.3.2: Select array element by index
|
|
1093
|
+
const value = node.value;
|
|
1094
|
+
if (!Array.isArray(value)) {
|
|
1095
|
+
return [];
|
|
1096
|
+
}
|
|
1097
|
+
let index = selector.index;
|
|
1098
|
+
// RFC 9535 §2.3.3.2: Negative index counts from end
|
|
1099
|
+
if (index < 0) {
|
|
1100
|
+
index = value.length + index;
|
|
1101
|
+
}
|
|
1102
|
+
if (index < 0 || index >= value.length) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1106
|
+
childPath.push(index);
|
|
1107
|
+
return [{
|
|
1108
|
+
value: value[index],
|
|
1109
|
+
path: formatNormalizedPath(childPath),
|
|
1110
|
+
}];
|
|
1111
|
+
}
|
|
1112
|
+
function evaluateSliceSelector(selector, node) {
|
|
1113
|
+
// RFC 9535 §2.3.4.2: Array slice
|
|
1114
|
+
const value = node.value;
|
|
1115
|
+
if (!Array.isArray(value)) {
|
|
1116
|
+
return [];
|
|
1117
|
+
}
|
|
1118
|
+
const len = value.length;
|
|
1119
|
+
const step = selector.step ?? 1;
|
|
1120
|
+
// RFC 9535 §2.3.4.2.1: step of 0 selects no elements
|
|
1121
|
+
if (step === 0) {
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
// RFC 9535 Table 8: Default values depend on step sign
|
|
1125
|
+
const defaultStart = step >= 0 ? 0 : len - 1;
|
|
1126
|
+
const defaultEnd = step >= 0 ? len : -len - 1;
|
|
1127
|
+
const start = selector.start ?? defaultStart;
|
|
1128
|
+
const end = selector.end ?? defaultEnd;
|
|
1129
|
+
// Normalize
|
|
1130
|
+
const nStart = normalize(start, len);
|
|
1131
|
+
const nEnd = normalize(end, len);
|
|
1132
|
+
// Compute bounds
|
|
1133
|
+
let lower, upper;
|
|
1134
|
+
if (step >= 0) {
|
|
1135
|
+
lower = Math.min(Math.max(nStart, 0), len);
|
|
1136
|
+
upper = Math.min(Math.max(nEnd, 0), len);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
lower = Math.min(Math.max(nEnd, -1), len - 1);
|
|
1140
|
+
upper = Math.min(Math.max(nStart, -1), len - 1);
|
|
1141
|
+
}
|
|
1142
|
+
const result = [];
|
|
1143
|
+
const basePath = parseNormalizedPath(node.path);
|
|
1144
|
+
if (step > 0) {
|
|
1145
|
+
for (let i = lower; i < upper; i += step) {
|
|
1146
|
+
const childPath = [...basePath, i];
|
|
1147
|
+
result.push({
|
|
1148
|
+
value: value[i],
|
|
1149
|
+
path: formatNormalizedPath(childPath),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
for (let i = upper; i > lower; i += step) {
|
|
1155
|
+
const childPath = [...basePath, i];
|
|
1156
|
+
result.push({
|
|
1157
|
+
value: value[i],
|
|
1158
|
+
path: formatNormalizedPath(childPath),
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return result;
|
|
1163
|
+
}
|
|
1164
|
+
function normalize(i, len) {
|
|
1165
|
+
return i >= 0 ? i : len + i;
|
|
1166
|
+
}
|
|
1167
|
+
function evaluateFilterSelector(selector, node, ctx) {
|
|
1168
|
+
// RFC 9535 §2.3.5.2: Filter children by logical expression
|
|
1169
|
+
const value = node.value;
|
|
1170
|
+
const result = [];
|
|
1171
|
+
if (Array.isArray(value)) {
|
|
1172
|
+
for (let i = 0; i < value.length; i++) {
|
|
1173
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1174
|
+
childPath.push(i);
|
|
1175
|
+
const childNode = {
|
|
1176
|
+
value: value[i],
|
|
1177
|
+
path: formatNormalizedPath(childPath),
|
|
1178
|
+
};
|
|
1179
|
+
const childCtx = {
|
|
1180
|
+
...ctx,
|
|
1181
|
+
current: value[i],
|
|
1182
|
+
currentPath: childPath,
|
|
1183
|
+
};
|
|
1184
|
+
if (evaluateLogicalExpr(selector.expression, childCtx)) {
|
|
1185
|
+
result.push(childNode);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
else if (value !== null && typeof value === 'object') {
|
|
1190
|
+
for (const key of Object.keys(value)) {
|
|
1191
|
+
const childPath = parseNormalizedPath(node.path);
|
|
1192
|
+
childPath.push(key);
|
|
1193
|
+
const childValue = value[key];
|
|
1194
|
+
const childNode = {
|
|
1195
|
+
value: childValue,
|
|
1196
|
+
path: formatNormalizedPath(childPath),
|
|
1197
|
+
};
|
|
1198
|
+
const childCtx = {
|
|
1199
|
+
...ctx,
|
|
1200
|
+
current: childValue,
|
|
1201
|
+
currentPath: childPath,
|
|
1202
|
+
};
|
|
1203
|
+
if (evaluateLogicalExpr(selector.expression, childCtx)) {
|
|
1204
|
+
result.push(childNode);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
1210
|
+
// =============================================================================
|
|
1211
|
+
// Logical Expression Evaluator
|
|
1212
|
+
// =============================================================================
|
|
1213
|
+
function evaluateLogicalExpr(expr, ctx) {
|
|
1214
|
+
switch (expr.type) {
|
|
1215
|
+
case 'or':
|
|
1216
|
+
return evaluateOrExpr(expr, ctx);
|
|
1217
|
+
case 'and':
|
|
1218
|
+
return evaluateAndExpr(expr, ctx);
|
|
1219
|
+
case 'not':
|
|
1220
|
+
return evaluateNotExpr(expr, ctx);
|
|
1221
|
+
case 'comparison':
|
|
1222
|
+
return evaluateComparisonExpr(expr, ctx);
|
|
1223
|
+
case 'test':
|
|
1224
|
+
return evaluateTestExpr(expr, ctx);
|
|
1225
|
+
case 'function':
|
|
1226
|
+
return evaluateFunctionAsBool(expr, ctx);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function evaluateOrExpr(expr, ctx) {
|
|
1230
|
+
for (const operand of expr.operands) {
|
|
1231
|
+
if (evaluateLogicalExpr(operand, ctx)) {
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
function evaluateAndExpr(expr, ctx) {
|
|
1238
|
+
for (const operand of expr.operands) {
|
|
1239
|
+
if (!evaluateLogicalExpr(operand, ctx)) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
function evaluateNotExpr(expr, ctx) {
|
|
1246
|
+
return !evaluateLogicalExpr(expr.operand, ctx);
|
|
1247
|
+
}
|
|
1248
|
+
function evaluateComparisonExpr(expr, ctx) {
|
|
1249
|
+
const left = evaluateComparable(expr.left, ctx);
|
|
1250
|
+
const right = evaluateComparable(expr.right, ctx);
|
|
1251
|
+
return compare(left, expr.operator, right);
|
|
1252
|
+
}
|
|
1253
|
+
function evaluateTestExpr(expr, ctx) {
|
|
1254
|
+
// RFC 9535 §2.3.5.2: Existence test - true if nodelist is non-empty
|
|
1255
|
+
const nodes = evaluateFilterQuery(expr.query, ctx);
|
|
1256
|
+
return nodes.length > 0;
|
|
1257
|
+
}
|
|
1258
|
+
function evaluateFunctionAsBool(expr, ctx) {
|
|
1259
|
+
// RFC 9535 §2.4: Functions returning LogicalType
|
|
1260
|
+
const result = evaluateFunction(expr, ctx);
|
|
1261
|
+
return result === true;
|
|
1262
|
+
}
|
|
1263
|
+
function evaluateComparable(comp, ctx) {
|
|
1264
|
+
switch (comp.type) {
|
|
1265
|
+
case 'literal':
|
|
1266
|
+
return comp.value;
|
|
1267
|
+
case 'singular-query':
|
|
1268
|
+
return evaluateSingularQuery(comp, ctx);
|
|
1269
|
+
case 'function':
|
|
1270
|
+
return evaluateFunction(comp, ctx);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function evaluateSingularQuery(query, ctx) {
|
|
1274
|
+
const startValue = query.root === '$' ? ctx.root : ctx.current;
|
|
1275
|
+
const startPath = query.root === '$' ? [] : [...ctx.currentPath];
|
|
1276
|
+
let nodes = [{
|
|
1277
|
+
value: startValue,
|
|
1278
|
+
path: formatNormalizedPath(startPath),
|
|
1279
|
+
}];
|
|
1280
|
+
for (const segment of query.segments) {
|
|
1281
|
+
nodes = evaluateSegment(segment, nodes, ctx);
|
|
1282
|
+
}
|
|
1283
|
+
// RFC 9535 §2.3.5.1: Singular query produces at most one node
|
|
1284
|
+
return nodes.length === 1 ? nodes[0].value : undefined;
|
|
1285
|
+
}
|
|
1286
|
+
function evaluateFilterQuery(query, ctx) {
|
|
1287
|
+
const startValue = query.root === '$' ? ctx.root : ctx.current;
|
|
1288
|
+
const startPath = query.root === '$' ? [] : [...ctx.currentPath];
|
|
1289
|
+
let nodes = [{
|
|
1290
|
+
value: startValue,
|
|
1291
|
+
path: formatNormalizedPath(startPath),
|
|
1292
|
+
}];
|
|
1293
|
+
for (const segment of query.segments) {
|
|
1294
|
+
nodes = evaluateSegment(segment, nodes, ctx);
|
|
1295
|
+
}
|
|
1296
|
+
return nodes;
|
|
1297
|
+
}
|
|
1298
|
+
// =============================================================================
|
|
1299
|
+
// Comparison
|
|
1300
|
+
// =============================================================================
|
|
1301
|
+
function compare(left, op, right) {
|
|
1302
|
+
// RFC 9535 §2.3.5.2.2: Comparison semantics
|
|
1303
|
+
switch (op) {
|
|
1304
|
+
case '==':
|
|
1305
|
+
return deepEqual(left, right);
|
|
1306
|
+
case '!=':
|
|
1307
|
+
return !deepEqual(left, right);
|
|
1308
|
+
case '<':
|
|
1309
|
+
return compareLessThan(left, right);
|
|
1310
|
+
case '<=':
|
|
1311
|
+
return compareLessThanOrEqual(left, right);
|
|
1312
|
+
case '>':
|
|
1313
|
+
return compareLessThan(right, left);
|
|
1314
|
+
case '>=':
|
|
1315
|
+
return compareLessThanOrEqual(right, left);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function deepEqual(a, b) {
|
|
1319
|
+
if (a === b)
|
|
1320
|
+
return true;
|
|
1321
|
+
if (typeof a !== typeof b)
|
|
1322
|
+
return false;
|
|
1323
|
+
if (a === null || b === null)
|
|
1324
|
+
return a === b;
|
|
1325
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1326
|
+
if (a.length !== b.length)
|
|
1327
|
+
return false;
|
|
1328
|
+
for (let i = 0; i < a.length; i++) {
|
|
1329
|
+
if (!deepEqual(a[i], b[i]))
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
1335
|
+
const keysA = Object.keys(a);
|
|
1336
|
+
const keysB = Object.keys(b);
|
|
1337
|
+
if (keysA.length !== keysB.length)
|
|
1338
|
+
return false;
|
|
1339
|
+
for (const key of keysA) {
|
|
1340
|
+
if (!keysB.includes(key))
|
|
1341
|
+
return false;
|
|
1342
|
+
if (!deepEqual(a[key], b[key]))
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
function compareLessThan(left, right) {
|
|
1350
|
+
// RFC 9535 §2.3.5.2.2: < only defined for numbers and strings of same type
|
|
1351
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1352
|
+
return left < right;
|
|
1353
|
+
}
|
|
1354
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
1355
|
+
return left < right;
|
|
1356
|
+
}
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
function compareLessThanOrEqual(left, right) {
|
|
1360
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1361
|
+
return left <= right;
|
|
1362
|
+
}
|
|
1363
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
1364
|
+
return left <= right;
|
|
1365
|
+
}
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
// =============================================================================
|
|
1369
|
+
// Built-in Functions
|
|
1370
|
+
// =============================================================================
|
|
1371
|
+
function evaluateFunction(expr, ctx) {
|
|
1372
|
+
switch (expr.name) {
|
|
1373
|
+
case 'length':
|
|
1374
|
+
return fnLength(expr.args, ctx);
|
|
1375
|
+
case 'count':
|
|
1376
|
+
return fnCount(expr.args, ctx);
|
|
1377
|
+
case 'match':
|
|
1378
|
+
return fnMatch(expr.args, ctx);
|
|
1379
|
+
case 'search':
|
|
1380
|
+
return fnSearch(expr.args, ctx);
|
|
1381
|
+
case 'value':
|
|
1382
|
+
return fnValue(expr.args, ctx);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// RFC 9535 §2.4.4: length() function
|
|
1386
|
+
function fnLength(args, ctx) {
|
|
1387
|
+
if (args.length !== 1)
|
|
1388
|
+
return null;
|
|
1389
|
+
const value = evaluateFunctionArg(args[0], ctx);
|
|
1390
|
+
if (typeof value === 'string') {
|
|
1391
|
+
return value.length;
|
|
1392
|
+
}
|
|
1393
|
+
if (Array.isArray(value)) {
|
|
1394
|
+
return value.length;
|
|
1395
|
+
}
|
|
1396
|
+
if (value !== null && typeof value === 'object') {
|
|
1397
|
+
return Object.keys(value).length;
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
// RFC 9535 §2.4.5: count() function
|
|
1402
|
+
function fnCount(args, ctx) {
|
|
1403
|
+
if (args.length !== 1)
|
|
1404
|
+
return 0;
|
|
1405
|
+
const arg = args[0];
|
|
1406
|
+
if (arg.type === 'query') {
|
|
1407
|
+
const nodes = evaluateFilterQuery(arg, ctx);
|
|
1408
|
+
return nodes.length;
|
|
1409
|
+
}
|
|
1410
|
+
return 0;
|
|
1411
|
+
}
|
|
1412
|
+
// RFC 9535 §2.4.6: match() function - full regex match
|
|
1413
|
+
function fnMatch(args, ctx) {
|
|
1414
|
+
if (args.length !== 2)
|
|
1415
|
+
return false;
|
|
1416
|
+
const value = evaluateFunctionArg(args[0], ctx);
|
|
1417
|
+
const pattern = evaluateFunctionArg(args[1], ctx);
|
|
1418
|
+
if (typeof value !== 'string' || typeof pattern !== 'string') {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
// RFC 9535 §2.4.6: Full match (anchored)
|
|
1423
|
+
const re = new RegExp(`^(?:${pattern})$`, 'u');
|
|
1424
|
+
return re.test(value);
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
// RFC 9535 §2.4.7: search() function - partial regex match
|
|
1431
|
+
function fnSearch(args, ctx) {
|
|
1432
|
+
if (args.length !== 2)
|
|
1433
|
+
return false;
|
|
1434
|
+
const value = evaluateFunctionArg(args[0], ctx);
|
|
1435
|
+
const pattern = evaluateFunctionArg(args[1], ctx);
|
|
1436
|
+
if (typeof value !== 'string' || typeof pattern !== 'string') {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const re = new RegExp(pattern, 'u');
|
|
1441
|
+
return re.test(value);
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// RFC 9535 §2.4.8: value() function
|
|
1448
|
+
function fnValue(args, ctx) {
|
|
1449
|
+
if (args.length !== 1)
|
|
1450
|
+
return null;
|
|
1451
|
+
const arg = args[0];
|
|
1452
|
+
if (arg.type === 'query') {
|
|
1453
|
+
const nodes = evaluateFilterQuery(arg, ctx);
|
|
1454
|
+
return nodes.length === 1 ? nodes[0].value : null;
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
function evaluateFunctionArg(arg, ctx) {
|
|
1459
|
+
switch (arg.type) {
|
|
1460
|
+
case 'literal':
|
|
1461
|
+
return arg.value;
|
|
1462
|
+
case 'query':
|
|
1463
|
+
// For value-type arguments, get the singular value
|
|
1464
|
+
const nodes = evaluateFilterQuery(arg, ctx);
|
|
1465
|
+
return nodes.length === 1 ? nodes[0].value : undefined;
|
|
1466
|
+
case 'function':
|
|
1467
|
+
return evaluateFunction(arg, ctx);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
//# sourceMappingURL=jsonpath.js.map
|