@jentic/arazzo-resolver 1.0.0-alpha.32 → 1.0.0-alpha.33

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentic/arazzo-resolver",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.33",
4
4
  "description": "Resolver for Arazzo Documents.",
5
5
  "keywords": [
6
6
  "arazzo",
@@ -49,7 +49,7 @@
49
49
  "license": "Apache-2.0",
50
50
  "dependencies": {
51
51
  "@babel/runtime-corejs3": "^8.0.0",
52
- "@jentic/arazzo-parser": "1.0.0-alpha.32",
52
+ "@jentic/arazzo-parser": "1.0.0-alpha.33",
53
53
  "@speclynx/apidom-datamodel": "^5.0.1",
54
54
  "@speclynx/apidom-error": "^5.0.1",
55
55
  "@speclynx/apidom-ns-arazzo-1": "^5.0.1",
@@ -72,5 +72,5 @@
72
72
  "README.md",
73
73
  "CHANGELOG.md"
74
74
  ],
75
- "gitHead": "a5ea37ac6a1bd3b56df95cdb308a9b62a4cc375b"
75
+ "gitHead": "a17270442ac27615af0348fad9c71817d818151f"
76
76
  }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.dereferenceSourceDescriptionPathItems = dereferenceSourceDescriptionPathItems;
7
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
8
+ var _empty = require("@speclynx/apidom-reference/configuration/empty");
9
+ var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
10
+ var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
11
+ var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
12
+ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
13
+ var _openapi = require("./openapi.cjs");
14
+ const isPathItem = element => (0, _apidomNsOpenapi.isPathItemElement)(element) || (0, _apidomNsOpenapi2.isPathItemElement)(element);
15
+ const MAX_REF_CHAIN = 20;
16
+
17
+ // evaluates the JSON Pointer of a URI fragment against an element tree
18
+ const evaluateFragment = (root, fragment) => {
19
+ const tokens = decodeURIComponent(fragment.replace(/^#/, '')).split('/').slice(1).map(token => token.replace(/~1/g, '/').replace(/~0/g, '~'));
20
+ let current = root;
21
+ for (const token of tokens) {
22
+ if ((0, _apidomDatamodel.isObjectElement)(current)) current = current.get(token);else if ((0, _apidomDatamodel.isArrayElement)(current)) current = current.get(Number(token));else return undefined;
23
+ }
24
+ return current;
25
+ };
26
+ const refractorFor = api => {
27
+ if ((0, _apidomNsOpenapi.isSwaggerElement)(api)) return _apidomNsOpenapi.refractPathItem;
28
+ if ((0, _apidomNsOpenapi2.isOpenApi3_0Element)(api)) return _apidomNsOpenapi2.refractPathItem;
29
+ if ((0, _apidomNsOpenapi3.isOpenApi3_1Element)(api)) return _apidomNsOpenapi3.refractPathItem;
30
+ return undefined;
31
+ };
32
+
33
+ /**
34
+ * Replaces the Path Items that consist of a `$ref` in the `openapi` source descriptions parsed
35
+ * along with an Arazzo Document (see the `sourceDescriptions` parser option) with the Path Items
36
+ * they point to, whether local (`#/components/pathItems/...`) or external.
37
+ *
38
+ * Only the chain of Path Item `$ref`s is followed. Nothing inside the referenced Path Items
39
+ * (responses, schemas, ...) is dereferenced, so an unrelated nested reference that can't be
40
+ * resolved never hides the operations. The source descriptions are modified in place, which makes
41
+ * the operations behind such Path Items discoverable, e.g. by `findStepOperation` of
42
+ * \@jentic/arazzo-parser.
43
+ *
44
+ * This is best effort and never rejects: a Path Item that can't be resolved is left as is.
45
+ *
46
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
47
+ * @param options - Reference options (uses defaultOptions when not provided)
48
+ * @returns A promise that resolves once all resolvable Path Items have been replaced
49
+ * @public
50
+ */
51
+ async function dereferenceSourceDescriptionPathItems(parseResult, options = {}) {
52
+ if (!(0, _apidomNsArazzo.isArazzoSpecification1Element)(parseResult.api)) return;
53
+ const mergedOptions = (0, _empty.mergeOptions)(_openapi.defaultOptions, options);
54
+ // external documents are fetched once, however many Path Items point into them
55
+ const documents = new Map();
56
+ const fetchDocument = uri => {
57
+ if (!documents.has(uri)) documents.set(uri, (0, _empty.parse)(uri, mergedOptions));
58
+ return documents.get(uri);
59
+ };
60
+ const pending = [];
61
+ parseResult.api.sourceDescriptions?.forEach(sourceDescription => {
62
+ if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sourceDescription)) return;
63
+ const sourceParseResult = sourceDescription.meta.get('parseResult');
64
+ const api = sourceParseResult?.api;
65
+ const sourceURI = sourceParseResult?.meta.get('retrievalURI');
66
+ if (!(0, _apidomNsOpenapi.isSwaggerElement)(api) && !(0, _apidomNsOpenapi2.isOpenApi3_0Element)(api) && !(0, _apidomNsOpenapi3.isOpenApi3_1Element)(api)) return;
67
+ const refract = refractorFor(api);
68
+ if (!refract || !sourceURI) return;
69
+
70
+ // follows the `$ref` of a Path Item to the Path Item it points to
71
+ const follow = async (pathItem, baseURI, root, visited) => {
72
+ if (!(0, _apidomDatamodel.isStringElement)(pathItem.$ref)) return pathItem;
73
+ const ref = pathItem.$ref.toValue();
74
+ const documentURI = _empty.url.resolve(baseURI, _empty.url.stripHash(ref));
75
+ const fragment = _empty.url.getHash(ref);
76
+ const key = `${documentURI}${fragment}`;
77
+ if (visited.has(key) || visited.size >= MAX_REF_CHAIN) return undefined;
78
+ visited.add(key);
79
+ let targetRoot = root;
80
+ if (documentURI !== _empty.url.stripHash(baseURI)) {
81
+ const document = await fetchDocument(documentURI);
82
+ targetRoot = (0, _apidomDatamodel.isObjectElement)(document.api) ? document.api : document.result;
83
+ }
84
+ const evaluated = evaluateFragment(targetRoot, fragment);
85
+ const target = isPathItem(evaluated) ? evaluated : refract(evaluated);
86
+ if (!isPathItem(target)) return undefined;
87
+
88
+ // fields next to the `$ref` override the ones of the referenced Path Item
89
+ const merged = (0, _apidomDatamodel.cloneShallow)(target);
90
+ if ((0, _apidomDatamodel.isObjectElement)(pathItem)) {
91
+ pathItem.forEach((value, member) => {
92
+ const name = String(member.toValue());
93
+ if (name !== '$ref') merged.set(name, value);
94
+ });
95
+ }
96
+ return follow(merged, documentURI, targetRoot, visited);
97
+ };
98
+
99
+ // Path Items in `paths` and in the reusable `components.pathItems` are both resolved, since
100
+ // steps can target either
101
+ const collections = [api.paths, api.components?.pathItems];
102
+ collections.forEach(collection => {
103
+ if (!(0, _apidomDatamodel.isObjectElement)(collection)) return;
104
+ collection.forEach((pathItem, member) => {
105
+ // in `components.pathItems` a `$ref`-only entry is refracted to a Reference, not a Path Item
106
+ const candidate = pathItem;
107
+ if (!(0, _apidomDatamodel.isObjectElement)(pathItem) || !(0, _apidomDatamodel.isStringElement)(candidate.$ref)) return;
108
+ pending.push(follow(candidate, sourceURI, api, new Set()).then(resolved => {
109
+ if (resolved) collection.set(String(member.toValue()), resolved);
110
+ }, () => {
111
+ // keep the unresolved Path Item
112
+ }));
113
+ });
114
+ });
115
+ });
116
+ await Promise.all(pending);
117
+ }
@@ -0,0 +1,111 @@
1
+ import { cloneShallow, isArrayElement, isObjectElement, isStringElement } from '@speclynx/apidom-datamodel';
2
+ import { mergeOptions, parse, url } from '@speclynx/apidom-reference/configuration/empty';
3
+ import { isArazzoSpecification1Element, isSourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
4
+ import { isSwaggerElement, isPathItemElement as isPathItem2Element, refractPathItem as refractPathItem2 } from '@speclynx/apidom-ns-openapi-2';
5
+ import { isOpenApi3_0Element, isPathItemElement as isPathItem3Element, refractPathItem as refractPathItem3_0 } from '@speclynx/apidom-ns-openapi-3-0';
6
+ import { isOpenApi3_1Element, refractPathItem as refractPathItem3_1 } from '@speclynx/apidom-ns-openapi-3-1';
7
+ import { defaultOptions as defaultOpenAPIOptions } from "./openapi.mjs";
8
+ const isPathItem = element => isPathItem2Element(element) || isPathItem3Element(element);
9
+ const MAX_REF_CHAIN = 20;
10
+
11
+ // evaluates the JSON Pointer of a URI fragment against an element tree
12
+ const evaluateFragment = (root, fragment) => {
13
+ const tokens = decodeURIComponent(fragment.replace(/^#/, '')).split('/').slice(1).map(token => token.replace(/~1/g, '/').replace(/~0/g, '~'));
14
+ let current = root;
15
+ for (const token of tokens) {
16
+ if (isObjectElement(current)) current = current.get(token);else if (isArrayElement(current)) current = current.get(Number(token));else return undefined;
17
+ }
18
+ return current;
19
+ };
20
+ const refractorFor = api => {
21
+ if (isSwaggerElement(api)) return refractPathItem2;
22
+ if (isOpenApi3_0Element(api)) return refractPathItem3_0;
23
+ if (isOpenApi3_1Element(api)) return refractPathItem3_1;
24
+ return undefined;
25
+ };
26
+
27
+ /**
28
+ * Replaces the Path Items that consist of a `$ref` in the `openapi` source descriptions parsed
29
+ * along with an Arazzo Document (see the `sourceDescriptions` parser option) with the Path Items
30
+ * they point to, whether local (`#/components/pathItems/...`) or external.
31
+ *
32
+ * Only the chain of Path Item `$ref`s is followed. Nothing inside the referenced Path Items
33
+ * (responses, schemas, ...) is dereferenced, so an unrelated nested reference that can't be
34
+ * resolved never hides the operations. The source descriptions are modified in place, which makes
35
+ * the operations behind such Path Items discoverable, e.g. by `findStepOperation` of
36
+ * \@jentic/arazzo-parser.
37
+ *
38
+ * This is best effort and never rejects: a Path Item that can't be resolved is left as is.
39
+ *
40
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
41
+ * @param options - Reference options (uses defaultOptions when not provided)
42
+ * @returns A promise that resolves once all resolvable Path Items have been replaced
43
+ * @public
44
+ */
45
+ export async function dereferenceSourceDescriptionPathItems(parseResult, options = {}) {
46
+ if (!isArazzoSpecification1Element(parseResult.api)) return;
47
+ const mergedOptions = mergeOptions(defaultOpenAPIOptions, options);
48
+ // external documents are fetched once, however many Path Items point into them
49
+ const documents = new Map();
50
+ const fetchDocument = uri => {
51
+ if (!documents.has(uri)) documents.set(uri, parse(uri, mergedOptions));
52
+ return documents.get(uri);
53
+ };
54
+ const pending = [];
55
+ parseResult.api.sourceDescriptions?.forEach(sourceDescription => {
56
+ if (!isSourceDescriptionElement(sourceDescription)) return;
57
+ const sourceParseResult = sourceDescription.meta.get('parseResult');
58
+ const api = sourceParseResult?.api;
59
+ const sourceURI = sourceParseResult?.meta.get('retrievalURI');
60
+ if (!isSwaggerElement(api) && !isOpenApi3_0Element(api) && !isOpenApi3_1Element(api)) return;
61
+ const refract = refractorFor(api);
62
+ if (!refract || !sourceURI) return;
63
+
64
+ // follows the `$ref` of a Path Item to the Path Item it points to
65
+ const follow = async (pathItem, baseURI, root, visited) => {
66
+ if (!isStringElement(pathItem.$ref)) return pathItem;
67
+ const ref = pathItem.$ref.toValue();
68
+ const documentURI = url.resolve(baseURI, url.stripHash(ref));
69
+ const fragment = url.getHash(ref);
70
+ const key = `${documentURI}${fragment}`;
71
+ if (visited.has(key) || visited.size >= MAX_REF_CHAIN) return undefined;
72
+ visited.add(key);
73
+ let targetRoot = root;
74
+ if (documentURI !== url.stripHash(baseURI)) {
75
+ const document = await fetchDocument(documentURI);
76
+ targetRoot = isObjectElement(document.api) ? document.api : document.result;
77
+ }
78
+ const evaluated = evaluateFragment(targetRoot, fragment);
79
+ const target = isPathItem(evaluated) ? evaluated : refract(evaluated);
80
+ if (!isPathItem(target)) return undefined;
81
+
82
+ // fields next to the `$ref` override the ones of the referenced Path Item
83
+ const merged = cloneShallow(target);
84
+ if (isObjectElement(pathItem)) {
85
+ pathItem.forEach((value, member) => {
86
+ const name = String(member.toValue());
87
+ if (name !== '$ref') merged.set(name, value);
88
+ });
89
+ }
90
+ return follow(merged, documentURI, targetRoot, visited);
91
+ };
92
+
93
+ // Path Items in `paths` and in the reusable `components.pathItems` are both resolved, since
94
+ // steps can target either
95
+ const collections = [api.paths, api.components?.pathItems];
96
+ collections.forEach(collection => {
97
+ if (!isObjectElement(collection)) return;
98
+ collection.forEach((pathItem, member) => {
99
+ // in `components.pathItems` a `$ref`-only entry is refracted to a Reference, not a Path Item
100
+ const candidate = pathItem;
101
+ if (!isObjectElement(pathItem) || !isStringElement(candidate.$ref)) return;
102
+ pending.push(follow(candidate, sourceURI, api, new Set()).then(resolved => {
103
+ if (resolved) collection.set(String(member.toValue()), resolved);
104
+ }, () => {
105
+ // keep the unresolved Path Item
106
+ }));
107
+ });
108
+ });
109
+ });
110
+ await Promise.all(pending);
111
+ }
package/src/index.cjs CHANGED
@@ -46,6 +46,13 @@ Object.defineProperty(exports, "dereferenceOpenAPIElement", {
46
46
  return _openapi.dereferenceElement;
47
47
  }
48
48
  });
49
+ Object.defineProperty(exports, "dereferenceSourceDescriptionPathItems", {
50
+ enumerable: true,
51
+ get: function () {
52
+ return _pathItems.dereferenceSourceDescriptionPathItems;
53
+ }
54
+ });
49
55
  var _arazzo = require("./dereference/arazzo.cjs");
50
56
  var _openapi = require("./dereference/openapi.cjs");
57
+ var _pathItems = require("./dereference/path-items.cjs");
51
58
  var _DereferenceError = _interopRequireDefault(require("./errors/DereferenceError.cjs"));
package/src/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
1
  export { defaultOptions as defaultDereferenceArazzoOptions, dereference as dereferenceArazzo, dereferenceElement as dereferenceArazzoElement } from "./dereference/arazzo.mjs";
2
2
  export { defaultOptions as defaultDereferenceOpenAPIOptions, dereference as dereferenceOpenAPI, dereferenceElement as dereferenceOpenAPIElement } from "./dereference/openapi.mjs";
3
+ export { dereferenceSourceDescriptionPathItems } from "./dereference/path-items.mjs";
3
4
  export { default as DereferenceError } from "./errors/DereferenceError.mjs";
@@ -216,4 +216,24 @@ export declare function dereferenceOpenAPIElement<T extends Element_2>(element:
216
216
  */
217
217
  export declare type DereferenceOpenAPIOptions = PartialDeep<ApiDOMReferenceOptions>;
218
218
 
219
+ /**
220
+ * Replaces the Path Items that consist of a `$ref` in the `openapi` source descriptions parsed
221
+ * along with an Arazzo Document (see the `sourceDescriptions` parser option) with the Path Items
222
+ * they point to, whether local (`#/components/pathItems/...`) or external.
223
+ *
224
+ * Only the chain of Path Item `$ref`s is followed. Nothing inside the referenced Path Items
225
+ * (responses, schemas, ...) is dereferenced, so an unrelated nested reference that can't be
226
+ * resolved never hides the operations. The source descriptions are modified in place, which makes
227
+ * the operations behind such Path Items discoverable, e.g. by `findStepOperation` of
228
+ * \@jentic/arazzo-parser.
229
+ *
230
+ * This is best effort and never rejects: a Path Item that can't be resolved is left as is.
231
+ *
232
+ * @param parseResult - The result of parsing an Arazzo Document with source descriptions
233
+ * @param options - Reference options (uses defaultOptions when not provided)
234
+ * @returns A promise that resolves once all resolvable Path Items have been replaced
235
+ * @public
236
+ */
237
+ export declare function dereferenceSourceDescriptionPathItems(parseResult: ParseResultElement, options?: DereferenceOpenAPIOptions): Promise<void>;
238
+
219
239
  export { }