@jarenjs/json 0.9.2 → 0.34.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/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/path.js
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
// - Singular queries (`$.a.b[3]`) compile to a direct property walk with
|
|
17
17
|
// no intermediate arrays.
|
|
18
18
|
// - Filter comparables compile to sentinel-returning getters; existence
|
|
19
|
-
// tests on singular queries never materialize nodelists.
|
|
19
|
+
// tests on singular queries never materialize nodelists. A `$`-rooted
|
|
20
|
+
// comparable is memoized per filter application (its value is invariant
|
|
21
|
+
// across the candidates), with the memo reset before each application so
|
|
22
|
+
// a mutated document re-queried under the same root reads fresh.
|
|
20
23
|
// - `match()`/`search()` with a literal pattern precompile their RegExp;
|
|
21
24
|
// dynamic patterns use a per-callsite monomorphic cache.
|
|
22
25
|
// - Normalized-path production (RFC 9535 section 2.7) is compiled lazily,
|
|
@@ -30,9 +33,17 @@ import {
|
|
|
30
33
|
compileSingularGetter,
|
|
31
34
|
compileSegmentV,
|
|
32
35
|
runSegmentsV,
|
|
36
|
+
compileSegmentG,
|
|
37
|
+
runSegmentsG,
|
|
33
38
|
compileSegmentP,
|
|
34
39
|
runSegmentsP,
|
|
40
|
+
scanArrayIndex,
|
|
41
|
+
appendName,
|
|
35
42
|
} from './segments.js';
|
|
43
|
+
import {
|
|
44
|
+
parseJSONPointer,
|
|
45
|
+
encodeJSONPointerSegment,
|
|
46
|
+
} from './pointer.js';
|
|
36
47
|
import {
|
|
37
48
|
CC_TAB,
|
|
38
49
|
CC_LF,
|
|
@@ -64,7 +75,12 @@ import {
|
|
|
64
75
|
CC_UNDERSCORE,
|
|
65
76
|
CC_PIPE,
|
|
66
77
|
isDigitCode,
|
|
78
|
+
isNameStartCode,
|
|
79
|
+
isNameCharCode,
|
|
67
80
|
} from '@jarenjs/core/scan';
|
|
81
|
+
import { deepFreeze } from '@jarenjs/core/object';
|
|
82
|
+
import { createBoundedCache, createWeakCache } from '@jarenjs/core/cache';
|
|
83
|
+
import { LabeledSyntaxError } from './errors.js';
|
|
68
84
|
|
|
69
85
|
/**
|
|
70
86
|
* Sentinel for the absence of a value ("Nothing" in RFC 9535 terms), as
|
|
@@ -72,32 +88,23 @@ import {
|
|
|
72
88
|
*/
|
|
73
89
|
export const JSONPATH_NOTHING = NOTHING;
|
|
74
90
|
|
|
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
91
|
/**
|
|
85
92
|
* Error thrown when a JSONPath query is not valid RFC 9535 syntax
|
|
86
93
|
* (including queries that are not well-typed per section 2.4.3).
|
|
87
94
|
*/
|
|
88
|
-
export class JSONPathSyntaxError extends
|
|
95
|
+
export class JSONPathSyntaxError extends LabeledSyntaxError {
|
|
89
96
|
constructor(message, source, position) {
|
|
90
|
-
super(
|
|
91
|
-
this.name = 'JSONPathSyntaxError';
|
|
92
|
-
this.source = source;
|
|
93
|
-
this.position = position;
|
|
97
|
+
super('JSONPathSyntaxError', 'JSONPath', message, source, position);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// Built-in function extensions (RFC 9535 section 2.4).
|
|
98
102
|
// Parameter/return types: 'value' = ValueType, 'nodes' = NodesType,
|
|
99
|
-
// 'logical' = LogicalType.
|
|
103
|
+
// 'logical' = LogicalType. Null-prototype, so an inherited member name
|
|
104
|
+
// (`constructor`, `tostring`) is an unknown function and not a
|
|
105
|
+
// half-formed descriptor.
|
|
100
106
|
const FUNCTIONS = {
|
|
107
|
+
__proto__: null,
|
|
101
108
|
length: { params: ['value'], returns: 'value' },
|
|
102
109
|
count: { params: ['nodes'], returns: 'value' },
|
|
103
110
|
match: { params: ['value', 'value'], returns: 'logical' },
|
|
@@ -105,21 +112,83 @@ const FUNCTIONS = {
|
|
|
105
112
|
value: { params: ['nodes'], returns: 'value' },
|
|
106
113
|
};
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
// function-name = LCALPHA *(LCALPHA / "_" / DIGIT) (RFC 9535 2.4.1)
|
|
116
|
+
const RE_FUNCTION_NAME = /^[a-z][a-z0-9_]*$/;
|
|
117
|
+
const TYPE_NAMES = ['value', 'nodes', 'logical'];
|
|
109
118
|
|
|
110
|
-
|
|
119
|
+
// Registries are validated once and memoized on the registry object, so
|
|
120
|
+
// compiling a thousand queries against one registry validates it once.
|
|
121
|
+
const FUNCTION_TABLES = new WeakMap();
|
|
111
122
|
|
|
112
|
-
function
|
|
113
|
-
return (
|
|
114
|
-
|| (c >= 0x61 && c <= 0x7A) // a-z
|
|
115
|
-
|| c === CC_UNDERSCORE
|
|
116
|
-
|| c >= 0x80; // any non-ASCII code unit
|
|
123
|
+
function registryError(message) {
|
|
124
|
+
return new TypeError(`options.pathFunctions: ${message}`);
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
function
|
|
120
|
-
|
|
127
|
+
function validateType(type, what) {
|
|
128
|
+
if (!TYPE_NAMES.includes(type))
|
|
129
|
+
throw registryError(`${what} must be one of ${TYPE_NAMES.join(', ')}`);
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Validate a registry of custom function extensions and merge it over
|
|
134
|
+
* the built-ins. A registry entry declares its parameter and result
|
|
135
|
+
* types exactly like a built-in, which is what lets the parser apply
|
|
136
|
+
* the same well-typedness rules (RFC 9535 section 2.4.3) to it.
|
|
137
|
+
* @param {object} functions - name to `{ params, returns, evaluate }`
|
|
138
|
+
* @returns {object} the merged, null-prototype function table
|
|
139
|
+
*/
|
|
140
|
+
function resolveFunctionTable(functions) {
|
|
141
|
+
const cached = FUNCTION_TABLES.get(functions);
|
|
142
|
+
if (cached !== undefined)
|
|
143
|
+
return cached;
|
|
144
|
+
const table = { __proto__: null, ...FUNCTIONS };
|
|
145
|
+
for (const name of Object.keys(functions)) {
|
|
146
|
+
const def = functions[name];
|
|
147
|
+
if (!RE_FUNCTION_NAME.test(name))
|
|
148
|
+
throw registryError(`'${name}' is not a valid function name`);
|
|
149
|
+
// section 2.4.1: an extension must not redefine a built-in, and a
|
|
150
|
+
// literal keyword would be unreadable as a call
|
|
151
|
+
if (FUNCTIONS[name] !== undefined)
|
|
152
|
+
throw registryError(`'${name}' is a built-in function`);
|
|
153
|
+
if (name === 'true' || name === 'false' || name === 'null')
|
|
154
|
+
throw registryError(`'${name}' is a literal, not a function name`);
|
|
155
|
+
if (def === null || typeof def !== 'object')
|
|
156
|
+
throw registryError(`'${name}' must be a { params, returns, evaluate } object`);
|
|
157
|
+
if (!Array.isArray(def.params))
|
|
158
|
+
throw registryError(`'${name}'.params must be an array of parameter types`);
|
|
159
|
+
for (let i = 0; i < def.params.length; i++)
|
|
160
|
+
validateType(def.params[i], `'${name}'.params[${i}]`);
|
|
161
|
+
validateType(def.returns, `'${name}'.returns`);
|
|
162
|
+
if (typeof def.evaluate !== 'function')
|
|
163
|
+
throw registryError(`'${name}'.evaluate must be a function`);
|
|
164
|
+
table[name] = {
|
|
165
|
+
params: [...def.params],
|
|
166
|
+
returns: def.returns,
|
|
167
|
+
evaluate: def.evaluate,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
Object.freeze(table);
|
|
171
|
+
FUNCTION_TABLES.set(functions, table);
|
|
172
|
+
return table;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Resolve the function table a parse should use. The overwhelmingly
|
|
176
|
+
// common call has no options at all and reaches the built-ins directly.
|
|
177
|
+
function functionTableOf(options) {
|
|
178
|
+
if (options == null)
|
|
179
|
+
return FUNCTIONS;
|
|
180
|
+
const functions = options.pathFunctions;
|
|
181
|
+
if (functions == null)
|
|
182
|
+
return FUNCTIONS;
|
|
183
|
+
if (typeof functions !== 'object' || Array.isArray(functions))
|
|
184
|
+
throw registryError('must be a plain object of function extensions');
|
|
185
|
+
return resolveFunctionTable(functions);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
|
|
190
|
+
//#region parser
|
|
191
|
+
|
|
123
192
|
/**
|
|
124
193
|
* Selects a named object member (RFC 9535 name selector).
|
|
125
194
|
* @typedef {Object} JSONPathNameSelector
|
|
@@ -196,6 +265,7 @@ function isNameCharCode(c) {
|
|
|
196
265
|
* values: (data: any) => any[],
|
|
197
266
|
* first: (data: any) => any,
|
|
198
267
|
* exists: (data: any) => boolean,
|
|
268
|
+
* iterate: (data: any) => Generator<any>,
|
|
199
269
|
* nodes: (data: any) => JSONPathNode[],
|
|
200
270
|
* paths: (data: any) => string[],
|
|
201
271
|
* source: string,
|
|
@@ -203,16 +273,43 @@ function isNameCharCode(c) {
|
|
|
203
273
|
* }} JSONPathQuery
|
|
204
274
|
*/
|
|
205
275
|
|
|
276
|
+
/**
|
|
277
|
+
* A custom JSONPath function extension (RFC 9535 section 2.4). The
|
|
278
|
+
* declared types are what the parser type-checks call sites against
|
|
279
|
+
* (section 2.4.3), exactly as it does for the five built-ins.
|
|
280
|
+
*
|
|
281
|
+
* `evaluate` receives one argument per declared parameter: a `value`
|
|
282
|
+
* parameter arrives as a JSON value or `JSONPATH_NOTHING`, a `nodes`
|
|
283
|
+
* parameter as an array of the selected values, a `logical` parameter
|
|
284
|
+
* as a boolean. Its result must match `returns` - a `value` function
|
|
285
|
+
* may return `undefined` to mean Nothing.
|
|
286
|
+
* @typedef {Object} JSONPathFunction
|
|
287
|
+
* @property {('value'|'nodes'|'logical')[]} params - Declared parameter types
|
|
288
|
+
* @property {'value'|'nodes'|'logical'} returns - Declared result type
|
|
289
|
+
* @property {(...args: any[]) => any} evaluate - The implementation
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Options accepted by the JSONPath entry points.
|
|
294
|
+
* @typedef {Object} JSONPathOptions
|
|
295
|
+
* @property {Record<string, JSONPathFunction>} [pathFunctions] - Custom
|
|
296
|
+
* function extensions, by name. A name must match the RFC's
|
|
297
|
+
* `function-name` production and must not redefine a built-in.
|
|
298
|
+
*/
|
|
299
|
+
|
|
206
300
|
/**
|
|
207
301
|
* Parse a JSONPath query string into an AST.
|
|
208
302
|
* @param {string} source - The JSONPath expression (e.g. `$.store.book[?@.price < 10].title`)
|
|
303
|
+
* @param {JSONPathOptions} [options] - Parse options
|
|
209
304
|
* @returns {JSONPathAst} The parsed query AST
|
|
210
305
|
* @throws {JSONPathSyntaxError} When the query violates the RFC 9535 grammar
|
|
306
|
+
* @throws {TypeError} When `options.pathFunctions` is not a valid registry
|
|
211
307
|
*/
|
|
212
|
-
export function parseJSONPath(source) {
|
|
308
|
+
export function parseJSONPath(source, options = undefined) {
|
|
213
309
|
if (typeof source !== 'string')
|
|
214
310
|
throw new JSONPathSyntaxError('query must be a string', String(source), 0);
|
|
215
311
|
|
|
312
|
+
const FUNCS = functionTableOf(options);
|
|
216
313
|
const len = source.length;
|
|
217
314
|
let pos = 0;
|
|
218
315
|
|
|
@@ -247,7 +344,7 @@ export function parseJSONPath(source) {
|
|
|
247
344
|
|
|
248
345
|
function parseMemberNameShorthand() {
|
|
249
346
|
const start = pos;
|
|
250
|
-
if (!
|
|
347
|
+
if (!isNameStartCode(cc(pos)))
|
|
251
348
|
fail('expected member name');
|
|
252
349
|
while (pos < len) {
|
|
253
350
|
const c = source.charCodeAt(pos);
|
|
@@ -779,7 +876,7 @@ export function parseJSONPath(source) {
|
|
|
779
876
|
}
|
|
780
877
|
|
|
781
878
|
function parseFunctionExpr(name, at) {
|
|
782
|
-
const def =
|
|
879
|
+
const def = FUNCS[name];
|
|
783
880
|
if (def === undefined)
|
|
784
881
|
fail(`unknown function '${name}'`, at);
|
|
785
882
|
pos++; // consume '('
|
|
@@ -787,7 +884,12 @@ export function parseJSONPath(source) {
|
|
|
787
884
|
const args = [];
|
|
788
885
|
if (cc(pos) !== CC_RPAREN) {
|
|
789
886
|
for (;;) {
|
|
790
|
-
|
|
887
|
+
// argument parsing is type-directed: a LogicalType parameter
|
|
888
|
+
// takes the whole logical-expr production (RFC 9535 2.4.3), so
|
|
889
|
+
// `!`, `(`, comparisons and `&&`/`||` are only legal there
|
|
890
|
+
args.push(def.params[args.length] === 'logical'
|
|
891
|
+
? { kind: 'logical', expr: parseLogicalOr() }
|
|
892
|
+
: parseFunctionArg());
|
|
791
893
|
skipWS();
|
|
792
894
|
if (cc(pos) === CC_COMMA) {
|
|
793
895
|
pos++;
|
|
@@ -816,15 +918,22 @@ export function parseJSONPath(source) {
|
|
|
816
918
|
continue;
|
|
817
919
|
fail(`argument ${i + 1} of '${name}' must be of type ValueType`, at);
|
|
818
920
|
}
|
|
819
|
-
else
|
|
921
|
+
else if (param === 'nodes') {
|
|
820
922
|
if (arg.kind === 'query')
|
|
821
923
|
continue;
|
|
822
924
|
if (arg.kind === 'func' && arg.returns === 'nodes')
|
|
823
925
|
continue;
|
|
824
926
|
fail(`argument ${i + 1} of '${name}' must be a query (NodesType)`, at);
|
|
825
927
|
}
|
|
928
|
+
// 'logical': parseLogicalOr only produces well-typed logical
|
|
929
|
+
// expressions, and it already rejects a ValueType function there
|
|
930
|
+
}
|
|
931
|
+
const node = { kind: 'func', name, args, returns: def.returns };
|
|
932
|
+
if (def.evaluate !== undefined) {
|
|
933
|
+
node.params = def.params;
|
|
934
|
+
node.evaluate = def.evaluate;
|
|
826
935
|
}
|
|
827
|
-
return
|
|
936
|
+
return node;
|
|
828
937
|
}
|
|
829
938
|
|
|
830
939
|
//#endregion
|
|
@@ -860,6 +969,9 @@ export function parseJSONPath(source) {
|
|
|
860
969
|
* - `query(data)` / `query.values(data)` - array of matched values
|
|
861
970
|
* - `query.first(data)` - first matched value, or `undefined`
|
|
862
971
|
* - `query.exists(data)` - true when the query selects at least one node
|
|
972
|
+
* - `query.iterate(data)` - a generator yielding matched values on
|
|
973
|
+
* demand, in document order; `first`/`exists` are one pull of it, so
|
|
974
|
+
* none of the three builds a nodelist it does not need
|
|
863
975
|
* - `query.nodes(data)` - array of `{ path, value }` with normalized paths
|
|
864
976
|
* - `query.paths(data)` - array of normalized paths (RFC 9535 section 2.7)
|
|
865
977
|
* - `query.source` - the original query string
|
|
@@ -867,18 +979,20 @@ export function parseJSONPath(source) {
|
|
|
867
979
|
* compiled path mode must agree with the eagerly compiled value mode)
|
|
868
980
|
*
|
|
869
981
|
* @param {string} source - The JSONPath expression
|
|
982
|
+
* @param {JSONPathOptions} [options] - Compile options
|
|
870
983
|
* @returns {JSONPathQuery} The compiled query function
|
|
871
984
|
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
985
|
+
* @throws {TypeError} When `options.pathFunctions` is not a valid registry
|
|
872
986
|
* @example
|
|
873
987
|
* const q = compileJSONPath('$.store.book[?@.price < 10].title');
|
|
874
988
|
* q(data); // ['Sayings of the Century', 'Moby Dick']
|
|
875
989
|
* q.paths(data); // ["$['store']['book'][0]['title']", ...]
|
|
876
990
|
*/
|
|
877
|
-
export function compileJSONPath(source) {
|
|
878
|
-
const ast = deepFreeze(parseJSONPath(source));
|
|
991
|
+
export function compileJSONPath(source, options = undefined) {
|
|
992
|
+
const ast = deepFreeze(parseJSONPath(source, options));
|
|
879
993
|
const segments = ast.segments;
|
|
880
994
|
|
|
881
|
-
let values, first, exists;
|
|
995
|
+
let values, first, exists, iterate;
|
|
882
996
|
if (isSingularSegments(segments)) {
|
|
883
997
|
const getter = compileSingularGetter(segments, false);
|
|
884
998
|
values = (data) => {
|
|
@@ -890,15 +1004,29 @@ export function compileJSONPath(source) {
|
|
|
890
1004
|
return v === NOTHING ? undefined : v;
|
|
891
1005
|
};
|
|
892
1006
|
exists = (data) => getter(data, data) !== NOTHING;
|
|
1007
|
+
iterate = function* iterateSingular(data) {
|
|
1008
|
+
const v = getter(data, data);
|
|
1009
|
+
if (v !== NOTHING)
|
|
1010
|
+
yield v;
|
|
1011
|
+
};
|
|
893
1012
|
}
|
|
894
1013
|
else {
|
|
895
1014
|
const segs = segments.map(compileSegmentV);
|
|
896
1015
|
values = (data) => runSegmentsV(segs, data, data);
|
|
1016
|
+
// the lazy chain is a second compilation of the same selectors, so
|
|
1017
|
+
// it is built on first use - a query that only ever calls values()
|
|
1018
|
+
// never pays for it
|
|
1019
|
+
let gens = null;
|
|
1020
|
+
iterate = (data) => {
|
|
1021
|
+
if (gens === null)
|
|
1022
|
+
gens = segments.map(compileSegmentG);
|
|
1023
|
+
return runSegmentsG(gens, 0, data, data);
|
|
1024
|
+
};
|
|
897
1025
|
first = (data) => {
|
|
898
|
-
const
|
|
899
|
-
return
|
|
1026
|
+
const r = iterate(data).next();
|
|
1027
|
+
return r.done ? undefined : r.value;
|
|
900
1028
|
};
|
|
901
|
-
exists = (data) =>
|
|
1029
|
+
exists = (data) => !iterate(data).next().done;
|
|
902
1030
|
}
|
|
903
1031
|
|
|
904
1032
|
// nodes mode is compiled lazily; value-only queries never pay for it
|
|
@@ -913,6 +1041,7 @@ export function compileJSONPath(source) {
|
|
|
913
1041
|
query.values = values;
|
|
914
1042
|
query.first = first;
|
|
915
1043
|
query.exists = exists;
|
|
1044
|
+
query.iterate = iterate;
|
|
916
1045
|
query.nodes = (data) => {
|
|
917
1046
|
const { vals, paths } = runNodes(data);
|
|
918
1047
|
const nodes = new Array(vals.length);
|
|
@@ -926,26 +1055,31 @@ export function compileJSONPath(source) {
|
|
|
926
1055
|
return query;
|
|
927
1056
|
}
|
|
928
1057
|
|
|
929
|
-
const QUERY_CACHE =
|
|
930
|
-
|
|
1058
|
+
const QUERY_CACHE = createBoundedCache(512);
|
|
1059
|
+
// One cache per registry, because the same source compiles differently
|
|
1060
|
+
// under different extensions; keyed weakly so a registry that goes out
|
|
1061
|
+
// of scope takes its compiled queries with it. A WeakMap of bounded
|
|
1062
|
+
// caches — the two axes of `@jarenjs/core/cache`, composed.
|
|
1063
|
+
const REGISTRY_CACHES = createWeakCache();
|
|
1064
|
+
const boundedCacheFor = () => createBoundedCache(512);
|
|
931
1065
|
|
|
932
1066
|
/**
|
|
933
1067
|
* Apply a JSONPath query to a JSON value in one call. Compiled queries
|
|
934
|
-
* are cached (
|
|
935
|
-
* string reuse the compiled function.
|
|
1068
|
+
* are cached (bounded LRU, 512 entries), so repeated calls with the
|
|
1069
|
+
* same query string reuse the compiled function. A query compiled
|
|
1070
|
+
* against a function-extension registry is cached under that registry.
|
|
936
1071
|
* @param {string} source - The JSONPath expression
|
|
937
1072
|
* @param {any} data - The JSON value to query
|
|
1073
|
+
* @param {JSONPathOptions} [options] - Compile options
|
|
938
1074
|
* @returns {any[]} Array of matched values
|
|
939
1075
|
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
940
1076
|
*/
|
|
941
|
-
export function queryJSONPath(source, data) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
QUERY_CACHE.set(source, query);
|
|
948
|
-
}
|
|
1077
|
+
export function queryJSONPath(source, data, options = undefined) {
|
|
1078
|
+
const functions = options == null ? null : options.pathFunctions;
|
|
1079
|
+
const cache = functions == null
|
|
1080
|
+
? QUERY_CACHE
|
|
1081
|
+
: REGISTRY_CACHES.getOrCreate(functions, boundedCacheFor);
|
|
1082
|
+
const query = cache.getOrCreate(source, (src) => compileJSONPath(src, options));
|
|
949
1083
|
return query(data);
|
|
950
1084
|
}
|
|
951
1085
|
|
|
@@ -953,25 +1087,158 @@ export function queryJSONPath(source, data) {
|
|
|
953
1087
|
* Validates a JSONPath expression strictly against the RFC 9535 grammar,
|
|
954
1088
|
* including well-typedness of function expressions. Unlike the heuristic
|
|
955
1089
|
* `isValidJSONPath` in basic.js, this uses the full parser.
|
|
1090
|
+
*
|
|
1091
|
+
* Without options this recognizes the five built-in functions and
|
|
1092
|
+
* nothing else, which is what the registered `json-path` string format
|
|
1093
|
+
* asserts: a format is a property of the string itself, so it must mean
|
|
1094
|
+
* the same thing in every schema, independent of which extensions some
|
|
1095
|
+
* host happens to have installed. Pass `options.pathFunctions` to
|
|
1096
|
+
* validate against a registry instead - a host that wants its
|
|
1097
|
+
* extensions asserted registers a tester bound to them.
|
|
1098
|
+
*
|
|
956
1099
|
* @param {string} str - The JSONPath expression to validate
|
|
1100
|
+
* @param {JSONPathOptions} [options] - Compile options
|
|
957
1101
|
* @returns {boolean} True when the string is a valid RFC 9535 query
|
|
958
1102
|
* @example
|
|
959
1103
|
* isValidJSONPathStrict('$.store.book[?@.price < 10]'); // true
|
|
960
1104
|
* isValidJSONPathStrict('$.store.book[0 5]'); // false
|
|
961
1105
|
* isValidJSONPathStrict('@.name'); // false (queries start at $)
|
|
962
1106
|
*/
|
|
963
|
-
export function isValidJSONPathStrict(str) {
|
|
1107
|
+
export function isValidJSONPathStrict(str, options = undefined) {
|
|
964
1108
|
if (typeof str !== 'string')
|
|
965
1109
|
return false;
|
|
966
1110
|
try {
|
|
967
|
-
parseJSONPath(str);
|
|
1111
|
+
parseJSONPath(str, options);
|
|
968
1112
|
return true;
|
|
969
1113
|
}
|
|
970
|
-
catch {
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
// a malformed registry is the host's bug, not the string's
|
|
1116
|
+
if (!(error instanceof JSONPathSyntaxError))
|
|
1117
|
+
throw error;
|
|
971
1118
|
return false;
|
|
972
1119
|
}
|
|
973
1120
|
}
|
|
974
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* The head of a variable-rooted path string: `$` followed by a variable
|
|
1124
|
+
* name. The Jaren query format writes a path relative to a bound
|
|
1125
|
+
* variable as `$name` plus ordinary RFC 9535 segments, and the query
|
|
1126
|
+
* normalizer splits on exactly this production - so schema-time
|
|
1127
|
+
* validation and compile-time parsing agree on where the segments start.
|
|
1128
|
+
*/
|
|
1129
|
+
export const RE_JSONPATH_VARIABLE_HEAD = /^\$([A-Za-z_][A-Za-z0-9_]*)/;
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Validates a variable-rooted path string: `$name`, optionally followed
|
|
1133
|
+
* by RFC 9535 segments (`$book.price`, `$b[?@.isbn]`, `$item`).
|
|
1134
|
+
*
|
|
1135
|
+
* This is the counterpart of `isValidJSONPathStrict` for paths whose
|
|
1136
|
+
* root is a bound variable rather than the document. Such a string is
|
|
1137
|
+
* not a valid RFC 9535 query - the RFC's root identifier is `$` alone -
|
|
1138
|
+
* so it can only be checked by recognizing the head and validating the
|
|
1139
|
+
* tail as segments, which is what the query normalizer does before it
|
|
1140
|
+
* raises `JQ0004`. Without this, a schema could only pattern-check the
|
|
1141
|
+
* head and had to leave the segment grammar to the compiler.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {string} str - The variable-rooted path string to validate
|
|
1144
|
+
* @param {JSONPathOptions} [options] - Parse options
|
|
1145
|
+
* @returns {boolean} True when the string is a well-formed variable-rooted path
|
|
1146
|
+
* @example
|
|
1147
|
+
* isValidJSONPathSegments('$book.price'); // true
|
|
1148
|
+
* isValidJSONPathSegments('$book'); // true (no segments)
|
|
1149
|
+
* isValidJSONPathSegments('$book.price['); // false (unterminated segment)
|
|
1150
|
+
* isValidJSONPathSegments('$.price'); // false (no variable name; that is json-path)
|
|
1151
|
+
*/
|
|
1152
|
+
export function isValidJSONPathSegments(str, options = undefined) {
|
|
1153
|
+
if (typeof str !== 'string')
|
|
1154
|
+
return false;
|
|
1155
|
+
const head = RE_JSONPATH_VARIABLE_HEAD.exec(str);
|
|
1156
|
+
if (head === null)
|
|
1157
|
+
return false;
|
|
1158
|
+
const rest = str.slice(head[0].length);
|
|
1159
|
+
if (rest === '')
|
|
1160
|
+
return true;
|
|
1161
|
+
try {
|
|
1162
|
+
// the tail is validated by parsing it under a substituted root,
|
|
1163
|
+
// exactly as the normalizer compiles it
|
|
1164
|
+
parseJSONPath('$' + rest, options);
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
if (!(error instanceof JSONPathSyntaxError))
|
|
1169
|
+
throw error;
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
//#endregion
|
|
1175
|
+
|
|
1176
|
+
//#region normalized path <-> JSON Pointer bridge
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Convert a singular JSONPath query - which includes every RFC 9535
|
|
1180
|
+
* normalized path - to an RFC 6901 JSON Pointer, so the two addressing
|
|
1181
|
+
* standards compose.
|
|
1182
|
+
*
|
|
1183
|
+
* Any singular form is accepted (`$['store']['book'][0]`, `$.store.book[0]`);
|
|
1184
|
+
* non-singular queries and negative (from-the-end) indexes are rejected,
|
|
1185
|
+
* because a pointer cannot express them.
|
|
1186
|
+
*
|
|
1187
|
+
* @param {string} source - A singular JSONPath query
|
|
1188
|
+
* @returns {string} The equivalent JSON Pointer
|
|
1189
|
+
* @throws {JSONPathSyntaxError} When the query is invalid, not singular,
|
|
1190
|
+
* or uses a negative index
|
|
1191
|
+
* @example
|
|
1192
|
+
* jsonPointerFromJSONPath("$['store']['book'][0]['a/b']"); // '/store/book/0/a~1b'
|
|
1193
|
+
*/
|
|
1194
|
+
export function jsonPointerFromJSONPath(source) {
|
|
1195
|
+
const { segments } = parseJSONPath(source);
|
|
1196
|
+
if (!isSingularSegments(segments))
|
|
1197
|
+
throw new JSONPathSyntaxError('only a singular query converts to a JSON Pointer', source, 0);
|
|
1198
|
+
let pointer = '';
|
|
1199
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1200
|
+
const sel = segments[i].selectors[0];
|
|
1201
|
+
if (sel.kind === 'name') {
|
|
1202
|
+
pointer += '/' + encodeJSONPointerSegment(sel.name);
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
if (sel.index < 0)
|
|
1206
|
+
throw new JSONPathSyntaxError('a negative index has no JSON Pointer form', source, 0);
|
|
1207
|
+
pointer += '/' + sel.index;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return pointer;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Convert an RFC 6901 JSON Pointer to an RFC 9535 normalized path.
|
|
1215
|
+
*
|
|
1216
|
+
* A pointer token is one text with two readings (RFC 6901 lets `"2"`
|
|
1217
|
+
* address both a `"2"` member and array element 2); a normalized path
|
|
1218
|
+
* must pick one. Convention: a token that is a valid array index (digits,
|
|
1219
|
+
* no leading zeros) becomes an index selector `[2]`, everything else a
|
|
1220
|
+
* name selector `['name']`. A pointer addressing an object member that
|
|
1221
|
+
* merely looks like an index is therefore converted to the index form -
|
|
1222
|
+
* convert with the document in hand (e.g. via `query.paths`) when that
|
|
1223
|
+
* distinction matters.
|
|
1224
|
+
*
|
|
1225
|
+
* @param {string} pointer - The JSON Pointer (e.g. `/store/book/0`)
|
|
1226
|
+
* @returns {string} The normalized path (e.g. `$['store']['book'][0]`)
|
|
1227
|
+
* @throws {JSONPointerSyntaxError} When the pointer is not valid RFC 6901
|
|
1228
|
+
* @example
|
|
1229
|
+
* jsonPathFromJSONPointer('/store/book/0'); // "$['store']['book'][0]"
|
|
1230
|
+
*/
|
|
1231
|
+
export function jsonPathFromJSONPointer(pointer) {
|
|
1232
|
+
const segments = parseJSONPointer(pointer);
|
|
1233
|
+
let path = '$';
|
|
1234
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1235
|
+
const name = segments[i];
|
|
1236
|
+
const index = scanArrayIndex(name, 0, name.length);
|
|
1237
|
+
path = index >= 0 ? path + '[' + index + ']' : appendName(path, name);
|
|
1238
|
+
}
|
|
1239
|
+
return path;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
975
1242
|
//#endregion
|
|
976
1243
|
|
|
977
1244
|
//#endregion
|