@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/patch.js
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
//#region JSON Patch (RFC 6902) + JSON Merge Patch (RFC 7396)
|
|
2
|
+
// JSON Patch: https://datatracker.ietf.org/doc/html/rfc6902
|
|
3
|
+
// JSON Merge Patch: https://datatracker.ietf.org/doc/html/rfc7396
|
|
4
|
+
//
|
|
5
|
+
// Both formats compile in the house two-stage style:
|
|
6
|
+
//
|
|
7
|
+
// 1. `compileJSONPatch` validates the patch document once (all `JP0xxx`
|
|
8
|
+
// errors, with a `docPath` into the *patch* document), pre-parses
|
|
9
|
+
// every `path`/`from` through the strict RFC 6901 parser and
|
|
10
|
+
// specializes one closure per operation. `compileMergePatch`
|
|
11
|
+
// pre-splits a merge patch into remove/set/merge plans.
|
|
12
|
+
// 2. Applying replays the closures against a copy-on-write state:
|
|
13
|
+
// the input document is never mutated, untouched subtrees are shared
|
|
14
|
+
// by reference with the result (the JSLT `share` discipline), and a
|
|
15
|
+
// failing operation aborts the whole application (RFC 6902
|
|
16
|
+
// section 5) for free because the input root was never touched.
|
|
17
|
+
//
|
|
18
|
+
// Copy-on-write: an application tracks the set of nodes it has already
|
|
19
|
+
// cloned ("owned"). The first write along a path shallow-clones the spine
|
|
20
|
+
// from the root down; later writes find the spine in the owned set and
|
|
21
|
+
// mutate the clones in place, so k operations touching one region cost
|
|
22
|
+
// one spine copy, not k.
|
|
23
|
+
//
|
|
24
|
+
// `createJSONPatch` and `createMergePatch` are the structural diffs;
|
|
25
|
+
// `applyJSONPatch(doc, createJSONPatch(a, b))` reproduces `b` from `a`.
|
|
26
|
+
//
|
|
27
|
+
// Error codes:
|
|
28
|
+
// JP0001 - the patch document or an operation has the wrong shape
|
|
29
|
+
// JP0002 - unknown or missing `op` member
|
|
30
|
+
// JP0003 - missing or invalid JSON Pointer (`path`/`from`)
|
|
31
|
+
// JP0004 - missing `value` member
|
|
32
|
+
// JP0005 - `from` is a proper prefix of `path` in a move
|
|
33
|
+
// JP2001 - the target location does not exist
|
|
34
|
+
// JP2002 - invalid array position
|
|
35
|
+
// JP2003 - the root of the document cannot be removed
|
|
36
|
+
// JP2004 - a `test` operation failed
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
cloneJson,
|
|
40
|
+
equalsJson,
|
|
41
|
+
isJsonContainer,
|
|
42
|
+
isJsonObject,
|
|
43
|
+
setObjectMember,
|
|
44
|
+
stableStringify,
|
|
45
|
+
} from '@jarenjs/core/object';
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
parseJSONPointer,
|
|
49
|
+
encodeJSONPointerSegment,
|
|
50
|
+
} from './pointer.js';
|
|
51
|
+
|
|
52
|
+
import { NOTHING, scanArrayIndex } from './segments.js';
|
|
53
|
+
|
|
54
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
55
|
+
import { CodedDocPathError } from './errors.js';
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
makeState,
|
|
59
|
+
ownedRoot,
|
|
60
|
+
ownedChild,
|
|
61
|
+
readSteps,
|
|
62
|
+
} from './cow.js';
|
|
63
|
+
|
|
64
|
+
const hasOwn = Object.hasOwn;
|
|
65
|
+
|
|
66
|
+
//#region errors
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Error thrown when a JSON Patch document is rejected at compile time
|
|
70
|
+
* (`JP0xxx` codes). `docPath` is an RFC 6901 JSON Pointer into the
|
|
71
|
+
* patch document (e.g. `/2/from`). A wrapped pointer syntax error is
|
|
72
|
+
* exposed through `cause`.
|
|
73
|
+
*/
|
|
74
|
+
export class JsonPatchCompileError extends CodedDocPathError {
|
|
75
|
+
constructor(code, message, docPath, cause = undefined) {
|
|
76
|
+
super('JsonPatchCompileError', code, message, docPath, cause);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error thrown when applying a compiled JSON Patch fails (`JP2xxx`
|
|
82
|
+
* codes). `docPath` points at the failing operation in the patch
|
|
83
|
+
* document; `dataPath` is the operation's target location in the
|
|
84
|
+
* document being patched — both render, separately identifiable, per
|
|
85
|
+
* the `@jarenjs/core` coded contract (`at` = document, `in data` =
|
|
86
|
+
* data).
|
|
87
|
+
*/
|
|
88
|
+
export class JsonPatchRuntimeError extends CodedError {
|
|
89
|
+
constructor(code, reason, docPath, dataPath) {
|
|
90
|
+
super('JsonPatchRuntimeError', code, reason, { docPath, dataPath });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
|
|
96
|
+
//#region copy-on-write machinery
|
|
97
|
+
// The generic pieces (owned-set state, spine cloning, step reads) live
|
|
98
|
+
// in the package-internal cow.js, shared with the standalone write
|
|
99
|
+
// operations (write.js). This region keeps only what is specific to the
|
|
100
|
+
// patch engine: the walk that raises JsonPatchRuntimeError with both a
|
|
101
|
+
// patch docPath and a target dataPath.
|
|
102
|
+
|
|
103
|
+
function pathError(code, message, docPath, dataPath) {
|
|
104
|
+
return new JsonPatchRuntimeError(code, message, docPath, dataPath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Walk to the parent of the target location, cloning the spine on first
|
|
108
|
+
// touch. `t` is a compiled target; the walk covers segments [0, len-1).
|
|
109
|
+
function walkOwnedParent(state, t, docPath) {
|
|
110
|
+
const names = t.names;
|
|
111
|
+
const indexes = t.indexes;
|
|
112
|
+
const plen = t.len - 1;
|
|
113
|
+
let v = ownedRoot(state);
|
|
114
|
+
for (let i = 0; i < plen; i++) {
|
|
115
|
+
if (Array.isArray(v)) {
|
|
116
|
+
const idx = indexes[i];
|
|
117
|
+
if (idx < 0 || idx >= v.length)
|
|
118
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
119
|
+
v = ownedChild(state, v, v[idx], idx);
|
|
120
|
+
}
|
|
121
|
+
else if (typeof v === 'object' && v !== null) {
|
|
122
|
+
const name = names[i];
|
|
123
|
+
if (!hasOwn(v, name))
|
|
124
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
125
|
+
v = ownedChild(state, v, v[name], name);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Read the full target location without cloning anything.
|
|
135
|
+
function readTarget(root, t) {
|
|
136
|
+
return readSteps(root, t.names, t.indexes, t.len, NOTHING);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function containsOwned(v, owned) {
|
|
140
|
+
if (!isJsonContainer(v))
|
|
141
|
+
return false;
|
|
142
|
+
if (owned.has(v))
|
|
143
|
+
return true;
|
|
144
|
+
if (Array.isArray(v)) {
|
|
145
|
+
for (let i = 0; i < v.length; i++) {
|
|
146
|
+
if (containsOwned(v[i], owned))
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
for (const key in v) {
|
|
152
|
+
if (hasOwn(v, key) && containsOwned(v[key], owned))
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The value a `copy` inserts. Sharing the source reference is safe only
|
|
159
|
+
// while no clone of this application lives inside it: a later operation
|
|
160
|
+
// walking the second location would find an owned node and mutate both
|
|
161
|
+
// aliases. In-place mode owns everything, so it always deep-copies.
|
|
162
|
+
function copyForInsert(value, owned) {
|
|
163
|
+
if (!isJsonContainer(value))
|
|
164
|
+
return value;
|
|
165
|
+
if (owned === null)
|
|
166
|
+
return cloneJson(value);
|
|
167
|
+
if (owned.size !== 0 && containsOwned(value, owned))
|
|
168
|
+
return cloneJson(value);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
|
|
174
|
+
//#region operation primitives
|
|
175
|
+
|
|
176
|
+
// Change tracking (`changes` option): one pointer per successful write,
|
|
177
|
+
// pushed by the operation primitives below. `state.changes` is `null`
|
|
178
|
+
// when tracking is off - a single monomorphic null check per write.
|
|
179
|
+
function recordChange(state, pointer) {
|
|
180
|
+
if (state.changes !== null)
|
|
181
|
+
state.changes.push(pointer);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// add semantics at a non-root target: array insert (with shift, `-`
|
|
185
|
+
// appends), object member set-or-replace (RFC 6902 section 4.1).
|
|
186
|
+
function insertAt(state, t, value, docPath) {
|
|
187
|
+
const parent = walkOwnedParent(state, t, docPath);
|
|
188
|
+
if (Array.isArray(parent)) {
|
|
189
|
+
const len = parent.length;
|
|
190
|
+
const idx = t.lastName === '-' ? len : t.lastIndex;
|
|
191
|
+
if (idx < 0 || idx > len)
|
|
192
|
+
throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
|
|
193
|
+
if (idx === len) {
|
|
194
|
+
parent.push(value);
|
|
195
|
+
// append shifts nothing: the new element's location is precise
|
|
196
|
+
recordChange(state, t.parentPointer + '/' + idx);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
parent.splice(idx, 0, value);
|
|
200
|
+
// insert shifts every later element: the whole array changed
|
|
201
|
+
recordChange(state, t.parentPointer);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (isJsonObject(parent)) {
|
|
206
|
+
setObjectMember(parent, t.lastName, value);
|
|
207
|
+
recordChange(state, t.pointer);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// remove semantics at a non-root target; returns the removed value so
|
|
214
|
+
// `move` can re-insert it (RFC 6902 sections 4.2, 4.4).
|
|
215
|
+
function extractAt(state, t, docPath) {
|
|
216
|
+
const parent = walkOwnedParent(state, t, docPath);
|
|
217
|
+
if (Array.isArray(parent)) {
|
|
218
|
+
const idx = t.lastName === '-' ? -1 : t.lastIndex;
|
|
219
|
+
if (idx < 0)
|
|
220
|
+
throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
|
|
221
|
+
if (idx >= parent.length)
|
|
222
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
223
|
+
const value = parent[idx];
|
|
224
|
+
parent.splice(idx, 1);
|
|
225
|
+
// removal shifts every later element: the whole array changed
|
|
226
|
+
recordChange(state, t.parentPointer);
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
if (isJsonObject(parent)) {
|
|
230
|
+
const name = t.lastName;
|
|
231
|
+
if (!hasOwn(parent, name))
|
|
232
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
233
|
+
const value = parent[name];
|
|
234
|
+
delete parent[name];
|
|
235
|
+
recordChange(state, t.pointer);
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// replace semantics at a non-root target: the location must already
|
|
242
|
+
// exist (RFC 6902 section 4.3).
|
|
243
|
+
function replaceAt(state, t, value, docPath) {
|
|
244
|
+
const parent = walkOwnedParent(state, t, docPath);
|
|
245
|
+
if (Array.isArray(parent)) {
|
|
246
|
+
const idx = t.lastName === '-' ? -1 : t.lastIndex;
|
|
247
|
+
if (idx < 0)
|
|
248
|
+
throw pathError('JP2002', `invalid array position '${t.lastName}' in '${t.pointer}'`, docPath, t.pointer);
|
|
249
|
+
if (idx >= parent.length)
|
|
250
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
251
|
+
parent[idx] = value;
|
|
252
|
+
recordChange(state, t.pointer);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (isJsonObject(parent)) {
|
|
256
|
+
const name = t.lastName;
|
|
257
|
+
if (!hasOwn(parent, name))
|
|
258
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
259
|
+
setObjectMember(parent, name, value);
|
|
260
|
+
recordChange(state, t.pointer);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
throw pathError('JP2001', `the path '${t.pointer}' does not exist`, docPath, t.pointer);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
|
|
268
|
+
//#region patch compiler
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A single RFC 6902 operation object.
|
|
272
|
+
* @typedef {{op: 'add'|'remove'|'replace'|'move'|'copy'|'test', path: string,
|
|
273
|
+
* value?: any, from?: string}} JsonPatchOperation
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A compiled JSON Patch: applies the patch to a document and returns
|
|
278
|
+
* the patched document.
|
|
279
|
+
* @typedef {(doc: any) => any} JsonPatchApplier
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A compiled JSON Patch with change tracking (`changes: true`): applies
|
|
284
|
+
* the patch and returns the patched document together with the changed
|
|
285
|
+
* locations (see `JsonPatchOptions`).
|
|
286
|
+
* @typedef {(doc: any) => { doc: any, changes: string[] }} JsonPatchChangesApplier
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Options for `compileJSONPatch` / `applyJSONPatch`.
|
|
291
|
+
* @typedef {Object} JsonPatchOptions
|
|
292
|
+
* @property {boolean} [mutate] - Apply in place instead of copy-on-write.
|
|
293
|
+
* Faster, but the input document is modified and a failing operation
|
|
294
|
+
* leaves it partially patched (application is no longer atomic).
|
|
295
|
+
* @property {'share'|'fresh'} [values] - How operation values enter the
|
|
296
|
+
* result: `'share'` (default) inserts them by reference, so results of
|
|
297
|
+
* repeated applications share structure with the patch document and
|
|
298
|
+
* must be treated as immutable; `'fresh'` deep-copies per application.
|
|
299
|
+
* In-place mode always behaves as `'fresh'`.
|
|
300
|
+
* @property {boolean} [changes] - Track changed locations: the applier
|
|
301
|
+
* returns `{ doc, changes }` where `changes` is an array of JSON
|
|
302
|
+
* Pointers, one per successful write, in application order and not
|
|
303
|
+
* deduplicated. The reported pointer is chosen to be *sound for
|
|
304
|
+
* invalidation* — everything at or below it (plus the identity of its
|
|
305
|
+
* ancestors) may have changed, and nothing outside the reported set
|
|
306
|
+
* did: object writes, array replaces and array appends report the
|
|
307
|
+
* written location itself; array inserts and removes that shift later
|
|
308
|
+
* elements report the parent array's pointer; a root write reports
|
|
309
|
+
* `''`. `test` operations report nothing.
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
function compileError(code, message, docPath, cause) {
|
|
313
|
+
return new JsonPatchCompileError(code, message, docPath, cause);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Parse and pre-compile a target pointer (`path` or `from`): decoded
|
|
317
|
+
// member names alongside pre-scanned array indexes (one token, two
|
|
318
|
+
// forms), plus the split-off last token for the mutating operations.
|
|
319
|
+
function parseTarget(op, index, member) {
|
|
320
|
+
const docPath = '/' + index + '/' + member;
|
|
321
|
+
if (!hasOwn(op, member))
|
|
322
|
+
throw compileError('JP0003', `the operation requires a '${member}' member`, docPath);
|
|
323
|
+
const pointer = op[member];
|
|
324
|
+
let names;
|
|
325
|
+
try {
|
|
326
|
+
names = parseJSONPointer(pointer);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
throw compileError('JP0003', `invalid JSON Pointer in '${member}'`, docPath, e);
|
|
330
|
+
}
|
|
331
|
+
const len = names.length;
|
|
332
|
+
const indexes = new Array(len);
|
|
333
|
+
for (let i = 0; i < len; i++)
|
|
334
|
+
indexes[i] = scanArrayIndex(names[i], 0, names[i].length);
|
|
335
|
+
return {
|
|
336
|
+
pointer,
|
|
337
|
+
names,
|
|
338
|
+
indexes,
|
|
339
|
+
len,
|
|
340
|
+
lastName: len === 0 ? '' : names[len - 1],
|
|
341
|
+
lastIndex: len === 0 ? -1 : indexes[len - 1],
|
|
342
|
+
// the parent location, for shift-style change reports (tokens never
|
|
343
|
+
// contain a raw '/', so the last separator bounds the last token)
|
|
344
|
+
parentPointer: len === 0 ? '' : pointer.slice(0, pointer.lastIndexOf('/')),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseValueGetter(op, index, fresh) {
|
|
349
|
+
if (!hasOwn(op, 'value'))
|
|
350
|
+
throw compileError('JP0004', "the operation requires a 'value' member", '/' + index);
|
|
351
|
+
const value = op.value;
|
|
352
|
+
if (!isJsonContainer(value))
|
|
353
|
+
return () => value;
|
|
354
|
+
if (fresh)
|
|
355
|
+
return () => cloneJson(value);
|
|
356
|
+
return () => value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isProperPrefix(a, b) {
|
|
360
|
+
const alen = a.length;
|
|
361
|
+
if (alen >= b.length)
|
|
362
|
+
return false;
|
|
363
|
+
for (let i = 0; i < alen; i++) {
|
|
364
|
+
if (a[i] !== b[i])
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readSource(state, from, docPath) {
|
|
371
|
+
const value = readTarget(state.root, from);
|
|
372
|
+
if (value === NOTHING)
|
|
373
|
+
throw pathError('JP2001', `the path '${from.pointer}' does not exist`, docPath, from.pointer);
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Compile one operation object into a `(state) => void` closure with
|
|
378
|
+
// every pointer parsed, every index scanned and every docPath string
|
|
379
|
+
// pre-bound.
|
|
380
|
+
function compileOperation(op, index, fresh) {
|
|
381
|
+
const docPath = '/' + index;
|
|
382
|
+
if (!isJsonObject(op))
|
|
383
|
+
throw compileError('JP0001', 'an operation must be an object', docPath);
|
|
384
|
+
if (!hasOwn(op, 'op') || typeof op.op !== 'string')
|
|
385
|
+
throw compileError('JP0002', "the operation requires a string 'op' member", docPath + '/op');
|
|
386
|
+
switch (op.op) {
|
|
387
|
+
case 'add': {
|
|
388
|
+
const t = parseTarget(op, index, 'path');
|
|
389
|
+
const getValue = parseValueGetter(op, index, fresh);
|
|
390
|
+
if (t.len === 0) {
|
|
391
|
+
return (state) => {
|
|
392
|
+
state.root = getValue();
|
|
393
|
+
recordChange(state, '');
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return (state) => {
|
|
397
|
+
insertAt(state, t, getValue(), docPath);
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
case 'remove': {
|
|
401
|
+
const t = parseTarget(op, index, 'path');
|
|
402
|
+
if (t.len === 0) {
|
|
403
|
+
return () => {
|
|
404
|
+
throw pathError('JP2003', 'the root of the document cannot be removed', docPath, '');
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return (state) => {
|
|
408
|
+
extractAt(state, t, docPath);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
case 'replace': {
|
|
412
|
+
const t = parseTarget(op, index, 'path');
|
|
413
|
+
const getValue = parseValueGetter(op, index, fresh);
|
|
414
|
+
if (t.len === 0) {
|
|
415
|
+
return (state) => {
|
|
416
|
+
state.root = getValue();
|
|
417
|
+
recordChange(state, '');
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return (state) => {
|
|
421
|
+
replaceAt(state, t, getValue(), docPath);
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
case 'move': {
|
|
425
|
+
const from = parseTarget(op, index, 'from');
|
|
426
|
+
const t = parseTarget(op, index, 'path');
|
|
427
|
+
if (isProperPrefix(from.names, t.names))
|
|
428
|
+
throw compileError('JP0005', "'from' may not be a proper prefix of 'path' in a move", docPath + '/from');
|
|
429
|
+
if (from.len === 0)
|
|
430
|
+
return () => { }; // '' to '' - moving the root onto itself
|
|
431
|
+
if (t.len === 0) {
|
|
432
|
+
return (state) => {
|
|
433
|
+
state.root = extractAt(state, from, docPath);
|
|
434
|
+
recordChange(state, '');
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return (state) => {
|
|
438
|
+
insertAt(state, t, extractAt(state, from, docPath), docPath);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
case 'copy': {
|
|
442
|
+
const from = parseTarget(op, index, 'from');
|
|
443
|
+
const t = parseTarget(op, index, 'path');
|
|
444
|
+
if (t.len === 0) {
|
|
445
|
+
return (state) => {
|
|
446
|
+
state.root = copyForInsert(readSource(state, from, docPath), state.owned);
|
|
447
|
+
recordChange(state, '');
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return (state) => {
|
|
451
|
+
insertAt(state, t, copyForInsert(readSource(state, from, docPath), state.owned), docPath);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
case 'test': {
|
|
455
|
+
const t = parseTarget(op, index, 'path');
|
|
456
|
+
if (!hasOwn(op, 'value'))
|
|
457
|
+
throw compileError('JP0004', "the operation requires a 'value' member", docPath);
|
|
458
|
+
const value = op.value;
|
|
459
|
+
return (state) => {
|
|
460
|
+
const actual = readTarget(state.root, t);
|
|
461
|
+
if (actual === NOTHING)
|
|
462
|
+
throw pathError('JP2004', `test failed: no value at '${t.pointer}'`, docPath, t.pointer);
|
|
463
|
+
if (!equalsJson(actual, value))
|
|
464
|
+
throw pathError('JP2004', `test failed at '${t.pointer}'`, docPath, t.pointer);
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
default:
|
|
468
|
+
throw compileError('JP0002', `unknown operation '${op.op}'`, docPath + '/op');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Compile a JSON Patch (RFC 6902) into a reusable applier.
|
|
474
|
+
*
|
|
475
|
+
* The patch document is validated once (`JsonPatchCompileError`,
|
|
476
|
+
* `JP0xxx`, with a `docPath` into the patch document); every pointer is
|
|
477
|
+
* pre-parsed and each operation becomes a specialized closure. Applying
|
|
478
|
+
* is copy-on-write: the input document is never mutated, untouched
|
|
479
|
+
* subtrees are shared with the result, and application is atomic - a
|
|
480
|
+
* failing operation (`JsonPatchRuntimeError`, `JP2xxx`) leaves nothing
|
|
481
|
+
* behind.
|
|
482
|
+
*
|
|
483
|
+
* With `changes: true` the applier is specialized at compile time to
|
|
484
|
+
* also report the changed locations: it returns `{ doc, changes }`,
|
|
485
|
+
* where `changes` holds one JSON Pointer per successful write with the
|
|
486
|
+
* invalidation-sound semantics documented on `JsonPatchOptions` — the
|
|
487
|
+
* primitive dirty-path consumers (view re-rendering, rule dependency
|
|
488
|
+
* memoization) build on.
|
|
489
|
+
*
|
|
490
|
+
* @param {JsonPatchOperation[]} patch - The RFC 6902 patch document
|
|
491
|
+
* @param {JsonPatchOptions} [options] - Application options
|
|
492
|
+
* @returns {JsonPatchApplier | JsonPatchChangesApplier} applier
|
|
493
|
+
* returning the patched document (or `{ doc, changes }` with the
|
|
494
|
+
* `changes` option)
|
|
495
|
+
* @throws {JsonPatchCompileError} When the patch document is invalid
|
|
496
|
+
* @example
|
|
497
|
+
* const apply = compileJSONPatch([
|
|
498
|
+
* { op: 'test', path: '/version', value: 5 },
|
|
499
|
+
* { op: 'replace', path: '/user/name', value: 'Bob' },
|
|
500
|
+
* { op: 'add', path: '/user/tags/-', value: 'admin' },
|
|
501
|
+
* ]);
|
|
502
|
+
* const next = apply(doc); // doc is untouched
|
|
503
|
+
* @example
|
|
504
|
+
* const applyTracked = compileJSONPatch(
|
|
505
|
+
* [{ op: 'replace', path: '/user/name', value: 'Bob' }],
|
|
506
|
+
* { changes: true });
|
|
507
|
+
* const { doc: next2, changes } = applyTracked(doc);
|
|
508
|
+
* // changes: ['/user/name']
|
|
509
|
+
*/
|
|
510
|
+
export function compileJSONPatch(patch, options = undefined) {
|
|
511
|
+
let mutate = false;
|
|
512
|
+
let values = 'share';
|
|
513
|
+
let changes = false;
|
|
514
|
+
if (options !== undefined && options !== null) {
|
|
515
|
+
mutate = options.mutate === true;
|
|
516
|
+
if (options.values !== undefined) {
|
|
517
|
+
if (options.values !== 'share' && options.values !== 'fresh')
|
|
518
|
+
throw new TypeError(`compileJSONPatch: unknown 'values' option '${options.values}'`);
|
|
519
|
+
values = options.values;
|
|
520
|
+
}
|
|
521
|
+
changes = options.changes === true;
|
|
522
|
+
}
|
|
523
|
+
const fresh = mutate || values === 'fresh';
|
|
524
|
+
if (!Array.isArray(patch))
|
|
525
|
+
throw compileError('JP0001', 'a JSON Patch document must be an array of operations', '');
|
|
526
|
+
const plen = patch.length;
|
|
527
|
+
const ops = new Array(plen);
|
|
528
|
+
for (let i = 0; i < plen; i++)
|
|
529
|
+
ops[i] = compileOperation(patch[i], i, fresh);
|
|
530
|
+
if (changes) {
|
|
531
|
+
const owned = mutate ? null : undefined;
|
|
532
|
+
return function applyJsonPatchTracked(doc) {
|
|
533
|
+
const state = makeState(doc, owned === null ? null : new Set());
|
|
534
|
+
state.changes = [];
|
|
535
|
+
for (let i = 0; i < plen; i++)
|
|
536
|
+
ops[i](state);
|
|
537
|
+
return { doc: state.root, changes: state.changes };
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (mutate) {
|
|
541
|
+
return function applyJsonPatchInPlace(doc) {
|
|
542
|
+
const state = makeState(doc, null);
|
|
543
|
+
state.changes = null;
|
|
544
|
+
for (let i = 0; i < plen; i++)
|
|
545
|
+
ops[i](state);
|
|
546
|
+
return state.root;
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return function applyJsonPatchCow(doc) {
|
|
550
|
+
const state = makeState(doc, new Set());
|
|
551
|
+
state.changes = null;
|
|
552
|
+
for (let i = 0; i < plen; i++)
|
|
553
|
+
ops[i](state);
|
|
554
|
+
return state.root;
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Apply a JSON Patch (RFC 6902) to a document in one shot. Compiles the
|
|
560
|
+
* patch and applies it once; on hot paths prefer `compileJSONPatch` and
|
|
561
|
+
* reuse the applier.
|
|
562
|
+
*
|
|
563
|
+
* @param {any} doc - The document to patch (never mutated unless
|
|
564
|
+
* `options.mutate` is set)
|
|
565
|
+
* @param {JsonPatchOperation[]} patch - The RFC 6902 patch document
|
|
566
|
+
* @param {JsonPatchOptions} [options] - Application options
|
|
567
|
+
* @returns {any} The patched document, or `{ doc, changes }` when
|
|
568
|
+
* `options.changes` is set
|
|
569
|
+
* @throws {JsonPatchCompileError} When the patch document is invalid
|
|
570
|
+
* @throws {JsonPatchRuntimeError} When an operation fails to apply
|
|
571
|
+
*/
|
|
572
|
+
export function applyJSONPatch(doc, patch, options = undefined) {
|
|
573
|
+
return compileJSONPatch(patch, options)(doc);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Returns true when `patch` is a structurally valid RFC 6902 patch
|
|
578
|
+
* document (an array of well-formed operation objects with valid
|
|
579
|
+
* pointers). Runtime applicability against a document is not checked.
|
|
580
|
+
* @param {any} patch - The candidate patch document
|
|
581
|
+
* @returns {boolean}
|
|
582
|
+
*/
|
|
583
|
+
export function isValidJSONPatch(patch) {
|
|
584
|
+
try {
|
|
585
|
+
compileJSONPatch(patch);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
if (e instanceof JsonPatchCompileError)
|
|
590
|
+
return false;
|
|
591
|
+
/* c8 ignore next -- compile only throws JsonPatchCompileError */
|
|
592
|
+
throw e;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
//#endregion
|
|
597
|
+
|
|
598
|
+
//#region structural diff (RFC 6902)
|
|
599
|
+
|
|
600
|
+
function appendPointer(path, key) {
|
|
601
|
+
return path + '/' + encodeJSONPointerSegment(key);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function diffObject(src, tgt, path, out, lcs) {
|
|
605
|
+
for (const key in src) {
|
|
606
|
+
if (!hasOwn(src, key))
|
|
607
|
+
continue;
|
|
608
|
+
if (!hasOwn(tgt, key))
|
|
609
|
+
out.push({ op: 'remove', path: appendPointer(path, key) });
|
|
610
|
+
else
|
|
611
|
+
diffValue(src[key], tgt[key], appendPointer(path, key), out, lcs);
|
|
612
|
+
}
|
|
613
|
+
for (const key in tgt) {
|
|
614
|
+
if (hasOwn(tgt, key) && !hasOwn(src, key))
|
|
615
|
+
out.push({ op: 'add', path: appendPointer(path, key), value: tgt[key] });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Emit the ops for one gap between two aligned (deep-equal) elements:
|
|
620
|
+
// `sRun` source elements at src[si...] became `tRun` target elements at
|
|
621
|
+
// tgt[ti...]. Overlapping positions are diffed in place - an edited
|
|
622
|
+
// element stays one `replace` rather than a remove plus an add - and the
|
|
623
|
+
// length difference is appended or removed.
|
|
624
|
+
//
|
|
625
|
+
// `cur` is the position of src[si] in the document as the ops so far
|
|
626
|
+
// have left it: an `add` shifts everything after it right (so the next
|
|
627
|
+
// insert goes one further along), while repeated `remove`s all land on
|
|
628
|
+
// the same index (each one shifts the rest left onto it).
|
|
629
|
+
function diffArrayGap(src, si, sRun, tgt, ti, tRun, cur, path, out, lcs) {
|
|
630
|
+
const both = sRun < tRun ? sRun : tRun;
|
|
631
|
+
for (let k = 0; k < both; k++)
|
|
632
|
+
diffValue(src[si + k], tgt[ti + k], path + '/' + (cur + k), out, lcs);
|
|
633
|
+
if (tRun > both) {
|
|
634
|
+
for (let k = both; k < tRun; k++)
|
|
635
|
+
out.push({ op: 'add', path: path + '/' + (cur + k), value: tgt[ti + k] });
|
|
636
|
+
return cur + tRun;
|
|
637
|
+
}
|
|
638
|
+
const at = path + '/' + (cur + both);
|
|
639
|
+
for (let k = both; k < sRun; k++)
|
|
640
|
+
out.push({ op: 'remove', path: at });
|
|
641
|
+
return cur + both;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Cell budget for the alignment table: beyond this the quadratic
|
|
645
|
+
// alignment costs more than the ops it saves, so the index-wise diff
|
|
646
|
+
// answers instead - still correct, just not minimal.
|
|
647
|
+
const ALIGN_MAX_CELLS = 1 << 20;
|
|
648
|
+
|
|
649
|
+
// Intern one element's stable serialization to an integer id, so the
|
|
650
|
+
// alignment's inner loop compares integers instead of walking two
|
|
651
|
+
// subtrees. The ids only ever act as a filter: different ids mean
|
|
652
|
+
// certainly different values, equal ids still have to pass the real
|
|
653
|
+
// comparison - so a collision costs one wasted comparison and can never
|
|
654
|
+
// align two unequal elements.
|
|
655
|
+
function internElement(ids, value) {
|
|
656
|
+
const key = stableStringify(value);
|
|
657
|
+
let id = ids.get(key);
|
|
658
|
+
if (id === undefined) {
|
|
659
|
+
id = ids.size;
|
|
660
|
+
ids.set(key, id);
|
|
661
|
+
}
|
|
662
|
+
return id;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Align src[s0,s1) with tgt[t0,t1) and emit the ops for the alignment.
|
|
666
|
+
//
|
|
667
|
+
// The cost model is the patch itself: keeping a deep-equal pair is free,
|
|
668
|
+
// rewriting one position in place is one step, and so is inserting or
|
|
669
|
+
// deleting a single element. Minimizing that is edit distance WITH
|
|
670
|
+
// substitutions, not a longest common subsequence - LCS maximizes kept
|
|
671
|
+
// elements, which is a different thing and loses on a permutation, where
|
|
672
|
+
// it pays a delete plus an insert for what one rewrite covers.
|
|
673
|
+
//
|
|
674
|
+
// Because pairing every overlapping position is itself a valid
|
|
675
|
+
// alignment, the result never takes more steps than the index-wise diff.
|
|
676
|
+
//
|
|
677
|
+
// `cur` tracks where the current source element sits in the document as
|
|
678
|
+
// the ops emitted so far have left it: an insert shifts the rest right,
|
|
679
|
+
// a delete shifts it left onto the same index.
|
|
680
|
+
function diffArrayAligned(src, s0, s1, tgt, t0, t1, path, out, lcs) {
|
|
681
|
+
const m = s1 - s0;
|
|
682
|
+
const n = t1 - t0;
|
|
683
|
+
const ids = new Map();
|
|
684
|
+
const sk = new Int32Array(m);
|
|
685
|
+
const tk = new Int32Array(n);
|
|
686
|
+
for (let i = 0; i < m; i++)
|
|
687
|
+
sk[i] = internElement(ids, src[s0 + i]);
|
|
688
|
+
for (let j = 0; j < n; j++)
|
|
689
|
+
tk[j] = internElement(ids, tgt[t0 + j]);
|
|
690
|
+
|
|
691
|
+
// backward DP, so the alignment replays front to back - the order the
|
|
692
|
+
// ops have to be emitted in
|
|
693
|
+
const width = n + 1;
|
|
694
|
+
const dp = new Uint32Array((m + 1) * width);
|
|
695
|
+
const last = m * width;
|
|
696
|
+
for (let j = n - 1; j >= 0; j--)
|
|
697
|
+
dp[last + j] = n - j;
|
|
698
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
699
|
+
const row = i * width;
|
|
700
|
+
const next = row + width;
|
|
701
|
+
dp[row + n] = m - i;
|
|
702
|
+
const key = sk[i];
|
|
703
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
704
|
+
const same = key === tk[j] && equalsJson(src[s0 + i], tgt[t0 + j]);
|
|
705
|
+
let best = dp[next + j + 1] + (same ? 0 : 1);
|
|
706
|
+
const del = dp[next + j] + 1;
|
|
707
|
+
if (del < best)
|
|
708
|
+
best = del;
|
|
709
|
+
const ins = dp[row + j + 1] + 1;
|
|
710
|
+
if (ins < best)
|
|
711
|
+
best = ins;
|
|
712
|
+
dp[row + j] = best;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let i = 0;
|
|
717
|
+
let j = 0;
|
|
718
|
+
let cur = s0;
|
|
719
|
+
while (i < m && j < n) {
|
|
720
|
+
const row = i * width;
|
|
721
|
+
const next = row + width;
|
|
722
|
+
const same = sk[i] === tk[j] && equalsJson(src[s0 + i], tgt[t0 + j]);
|
|
723
|
+
const best = dp[row + j];
|
|
724
|
+
if (best === dp[next + j + 1] + (same ? 0 : 1)) {
|
|
725
|
+
if (!same)
|
|
726
|
+
diffValue(src[s0 + i], tgt[t0 + j], path + '/' + cur, out, lcs);
|
|
727
|
+
i++;
|
|
728
|
+
j++;
|
|
729
|
+
cur++;
|
|
730
|
+
}
|
|
731
|
+
else if (best === dp[next + j] + 1) {
|
|
732
|
+
out.push({ op: 'remove', path: path + '/' + cur });
|
|
733
|
+
i++;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
out.push({ op: 'add', path: path + '/' + cur, value: tgt[t0 + j] });
|
|
737
|
+
j++;
|
|
738
|
+
cur++;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
for (; i < m; i++)
|
|
742
|
+
out.push({ op: 'remove', path: path + '/' + cur });
|
|
743
|
+
for (; j < n; j++, cur++)
|
|
744
|
+
out.push({ op: 'add', path: path + '/' + cur, value: tgt[t0 + j] });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Array diff. Both modes first trim the deep-equal common prefix and
|
|
748
|
+
// suffix, which alone makes in-place edits and head/tail insertions
|
|
749
|
+
// minimal and bounds the work to the changed middle.
|
|
750
|
+
//
|
|
751
|
+
// What differs is the middle. The default pairs it up index-wise, which
|
|
752
|
+
// is linear but turns a mid-array insertion into a run of per-index
|
|
753
|
+
// replaces (correct, not minimal). `arrayDiff: 'minimal'` aligns the
|
|
754
|
+
// middle instead, so an insertion or deletion is emitted as one op and
|
|
755
|
+
// only genuinely changed positions are rewritten - at O(m*n) time and
|
|
756
|
+
// space in the size of that middle.
|
|
757
|
+
function diffArray(src, tgt, path, out, lcs) {
|
|
758
|
+
const slen = src.length;
|
|
759
|
+
const tlen = tgt.length;
|
|
760
|
+
const minLen = slen < tlen ? slen : tlen;
|
|
761
|
+
let start = 0;
|
|
762
|
+
while (start < minLen && equalsJson(src[start], tgt[start]))
|
|
763
|
+
start++;
|
|
764
|
+
let sEnd = slen;
|
|
765
|
+
let tEnd = tlen;
|
|
766
|
+
while (sEnd > start && tEnd > start && equalsJson(src[sEnd - 1], tgt[tEnd - 1])) {
|
|
767
|
+
sEnd--;
|
|
768
|
+
tEnd--;
|
|
769
|
+
}
|
|
770
|
+
const sMid = sEnd - start;
|
|
771
|
+
const tMid = tEnd - start;
|
|
772
|
+
if (sMid === 0 && tMid === 0)
|
|
773
|
+
return;
|
|
774
|
+
|
|
775
|
+
// one run of pure inserts or pure deletes needs no alignment, and the
|
|
776
|
+
// budget keeps a large middle from turning quadratic
|
|
777
|
+
if (!lcs || sMid === 0 || tMid === 0 || (sMid + 1) * (tMid + 1) > ALIGN_MAX_CELLS) {
|
|
778
|
+
diffArrayGap(src, start, sMid, tgt, start, tMid, start, path, out, lcs);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
diffArrayAligned(src, start, sEnd, tgt, start, tEnd, path, out, lcs);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function diffValue(src, tgt, path, out, lcs) {
|
|
785
|
+
if (src === tgt)
|
|
786
|
+
return;
|
|
787
|
+
const sArr = Array.isArray(src);
|
|
788
|
+
const tArr = Array.isArray(tgt);
|
|
789
|
+
if (sArr && tArr) {
|
|
790
|
+
diffArray(src, tgt, path, out, lcs);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (!sArr && !tArr && isJsonContainer(src) && isJsonContainer(tgt)) {
|
|
794
|
+
diffObject(src, tgt, path, out, lcs);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (!equalsJson(src, tgt))
|
|
798
|
+
out.push({ op: 'replace', path, value: tgt });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Compute a JSON Patch (RFC 6902) that transforms `source` into
|
|
803
|
+
* `target`: `applyJSONPatch(source, createJSONPatch(source, target))`
|
|
804
|
+
* is deep-equal to `target`.
|
|
805
|
+
*
|
|
806
|
+
* Objects diff member-wise; arrays trim the common prefix/suffix and
|
|
807
|
+
* diff the middle index-wise, so in-place edits and head/tail
|
|
808
|
+
* insertions produce minimal patches while a mid-array insertion falls
|
|
809
|
+
* back to correct (but larger) per-index replaces. Emitted `value`
|
|
810
|
+
* members share references with `target`.
|
|
811
|
+
*
|
|
812
|
+
* `arrayDiff: 'minimal'` aligns the changed middle instead, so a
|
|
813
|
+
* mid-array insertion or deletion is emitted as one `add`/`remove` —
|
|
814
|
+
* the smallest patch, which is what matters when patches go over the
|
|
815
|
+
* wire. The alignment minimizes the patch itself (edit distance with
|
|
816
|
+
* substitutions, not a longest common subsequence: LCS maximizes kept
|
|
817
|
+
* elements, which costs a delete plus an insert on a permutation where
|
|
818
|
+
* one rewrite would do), so it never takes more alignment steps than
|
|
819
|
+
* the index-wise pairing.
|
|
820
|
+
*
|
|
821
|
+
* It is opt-in because it costs O(m*n) time and space in the length of
|
|
822
|
+
* that middle, against the default's linear pass; above a fixed cell
|
|
823
|
+
* budget a single array falls back to the index-wise diff, so the mode
|
|
824
|
+
* never turns a large diff quadratic. Both modes produce patches that
|
|
825
|
+
* reproduce `target` exactly.
|
|
826
|
+
*
|
|
827
|
+
* @param {any} source - The original document
|
|
828
|
+
* @param {any} target - The desired document
|
|
829
|
+
* @param {{ arrayDiff?: 'index' | 'minimal' }} [options] - `arrayDiff`
|
|
830
|
+
* selects the array strategy: `'index'` (default) or `'minimal'`
|
|
831
|
+
* @returns {JsonPatchOperation[]} The patch document (empty when equal)
|
|
832
|
+
* @throws {TypeError} When `arrayDiff` is not a known mode
|
|
833
|
+
* @example
|
|
834
|
+
* createJSONPatch({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 });
|
|
835
|
+
* // [{ op: 'replace', path: '/b', value: 3 },
|
|
836
|
+
* // { op: 'add', path: '/c', value: 4 }]
|
|
837
|
+
* @example
|
|
838
|
+
* const before = [{ id: 1 }, { id: 2 }, { id: 3, n: 0 }];
|
|
839
|
+
* const after = [{ id: 1 }, { id: 9 }, { id: 2 }, { id: 3, n: 1 }];
|
|
840
|
+
* createJSONPatch(before, after, { arrayDiff: 'minimal' });
|
|
841
|
+
* // [{ op: 'add', path: '/1', value: { id: 9 } },
|
|
842
|
+
* // { op: 'replace', path: '/3/n', value: 1 }]
|
|
843
|
+
*/
|
|
844
|
+
export function createJSONPatch(source, target, options = undefined) {
|
|
845
|
+
const mode = options == null || options.arrayDiff === undefined
|
|
846
|
+
? 'index'
|
|
847
|
+
: options.arrayDiff;
|
|
848
|
+
if (mode !== 'index' && mode !== 'minimal')
|
|
849
|
+
throw new TypeError(`unknown 'arrayDiff' option '${mode}'`);
|
|
850
|
+
const out = [];
|
|
851
|
+
diffValue(source, target, '', out, mode === 'minimal');
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
//#endregion
|
|
856
|
+
|
|
857
|
+
//#region JSON Merge Patch (RFC 7396)
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* A compiled JSON Merge Patch: applies the merge patch to a document
|
|
861
|
+
* and returns the patched document.
|
|
862
|
+
* @typedef {(doc: any) => any} JsonMergePatchApplier
|
|
863
|
+
*/
|
|
864
|
+
|
|
865
|
+
// Compile one merge-patch object level into a plan closure. The patch
|
|
866
|
+
// splits once into removes (null members), scalar/array sets and
|
|
867
|
+
// nested-object merges; applying is identity-preserving - a level that
|
|
868
|
+
// changes nothing returns the target reference unchanged.
|
|
869
|
+
function compileMergeObjectNode(patch) {
|
|
870
|
+
const removes = [];
|
|
871
|
+
const setKeys = [];
|
|
872
|
+
const setVals = [];
|
|
873
|
+
const mergeKeys = [];
|
|
874
|
+
const mergeFns = [];
|
|
875
|
+
for (const key in patch) {
|
|
876
|
+
if (!hasOwn(patch, key))
|
|
877
|
+
continue;
|
|
878
|
+
const v = patch[key];
|
|
879
|
+
if (v === null) {
|
|
880
|
+
removes.push(key);
|
|
881
|
+
}
|
|
882
|
+
else if (isJsonObject(v)) {
|
|
883
|
+
mergeKeys.push(key);
|
|
884
|
+
mergeFns.push(compileMergeObjectNode(v));
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
setKeys.push(key);
|
|
888
|
+
setVals.push(v);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const removeSet = removes.length > 0 ? new Set(removes) : null;
|
|
892
|
+
const rlen = removes.length;
|
|
893
|
+
const slen = setKeys.length;
|
|
894
|
+
const mlen = mergeKeys.length;
|
|
895
|
+
return function mergeNode(target) {
|
|
896
|
+
if (!isJsonObject(target)) {
|
|
897
|
+
// RFC 7396: a non-object target is replaced by merging into {}
|
|
898
|
+
const out = {};
|
|
899
|
+
for (let i = 0; i < slen; i++)
|
|
900
|
+
setObjectMember(out, setKeys[i], setVals[i]);
|
|
901
|
+
for (let i = 0; i < mlen; i++)
|
|
902
|
+
setObjectMember(out, mergeKeys[i], mergeFns[i](undefined));
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
let changed = false;
|
|
906
|
+
for (let i = 0; i < rlen; i++) {
|
|
907
|
+
if (hasOwn(target, removes[i])) {
|
|
908
|
+
changed = true;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!changed) {
|
|
913
|
+
for (let i = 0; i < slen; i++) {
|
|
914
|
+
const key = setKeys[i];
|
|
915
|
+
if (!hasOwn(target, key) || target[key] !== setVals[i]) {
|
|
916
|
+
changed = true;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const mres = mlen > 0 ? new Array(mlen) : null;
|
|
922
|
+
for (let i = 0; i < mlen; i++) {
|
|
923
|
+
const key = mergeKeys[i];
|
|
924
|
+
const prev = hasOwn(target, key) ? target[key] : undefined;
|
|
925
|
+
const sub = mergeFns[i](prev);
|
|
926
|
+
mres[i] = sub;
|
|
927
|
+
if (sub !== prev)
|
|
928
|
+
changed = true;
|
|
929
|
+
}
|
|
930
|
+
if (!changed)
|
|
931
|
+
return target;
|
|
932
|
+
const out = {};
|
|
933
|
+
for (const key in target) {
|
|
934
|
+
if (!hasOwn(target, key))
|
|
935
|
+
continue;
|
|
936
|
+
if (removeSet !== null && removeSet.has(key))
|
|
937
|
+
continue;
|
|
938
|
+
setObjectMember(out, key, target[key]);
|
|
939
|
+
}
|
|
940
|
+
for (let i = 0; i < slen; i++)
|
|
941
|
+
setObjectMember(out, setKeys[i], setVals[i]);
|
|
942
|
+
for (let i = 0; i < mlen; i++)
|
|
943
|
+
setObjectMember(out, mergeKeys[i], mres[i]);
|
|
944
|
+
return out;
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Compile a JSON Merge Patch (RFC 7396) into a reusable applier.
|
|
950
|
+
*
|
|
951
|
+
* The patch pre-splits once into remove/set/merge plans per level.
|
|
952
|
+
* Applying is copy-on-write and identity-preserving: unchanged subtrees
|
|
953
|
+
* (and a wholly unchanged document) come back by reference, set values
|
|
954
|
+
* are shared with the patch document, and the input is never mutated.
|
|
955
|
+
*
|
|
956
|
+
* @param {any} patch - The merge patch (any JSON value; a non-object
|
|
957
|
+
* replaces the document wholesale)
|
|
958
|
+
* @returns {JsonMergePatchApplier} applier returning the patched document
|
|
959
|
+
* @example
|
|
960
|
+
* const apply = compileMergePatch({ age: 31, temp: null });
|
|
961
|
+
* apply({ name: 'Alice', age: 30, temp: 'x' });
|
|
962
|
+
* // { name: 'Alice', age: 31 }
|
|
963
|
+
*/
|
|
964
|
+
export function compileMergePatch(patch) {
|
|
965
|
+
if (!isJsonObject(patch)) {
|
|
966
|
+
return function applyMergeReplace() {
|
|
967
|
+
return patch;
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const node = compileMergeObjectNode(patch);
|
|
971
|
+
return function applyMerge(doc) {
|
|
972
|
+
return node(doc);
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Apply a JSON Merge Patch (RFC 7396) to a document in one shot. On hot
|
|
978
|
+
* paths prefer `compileMergePatch` and reuse the applier.
|
|
979
|
+
*
|
|
980
|
+
* @param {any} doc - The document to patch (never mutated)
|
|
981
|
+
* @param {any} patch - The merge patch
|
|
982
|
+
* @returns {any} The patched document
|
|
983
|
+
*/
|
|
984
|
+
export function applyMergePatch(doc, patch) {
|
|
985
|
+
return compileMergePatch(patch)(doc);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function isEmptyObjectShallow(obj) {
|
|
989
|
+
for (const key in obj) {
|
|
990
|
+
if (hasOwn(obj, key))
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function diffMergeObjects(src, tgt) {
|
|
997
|
+
const patch = {};
|
|
998
|
+
for (const key in src) {
|
|
999
|
+
if (hasOwn(src, key) && !hasOwn(tgt, key))
|
|
1000
|
+
setObjectMember(patch, key, null);
|
|
1001
|
+
}
|
|
1002
|
+
for (const key in tgt) {
|
|
1003
|
+
if (!hasOwn(tgt, key))
|
|
1004
|
+
continue;
|
|
1005
|
+
const tv = tgt[key];
|
|
1006
|
+
if (!hasOwn(src, key)) {
|
|
1007
|
+
// an added member with value null is unrepresentable (null means
|
|
1008
|
+
// remove); either way the key ends up absent after applying
|
|
1009
|
+
if (tv !== null)
|
|
1010
|
+
setObjectMember(patch, key, tv);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const sv = src[key];
|
|
1014
|
+
if (sv === tv)
|
|
1015
|
+
continue;
|
|
1016
|
+
if (isJsonObject(sv) && isJsonObject(tv)) {
|
|
1017
|
+
const sub = diffMergeObjects(sv, tv);
|
|
1018
|
+
if (!isEmptyObjectShallow(sub))
|
|
1019
|
+
setObjectMember(patch, key, sub);
|
|
1020
|
+
}
|
|
1021
|
+
else if (!equalsJson(sv, tv)) {
|
|
1022
|
+
setObjectMember(patch, key, tv);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return patch;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Compute a JSON Merge Patch (RFC 7396) that transforms `source` into
|
|
1030
|
+
* `target`. Removed members become `null`, nested objects diff
|
|
1031
|
+
* recursively, and arrays (or any kind change) replace wholesale.
|
|
1032
|
+
*
|
|
1033
|
+
* RFC 7396 cannot represent a member whose target value is `null`:
|
|
1034
|
+
* the diff emits `null` (a removal), so applying yields an absent
|
|
1035
|
+
* member instead. Emitted values share references with `target`.
|
|
1036
|
+
*
|
|
1037
|
+
* @param {any} source - The original document
|
|
1038
|
+
* @param {any} target - The desired document
|
|
1039
|
+
* @returns {any} The merge patch (`{}` when nothing changed)
|
|
1040
|
+
* @example
|
|
1041
|
+
* createMergePatch({ a: 'b', c: 1 }, { a: 'x' });
|
|
1042
|
+
* // { a: 'x', c: null }
|
|
1043
|
+
*/
|
|
1044
|
+
export function createMergePatch(source, target) {
|
|
1045
|
+
if (!isJsonObject(source) || !isJsonObject(target))
|
|
1046
|
+
return target;
|
|
1047
|
+
return diffMergeObjects(source, target);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
//#endregion
|
|
1051
|
+
|
|
1052
|
+
//#endregion
|