@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/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@speclynx/apidom-overlay",
3
+ "version": "4.3.1",
4
+ "description": "Apply Overlay 1.x.y documents to API definitions.",
5
+ "keywords": [
6
+ "apidom",
7
+ "api",
8
+ "overlay",
9
+ "openapi-overlay",
10
+ "openapi",
11
+ "jsonpath",
12
+ "rfc9535",
13
+ "spec",
14
+ "specification",
15
+ "typescript",
16
+ "javascript"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org",
21
+ "provenance": false
22
+ },
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "unpkg": "./dist/apidom-overlay.browser.min.js",
26
+ "main": "./src/index.cjs",
27
+ "exports": {
28
+ "types": "./types/apidom-overlay.d.ts",
29
+ "import": "./src/index.mjs",
30
+ "require": "./src/index.cjs"
31
+ },
32
+ "types": "./types/apidom-overlay.d.ts",
33
+ "scripts": {
34
+ "build": "npm run clean && run-p --max-parallel ${CPU_CORES:-2} typescript:declaration build:es build:cjs build:umd:browser",
35
+ "build:es": "cross-env BABEL_ENV=es babel src --out-dir src --extensions '.ts' --out-file-extension '.mjs' --root-mode 'upward'",
36
+ "build:cjs": "cross-env BABEL_ENV=cjs babel src --out-dir src --extensions '.ts' --out-file-extension '.cjs' --root-mode 'upward'",
37
+ "build:umd:browser": "cross-env BABEL_ENV=browser webpack --config config/webpack/browser.config.js --progress",
38
+ "lint": "eslint ./",
39
+ "lint:fix": "eslint ./ --fix",
40
+ "clean": "rimraf --glob 'src/**/*.mjs' 'src/**/*.cjs' 'test/**/*.mjs' ./dist ./types",
41
+ "test": "npm run build:es && cross-env BABEL_ENV=es babel test --out-dir test --extensions '.ts' --out-file-extension '.mjs' --root-mode 'upward' && cross-env NODE_ENV=test mocha",
42
+ "test:update-snapshots": "cross-env UPDATE_SNAPSHOT=1 BABEL_ENV=cjs mocha",
43
+ "typescript:check-types": "tsc --noEmit",
44
+ "typescript:declaration": "tsc -p tsconfig.declaration.json && api-extractor run -l -c ./config/api-extractor/api-extractor.json",
45
+ "prepack": "copyfiles -u 3 ../../LICENSES/* LICENSES && copyfiles -u 2 ../../NOTICE .",
46
+ "postpack": "rimraf NOTICE LICENSES"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/speclynx/apidom.git"
51
+ },
52
+ "author": "Vladimir Gorej",
53
+ "license": "Apache-2.0",
54
+ "dependencies": {
55
+ "@babel/runtime-corejs3": "^7.28.4",
56
+ "@speclynx/apidom-core": "4.3.1",
57
+ "@speclynx/apidom-datamodel": "4.3.1",
58
+ "@speclynx/apidom-error": "4.3.1",
59
+ "@speclynx/apidom-json-path": "4.3.1",
60
+ "@speclynx/apidom-ns-overlay-1": "4.3.1",
61
+ "@speclynx/apidom-reference": "4.3.1"
62
+ },
63
+ "files": [
64
+ "src/**/*.mjs",
65
+ "src/**/*.cjs",
66
+ "dist/",
67
+ "types/apidom-overlay.d.ts",
68
+ "LICENSES",
69
+ "NOTICE",
70
+ "README.md",
71
+ "CHANGELOG.md"
72
+ ]
73
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
+ exports.__esModule = true;
5
+ exports.default = void 0;
6
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
7
+ var _apidomNsOverlay = require("@speclynx/apidom-ns-overlay-1");
8
+ var _apidomReference = require("@speclynx/apidom-reference");
9
+ var _apply = require("./apply.cjs");
10
+ var _OverlayError = _interopRequireDefault(require("./errors/OverlayError.cjs"));
11
+ /**
12
+ * @public
13
+ */
14
+
15
+ const defaultOptions = {
16
+ parse: {
17
+ parserOpts: {
18
+ strict: false,
19
+ style: true,
20
+ 'overlay-json-1': {
21
+ sourceMap: true,
22
+ style: false,
23
+ extends: true
24
+ },
25
+ 'overlay-yaml-1': {
26
+ sourceMap: true,
27
+ style: false,
28
+ extends: true
29
+ }
30
+ }
31
+ },
32
+ resolve: {
33
+ resolverOpts: {
34
+ fileAllowList: ['*']
35
+ }
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Applies an overlay document from a file/URL to its target document.
41
+ *
42
+ * Parses the overlay, resolves the target from `targetURI` or the overlay's
43
+ * `extends` field, parses the target with style preservation for round-trip fidelity,
44
+ * and applies all overlay actions.
45
+ *
46
+ * @param overlayURI - URI to the overlay document (file path or URL)
47
+ * @param targetURI - URI to the target document; if omitted, uses the overlay's `extends` field
48
+ * @param options - apidom-reference options + overlay merge options
49
+ * @returns ParseResultElement with the modified target document
50
+ *
51
+ * @public
52
+ */
53
+ const applyOverlay = async (overlayURI, targetURI, options = {
54
+ immutable: false,
55
+ strict: false
56
+ }) => {
57
+ const mergedOptions = (0, _apidomReference.mergeOptions)(defaultOptions, options);
58
+
59
+ // parse the overlay document along with document attached to `extends` field (if present)
60
+ const overlayParseResult = await (0, _apidomReference.parse)(overlayURI, mergedOptions);
61
+ const overlayElement = overlayParseResult.api;
62
+ if (!(0, _apidomNsOverlay.isOverlay1Element)(overlayElement)) {
63
+ throw new _OverlayError.default(`Failed to parse overlay document "${overlayURI}"`);
64
+ }
65
+ let targetParseResult;
66
+ if (targetURI) {
67
+ const targetOptions = (0, _apidomReference.mergeOptions)(defaultOptions, options);
68
+ targetParseResult = await (0, _apidomReference.parse)(targetURI, targetOptions);
69
+ } else {
70
+ const extendsElement = overlayElement.get('extends');
71
+ targetParseResult = extendsElement?.meta?.get('parseResult');
72
+ }
73
+ if (!(0, _apidomDatamodel.isParseResultElement)(targetParseResult)) {
74
+ throw new _OverlayError.default('No target specified: provide targetURI or set the "extends" field in the overlay document');
75
+ }
76
+
77
+ // apply overlay actions to the target (returns targetParseResult with result replaced)
78
+ return (0, _apply.applyOverlayApiDOM)(overlayParseResult, targetParseResult, options);
79
+ };
80
+ var _default = exports.default = applyOverlay;
@@ -0,0 +1,74 @@
1
+ import { isParseResultElement } from '@speclynx/apidom-datamodel';
2
+ import { isOverlay1Element } from '@speclynx/apidom-ns-overlay-1';
3
+ import { parse, mergeOptions } from '@speclynx/apidom-reference';
4
+ import { applyOverlayApiDOM } from "./apply.mjs";
5
+ import OverlayError from "./errors/OverlayError.mjs";
6
+ /**
7
+ * @public
8
+ */
9
+ const defaultOptions = {
10
+ parse: {
11
+ parserOpts: {
12
+ strict: false,
13
+ style: true,
14
+ 'overlay-json-1': {
15
+ sourceMap: true,
16
+ style: false,
17
+ extends: true
18
+ },
19
+ 'overlay-yaml-1': {
20
+ sourceMap: true,
21
+ style: false,
22
+ extends: true
23
+ }
24
+ }
25
+ },
26
+ resolve: {
27
+ resolverOpts: {
28
+ fileAllowList: ['*']
29
+ }
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Applies an overlay document from a file/URL to its target document.
35
+ *
36
+ * Parses the overlay, resolves the target from `targetURI` or the overlay's
37
+ * `extends` field, parses the target with style preservation for round-trip fidelity,
38
+ * and applies all overlay actions.
39
+ *
40
+ * @param overlayURI - URI to the overlay document (file path or URL)
41
+ * @param targetURI - URI to the target document; if omitted, uses the overlay's `extends` field
42
+ * @param options - apidom-reference options + overlay merge options
43
+ * @returns ParseResultElement with the modified target document
44
+ *
45
+ * @public
46
+ */
47
+ const applyOverlay = async (overlayURI, targetURI, options = {
48
+ immutable: false,
49
+ strict: false
50
+ }) => {
51
+ const mergedOptions = mergeOptions(defaultOptions, options);
52
+
53
+ // parse the overlay document along with document attached to `extends` field (if present)
54
+ const overlayParseResult = await parse(overlayURI, mergedOptions);
55
+ const overlayElement = overlayParseResult.api;
56
+ if (!isOverlay1Element(overlayElement)) {
57
+ throw new OverlayError(`Failed to parse overlay document "${overlayURI}"`);
58
+ }
59
+ let targetParseResult;
60
+ if (targetURI) {
61
+ const targetOptions = mergeOptions(defaultOptions, options);
62
+ targetParseResult = await parse(targetURI, targetOptions);
63
+ } else {
64
+ const extendsElement = overlayElement.get('extends');
65
+ targetParseResult = extendsElement?.meta?.get('parseResult');
66
+ }
67
+ if (!isParseResultElement(targetParseResult)) {
68
+ throw new OverlayError('No target specified: provide targetURI or set the "extends" field in the overlay document');
69
+ }
70
+
71
+ // apply overlay actions to the target (returns targetParseResult with result replaced)
72
+ return applyOverlayApiDOM(overlayParseResult, targetParseResult, options);
73
+ };
74
+ export default applyOverlay;
package/src/apply.cjs ADDED
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
+ exports.__esModule = true;
5
+ exports.applyUpdateAction = exports.applyRemoveAction = exports.applyOverlayApiDOM = exports.applyCopyAction = exports.applyAction = void 0;
6
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
7
+ var _apidomCore = require("@speclynx/apidom-core");
8
+ var _apidomJsonPath = require("@speclynx/apidom-json-path");
9
+ var _apidomNsOverlay = require("@speclynx/apidom-ns-overlay-1");
10
+ var _OverlayError = _interopRequireDefault(require("./errors/OverlayError.cjs"));
11
+ var _validate = require("./validate.cjs");
12
+ /**
13
+ * @public
14
+ */
15
+
16
+ const defaultApplyOptions = {
17
+ immutable: true,
18
+ strict: false
19
+ };
20
+
21
+ /**
22
+ * Default customMerge that enforces Overlay spec type compatibility.
23
+ * Throws OverlayError on incompatible type combinations.
24
+ */
25
+ const overlayCustomMerge = (_keyElement, options) => {
26
+ return (target, source) => {
27
+ const targetIsObject = (0, _apidomDatamodel.isObjectElement)(target);
28
+ const sourceIsObject = (0, _apidomDatamodel.isObjectElement)(source);
29
+ const targetIsArray = (0, _apidomDatamodel.isArrayElement)(target);
30
+ const sourceIsArray = (0, _apidomDatamodel.isArrayElement)(source);
31
+ if (targetIsObject !== sourceIsObject || targetIsArray !== sourceIsArray) {
32
+ throw new _OverlayError.default(`Incompatible types in overlay merge: cannot merge ` + `${targetIsObject ? 'object' : targetIsArray ? 'array' : 'primitive'} with ` + `${sourceIsObject ? 'object' : sourceIsArray ? 'array' : 'primitive'}`);
33
+ }
34
+ return (0, _apidomCore.deepmerge)(target, source, options);
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Merges a value into a target node following Overlay spec semantics.
40
+ */
41
+ const mergeValue = (targetElement, sourceElement, options = defaultApplyOptions) => {
42
+ const targetIsObject = (0, _apidomDatamodel.isObjectElement)(targetElement);
43
+ const targetIsArray = (0, _apidomDatamodel.isArrayElement)(targetElement);
44
+ const sourceIsObject = (0, _apidomDatamodel.isObjectElement)(sourceElement);
45
+ const sourceIsArray = (0, _apidomDatamodel.isArrayElement)(sourceElement);
46
+ const clone = options.immutable;
47
+ if (targetIsObject && sourceIsObject || targetIsArray && sourceIsArray) {
48
+ const mergeOpts = {
49
+ clone,
50
+ customMerge: overlayCustomMerge,
51
+ ...options.deepmerge
52
+ };
53
+ return (0, _apidomCore.deepmerge)(targetElement, sourceElement, mergeOpts);
54
+ }
55
+ if (!targetIsObject && !targetIsArray && !sourceIsObject && !sourceIsArray) {
56
+ return clone ? (0, _apidomDatamodel.cloneDeep)(sourceElement) : sourceElement;
57
+ }
58
+ throw new _OverlayError.default(`Incompatible types in overlay merge: cannot merge ` + `${targetIsObject ? 'object' : targetIsArray ? 'array' : 'primitive'} with ` + `${sourceIsObject ? 'object' : sourceIsArray ? 'array' : 'primitive'}`);
59
+ };
60
+
61
+ /**
62
+ * Resolves a normalized path to its parent element and child key.
63
+ */
64
+ const resolveParent = (normalizedPath, root) => {
65
+ const selectors = _apidomJsonPath.NormalizedPath.to(normalizedPath);
66
+ if (selectors.length === 0) return null; // root — no parent
67
+ const key = selectors[selectors.length - 1];
68
+ const parentPath = _apidomJsonPath.NormalizedPath.from(selectors.slice(0, -1));
69
+ const [parent] = (0, _apidomJsonPath.evaluate)(root, parentPath);
70
+ return {
71
+ parent,
72
+ key
73
+ };
74
+ };
75
+
76
+ /**
77
+ * Applies an update action to matched nodes.
78
+ * @public
79
+ */
80
+ const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
81
+ let result = options.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
82
+ for (const normalizedPath of normalizedPaths) {
83
+ const resolved = resolveParent(normalizedPath, result);
84
+ if (resolved === null) {
85
+ // targeting root
86
+ result = mergeValue(result, updateValue, options);
87
+ continue;
88
+ }
89
+ const {
90
+ parent,
91
+ key
92
+ } = resolved;
93
+ if ((0, _apidomDatamodel.isObjectElement)(parent)) {
94
+ const current = parent.get(key);
95
+ if (current !== undefined) {
96
+ parent.set(key, mergeValue(current, updateValue, options));
97
+ }
98
+ } else if ((0, _apidomDatamodel.isArrayElement)(parent)) {
99
+ const current = parent.get(key);
100
+ if (current !== undefined) {
101
+ parent.content[key] = mergeValue(current, updateValue, options);
102
+ }
103
+ }
104
+ }
105
+ return result;
106
+ };
107
+
108
+ /**
109
+ * Applies a copy action to matched nodes.
110
+ * @public
111
+ */
112
+ exports.applyUpdateAction = applyUpdateAction;
113
+ const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options = defaultApplyOptions) => {
114
+ const sourceMatches = (0, _apidomJsonPath.evaluate)(targetElement, copyExpression);
115
+ if (sourceMatches.length === 0) {
116
+ throw new _OverlayError.default(`Copy source JSONPath "${copyExpression}" selected zero nodes`);
117
+ }
118
+ if (sourceMatches.length > 1) {
119
+ throw new _OverlayError.default(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
120
+ }
121
+ const sourceValue = sourceMatches[0];
122
+ return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
123
+ };
124
+
125
+ /**
126
+ * Applies a remove action to matched nodes.
127
+ * @public
128
+ */
129
+ exports.applyCopyAction = applyCopyAction;
130
+ const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
131
+ const result = options.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
132
+ // reverse document order so higher array indices are removed first (avoids index shifting)
133
+ const sorted = [...normalizedPaths].reverse();
134
+ for (const normalizedPath of sorted) {
135
+ const resolved = resolveParent(normalizedPath, result);
136
+ if (resolved === null) {
137
+ throw new _OverlayError.default('Cannot remove the root node');
138
+ }
139
+ const {
140
+ parent,
141
+ key
142
+ } = resolved;
143
+ if ((0, _apidomDatamodel.isObjectElement)(parent)) {
144
+ parent.remove(key);
145
+ } else if ((0, _apidomDatamodel.isArrayElement)(parent)) {
146
+ parent.content.splice(key, 1);
147
+ }
148
+ }
149
+ return result;
150
+ };
151
+
152
+ /**
153
+ * Applies a single overlay action to a target element.
154
+ * Immutable — returns a new element with the action applied.
155
+ *
156
+ * @public
157
+ */
158
+ exports.applyRemoveAction = applyRemoveAction;
159
+ const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
160
+ const actionValidation = (0, _validate.validateAction)(actionElement);
161
+ if (!actionValidation.valid) throw actionValidation.error;
162
+ const targetExpression = (0, _apidomCore.toValue)(actionElement.target);
163
+ const matches = [];
164
+ const normalizedPaths = [];
165
+ (0, _apidomJsonPath.evaluate)(targetElement, targetExpression, {
166
+ callback: (value, normalizedPath) => {
167
+ matches.push(value);
168
+ normalizedPaths.push(normalizedPath);
169
+ }
170
+ });
171
+ if (matches.length === 0) {
172
+ if (options.strict) {
173
+ throw new _OverlayError.default(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
174
+ action: actionElement
175
+ });
176
+ }
177
+ return targetElement;
178
+ }
179
+ try {
180
+ // remove action
181
+ const removeValue = actionElement.removeField;
182
+ if (removeValue !== undefined && (0, _apidomCore.toValue)(removeValue) === true) {
183
+ return applyRemoveAction(normalizedPaths, targetElement, options);
184
+ }
185
+
186
+ // update action
187
+ if (actionElement.update !== undefined) {
188
+ const nodesValidation = (0, _validate.validateTargetNodes)(matches);
189
+ if (!nodesValidation.valid) throw nodesValidation.error;
190
+ return applyUpdateAction(normalizedPaths, actionElement.update, targetElement, options);
191
+ }
192
+
193
+ // copy action
194
+ if (actionElement.copy !== undefined) {
195
+ const nodesValidation = (0, _validate.validateTargetNodes)(matches);
196
+ if (!nodesValidation.valid) throw nodesValidation.error;
197
+ const copyExpression = (0, _apidomCore.toValue)(actionElement.copy);
198
+ return applyCopyAction(normalizedPaths, copyExpression, targetElement, options);
199
+ }
200
+ return targetElement;
201
+ } catch (error) {
202
+ // enrich OverlayError with action context, preserving original stack
203
+ if (error instanceof _OverlayError.default && !error.action) {
204
+ error.action = actionElement;
205
+ }
206
+ throw error;
207
+ }
208
+ };
209
+
210
+ /**
211
+ * Applies an entire overlay document to a target element.
212
+ * Immutable — returns a new element with all actions applied sequentially.
213
+ *
214
+ * @public
215
+ */
216
+ exports.applyAction = applyAction;
217
+ const applyOverlayApiDOM = (overlayElement, targetElement, options = defaultApplyOptions) => {
218
+ const overlay = overlayElement instanceof _apidomDatamodel.ParseResultElement ? overlayElement.api : overlayElement;
219
+ if (!(0, _apidomNsOverlay.isOverlay1Element)(overlay)) {
220
+ throw new _OverlayError.default('First argument must be an Overlay 1.x document');
221
+ }
222
+ let target;
223
+ if (targetElement instanceof _apidomDatamodel.ParseResultElement) {
224
+ if (targetElement.api === undefined) {
225
+ throw new _OverlayError.default('Target ParseResultElement contains no API element');
226
+ }
227
+ target = targetElement.api;
228
+ } else {
229
+ target = targetElement;
230
+ }
231
+ if (!(0, _apidomDatamodel.isArrayElement)(overlay.actions)) {
232
+ return targetElement;
233
+ }
234
+ for (const action of overlay.actions) {
235
+ target = applyAction(action, target, options);
236
+ }
237
+ if (targetElement instanceof _apidomDatamodel.ParseResultElement) {
238
+ targetElement.replaceResult(target);
239
+ return targetElement;
240
+ }
241
+ return target;
242
+ };
243
+ exports.applyOverlayApiDOM = applyOverlayApiDOM;