@speclynx/apidom-overlay 5.1.0 → 5.2.1
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/CHANGELOG.md +25 -0
- package/README.md +4 -3
- package/dist/apidom-overlay.browser.js +2038 -346
- package/dist/apidom-overlay.browser.min.js +1 -1
- package/package.json +8 -8
- package/src/apply/realms/apidom.cjs +144 -30
- package/src/apply/realms/apidom.mjs +145 -31
- package/src/apply/realms/pojo.cjs +14 -2
- package/src/apply/realms/pojo.mjs +14 -2
- package/src/apply/realms/uri.cjs +5 -2
- package/src/apply/realms/uri.mjs +5 -2
- package/src/diff/realms/apidom.cjs +7 -3
- package/src/diff/realms/apidom.mjs +8 -4
- package/types/apidom-overlay.d.ts +29 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speclynx/apidom-overlay",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Apply Overlay 1.x.y documents to API definitions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apidom",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"license": "Apache-2.0",
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@babel/runtime-corejs3": "^8.0.0",
|
|
56
|
-
"@speclynx/apidom-core": "5.1
|
|
57
|
-
"@speclynx/apidom-datamodel": "5.1
|
|
58
|
-
"@speclynx/apidom-error": "5.1
|
|
59
|
-
"@speclynx/apidom-json-path": "5.1
|
|
60
|
-
"@speclynx/apidom-ns-overlay-1": "5.1
|
|
61
|
-
"@speclynx/apidom-reference": "5.1
|
|
56
|
+
"@speclynx/apidom-core": "5.2.1",
|
|
57
|
+
"@speclynx/apidom-datamodel": "5.2.1",
|
|
58
|
+
"@speclynx/apidom-error": "5.2.1",
|
|
59
|
+
"@speclynx/apidom-json-path": "5.2.1",
|
|
60
|
+
"@speclynx/apidom-ns-overlay-1": "5.2.1",
|
|
61
|
+
"@speclynx/apidom-reference": "5.2.1",
|
|
62
62
|
"type-fest": "^5.8.0"
|
|
63
63
|
},
|
|
64
64
|
"files": [
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"README.md",
|
|
72
72
|
"CHANGELOG.md"
|
|
73
73
|
],
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "814e67e36b1e27546288c74a25eab8f9463943eb"
|
|
75
75
|
}
|
|
@@ -25,6 +25,18 @@ const defaultApplyOptions = {
|
|
|
25
25
|
strict: false
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Merges caller options over the defaults. A partial options object leaves the
|
|
30
|
+
* fields it omits undefined, which must still mean the documented defaults.
|
|
31
|
+
* `immutable` is spelled out because it is the only default that is not falsy,
|
|
32
|
+
* so a caller forwarding an undefined value must not end up mutating in place.
|
|
33
|
+
*/
|
|
34
|
+
const withDefaults = options => ({
|
|
35
|
+
...defaultApplyOptions,
|
|
36
|
+
...options,
|
|
37
|
+
immutable: options.immutable ?? defaultApplyOptions.immutable
|
|
38
|
+
});
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* Default customMerge that enforces Overlay spec type compatibility.
|
|
30
42
|
* Throws OverlayError on incompatible type combinations.
|
|
@@ -85,12 +97,37 @@ const resolveParent = (normalizedPath, root) => {
|
|
|
85
97
|
* @public
|
|
86
98
|
*/
|
|
87
99
|
const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
|
|
88
|
-
|
|
100
|
+
const opts = withDefaults(options);
|
|
101
|
+
let result = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
|
|
89
102
|
for (const normalizedPath of normalizedPaths) {
|
|
103
|
+
/**
|
|
104
|
+
* Every matched node gets its own copy of the update value. deepmerge
|
|
105
|
+
* shallow clones members, so the merged result shares key elements and
|
|
106
|
+
* non-mergeable values with its source regardless of the clone option —
|
|
107
|
+
* without this each target would alias every other target, and a copy
|
|
108
|
+
* action would alias the copy source in the target document.
|
|
109
|
+
*/
|
|
110
|
+
const clonedUpdateValue = (0, _apidomDatamodel.cloneDeep)(updateValue);
|
|
90
111
|
const resolved = resolveParent(normalizedPath, result);
|
|
91
112
|
if (resolved === null) {
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Targeting root. There is no parent slot to write the merged element
|
|
115
|
+
* into, so the merge is transplanted back onto the target itself — every
|
|
116
|
+
* other target shape updates the document in place and the root must not
|
|
117
|
+
* be the silent exception. Merging always yields the target's own
|
|
118
|
+
* constructor, so the transplant is type-safe. Two cases cannot be
|
|
119
|
+
* written in place and fall back to rebinding: a primitive root replaced
|
|
120
|
+
* by a differently typed value, and a frozen root, whose properties
|
|
121
|
+
* cannot be assigned.
|
|
122
|
+
*/
|
|
123
|
+
const merged = mergeValue(result, clonedUpdateValue, opts);
|
|
124
|
+
if (merged.constructor === result.constructor && !result.isFrozen) {
|
|
125
|
+
result.content = merged.content;
|
|
126
|
+
result.meta = merged.meta;
|
|
127
|
+
result.attributes = merged.attributes;
|
|
128
|
+
} else {
|
|
129
|
+
result = merged;
|
|
130
|
+
}
|
|
94
131
|
continue;
|
|
95
132
|
}
|
|
96
133
|
const {
|
|
@@ -100,12 +137,12 @@ const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options
|
|
|
100
137
|
if ((0, _apidomDatamodel.isObjectElement)(parent)) {
|
|
101
138
|
const current = parent.get(key);
|
|
102
139
|
if (current !== undefined) {
|
|
103
|
-
parent.set(key, mergeValue(current,
|
|
140
|
+
parent.set(key, mergeValue(current, clonedUpdateValue, opts));
|
|
104
141
|
}
|
|
105
142
|
} else if ((0, _apidomDatamodel.isArrayElement)(parent)) {
|
|
106
143
|
const current = parent.get(key);
|
|
107
144
|
if (current !== undefined) {
|
|
108
|
-
parent.content[key] = mergeValue(current,
|
|
145
|
+
parent.content[key] = mergeValue(current, clonedUpdateValue, opts);
|
|
109
146
|
}
|
|
110
147
|
}
|
|
111
148
|
}
|
|
@@ -125,7 +162,13 @@ const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options
|
|
|
125
162
|
if (sourceMatches.length > 1) {
|
|
126
163
|
throw new _OverlayError.default(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
|
|
127
164
|
}
|
|
128
|
-
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Snapshot the copy source. Matched targets are updated in document order and
|
|
168
|
+
* the source may sit under one of them, so a live reference would let later
|
|
169
|
+
* targets copy a source that an earlier target already modified.
|
|
170
|
+
*/
|
|
171
|
+
const sourceValue = (0, _apidomDatamodel.cloneDeep)(sourceMatches[0]);
|
|
129
172
|
return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
|
|
130
173
|
};
|
|
131
174
|
|
|
@@ -135,7 +178,8 @@ const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options
|
|
|
135
178
|
*/
|
|
136
179
|
exports.applyCopyAction = applyCopyAction;
|
|
137
180
|
const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
|
|
138
|
-
const
|
|
181
|
+
const opts = withDefaults(options);
|
|
182
|
+
const result = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
|
|
139
183
|
// reverse document order so higher array indices are removed first (avoids index shifting)
|
|
140
184
|
const sorted = [...normalizedPaths].reverse();
|
|
141
185
|
for (const normalizedPath of sorted) {
|
|
@@ -158,14 +202,20 @@ const applyRemoveAction = (normalizedPaths, targetElement, options = defaultAppl
|
|
|
158
202
|
|
|
159
203
|
/**
|
|
160
204
|
* Applies a single overlay action to a target element.
|
|
161
|
-
*
|
|
205
|
+
*
|
|
206
|
+
* Immutable by default. The target is cloned once up front and the action is
|
|
207
|
+
* applied to the clone, so the returned element is always independent of the
|
|
208
|
+
* input, including when the action changes nothing. Pass `immutable: false` to
|
|
209
|
+
* apply the action in place; the one shape that cannot be applied in place is
|
|
210
|
+
* a `target: $` action replacing a primitive root with a differently typed
|
|
211
|
+
* value, so the return value is authoritative in both modes.
|
|
162
212
|
*
|
|
163
213
|
* @public
|
|
164
214
|
*/
|
|
165
215
|
exports.applyRemoveAction = applyRemoveAction;
|
|
166
216
|
const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
|
|
167
|
-
const
|
|
168
|
-
const tracer =
|
|
217
|
+
const opts = withDefaults(options);
|
|
218
|
+
const tracer = opts.tracer ?? (opts.trace ? new _TraceBuilder.default(opts.trace) : undefined);
|
|
169
219
|
const actionValidation = (0, _validate.validateAction)(actionElement);
|
|
170
220
|
if (!actionValidation.valid) throw actionValidation.error;
|
|
171
221
|
const targetExpression = (0, _apidomCore.toValue)(actionElement.target);
|
|
@@ -178,7 +228,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
178
228
|
}
|
|
179
229
|
});
|
|
180
230
|
if (matches.length === 0) {
|
|
181
|
-
if (
|
|
231
|
+
if (opts.strict) {
|
|
182
232
|
const error = new _OverlayError.default(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
|
|
183
233
|
action: actionElement
|
|
184
234
|
});
|
|
@@ -198,13 +248,20 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
198
248
|
matchCount: 0,
|
|
199
249
|
normalizedPaths: []
|
|
200
250
|
});
|
|
201
|
-
return targetElement;
|
|
251
|
+
return opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
|
|
202
252
|
}
|
|
253
|
+
|
|
254
|
+
// clone once, then apply the action mutably to the clone
|
|
255
|
+
const workingElement = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
|
|
256
|
+
const mutatingOptions = {
|
|
257
|
+
...opts,
|
|
258
|
+
immutable: false
|
|
259
|
+
};
|
|
203
260
|
try {
|
|
204
261
|
// remove action
|
|
205
262
|
const removeValue = actionElement.removeField;
|
|
206
263
|
if (removeValue !== undefined && (0, _apidomCore.toValue)(removeValue) === true) {
|
|
207
|
-
const result = applyRemoveAction(normalizedPaths,
|
|
264
|
+
const result = applyRemoveAction(normalizedPaths, workingElement, mutatingOptions);
|
|
208
265
|
tracer?.action({
|
|
209
266
|
target: targetExpression,
|
|
210
267
|
type: 'remove',
|
|
@@ -218,7 +275,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
218
275
|
if (actionElement.update !== undefined) {
|
|
219
276
|
const nodesValidation = (0, _validate.validateTargetNodes)(matches);
|
|
220
277
|
if (!nodesValidation.valid) throw nodesValidation.error;
|
|
221
|
-
const result = applyUpdateAction(normalizedPaths, actionElement.update,
|
|
278
|
+
const result = applyUpdateAction(normalizedPaths, actionElement.update, workingElement, mutatingOptions);
|
|
222
279
|
tracer?.action({
|
|
223
280
|
target: targetExpression,
|
|
224
281
|
type: 'update',
|
|
@@ -233,7 +290,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
233
290
|
const nodesValidation = (0, _validate.validateTargetNodes)(matches);
|
|
234
291
|
if (!nodesValidation.valid) throw nodesValidation.error;
|
|
235
292
|
const copyExpression = (0, _apidomCore.toValue)(actionElement.copy);
|
|
236
|
-
const result = applyCopyAction(normalizedPaths, copyExpression,
|
|
293
|
+
const result = applyCopyAction(normalizedPaths, copyExpression, workingElement, mutatingOptions);
|
|
237
294
|
tracer?.action({
|
|
238
295
|
target: targetExpression,
|
|
239
296
|
type: 'copy',
|
|
@@ -248,7 +305,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
248
305
|
matchCount: matches.length,
|
|
249
306
|
normalizedPaths
|
|
250
307
|
});
|
|
251
|
-
return
|
|
308
|
+
return workingElement;
|
|
252
309
|
} catch (error) {
|
|
253
310
|
// enrich OverlayError with action context, preserving original stack
|
|
254
311
|
if (error instanceof _OverlayError.default && !error.action) {
|
|
@@ -268,7 +325,22 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
|
|
|
268
325
|
|
|
269
326
|
/**
|
|
270
327
|
* Applies an entire overlay document to a target element.
|
|
271
|
-
*
|
|
328
|
+
*
|
|
329
|
+
* Immutable by default. The target is cloned once up front and all actions are
|
|
330
|
+
* applied to the clone sequentially, so the returned element is always
|
|
331
|
+
* independent of the input, including when no action changes anything. Pass
|
|
332
|
+
* `immutable: false` to apply the actions in place; the one shape that cannot
|
|
333
|
+
* be applied in place is a `target: $` action replacing a primitive root with
|
|
334
|
+
* a differently typed value, so the return value is authoritative in both
|
|
335
|
+
* modes.
|
|
336
|
+
*
|
|
337
|
+
* When the target is a `ParseResultElement`, a new parse result is returned
|
|
338
|
+
* carrying the new tree as its result element; the caller's parse result and
|
|
339
|
+
* its result element are left untouched. Every other child — annotations, and
|
|
340
|
+
* an `api` element that is not the result element itself — is shared with the
|
|
341
|
+
* input rather than copied, since no overlay action can reach it.
|
|
342
|
+
* Under `immutable: false` the caller's parse result is updated in place
|
|
343
|
+
* instead, with its result element replaced by the new tree.
|
|
272
344
|
*
|
|
273
345
|
* @public
|
|
274
346
|
*/
|
|
@@ -278,31 +350,73 @@ const applyOverlay = (overlayElement, targetElement, options = defaultApplyOptio
|
|
|
278
350
|
if (!(0, _apidomNsOverlay.isOverlay1Element)(overlay)) {
|
|
279
351
|
throw new _OverlayError.default('First argument must be an Overlay 1.x document');
|
|
280
352
|
}
|
|
353
|
+
const opts = withDefaults(options);
|
|
354
|
+
let parseResult;
|
|
355
|
+
let resultElement;
|
|
281
356
|
let target;
|
|
282
357
|
if (targetElement instanceof _apidomDatamodel.ParseResultElement) {
|
|
283
358
|
if (targetElement.result === undefined) {
|
|
284
359
|
throw new _OverlayError.default('Target ParseResultElement contains no result element');
|
|
285
360
|
}
|
|
286
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Only the result element changes, so the wrapper is cloned shallowly: the
|
|
363
|
+
* clone gets its own content list, which the new tree replaces the result
|
|
364
|
+
* element in, and every child the overlay never touches is shared with the
|
|
365
|
+
* input instead of copied.
|
|
366
|
+
*/
|
|
367
|
+
parseResult = opts.immutable ? (0, _apidomDatamodel.cloneShallow)(targetElement) : targetElement;
|
|
368
|
+
resultElement = parseResult.result;
|
|
369
|
+
target = resultElement;
|
|
287
370
|
} else {
|
|
288
371
|
target = targetElement;
|
|
289
372
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
373
|
+
target = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(target) : target;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Constructed only once the target has been validated: the constructor writes
|
|
377
|
+
* a success verdict into the caller's trace object, which must not survive a
|
|
378
|
+
* throw from the checks above.
|
|
379
|
+
*/
|
|
380
|
+
const tracer = opts.trace ? new _TraceBuilder.default(opts.trace) : undefined;
|
|
294
381
|
const internalOptions = tracer ? {
|
|
295
|
-
...
|
|
382
|
+
...opts,
|
|
296
383
|
tracer
|
|
297
|
-
} :
|
|
298
|
-
|
|
299
|
-
|
|
384
|
+
} : opts;
|
|
385
|
+
// clone once, then apply the actions mutably to the clone
|
|
386
|
+
const mutatingOptions = {
|
|
387
|
+
...internalOptions,
|
|
388
|
+
immutable: false
|
|
389
|
+
};
|
|
390
|
+
if ((0, _apidomDatamodel.isArrayElement)(overlay.actions)) {
|
|
391
|
+
for (const action of overlay.actions) {
|
|
392
|
+
target = applyAction(action, target, mutatingOptions);
|
|
393
|
+
}
|
|
300
394
|
}
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
395
|
+
if (parseResult !== undefined) {
|
|
396
|
+
/**
|
|
397
|
+
* The result marker is a class on the element itself, and replacing a
|
|
398
|
+
* primitive root swaps in the update value's meta wholesale, so an action
|
|
399
|
+
* can strip it. Restore it before the element goes back into the wrapper,
|
|
400
|
+
* otherwise the returned parse result has no result element at all.
|
|
401
|
+
*/
|
|
402
|
+
if (!(0, _apidomDatamodel.includesClasses)(target, ['result'])) {
|
|
403
|
+
target.classes.push('result');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* replaceResult locates the slot by scanning for the marker, so the element
|
|
408
|
+
* being replaced has to carry one too. An earlier action that stripped the
|
|
409
|
+
* marker off the element still sitting in the wrapper would otherwise leave
|
|
410
|
+
* that lookup with nothing to find, and the new tree would be dropped on
|
|
411
|
+
* the floor.
|
|
412
|
+
*/
|
|
413
|
+
if (target !== resultElement) {
|
|
414
|
+
if (!(0, _apidomDatamodel.includesClasses)(resultElement, ['result'])) {
|
|
415
|
+
resultElement.classes.push('result');
|
|
416
|
+
}
|
|
417
|
+
parseResult.replaceResult(target);
|
|
304
418
|
}
|
|
305
|
-
return
|
|
419
|
+
return parseResult;
|
|
306
420
|
}
|
|
307
421
|
return target;
|
|
308
422
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isObjectElement, isArrayElement, cloneDeep, ParseResultElement } from '@speclynx/apidom-datamodel';
|
|
1
|
+
import { isObjectElement, isArrayElement, includesClasses, cloneDeep, cloneShallow, ParseResultElement } from '@speclynx/apidom-datamodel';
|
|
2
2
|
import { deepmerge, toValue } from '@speclynx/apidom-core';
|
|
3
3
|
import { evaluate, NormalizedPath } from '@speclynx/apidom-json-path';
|
|
4
4
|
import { isOverlay1Element } from '@speclynx/apidom-ns-overlay-1';
|
|
@@ -16,6 +16,18 @@ const defaultApplyOptions = {
|
|
|
16
16
|
strict: false
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Merges caller options over the defaults. A partial options object leaves the
|
|
21
|
+
* fields it omits undefined, which must still mean the documented defaults.
|
|
22
|
+
* `immutable` is spelled out because it is the only default that is not falsy,
|
|
23
|
+
* so a caller forwarding an undefined value must not end up mutating in place.
|
|
24
|
+
*/
|
|
25
|
+
const withDefaults = options => ({
|
|
26
|
+
...defaultApplyOptions,
|
|
27
|
+
...options,
|
|
28
|
+
immutable: options.immutable ?? defaultApplyOptions.immutable
|
|
29
|
+
});
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* Default customMerge that enforces Overlay spec type compatibility.
|
|
21
33
|
* Throws OverlayError on incompatible type combinations.
|
|
@@ -76,12 +88,37 @@ const resolveParent = (normalizedPath, root) => {
|
|
|
76
88
|
* @public
|
|
77
89
|
*/
|
|
78
90
|
export const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
|
|
79
|
-
|
|
91
|
+
const opts = withDefaults(options);
|
|
92
|
+
let result = opts.immutable ? cloneDeep(targetElement) : targetElement;
|
|
80
93
|
for (const normalizedPath of normalizedPaths) {
|
|
94
|
+
/**
|
|
95
|
+
* Every matched node gets its own copy of the update value. deepmerge
|
|
96
|
+
* shallow clones members, so the merged result shares key elements and
|
|
97
|
+
* non-mergeable values with its source regardless of the clone option —
|
|
98
|
+
* without this each target would alias every other target, and a copy
|
|
99
|
+
* action would alias the copy source in the target document.
|
|
100
|
+
*/
|
|
101
|
+
const clonedUpdateValue = cloneDeep(updateValue);
|
|
81
102
|
const resolved = resolveParent(normalizedPath, result);
|
|
82
103
|
if (resolved === null) {
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Targeting root. There is no parent slot to write the merged element
|
|
106
|
+
* into, so the merge is transplanted back onto the target itself — every
|
|
107
|
+
* other target shape updates the document in place and the root must not
|
|
108
|
+
* be the silent exception. Merging always yields the target's own
|
|
109
|
+
* constructor, so the transplant is type-safe. Two cases cannot be
|
|
110
|
+
* written in place and fall back to rebinding: a primitive root replaced
|
|
111
|
+
* by a differently typed value, and a frozen root, whose properties
|
|
112
|
+
* cannot be assigned.
|
|
113
|
+
*/
|
|
114
|
+
const merged = mergeValue(result, clonedUpdateValue, opts);
|
|
115
|
+
if (merged.constructor === result.constructor && !result.isFrozen) {
|
|
116
|
+
result.content = merged.content;
|
|
117
|
+
result.meta = merged.meta;
|
|
118
|
+
result.attributes = merged.attributes;
|
|
119
|
+
} else {
|
|
120
|
+
result = merged;
|
|
121
|
+
}
|
|
85
122
|
continue;
|
|
86
123
|
}
|
|
87
124
|
const {
|
|
@@ -91,12 +128,12 @@ export const applyUpdateAction = (normalizedPaths, updateValue, targetElement, o
|
|
|
91
128
|
if (isObjectElement(parent)) {
|
|
92
129
|
const current = parent.get(key);
|
|
93
130
|
if (current !== undefined) {
|
|
94
|
-
parent.set(key, mergeValue(current,
|
|
131
|
+
parent.set(key, mergeValue(current, clonedUpdateValue, opts));
|
|
95
132
|
}
|
|
96
133
|
} else if (isArrayElement(parent)) {
|
|
97
134
|
const current = parent.get(key);
|
|
98
135
|
if (current !== undefined) {
|
|
99
|
-
parent.content[key] = mergeValue(current,
|
|
136
|
+
parent.content[key] = mergeValue(current, clonedUpdateValue, opts);
|
|
100
137
|
}
|
|
101
138
|
}
|
|
102
139
|
}
|
|
@@ -115,7 +152,13 @@ export const applyCopyAction = (normalizedPaths, copyExpression, targetElement,
|
|
|
115
152
|
if (sourceMatches.length > 1) {
|
|
116
153
|
throw new OverlayError(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
|
|
117
154
|
}
|
|
118
|
-
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Snapshot the copy source. Matched targets are updated in document order and
|
|
158
|
+
* the source may sit under one of them, so a live reference would let later
|
|
159
|
+
* targets copy a source that an earlier target already modified.
|
|
160
|
+
*/
|
|
161
|
+
const sourceValue = cloneDeep(sourceMatches[0]);
|
|
119
162
|
return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
|
|
120
163
|
};
|
|
121
164
|
|
|
@@ -124,7 +167,8 @@ export const applyCopyAction = (normalizedPaths, copyExpression, targetElement,
|
|
|
124
167
|
* @public
|
|
125
168
|
*/
|
|
126
169
|
export const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
|
|
127
|
-
const
|
|
170
|
+
const opts = withDefaults(options);
|
|
171
|
+
const result = opts.immutable ? cloneDeep(targetElement) : targetElement;
|
|
128
172
|
// reverse document order so higher array indices are removed first (avoids index shifting)
|
|
129
173
|
const sorted = [...normalizedPaths].reverse();
|
|
130
174
|
for (const normalizedPath of sorted) {
|
|
@@ -147,13 +191,19 @@ export const applyRemoveAction = (normalizedPaths, targetElement, options = defa
|
|
|
147
191
|
|
|
148
192
|
/**
|
|
149
193
|
* Applies a single overlay action to a target element.
|
|
150
|
-
*
|
|
194
|
+
*
|
|
195
|
+
* Immutable by default. The target is cloned once up front and the action is
|
|
196
|
+
* applied to the clone, so the returned element is always independent of the
|
|
197
|
+
* input, including when the action changes nothing. Pass `immutable: false` to
|
|
198
|
+
* apply the action in place; the one shape that cannot be applied in place is
|
|
199
|
+
* a `target: $` action replacing a primitive root with a differently typed
|
|
200
|
+
* value, so the return value is authoritative in both modes.
|
|
151
201
|
*
|
|
152
202
|
* @public
|
|
153
203
|
*/
|
|
154
204
|
export const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
|
|
155
|
-
const
|
|
156
|
-
const tracer =
|
|
205
|
+
const opts = withDefaults(options);
|
|
206
|
+
const tracer = opts.tracer ?? (opts.trace ? new TraceBuilder(opts.trace) : undefined);
|
|
157
207
|
const actionValidation = validateAction(actionElement);
|
|
158
208
|
if (!actionValidation.valid) throw actionValidation.error;
|
|
159
209
|
const targetExpression = toValue(actionElement.target);
|
|
@@ -166,7 +216,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
166
216
|
}
|
|
167
217
|
});
|
|
168
218
|
if (matches.length === 0) {
|
|
169
|
-
if (
|
|
219
|
+
if (opts.strict) {
|
|
170
220
|
const error = new OverlayError(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
|
|
171
221
|
action: actionElement
|
|
172
222
|
});
|
|
@@ -186,13 +236,20 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
186
236
|
matchCount: 0,
|
|
187
237
|
normalizedPaths: []
|
|
188
238
|
});
|
|
189
|
-
return targetElement;
|
|
239
|
+
return opts.immutable ? cloneDeep(targetElement) : targetElement;
|
|
190
240
|
}
|
|
241
|
+
|
|
242
|
+
// clone once, then apply the action mutably to the clone
|
|
243
|
+
const workingElement = opts.immutable ? cloneDeep(targetElement) : targetElement;
|
|
244
|
+
const mutatingOptions = {
|
|
245
|
+
...opts,
|
|
246
|
+
immutable: false
|
|
247
|
+
};
|
|
191
248
|
try {
|
|
192
249
|
// remove action
|
|
193
250
|
const removeValue = actionElement.removeField;
|
|
194
251
|
if (removeValue !== undefined && toValue(removeValue) === true) {
|
|
195
|
-
const result = applyRemoveAction(normalizedPaths,
|
|
252
|
+
const result = applyRemoveAction(normalizedPaths, workingElement, mutatingOptions);
|
|
196
253
|
tracer?.action({
|
|
197
254
|
target: targetExpression,
|
|
198
255
|
type: 'remove',
|
|
@@ -206,7 +263,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
206
263
|
if (actionElement.update !== undefined) {
|
|
207
264
|
const nodesValidation = validateTargetNodes(matches);
|
|
208
265
|
if (!nodesValidation.valid) throw nodesValidation.error;
|
|
209
|
-
const result = applyUpdateAction(normalizedPaths, actionElement.update,
|
|
266
|
+
const result = applyUpdateAction(normalizedPaths, actionElement.update, workingElement, mutatingOptions);
|
|
210
267
|
tracer?.action({
|
|
211
268
|
target: targetExpression,
|
|
212
269
|
type: 'update',
|
|
@@ -221,7 +278,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
221
278
|
const nodesValidation = validateTargetNodes(matches);
|
|
222
279
|
if (!nodesValidation.valid) throw nodesValidation.error;
|
|
223
280
|
const copyExpression = toValue(actionElement.copy);
|
|
224
|
-
const result = applyCopyAction(normalizedPaths, copyExpression,
|
|
281
|
+
const result = applyCopyAction(normalizedPaths, copyExpression, workingElement, mutatingOptions);
|
|
225
282
|
tracer?.action({
|
|
226
283
|
target: targetExpression,
|
|
227
284
|
type: 'copy',
|
|
@@ -236,7 +293,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
236
293
|
matchCount: matches.length,
|
|
237
294
|
normalizedPaths
|
|
238
295
|
});
|
|
239
|
-
return
|
|
296
|
+
return workingElement;
|
|
240
297
|
} catch (error) {
|
|
241
298
|
// enrich OverlayError with action context, preserving original stack
|
|
242
299
|
if (error instanceof OverlayError && !error.action) {
|
|
@@ -256,7 +313,22 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
|
|
|
256
313
|
|
|
257
314
|
/**
|
|
258
315
|
* Applies an entire overlay document to a target element.
|
|
259
|
-
*
|
|
316
|
+
*
|
|
317
|
+
* Immutable by default. The target is cloned once up front and all actions are
|
|
318
|
+
* applied to the clone sequentially, so the returned element is always
|
|
319
|
+
* independent of the input, including when no action changes anything. Pass
|
|
320
|
+
* `immutable: false` to apply the actions in place; the one shape that cannot
|
|
321
|
+
* be applied in place is a `target: $` action replacing a primitive root with
|
|
322
|
+
* a differently typed value, so the return value is authoritative in both
|
|
323
|
+
* modes.
|
|
324
|
+
*
|
|
325
|
+
* When the target is a `ParseResultElement`, a new parse result is returned
|
|
326
|
+
* carrying the new tree as its result element; the caller's parse result and
|
|
327
|
+
* its result element are left untouched. Every other child — annotations, and
|
|
328
|
+
* an `api` element that is not the result element itself — is shared with the
|
|
329
|
+
* input rather than copied, since no overlay action can reach it.
|
|
330
|
+
* Under `immutable: false` the caller's parse result is updated in place
|
|
331
|
+
* instead, with its result element replaced by the new tree.
|
|
260
332
|
*
|
|
261
333
|
* @public
|
|
262
334
|
*/
|
|
@@ -265,31 +337,73 @@ export const applyOverlay = (overlayElement, targetElement, options = defaultApp
|
|
|
265
337
|
if (!isOverlay1Element(overlay)) {
|
|
266
338
|
throw new OverlayError('First argument must be an Overlay 1.x document');
|
|
267
339
|
}
|
|
340
|
+
const opts = withDefaults(options);
|
|
341
|
+
let parseResult;
|
|
342
|
+
let resultElement;
|
|
268
343
|
let target;
|
|
269
344
|
if (targetElement instanceof ParseResultElement) {
|
|
270
345
|
if (targetElement.result === undefined) {
|
|
271
346
|
throw new OverlayError('Target ParseResultElement contains no result element');
|
|
272
347
|
}
|
|
273
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Only the result element changes, so the wrapper is cloned shallowly: the
|
|
350
|
+
* clone gets its own content list, which the new tree replaces the result
|
|
351
|
+
* element in, and every child the overlay never touches is shared with the
|
|
352
|
+
* input instead of copied.
|
|
353
|
+
*/
|
|
354
|
+
parseResult = opts.immutable ? cloneShallow(targetElement) : targetElement;
|
|
355
|
+
resultElement = parseResult.result;
|
|
356
|
+
target = resultElement;
|
|
274
357
|
} else {
|
|
275
358
|
target = targetElement;
|
|
276
359
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
360
|
+
target = opts.immutable ? cloneDeep(target) : target;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Constructed only once the target has been validated: the constructor writes
|
|
364
|
+
* a success verdict into the caller's trace object, which must not survive a
|
|
365
|
+
* throw from the checks above.
|
|
366
|
+
*/
|
|
367
|
+
const tracer = opts.trace ? new TraceBuilder(opts.trace) : undefined;
|
|
281
368
|
const internalOptions = tracer ? {
|
|
282
|
-
...
|
|
369
|
+
...opts,
|
|
283
370
|
tracer
|
|
284
|
-
} :
|
|
285
|
-
|
|
286
|
-
|
|
371
|
+
} : opts;
|
|
372
|
+
// clone once, then apply the actions mutably to the clone
|
|
373
|
+
const mutatingOptions = {
|
|
374
|
+
...internalOptions,
|
|
375
|
+
immutable: false
|
|
376
|
+
};
|
|
377
|
+
if (isArrayElement(overlay.actions)) {
|
|
378
|
+
for (const action of overlay.actions) {
|
|
379
|
+
target = applyAction(action, target, mutatingOptions);
|
|
380
|
+
}
|
|
287
381
|
}
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
382
|
+
if (parseResult !== undefined) {
|
|
383
|
+
/**
|
|
384
|
+
* The result marker is a class on the element itself, and replacing a
|
|
385
|
+
* primitive root swaps in the update value's meta wholesale, so an action
|
|
386
|
+
* can strip it. Restore it before the element goes back into the wrapper,
|
|
387
|
+
* otherwise the returned parse result has no result element at all.
|
|
388
|
+
*/
|
|
389
|
+
if (!includesClasses(target, ['result'])) {
|
|
390
|
+
target.classes.push('result');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* replaceResult locates the slot by scanning for the marker, so the element
|
|
395
|
+
* being replaced has to carry one too. An earlier action that stripped the
|
|
396
|
+
* marker off the element still sitting in the wrapper would otherwise leave
|
|
397
|
+
* that lookup with nothing to find, and the new tree would be dropped on
|
|
398
|
+
* the floor.
|
|
399
|
+
*/
|
|
400
|
+
if (target !== resultElement) {
|
|
401
|
+
if (!includesClasses(resultElement, ['result'])) {
|
|
402
|
+
resultElement.classes.push('result');
|
|
403
|
+
}
|
|
404
|
+
parseResult.replaceResult(target);
|
|
291
405
|
}
|
|
292
|
-
return
|
|
406
|
+
return parseResult;
|
|
293
407
|
}
|
|
294
408
|
return target;
|
|
295
409
|
};
|
|
@@ -11,15 +11,27 @@ var _apidom = require("./apidom.cjs");
|
|
|
11
11
|
/**
|
|
12
12
|
* Applies a single overlay action (POJO) to a target (POJO).
|
|
13
13
|
*
|
|
14
|
+
* The refracted target never escapes this function and the result is converted
|
|
15
|
+
* straight back to a POJO, so it is applied in place.
|
|
16
|
+
*
|
|
14
17
|
* @public
|
|
15
18
|
*/
|
|
16
|
-
const applyAction = (action, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyAction)((0, _apidomNsOverlay.refractAction)(action), (0, _apidomDatamodel.refract)(target),
|
|
19
|
+
const applyAction = (action, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyAction)((0, _apidomNsOverlay.refractAction)(action), (0, _apidomDatamodel.refract)(target), {
|
|
20
|
+
...options,
|
|
21
|
+
immutable: false
|
|
22
|
+
}));
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Applies an entire overlay document (POJO) to a target (POJO).
|
|
20
26
|
*
|
|
27
|
+
* The refracted target never escapes this function and the result is converted
|
|
28
|
+
* straight back to a POJO, so it is applied in place.
|
|
29
|
+
*
|
|
21
30
|
* @public
|
|
22
31
|
*/
|
|
23
32
|
exports.applyAction = applyAction;
|
|
24
|
-
const applyOverlay = (overlay, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyOverlay)((0, _apidomNsOverlay.refractOverlay1)(overlay), (0, _apidomDatamodel.refract)(target),
|
|
33
|
+
const applyOverlay = (overlay, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyOverlay)((0, _apidomNsOverlay.refractOverlay1)(overlay), (0, _apidomDatamodel.refract)(target), {
|
|
34
|
+
...options,
|
|
35
|
+
immutable: false
|
|
36
|
+
}));
|
|
25
37
|
exports.applyOverlay = applyOverlay;
|