@jarenjs/json 0.9.2 → 0.34.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 +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/write.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
//#region Write operations
|
|
2
|
+
// Standalone compiled write operations: set / insert / remove at a
|
|
3
|
+
// JSON Pointer (RFC 6901), a normalized path (RFC 9535 section 2.7,
|
|
4
|
+
// or any singular query), or at every node an arbitrary JSONPath query
|
|
5
|
+
// selects - with copy-on-write application throughout.
|
|
6
|
+
//
|
|
7
|
+
// The pointer-addressed writers follow the two-stage house pipeline:
|
|
8
|
+
// the target (pointer or singular query - dispatch decided once on the
|
|
9
|
+
// first character) parses into the cow.js step encoding and the writer
|
|
10
|
+
// closes over it. The JSONPath writers compile the query once; applying
|
|
11
|
+
// runs it in nodes mode and rewrites the matched locations in REVERSE
|
|
12
|
+
// document order - descendants before ancestors, later siblings before
|
|
13
|
+
// earlier ones - so array-index shifts from inserts/removes never
|
|
14
|
+
// invalidate the remaining locations, and an ancestor rewrite
|
|
15
|
+
// deterministically wins over rewrites inside it.
|
|
16
|
+
//
|
|
17
|
+
// Application is copy-on-write via the shared owned-set machinery
|
|
18
|
+
// (cow.js): the input document is never mutated, only the written spine
|
|
19
|
+
// is cloned (once, however many locations share it), untouched subtrees
|
|
20
|
+
// are shared with the result. `{ mutate: true }` patches in place.
|
|
21
|
+
//
|
|
22
|
+
// Error codes:
|
|
23
|
+
// JW0001 - the write target has the wrong form (not a pointer or a
|
|
24
|
+
// singular JSONPath query)
|
|
25
|
+
// JW2001 - the target location does not exist
|
|
26
|
+
// JW2002 - invalid array position
|
|
27
|
+
// JW2003 - the root of the document cannot be removed
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
CC_DOLLAR,
|
|
31
|
+
CC_SQUOTE,
|
|
32
|
+
CC_BACKSLASH,
|
|
33
|
+
CC_RBRACKET,
|
|
34
|
+
CC_0,
|
|
35
|
+
} from '@jarenjs/core/scan';
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
parseJSONPointer,
|
|
39
|
+
} from './pointer.js';
|
|
40
|
+
|
|
41
|
+
import { parseJSONPath, compileJSONPath } from './path.js';
|
|
42
|
+
|
|
43
|
+
import { scanArrayIndex, isSingularSegments } from './segments.js';
|
|
44
|
+
|
|
45
|
+
import { isJsonObject, setObjectMember } from '@jarenjs/core/object';
|
|
46
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
makeState,
|
|
50
|
+
ownedRoot,
|
|
51
|
+
ownedChild,
|
|
52
|
+
stepArrayIndex,
|
|
53
|
+
} from './cow.js';
|
|
54
|
+
|
|
55
|
+
const hasOwn = Object.hasOwn;
|
|
56
|
+
|
|
57
|
+
//#region errors
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error thrown when a write target is malformed (`JW0001`) or a write
|
|
61
|
+
* fails to apply (`JW2xxx`). `dataPath` is the write target as given
|
|
62
|
+
* (a JSON Pointer or JSONPath), or the normalized path of the failing
|
|
63
|
+
* location for query-selected writes — rendered `in data <path>` per
|
|
64
|
+
* the `@jarenjs/core` coded contract, because it locates *data*, not a
|
|
65
|
+
* document.
|
|
66
|
+
*/
|
|
67
|
+
export class JsonWriteError extends CodedError {
|
|
68
|
+
constructor(code, reason, dataPath) {
|
|
69
|
+
super('JsonWriteError', code, reason, { dataPath });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeError(code, message, dataPath) {
|
|
74
|
+
return new JsonWriteError(code, message, dataPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
|
|
79
|
+
//#region targets
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a write target into the cow.js step encoding. A target starting
|
|
83
|
+
* with `$` is a JSONPath and must be singular (normalized paths are);
|
|
84
|
+
* `''` or a leading `/` is an RFC 6901 pointer (one token, two forms).
|
|
85
|
+
* The dispatch is decided once at compile time.
|
|
86
|
+
*/
|
|
87
|
+
function parseWriteTarget(target) {
|
|
88
|
+
if (typeof target !== 'string')
|
|
89
|
+
throw writeError('JW0001', 'a write target must be a string', String(target));
|
|
90
|
+
if (target.length !== 0 && target.charCodeAt(0) === CC_DOLLAR) {
|
|
91
|
+
const { segments } = parseJSONPath(target);
|
|
92
|
+
if (!isSingularSegments(segments))
|
|
93
|
+
throw writeError('JW0001', 'a write target must be a singular JSONPath query', target);
|
|
94
|
+
const len = segments.length;
|
|
95
|
+
const names = new Array(len);
|
|
96
|
+
const indexes = new Array(len);
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
const sel = segments[i].selectors[0];
|
|
99
|
+
if (sel.kind === 'name') {
|
|
100
|
+
names[i] = sel.name;
|
|
101
|
+
indexes[i] = -1;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
names[i] = null;
|
|
105
|
+
indexes[i] = sel.index;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { pointer: target, names, indexes, len };
|
|
109
|
+
}
|
|
110
|
+
const names = parseJSONPointer(target);
|
|
111
|
+
const len = names.length;
|
|
112
|
+
const indexes = new Array(len);
|
|
113
|
+
for (let i = 0; i < len; i++)
|
|
114
|
+
indexes[i] = scanArrayIndex(names[i], 0, names[i].length);
|
|
115
|
+
return { pointer: target, names, indexes, len };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseMutate(options) {
|
|
119
|
+
return options !== undefined && options !== null && options.mutate === true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseParents(options) {
|
|
123
|
+
if (options === undefined || options === null || options.parents === undefined)
|
|
124
|
+
return false;
|
|
125
|
+
if (options.parents === 'reject')
|
|
126
|
+
return false;
|
|
127
|
+
if (options.parents === 'create')
|
|
128
|
+
return true;
|
|
129
|
+
throw new TypeError(`unknown 'parents' option '${options.parents}'`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
|
|
134
|
+
//#region walk and leaf operations
|
|
135
|
+
|
|
136
|
+
// Walk to the parent of the target location, cloning the spine on first
|
|
137
|
+
// touch. Steps use the cow.js encoding; a miss raises JW2001.
|
|
138
|
+
function walkOwnedParent(state, names, indexes, plen, dataPath) {
|
|
139
|
+
let v = ownedRoot(state);
|
|
140
|
+
for (let i = 0; i < plen; i++) {
|
|
141
|
+
if (Array.isArray(v)) {
|
|
142
|
+
const idx = stepArrayIndex(v, names[i], indexes[i]);
|
|
143
|
+
if (idx < 0 || idx >= v.length)
|
|
144
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
145
|
+
v = ownedChild(state, v, v[idx], idx);
|
|
146
|
+
}
|
|
147
|
+
else if (typeof v === 'object' && v !== null) {
|
|
148
|
+
const name = names[i];
|
|
149
|
+
if (name === null || !hasOwn(v, name))
|
|
150
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
151
|
+
v = ownedChild(state, v, v[name], name);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return v;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A value may be an updater function `(oldValue, location) => next`. */
|
|
161
|
+
function resolveValue(value, oldValue, location) {
|
|
162
|
+
return typeof value === 'function' ? value(oldValue, location) : value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The container a created step should hold, decided by the FOLLOWING
|
|
166
|
+
// step: an array for index-shaped steps ('-', a typed index, or a
|
|
167
|
+
// numeric token), an object otherwise - the same inference schema-less
|
|
168
|
+
// form data uses (@jarenjs/forms).
|
|
169
|
+
function createdContainer(names, indexes, step) {
|
|
170
|
+
return names[step] === null || names[step] === '-' || indexes[step] >= 0
|
|
171
|
+
? []
|
|
172
|
+
: {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// walkOwnedParent with `parents: 'create'` semantics: missing members
|
|
176
|
+
// and one-past-the-end array slots grow fresh containers; a scalar (or
|
|
177
|
+
// null) on the spine is REPLACED by a fresh container. Fresh containers
|
|
178
|
+
// are owned by construction, so later writes mutate them in place.
|
|
179
|
+
function walkCreateParent(state, names, indexes, plen, dataPath) {
|
|
180
|
+
let v = ownedRoot(state);
|
|
181
|
+
if (v === null || typeof v !== 'object') {
|
|
182
|
+
const fresh = createdContainer(names, indexes, 0);
|
|
183
|
+
if (state.owned !== null)
|
|
184
|
+
state.owned.add(fresh);
|
|
185
|
+
state.root = fresh;
|
|
186
|
+
v = fresh;
|
|
187
|
+
}
|
|
188
|
+
for (let i = 0; i < plen; i++) {
|
|
189
|
+
if (Array.isArray(v)) {
|
|
190
|
+
const idx = names[i] === '-' ? v.length : stepArrayIndex(v, names[i], indexes[i]);
|
|
191
|
+
if (idx < 0 || idx > v.length)
|
|
192
|
+
throw writeError('JW2002', `invalid array position '${names[i] === null ? indexes[i] : names[i]}'`, dataPath);
|
|
193
|
+
const child = idx === v.length ? undefined : v[idx];
|
|
194
|
+
if (child !== null && typeof child === 'object') {
|
|
195
|
+
v = ownedChild(state, v, child, idx);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const fresh = createdContainer(names, indexes, i + 1);
|
|
199
|
+
if (state.owned !== null)
|
|
200
|
+
state.owned.add(fresh);
|
|
201
|
+
v[idx] = fresh;
|
|
202
|
+
v = fresh;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const name = names[i];
|
|
207
|
+
if (name === null)
|
|
208
|
+
throw writeError('JW2001', 'a typed index step cannot address an object member', dataPath);
|
|
209
|
+
const child = hasOwn(v, name) ? v[name] : undefined;
|
|
210
|
+
if (child !== null && typeof child === 'object') {
|
|
211
|
+
v = ownedChild(state, v, child, name);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const fresh = createdContainer(names, indexes, i + 1);
|
|
215
|
+
if (state.owned !== null)
|
|
216
|
+
state.owned.add(fresh);
|
|
217
|
+
setObjectMember(v, name, fresh);
|
|
218
|
+
v = fresh;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return v;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// set semantics: replace the element / member, create the member when
|
|
226
|
+
// absent, extend an array by one at index === length (or '-').
|
|
227
|
+
function leafSet(parent, name, index, value, dataPath) {
|
|
228
|
+
if (Array.isArray(parent)) {
|
|
229
|
+
const len = parent.length;
|
|
230
|
+
const idx = name === '-' ? len : stepArrayIndex(parent, name, index);
|
|
231
|
+
if (idx < 0 || idx > len)
|
|
232
|
+
throw writeError('JW2002', `invalid array position '${name === null ? index : name}'`, dataPath);
|
|
233
|
+
if (idx === len)
|
|
234
|
+
parent.push(resolveValue(value, undefined, dataPath));
|
|
235
|
+
else
|
|
236
|
+
parent[idx] = resolveValue(value, parent[idx], dataPath);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isJsonObject(parent)) {
|
|
240
|
+
if (name === null)
|
|
241
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
242
|
+
const old = hasOwn(parent, name) ? parent[name] : undefined;
|
|
243
|
+
setObjectMember(parent, name, resolveValue(value, old, dataPath));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// insert semantics: RFC 6902 `add` - array insert with shift ('-'
|
|
250
|
+
// appends), object member set-or-create.
|
|
251
|
+
function leafInsert(parent, name, index, value, dataPath) {
|
|
252
|
+
if (Array.isArray(parent)) {
|
|
253
|
+
const len = parent.length;
|
|
254
|
+
const idx = name === '-' ? len : stepArrayIndex(parent, name, index);
|
|
255
|
+
if (idx < 0 || idx > len)
|
|
256
|
+
throw writeError('JW2002', `invalid array position '${name === null ? index : name}'`, dataPath);
|
|
257
|
+
if (idx === len)
|
|
258
|
+
parent.push(value);
|
|
259
|
+
else
|
|
260
|
+
parent.splice(idx, 0, value);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (isJsonObject(parent)) {
|
|
264
|
+
if (name === null)
|
|
265
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
266
|
+
setObjectMember(parent, name, value);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// remove semantics: delete the element (with shift) / member. `lenient`
|
|
273
|
+
// turns a missing location into a no-op (the query-selected writers).
|
|
274
|
+
function leafRemove(parent, name, index, dataPath, lenient) {
|
|
275
|
+
if (Array.isArray(parent)) {
|
|
276
|
+
const idx = name === '-' ? -1 : stepArrayIndex(parent, name, index);
|
|
277
|
+
if (idx < 0 || idx >= parent.length) {
|
|
278
|
+
if (lenient)
|
|
279
|
+
return;
|
|
280
|
+
throw writeError(idx < 0 ? 'JW2002' : 'JW2001',
|
|
281
|
+
idx < 0 ? `invalid array position '${name === null ? index : name}'` : 'the location does not exist',
|
|
282
|
+
dataPath);
|
|
283
|
+
}
|
|
284
|
+
parent.splice(idx, 1);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (isJsonObject(parent) && name !== null && hasOwn(parent, name)) {
|
|
288
|
+
delete parent[name];
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!lenient)
|
|
292
|
+
throw writeError('JW2001', 'the location does not exist', dataPath);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
|
|
297
|
+
//#region pointer-addressed writers
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Options for the compiled write operations.
|
|
301
|
+
* @typedef {Object} JsonWriteOptions
|
|
302
|
+
* @property {boolean} [mutate] - Apply in place instead of copy-on-write.
|
|
303
|
+
* @property {'reject'|'create'} [parents] - What a missing spine means
|
|
304
|
+
* for setters and inserters: `'reject'` (default) raises `JW2001`;
|
|
305
|
+
* `'create'` grows fresh containers along the way — an array when the
|
|
306
|
+
* following step is index-shaped (`-`, a typed index, or a numeric
|
|
307
|
+
* token), an object otherwise — and REPLACES a scalar or `null` found
|
|
308
|
+
* on the spine. The schema-less form-data discipline (@jarenjs/forms
|
|
309
|
+
* `setValueAtPointer` is this option plus undefined-deletes).
|
|
310
|
+
* @property {Record<string, import('./path.js').JSONPathFunction>} [pathFunctions]
|
|
311
|
+
* Custom JSONPath function extensions, for the JSONPath-addressed
|
|
312
|
+
* writers only. A pointer-addressed target is a singular query, which
|
|
313
|
+
* has no filters and so no function calls.
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* A compiled setter/inserter: applies the write and returns the new
|
|
318
|
+
* document. `value` may be an updater function `(oldValue, location) =>
|
|
319
|
+
* next` for setters.
|
|
320
|
+
* @typedef {(root: any, value: any) => any} JsonWriter
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A compiled remover: removes the location and returns the new document.
|
|
325
|
+
* @typedef {(root: any) => any} JsonRemover
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Compile a `set` at a JSON Pointer, a normalized path, or any singular
|
|
330
|
+
* JSONPath query. Set replaces the addressed element or member, creates
|
|
331
|
+
* the member when absent (parents must exist), and extends an array by
|
|
332
|
+
* one at index == length (`/arr/-` appends). `value` may be an updater
|
|
333
|
+
* function `(oldValue, location) => next`.
|
|
334
|
+
*
|
|
335
|
+
* Application is copy-on-write: the input is never mutated and
|
|
336
|
+
* untouched subtrees are shared with the result.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} target - The write target (e.g. `/user/name`, `$['user']['name']`)
|
|
339
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
340
|
+
* @returns {JsonWriter} `(root, value) => newRoot`
|
|
341
|
+
* @throws {JsonWriteError} `JW0001` when the target is not a pointer or
|
|
342
|
+
* singular query
|
|
343
|
+
* @example
|
|
344
|
+
* const setZip = compileJSONPointerSetter('/address/zip');
|
|
345
|
+
* setZip(doc, '10999'); // doc untouched, spine cloned once
|
|
346
|
+
*/
|
|
347
|
+
export function compileJSONPointerSetter(target, options = undefined) {
|
|
348
|
+
const t = parseWriteTarget(target);
|
|
349
|
+
const mutate = parseMutate(options);
|
|
350
|
+
const create = parseParents(options);
|
|
351
|
+
if (t.len === 0)
|
|
352
|
+
return (root, value) => resolveValue(value, root, t.pointer);
|
|
353
|
+
const plen = t.len - 1;
|
|
354
|
+
const walk = create ? walkCreateParent : walkOwnedParent;
|
|
355
|
+
return function setAt(root, value) {
|
|
356
|
+
const state = makeState(root, mutate ? null : new Set());
|
|
357
|
+
const parent = walk(state, t.names, t.indexes, plen, t.pointer);
|
|
358
|
+
leafSet(parent, t.names[plen], t.indexes[plen], value, t.pointer);
|
|
359
|
+
return state.root;
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Compile an `insert` at a JSON Pointer, a normalized path, or any
|
|
365
|
+
* singular JSONPath query - RFC 6902 `add` semantics: array elements
|
|
366
|
+
* shift right (`/arr/-` appends), object members are set-or-created.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} target - The write target
|
|
369
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
370
|
+
* @returns {JsonWriter} `(root, value) => newRoot`
|
|
371
|
+
* @throws {JsonWriteError} `JW0001` when the target is not a pointer or
|
|
372
|
+
* singular query
|
|
373
|
+
*/
|
|
374
|
+
export function compileJSONPointerInserter(target, options = undefined) {
|
|
375
|
+
const t = parseWriteTarget(target);
|
|
376
|
+
const mutate = parseMutate(options);
|
|
377
|
+
const create = parseParents(options);
|
|
378
|
+
if (t.len === 0)
|
|
379
|
+
return (root, value) => value;
|
|
380
|
+
const plen = t.len - 1;
|
|
381
|
+
const walk = create ? walkCreateParent : walkOwnedParent;
|
|
382
|
+
return function insertAt(root, value) {
|
|
383
|
+
const state = makeState(root, mutate ? null : new Set());
|
|
384
|
+
const parent = walk(state, t.names, t.indexes, plen, t.pointer);
|
|
385
|
+
leafInsert(parent, t.names[plen], t.indexes[plen], value, t.pointer);
|
|
386
|
+
return state.root;
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Compile a `remove` at a JSON Pointer, a normalized path, or any
|
|
392
|
+
* singular JSONPath query. The location must exist (`JW2001`); array
|
|
393
|
+
* elements shift left.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} target - The write target
|
|
396
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
397
|
+
* @returns {JsonRemover} `(root) => newRoot`
|
|
398
|
+
* @throws {JsonWriteError} `JW0001` when the target is not a pointer or
|
|
399
|
+
* singular query
|
|
400
|
+
*/
|
|
401
|
+
export function compileJSONPointerRemover(target, options = undefined) {
|
|
402
|
+
const t = parseWriteTarget(target);
|
|
403
|
+
const mutate = parseMutate(options);
|
|
404
|
+
if (t.len === 0) {
|
|
405
|
+
return () => {
|
|
406
|
+
throw writeError('JW2003', 'the root of the document cannot be removed', t.pointer);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const plen = t.len - 1;
|
|
410
|
+
return function removeAt(root) {
|
|
411
|
+
const state = makeState(root, mutate ? null : new Set());
|
|
412
|
+
const parent = walkOwnedParent(state, t.names, t.indexes, plen, t.pointer);
|
|
413
|
+
leafRemove(parent, t.names[plen], t.indexes[plen], t.pointer, false);
|
|
414
|
+
return state.root;
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* One-shot `set` at a pointer / normalized path / singular query. On hot
|
|
420
|
+
* paths prefer `compileJSONPointerSetter` and reuse the writer.
|
|
421
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
422
|
+
* @param {string} target - The write target
|
|
423
|
+
* @param {any} value - The value, or an updater `(oldValue, location) => next`
|
|
424
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
425
|
+
* @returns {any} The new document
|
|
426
|
+
*/
|
|
427
|
+
export function setAtJSONPointer(root, target, value, options = undefined) {
|
|
428
|
+
return compileJSONPointerSetter(target, options)(root, value);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* One-shot `insert` at a pointer / normalized path / singular query.
|
|
433
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
434
|
+
* @param {string} target - The write target
|
|
435
|
+
* @param {any} value - The value to insert
|
|
436
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
437
|
+
* @returns {any} The new document
|
|
438
|
+
*/
|
|
439
|
+
export function insertAtJSONPointer(root, target, value, options = undefined) {
|
|
440
|
+
return compileJSONPointerInserter(target, options)(root, value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* One-shot `remove` at a pointer / normalized path / singular query.
|
|
445
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
446
|
+
* @param {string} target - The write target
|
|
447
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
448
|
+
* @returns {any} The new document
|
|
449
|
+
*/
|
|
450
|
+
export function removeAtJSONPointer(root, target, options = undefined) {
|
|
451
|
+
return compileJSONPointerRemover(target, options)(root);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
|
|
456
|
+
//#region JSONPath-selected writers
|
|
457
|
+
|
|
458
|
+
// Scan a normalized path produced by the nodes-mode engine back into
|
|
459
|
+
// typed steps. The engine only emits `['name']` (with the section 2.7
|
|
460
|
+
// escape set) and `[index]` selectors, so this scanner is total for its
|
|
461
|
+
// input; it is never exposed to user text.
|
|
462
|
+
function scanNormalizedSteps(path) {
|
|
463
|
+
const names = [];
|
|
464
|
+
const indexes = [];
|
|
465
|
+
const len = path.length;
|
|
466
|
+
let pos = 1; // skip '$'
|
|
467
|
+
while (pos < len) {
|
|
468
|
+
pos++; // consume '['
|
|
469
|
+
if (path.charCodeAt(pos) === CC_SQUOTE) {
|
|
470
|
+
pos++;
|
|
471
|
+
let name = '';
|
|
472
|
+
let chunk = pos;
|
|
473
|
+
while (path.charCodeAt(pos) !== CC_SQUOTE) {
|
|
474
|
+
if (path.charCodeAt(pos) === CC_BACKSLASH) {
|
|
475
|
+
name += path.slice(chunk, pos);
|
|
476
|
+
const esc = path.charCodeAt(pos + 1);
|
|
477
|
+
if (esc === 0x75 /* 'u' */) {
|
|
478
|
+
name += String.fromCharCode(parseInt(path.slice(pos + 2, pos + 6), 16));
|
|
479
|
+
pos += 6;
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
name += esc === 0x62 ? '\b'
|
|
483
|
+
: esc === 0x74 ? '\t'
|
|
484
|
+
: esc === 0x6E ? '\n'
|
|
485
|
+
: esc === 0x66 ? '\f'
|
|
486
|
+
: esc === 0x72 ? '\r'
|
|
487
|
+
: String.fromCharCode(esc); // \' and \\
|
|
488
|
+
pos += 2;
|
|
489
|
+
}
|
|
490
|
+
chunk = pos;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
pos++;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
names.push(name + path.slice(chunk, pos));
|
|
497
|
+
indexes.push(-1);
|
|
498
|
+
pos += 2; // consume "']"
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
let index = 0;
|
|
502
|
+
while (path.charCodeAt(pos) !== CC_RBRACKET) {
|
|
503
|
+
index = index * 10 + (path.charCodeAt(pos) - CC_0);
|
|
504
|
+
pos++;
|
|
505
|
+
}
|
|
506
|
+
names.push(null);
|
|
507
|
+
indexes.push(index);
|
|
508
|
+
pos++; // consume ']'
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { names, indexes, len: names.length };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// The shared apply loop of the query-selected writers: run the compiled
|
|
515
|
+
// query in nodes mode, then rewrite the matched locations in reverse
|
|
516
|
+
// document order (dedupe first - RFC 9535 nodelists may repeat a node).
|
|
517
|
+
function applyAtNodes(query, root, mutate, leaf) {
|
|
518
|
+
const paths = query.paths(root);
|
|
519
|
+
if (paths.length === 0)
|
|
520
|
+
return root;
|
|
521
|
+
const state = makeState(root, mutate ? null : new Set());
|
|
522
|
+
const seen = paths.length > 1 ? new Set() : null;
|
|
523
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
524
|
+
const path = paths[i];
|
|
525
|
+
if (seen !== null) {
|
|
526
|
+
if (seen.has(path))
|
|
527
|
+
continue;
|
|
528
|
+
seen.add(path);
|
|
529
|
+
}
|
|
530
|
+
leaf(state, path);
|
|
531
|
+
}
|
|
532
|
+
return state.root;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Compile a `set` at every node a JSONPath query selects. Applying runs
|
|
537
|
+
* the query against the document and replaces each matched node; `value`
|
|
538
|
+
* may be an updater function `(oldValue, normalizedPath) => next`.
|
|
539
|
+
* Matching nothing is a no-op. Locations are rewritten in reverse
|
|
540
|
+
* document order, so when matches nest, the ancestor's rewrite wins.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} path - The JSONPath query (e.g. `$..price`)
|
|
543
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
544
|
+
* @returns {JsonWriter} `(root, value) => newRoot`
|
|
545
|
+
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
546
|
+
* @example
|
|
547
|
+
* const addVat = compileJSONPathSetter('$..price');
|
|
548
|
+
* addVat(doc, (price) => price * 1.21);
|
|
549
|
+
*/
|
|
550
|
+
export function compileJSONPathSetter(path, options = undefined) {
|
|
551
|
+
const query = compileJSONPath(path, options);
|
|
552
|
+
const mutate = parseMutate(options);
|
|
553
|
+
return function setAtMatches(root, value) {
|
|
554
|
+
return applyAtNodes(query, root, mutate, (state, p) => {
|
|
555
|
+
if (p === '$') {
|
|
556
|
+
state.root = resolveValue(value, state.root, p);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const t = scanNormalizedSteps(p);
|
|
560
|
+
const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
|
|
561
|
+
leafSet(parent, t.names[t.len - 1], t.indexes[t.len - 1], value, p);
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Compile an `insert` at every node a JSONPath query selects - RFC 6902
|
|
568
|
+
* `add` semantics per location: the value is inserted *before* each
|
|
569
|
+
* matched array element (later siblings shift right), and replaces
|
|
570
|
+
* matched object members. Matching nothing is a no-op.
|
|
571
|
+
*
|
|
572
|
+
* @param {string} path - The JSONPath query (e.g. `$.list[0]`)
|
|
573
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
574
|
+
* @returns {JsonWriter} `(root, value) => newRoot`
|
|
575
|
+
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
576
|
+
*/
|
|
577
|
+
export function compileJSONPathInserter(path, options = undefined) {
|
|
578
|
+
const query = compileJSONPath(path, options);
|
|
579
|
+
const mutate = parseMutate(options);
|
|
580
|
+
return function insertAtMatches(root, value) {
|
|
581
|
+
return applyAtNodes(query, root, mutate, (state, p) => {
|
|
582
|
+
if (p === '$') {
|
|
583
|
+
state.root = value;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const t = scanNormalizedSteps(p);
|
|
587
|
+
const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
|
|
588
|
+
leafInsert(parent, t.names[t.len - 1], t.indexes[t.len - 1], value, p);
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Compile a `remove` of every node a JSONPath query selects. Array
|
|
595
|
+
* elements are removed with shift; removals apply in reverse document
|
|
596
|
+
* order, so multiple removals from one array (and nested removals)
|
|
597
|
+
* compose correctly. Matching nothing is a no-op; selecting the root
|
|
598
|
+
* raises `JW2003`.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} path - The JSONPath query (e.g. `$.store.book[?@.price > 20]`)
|
|
601
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
602
|
+
* @returns {JsonRemover} `(root) => newRoot`
|
|
603
|
+
* @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
|
|
604
|
+
* @example
|
|
605
|
+
* const dropExpensive = compileJSONPathRemover('$.store.book[?@.price > 20]');
|
|
606
|
+
* dropExpensive(doc); // matched books removed, everything else shared
|
|
607
|
+
*/
|
|
608
|
+
export function compileJSONPathRemover(path, options = undefined) {
|
|
609
|
+
const query = compileJSONPath(path, options);
|
|
610
|
+
const mutate = parseMutate(options);
|
|
611
|
+
return function removeMatches(root) {
|
|
612
|
+
return applyAtNodes(query, root, mutate, (state, p) => {
|
|
613
|
+
if (p === '$')
|
|
614
|
+
throw writeError('JW2003', 'the root of the document cannot be removed', p);
|
|
615
|
+
const t = scanNormalizedSteps(p);
|
|
616
|
+
const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
|
|
617
|
+
leafRemove(parent, t.names[t.len - 1], t.indexes[t.len - 1], p, true);
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* One-shot `set` at every node a JSONPath query selects. On hot paths
|
|
624
|
+
* prefer `compileJSONPathSetter` and reuse the writer.
|
|
625
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
626
|
+
* @param {string} path - The JSONPath query
|
|
627
|
+
* @param {any} value - The value, or an updater `(oldValue, normalizedPath) => next`
|
|
628
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
629
|
+
* @returns {any} The new document
|
|
630
|
+
*/
|
|
631
|
+
export function setAtJSONPath(root, path, value, options = undefined) {
|
|
632
|
+
return compileJSONPathSetter(path, options)(root, value);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* One-shot `insert` at every node a JSONPath query selects.
|
|
637
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
638
|
+
* @param {string} path - The JSONPath query
|
|
639
|
+
* @param {any} value - The value to insert
|
|
640
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
641
|
+
* @returns {any} The new document
|
|
642
|
+
*/
|
|
643
|
+
export function insertAtJSONPath(root, path, value, options = undefined) {
|
|
644
|
+
return compileJSONPathInserter(path, options)(root, value);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* One-shot `remove` of every node a JSONPath query selects.
|
|
649
|
+
* @param {any} root - The document (never mutated unless `options.mutate`)
|
|
650
|
+
* @param {string} path - The JSONPath query
|
|
651
|
+
* @param {JsonWriteOptions} [options] - Write options
|
|
652
|
+
* @returns {any} The new document
|
|
653
|
+
*/
|
|
654
|
+
export function removeAtJSONPath(root, path, options = undefined) {
|
|
655
|
+
return compileJSONPathRemover(path, options)(root);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
//#endregion
|
|
659
|
+
|
|
660
|
+
//#endregion
|