@speclynx/apidom-overlay 4.3.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/src/apply.mjs ADDED
@@ -0,0 +1,232 @@
1
+ import { isObjectElement, isArrayElement, cloneDeep, ParseResultElement } from '@speclynx/apidom-datamodel';
2
+ import { deepmerge, toValue } from '@speclynx/apidom-core';
3
+ import { evaluate, NormalizedPath } from '@speclynx/apidom-json-path';
4
+ import { isOverlay1Element } from '@speclynx/apidom-ns-overlay-1';
5
+ import OverlayError from "./errors/OverlayError.mjs";
6
+ import { validateAction, validateTargetNodes } from "./validate.mjs";
7
+ /**
8
+ * @public
9
+ */
10
+ const defaultApplyOptions = {
11
+ immutable: true,
12
+ strict: false
13
+ };
14
+
15
+ /**
16
+ * Default customMerge that enforces Overlay spec type compatibility.
17
+ * Throws OverlayError on incompatible type combinations.
18
+ */
19
+ const overlayCustomMerge = (_keyElement, options) => {
20
+ return (target, source) => {
21
+ const targetIsObject = isObjectElement(target);
22
+ const sourceIsObject = isObjectElement(source);
23
+ const targetIsArray = isArrayElement(target);
24
+ const sourceIsArray = isArrayElement(source);
25
+ if (targetIsObject !== sourceIsObject || targetIsArray !== sourceIsArray) {
26
+ throw new OverlayError(`Incompatible types in overlay merge: cannot merge ` + `${targetIsObject ? 'object' : targetIsArray ? 'array' : 'primitive'} with ` + `${sourceIsObject ? 'object' : sourceIsArray ? 'array' : 'primitive'}`);
27
+ }
28
+ return deepmerge(target, source, options);
29
+ };
30
+ };
31
+
32
+ /**
33
+ * Merges a value into a target node following Overlay spec semantics.
34
+ */
35
+ const mergeValue = (targetElement, sourceElement, options = defaultApplyOptions) => {
36
+ const targetIsObject = isObjectElement(targetElement);
37
+ const targetIsArray = isArrayElement(targetElement);
38
+ const sourceIsObject = isObjectElement(sourceElement);
39
+ const sourceIsArray = isArrayElement(sourceElement);
40
+ const clone = options.immutable;
41
+ if (targetIsObject && sourceIsObject || targetIsArray && sourceIsArray) {
42
+ const mergeOpts = {
43
+ clone,
44
+ customMerge: overlayCustomMerge,
45
+ ...options.deepmerge
46
+ };
47
+ return deepmerge(targetElement, sourceElement, mergeOpts);
48
+ }
49
+ if (!targetIsObject && !targetIsArray && !sourceIsObject && !sourceIsArray) {
50
+ return clone ? cloneDeep(sourceElement) : sourceElement;
51
+ }
52
+ throw new OverlayError(`Incompatible types in overlay merge: cannot merge ` + `${targetIsObject ? 'object' : targetIsArray ? 'array' : 'primitive'} with ` + `${sourceIsObject ? 'object' : sourceIsArray ? 'array' : 'primitive'}`);
53
+ };
54
+
55
+ /**
56
+ * Resolves a normalized path to its parent element and child key.
57
+ */
58
+ const resolveParent = (normalizedPath, root) => {
59
+ const selectors = NormalizedPath.to(normalizedPath);
60
+ if (selectors.length === 0) return null; // root — no parent
61
+ const key = selectors[selectors.length - 1];
62
+ const parentPath = NormalizedPath.from(selectors.slice(0, -1));
63
+ const [parent] = evaluate(root, parentPath);
64
+ return {
65
+ parent,
66
+ key
67
+ };
68
+ };
69
+
70
+ /**
71
+ * Applies an update action to matched nodes.
72
+ * @public
73
+ */
74
+ export const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
75
+ let result = options.immutable ? cloneDeep(targetElement) : targetElement;
76
+ for (const normalizedPath of normalizedPaths) {
77
+ const resolved = resolveParent(normalizedPath, result);
78
+ if (resolved === null) {
79
+ // targeting root
80
+ result = mergeValue(result, updateValue, options);
81
+ continue;
82
+ }
83
+ const {
84
+ parent,
85
+ key
86
+ } = resolved;
87
+ if (isObjectElement(parent)) {
88
+ const current = parent.get(key);
89
+ if (current !== undefined) {
90
+ parent.set(key, mergeValue(current, updateValue, options));
91
+ }
92
+ } else if (isArrayElement(parent)) {
93
+ const current = parent.get(key);
94
+ if (current !== undefined) {
95
+ parent.content[key] = mergeValue(current, updateValue, options);
96
+ }
97
+ }
98
+ }
99
+ return result;
100
+ };
101
+
102
+ /**
103
+ * Applies a copy action to matched nodes.
104
+ * @public
105
+ */
106
+ export const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options = defaultApplyOptions) => {
107
+ const sourceMatches = evaluate(targetElement, copyExpression);
108
+ if (sourceMatches.length === 0) {
109
+ throw new OverlayError(`Copy source JSONPath "${copyExpression}" selected zero nodes`);
110
+ }
111
+ if (sourceMatches.length > 1) {
112
+ throw new OverlayError(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
113
+ }
114
+ const sourceValue = sourceMatches[0];
115
+ return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
116
+ };
117
+
118
+ /**
119
+ * Applies a remove action to matched nodes.
120
+ * @public
121
+ */
122
+ export const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
123
+ const result = options.immutable ? cloneDeep(targetElement) : targetElement;
124
+ // reverse document order so higher array indices are removed first (avoids index shifting)
125
+ const sorted = [...normalizedPaths].reverse();
126
+ for (const normalizedPath of sorted) {
127
+ const resolved = resolveParent(normalizedPath, result);
128
+ if (resolved === null) {
129
+ throw new OverlayError('Cannot remove the root node');
130
+ }
131
+ const {
132
+ parent,
133
+ key
134
+ } = resolved;
135
+ if (isObjectElement(parent)) {
136
+ parent.remove(key);
137
+ } else if (isArrayElement(parent)) {
138
+ parent.content.splice(key, 1);
139
+ }
140
+ }
141
+ return result;
142
+ };
143
+
144
+ /**
145
+ * Applies a single overlay action to a target element.
146
+ * Immutable — returns a new element with the action applied.
147
+ *
148
+ * @public
149
+ */
150
+ export const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
151
+ const actionValidation = validateAction(actionElement);
152
+ if (!actionValidation.valid) throw actionValidation.error;
153
+ const targetExpression = toValue(actionElement.target);
154
+ const matches = [];
155
+ const normalizedPaths = [];
156
+ evaluate(targetElement, targetExpression, {
157
+ callback: (value, normalizedPath) => {
158
+ matches.push(value);
159
+ normalizedPaths.push(normalizedPath);
160
+ }
161
+ });
162
+ if (matches.length === 0) {
163
+ if (options.strict) {
164
+ throw new OverlayError(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
165
+ action: actionElement
166
+ });
167
+ }
168
+ return targetElement;
169
+ }
170
+ try {
171
+ // remove action
172
+ const removeValue = actionElement.removeField;
173
+ if (removeValue !== undefined && toValue(removeValue) === true) {
174
+ return applyRemoveAction(normalizedPaths, targetElement, options);
175
+ }
176
+
177
+ // update action
178
+ if (actionElement.update !== undefined) {
179
+ const nodesValidation = validateTargetNodes(matches);
180
+ if (!nodesValidation.valid) throw nodesValidation.error;
181
+ return applyUpdateAction(normalizedPaths, actionElement.update, targetElement, options);
182
+ }
183
+
184
+ // copy action
185
+ if (actionElement.copy !== undefined) {
186
+ const nodesValidation = validateTargetNodes(matches);
187
+ if (!nodesValidation.valid) throw nodesValidation.error;
188
+ const copyExpression = toValue(actionElement.copy);
189
+ return applyCopyAction(normalizedPaths, copyExpression, targetElement, options);
190
+ }
191
+ return targetElement;
192
+ } catch (error) {
193
+ // enrich OverlayError with action context, preserving original stack
194
+ if (error instanceof OverlayError && !error.action) {
195
+ error.action = actionElement;
196
+ }
197
+ throw error;
198
+ }
199
+ };
200
+
201
+ /**
202
+ * Applies an entire overlay document to a target element.
203
+ * Immutable — returns a new element with all actions applied sequentially.
204
+ *
205
+ * @public
206
+ */
207
+ export const applyOverlayApiDOM = (overlayElement, targetElement, options = defaultApplyOptions) => {
208
+ const overlay = overlayElement instanceof ParseResultElement ? overlayElement.api : overlayElement;
209
+ if (!isOverlay1Element(overlay)) {
210
+ throw new OverlayError('First argument must be an Overlay 1.x document');
211
+ }
212
+ let target;
213
+ if (targetElement instanceof ParseResultElement) {
214
+ if (targetElement.api === undefined) {
215
+ throw new OverlayError('Target ParseResultElement contains no API element');
216
+ }
217
+ target = targetElement.api;
218
+ } else {
219
+ target = targetElement;
220
+ }
221
+ if (!isArrayElement(overlay.actions)) {
222
+ return targetElement;
223
+ }
224
+ for (const action of overlay.actions) {
225
+ target = applyAction(action, target, options);
226
+ }
227
+ if (targetElement instanceof ParseResultElement) {
228
+ targetElement.replaceResult(target);
229
+ return targetElement;
230
+ }
231
+ return target;
232
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _apidomError = require("@speclynx/apidom-error");
6
+ /**
7
+ * @public
8
+ */
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ class OverlayError extends _apidomError.ApiDOMStructuredError {
14
+ action;
15
+ member;
16
+ constructor(message, options) {
17
+ super(message, options);
18
+ this.action = options?.action;
19
+ this.member = options?.member;
20
+ }
21
+ }
22
+ var _default = exports.default = OverlayError;
@@ -0,0 +1,19 @@
1
+ import { ApiDOMStructuredError } from '@speclynx/apidom-error';
2
+
3
+ /**
4
+ * @public
5
+ */
6
+
7
+ /**
8
+ * @public
9
+ */
10
+ class OverlayError extends ApiDOMStructuredError {
11
+ action;
12
+ member;
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.action = options?.action;
16
+ this.member = options?.member;
17
+ }
18
+ }
19
+ export default OverlayError;
package/src/index.cjs ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
+ exports.__esModule = true;
5
+ exports.validateAction = exports.default = exports.applyOverlayApiDOM = exports.applyAction = exports.OverlayError = void 0;
6
+ var _apply = require("./apply.cjs");
7
+ exports.applyAction = _apply.applyAction;
8
+ exports.applyOverlayApiDOM = _apply.applyOverlayApiDOM;
9
+ var _validate = require("./validate.cjs");
10
+ exports.validateAction = _validate.validateAction;
11
+ var _OverlayError = _interopRequireDefault(require("./errors/OverlayError.cjs"));
12
+ exports.OverlayError = _OverlayError.default;
13
+ var _applyUri = _interopRequireDefault(require("./apply-uri.cjs"));
14
+ exports.default = _applyUri.default;
package/src/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ export { applyAction, applyOverlayApiDOM } from "./apply.mjs";
2
+ export { validateAction } from "./validate.mjs";
3
+ export { default as OverlayError } from "./errors/OverlayError.mjs";
4
+ export { default } from "./apply-uri.mjs";
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
+ exports.__esModule = true;
5
+ exports.validateTargetNodes = exports.validateAction = void 0;
6
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
7
+ var _apidomNsOverlay = require("@speclynx/apidom-ns-overlay-1");
8
+ var _OverlayError = _interopRequireDefault(require("./errors/OverlayError.cjs"));
9
+ /**
10
+ * @public
11
+ */
12
+
13
+ const valid = {
14
+ valid: true
15
+ };
16
+ const invalid = error => ({
17
+ valid: false,
18
+ error
19
+ });
20
+
21
+ /**
22
+ * Validates that an ActionElement has the correct shape per the Overlay spec.
23
+ *
24
+ * @public
25
+ */
26
+ const validateAction = actionElement => {
27
+ if (!(0, _apidomNsOverlay.isActionElement)(actionElement)) {
28
+ return invalid(new _OverlayError.default('Action must be an ActionElement', {
29
+ action: actionElement
30
+ }));
31
+ }
32
+ if (!(0, _apidomDatamodel.isStringElement)(actionElement.target)) {
33
+ return invalid(new _OverlayError.default('Action "target" field is required and must be a string', {
34
+ action: actionElement,
35
+ member: actionElement.getMember('target')
36
+ }));
37
+ }
38
+ if (actionElement.update !== undefined && actionElement.copy !== undefined) {
39
+ return invalid(new _OverlayError.default('Action must not have both "update" and "copy" fields', {
40
+ action: actionElement
41
+ }));
42
+ }
43
+ if (actionElement.copy !== undefined && !(0, _apidomDatamodel.isStringElement)(actionElement.copy)) {
44
+ return invalid(new _OverlayError.default('Action "copy" field must be a string (JSONPath expression)', {
45
+ action: actionElement,
46
+ member: actionElement.getMember('copy')
47
+ }));
48
+ }
49
+ if (actionElement.removeField !== undefined && !(0, _apidomDatamodel.isBooleanElement)(actionElement.removeField)) {
50
+ return invalid(new _OverlayError.default('Action "remove" field must be a boolean', {
51
+ action: actionElement,
52
+ member: actionElement.getMember('remove')
53
+ }));
54
+ }
55
+ return valid;
56
+ };
57
+ exports.validateAction = validateAction;
58
+ const categorizeNode = node => {
59
+ if ((0, _apidomDatamodel.isObjectElement)(node)) return 'object';
60
+ if ((0, _apidomDatamodel.isArrayElement)(node)) return 'array';
61
+ return 'primitive';
62
+ };
63
+
64
+ /**
65
+ * Validates that all matched target nodes are the same type
66
+ * when there are 2 or more matches (spec requirement for update/copy).
67
+ *
68
+ * @public
69
+ */
70
+ const validateTargetNodes = nodes => {
71
+ if (nodes.length < 2) return valid;
72
+ const firstCategory = categorizeNode(nodes[0]);
73
+ for (let i = 1; i < nodes.length; i++) {
74
+ const category = categorizeNode(nodes[i]);
75
+ if (category !== firstCategory) {
76
+ return invalid(new _OverlayError.default(`All target nodes must be the same type when selecting multiple nodes. ` + `Expected "${firstCategory}" but found "${category}" at index ${i}`));
77
+ }
78
+ }
79
+ return valid;
80
+ };
81
+ exports.validateTargetNodes = validateTargetNodes;
@@ -0,0 +1,73 @@
1
+ import { isStringElement, isBooleanElement, isObjectElement, isArrayElement } from '@speclynx/apidom-datamodel';
2
+ import { isActionElement } from '@speclynx/apidom-ns-overlay-1';
3
+ import OverlayError from "./errors/OverlayError.mjs";
4
+ /**
5
+ * @public
6
+ */
7
+ const valid = {
8
+ valid: true
9
+ };
10
+ const invalid = error => ({
11
+ valid: false,
12
+ error
13
+ });
14
+
15
+ /**
16
+ * Validates that an ActionElement has the correct shape per the Overlay spec.
17
+ *
18
+ * @public
19
+ */
20
+ export const validateAction = actionElement => {
21
+ if (!isActionElement(actionElement)) {
22
+ return invalid(new OverlayError('Action must be an ActionElement', {
23
+ action: actionElement
24
+ }));
25
+ }
26
+ if (!isStringElement(actionElement.target)) {
27
+ return invalid(new OverlayError('Action "target" field is required and must be a string', {
28
+ action: actionElement,
29
+ member: actionElement.getMember('target')
30
+ }));
31
+ }
32
+ if (actionElement.update !== undefined && actionElement.copy !== undefined) {
33
+ return invalid(new OverlayError('Action must not have both "update" and "copy" fields', {
34
+ action: actionElement
35
+ }));
36
+ }
37
+ if (actionElement.copy !== undefined && !isStringElement(actionElement.copy)) {
38
+ return invalid(new OverlayError('Action "copy" field must be a string (JSONPath expression)', {
39
+ action: actionElement,
40
+ member: actionElement.getMember('copy')
41
+ }));
42
+ }
43
+ if (actionElement.removeField !== undefined && !isBooleanElement(actionElement.removeField)) {
44
+ return invalid(new OverlayError('Action "remove" field must be a boolean', {
45
+ action: actionElement,
46
+ member: actionElement.getMember('remove')
47
+ }));
48
+ }
49
+ return valid;
50
+ };
51
+ const categorizeNode = node => {
52
+ if (isObjectElement(node)) return 'object';
53
+ if (isArrayElement(node)) return 'array';
54
+ return 'primitive';
55
+ };
56
+
57
+ /**
58
+ * Validates that all matched target nodes are the same type
59
+ * when there are 2 or more matches (spec requirement for update/copy).
60
+ *
61
+ * @public
62
+ */
63
+ export const validateTargetNodes = nodes => {
64
+ if (nodes.length < 2) return valid;
65
+ const firstCategory = categorizeNode(nodes[0]);
66
+ for (let i = 1; i < nodes.length; i++) {
67
+ const category = categorizeNode(nodes[i]);
68
+ if (category !== firstCategory) {
69
+ return invalid(new OverlayError(`All target nodes must be the same type when selecting multiple nodes. ` + `Expected "${firstCategory}" but found "${category}" at index ${i}`));
70
+ }
71
+ }
72
+ return valid;
73
+ };
@@ -0,0 +1,92 @@
1
+ import { ActionElement } from '@speclynx/apidom-ns-overlay-1';
2
+ import { ApiDOMReferenceOptions } from '@speclynx/apidom-reference';
3
+ import { ApiDOMStructuredError } from '@speclynx/apidom-error';
4
+ import { DeepMergeUserOptions } from '@speclynx/apidom-core';
5
+ import { Element as Element_2 } from '@speclynx/apidom-datamodel';
6
+ import { MemberElement } from '@speclynx/apidom-datamodel';
7
+ import { Overlay1Element } from '@speclynx/apidom-ns-overlay-1';
8
+ import { ParseResultElement } from '@speclynx/apidom-datamodel';
9
+
10
+ /**
11
+ * Applies a single overlay action to a target element.
12
+ * Immutable — returns a new element with the action applied.
13
+ *
14
+ * @public
15
+ */
16
+ export declare const applyAction: (actionElement: ActionElement, targetElement: Element_2, options?: ApplyOptions) => Element_2;
17
+
18
+ /**
19
+ * @public
20
+ */
21
+ export declare interface ApplyOptions {
22
+ readonly deepmerge?: DeepMergeUserOptions;
23
+ readonly strict?: boolean;
24
+ readonly immutable?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Applies an overlay document from a file/URL to its target document.
29
+ *
30
+ * Parses the overlay, resolves the target from `targetURI` or the overlay's
31
+ * `extends` field, parses the target with style preservation for round-trip fidelity,
32
+ * and applies all overlay actions.
33
+ *
34
+ * @param overlayURI - URI to the overlay document (file path or URL)
35
+ * @param targetURI - URI to the target document; if omitted, uses the overlay's `extends` field
36
+ * @param options - apidom-reference options + overlay merge options
37
+ * @returns ParseResultElement with the modified target document
38
+ *
39
+ * @public
40
+ */
41
+ declare const applyOverlay: (overlayURI: string, targetURI?: string, options?: ApplyOverlayOptions) => Promise<ParseResultElement>;
42
+ export default applyOverlay;
43
+
44
+ /**
45
+ * Applies an entire overlay document to a target element.
46
+ * Immutable — returns a new element with all actions applied sequentially.
47
+ *
48
+ * @public
49
+ */
50
+ export declare const applyOverlayApiDOM: <T extends Element_2 | ParseResultElement>(overlayElement: Overlay1Element | ParseResultElement, targetElement: T, options?: ApplyOptions) => T;
51
+
52
+ /**
53
+ * @public
54
+ */
55
+ export declare interface ApplyOverlayOptions extends Partial<ApiDOMReferenceOptions>, ApplyOptions {
56
+ }
57
+
58
+ /**
59
+ * @public
60
+ */
61
+ export declare class OverlayError extends ApiDOMStructuredError {
62
+ action?: ActionElement;
63
+ member?: MemberElement;
64
+ constructor(message?: string, options?: OverlayErrorOptions);
65
+ }
66
+
67
+ /**
68
+ * @public
69
+ */
70
+ export declare interface OverlayErrorOptions {
71
+ readonly cause?: Error;
72
+ readonly action?: ActionElement;
73
+ readonly member?: MemberElement;
74
+ readonly [key: string]: unknown;
75
+ }
76
+
77
+ /**
78
+ * Validates that an ActionElement has the correct shape per the Overlay spec.
79
+ *
80
+ * @public
81
+ */
82
+ export declare const validateAction: (actionElement: ActionElement) => ValidationResult;
83
+
84
+ /**
85
+ * @public
86
+ */
87
+ export declare interface ValidationResult {
88
+ readonly valid: boolean;
89
+ readonly error?: OverlayError;
90
+ }
91
+
92
+ export { }