@speclynx/apidom-reference 2.8.0 → 2.10.0
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 +14 -0
- package/README.md +208 -5
- package/dist/apidom-reference.browser.js +167 -77
- package/dist/apidom-reference.browser.min.js +1 -1
- package/package.json +25 -25
- package/src/dereference/strategies/arazzo-1/index.cjs +3 -2
- package/src/dereference/strategies/arazzo-1/index.mjs +3 -2
- package/src/dereference/strategies/arazzo-1/{source-description.cjs → source-descriptions.cjs} +98 -29
- package/src/dereference/strategies/arazzo-1/{source-description.mjs → source-descriptions.mjs} +99 -28
- package/src/parse/parsers/arazzo-json-1/source-descriptions.cjs +33 -15
- package/src/parse/parsers/arazzo-json-1/source-descriptions.mjs +33 -15
- package/types/dereference/strategies/arazzo-1/index.d.ts +1 -0
- package/types/dereference/strategies/arazzo-1/source-descriptions.d.ts +44 -0
- package/types/parse/parsers/arazzo-json-1/source-descriptions.d.ts +23 -8
- package/types/dereference/strategies/arazzo-1/source-description.d.ts +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speclynx/apidom-reference",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Advanced algorithms for semantic ApiDOM manipulations like dereferencing or resolution.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -231,29 +231,29 @@
|
|
|
231
231
|
"license": "Apache-2.0",
|
|
232
232
|
"dependencies": {
|
|
233
233
|
"@babel/runtime-corejs3": "^7.28.4",
|
|
234
|
-
"@speclynx/apidom-core": "^2.
|
|
235
|
-
"@speclynx/apidom-datamodel": "^2.
|
|
236
|
-
"@speclynx/apidom-error": "^2.
|
|
237
|
-
"@speclynx/apidom-json-pointer": "^2.
|
|
238
|
-
"@speclynx/apidom-ns-arazzo-1": "^2.
|
|
239
|
-
"@speclynx/apidom-ns-asyncapi-2": "^2.
|
|
240
|
-
"@speclynx/apidom-ns-json-schema-2020-12": "^2.
|
|
241
|
-
"@speclynx/apidom-ns-openapi-2": "^2.
|
|
242
|
-
"@speclynx/apidom-ns-openapi-3-0": "^2.
|
|
243
|
-
"@speclynx/apidom-ns-openapi-3-1": "^2.
|
|
244
|
-
"@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.
|
|
245
|
-
"@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.
|
|
246
|
-
"@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.
|
|
247
|
-
"@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.
|
|
248
|
-
"@speclynx/apidom-parser-adapter-json": "^2.
|
|
249
|
-
"@speclynx/apidom-parser-adapter-openapi-json-2": "^2.
|
|
250
|
-
"@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.
|
|
251
|
-
"@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.
|
|
252
|
-
"@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.
|
|
253
|
-
"@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.
|
|
254
|
-
"@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.
|
|
255
|
-
"@speclynx/apidom-parser-adapter-yaml-1-2": "^2.
|
|
256
|
-
"@speclynx/apidom-traverse": "^2.
|
|
234
|
+
"@speclynx/apidom-core": "^2.10.0",
|
|
235
|
+
"@speclynx/apidom-datamodel": "^2.10.0",
|
|
236
|
+
"@speclynx/apidom-error": "^2.10.0",
|
|
237
|
+
"@speclynx/apidom-json-pointer": "^2.10.0",
|
|
238
|
+
"@speclynx/apidom-ns-arazzo-1": "^2.10.0",
|
|
239
|
+
"@speclynx/apidom-ns-asyncapi-2": "^2.10.0",
|
|
240
|
+
"@speclynx/apidom-ns-json-schema-2020-12": "^2.10.0",
|
|
241
|
+
"@speclynx/apidom-ns-openapi-2": "^2.10.0",
|
|
242
|
+
"@speclynx/apidom-ns-openapi-3-0": "^2.10.0",
|
|
243
|
+
"@speclynx/apidom-ns-openapi-3-1": "^2.10.0",
|
|
244
|
+
"@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.10.0",
|
|
245
|
+
"@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.10.0",
|
|
246
|
+
"@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.10.0",
|
|
247
|
+
"@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.10.0",
|
|
248
|
+
"@speclynx/apidom-parser-adapter-json": "^2.10.0",
|
|
249
|
+
"@speclynx/apidom-parser-adapter-openapi-json-2": "^2.10.0",
|
|
250
|
+
"@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.10.0",
|
|
251
|
+
"@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.10.0",
|
|
252
|
+
"@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.10.0",
|
|
253
|
+
"@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.10.0",
|
|
254
|
+
"@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.10.0",
|
|
255
|
+
"@speclynx/apidom-parser-adapter-yaml-1-2": "^2.10.0",
|
|
256
|
+
"@speclynx/apidom-traverse": "^2.10.0",
|
|
257
257
|
"@swaggerexpert/arazzo-runtime-expression": "^2.0.2",
|
|
258
258
|
"axios": "^1.13.0",
|
|
259
259
|
"minimatch": "^7.4.6",
|
|
@@ -274,5 +274,5 @@
|
|
|
274
274
|
"README.md",
|
|
275
275
|
"CHANGELOG.md"
|
|
276
276
|
],
|
|
277
|
-
"gitHead": "
|
|
277
|
+
"gitHead": "c2de0d651a39aff9118bb852e88a884d2d0aeb1b"
|
|
278
278
|
}
|
|
@@ -12,7 +12,8 @@ var _Reference = _interopRequireDefault(require("../../../Reference.cjs"));
|
|
|
12
12
|
var _ReferenceSet = _interopRequireDefault(require("../../../ReferenceSet.cjs"));
|
|
13
13
|
var _visitor = _interopRequireDefault(require("./visitor.cjs"));
|
|
14
14
|
exports.Arazzo1DereferenceVisitor = _visitor.default;
|
|
15
|
-
var
|
|
15
|
+
var _sourceDescriptions = require("./source-descriptions.cjs");
|
|
16
|
+
exports.dereferenceSourceDescriptions = _sourceDescriptions.dereferenceSourceDescriptions;
|
|
16
17
|
var _util = require("./util.cjs");
|
|
17
18
|
exports.resolveSchema$refField = _util.resolveSchema$refField;
|
|
18
19
|
exports.resolveSchema$idField = _util.resolveSchema$idField;
|
|
@@ -83,7 +84,7 @@ class Arazzo1DereferenceStrategy extends _DereferenceStrategy.default {
|
|
|
83
84
|
*/
|
|
84
85
|
const shouldDereferenceSourceDescriptions = options?.dereference?.strategyOpts?.[this.name]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
|
|
85
86
|
if (shouldDereferenceSourceDescriptions) {
|
|
86
|
-
const sourceDescriptions = await (0,
|
|
87
|
+
const sourceDescriptions = await (0, _sourceDescriptions.dereferenceSourceDescriptions)(dereferencedElement, reference.uri, options, this.name);
|
|
87
88
|
dereferencedElement.push(...sourceDescriptions);
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -5,7 +5,7 @@ import DereferenceStrategy from "../DereferenceStrategy.mjs";
|
|
|
5
5
|
import Reference from "../../../Reference.mjs";
|
|
6
6
|
import ReferenceSet from "../../../ReferenceSet.mjs";
|
|
7
7
|
import Arazzo1DereferenceVisitor from "./visitor.mjs";
|
|
8
|
-
import { dereferenceSourceDescriptions } from "./source-
|
|
8
|
+
import { dereferenceSourceDescriptions } from "./source-descriptions.mjs";
|
|
9
9
|
/**
|
|
10
10
|
* @public
|
|
11
11
|
*/
|
|
@@ -71,7 +71,7 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
|
|
|
71
71
|
*/
|
|
72
72
|
const shouldDereferenceSourceDescriptions = options?.dereference?.strategyOpts?.[this.name]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
|
|
73
73
|
if (shouldDereferenceSourceDescriptions) {
|
|
74
|
-
const sourceDescriptions = await dereferenceSourceDescriptions(dereferencedElement, reference, options);
|
|
74
|
+
const sourceDescriptions = await dereferenceSourceDescriptions(dereferencedElement, reference.uri, options, this.name);
|
|
75
75
|
dereferencedElement.push(...sourceDescriptions);
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -98,4 +98,5 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
|
|
|
98
98
|
}
|
|
99
99
|
export { Arazzo1DereferenceVisitor };
|
|
100
100
|
export { resolveSchema$refField, resolveSchema$idField, maybeRefractToJSONSchemaElement } from "./util.mjs";
|
|
101
|
+
export { dereferenceSourceDescriptions } from "./source-descriptions.mjs";
|
|
101
102
|
export default Arazzo1DereferenceStrategy;
|
package/src/dereference/strategies/arazzo-1/{source-description.cjs → source-descriptions.cjs}
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
|
|
4
3
|
var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
|
|
5
4
|
exports.__esModule = true;
|
|
6
5
|
exports.dereferenceSourceDescriptions = dereferenceSourceDescriptions;
|
|
@@ -12,9 +11,7 @@ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
|
|
|
12
11
|
var _apidomCore = require("@speclynx/apidom-core");
|
|
13
12
|
var url = _interopRequireWildcard(require("../../../util/url.cjs"));
|
|
14
13
|
var _util = require("../../../options/util.cjs");
|
|
15
|
-
var _index =
|
|
16
|
-
// shared key for recursion state (works across JSON/YAML documents)
|
|
17
|
-
const ARAZZO_DEREFERENCE_RECURSION_KEY = 'arazzo-1';
|
|
14
|
+
var _index = _interopRequireWildcard(require("../../index.cjs"));
|
|
18
15
|
/**
|
|
19
16
|
* Dereferences a single source description element.
|
|
20
17
|
* Returns ParseResultElement on success, or with annotation if skipped.
|
|
@@ -39,7 +36,10 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
39
36
|
parseResult.push(annotation);
|
|
40
37
|
return parseResult;
|
|
41
38
|
}
|
|
42
|
-
|
|
39
|
+
|
|
40
|
+
// normalize URI for consistent cycle detection and refSet cache key matching
|
|
41
|
+
const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
|
|
42
|
+
parseResult.setMetaProperty('retrievalURI', retrievalURI);
|
|
43
43
|
|
|
44
44
|
// skip if already visited (cycle detection)
|
|
45
45
|
if (ctx.visitedUrls.has(retrievalURI)) {
|
|
@@ -49,20 +49,54 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
49
49
|
return parseResult;
|
|
50
50
|
}
|
|
51
51
|
ctx.visitedUrls.add(retrievalURI);
|
|
52
|
+
|
|
53
|
+
// check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
|
|
54
|
+
const existingParseResult = sourceDescription.meta.get('parseResult');
|
|
52
55
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
let sourceDescriptionDereferenced;
|
|
57
|
+
if ((0, _apidomDatamodel.isParseResultElement)(existingParseResult)) {
|
|
58
|
+
// use existing parsed result - just dereference it (no re-fetch/re-parse)
|
|
59
|
+
sourceDescriptionDereferenced = await (0, _index.dereferenceApiDOM)(existingParseResult, (0, _util.merge)(ctx.options, {
|
|
60
|
+
parse: {
|
|
61
|
+
mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
|
|
62
|
+
},
|
|
63
|
+
resolve: {
|
|
64
|
+
baseURI: retrievalURI
|
|
65
|
+
},
|
|
66
|
+
dereference: {
|
|
67
|
+
strategyOpts: {
|
|
68
|
+
// nested documents should dereference all their source descriptions
|
|
69
|
+
// (parent's name filter doesn't apply to nested documents)
|
|
70
|
+
// set at strategy-specific level to override any inherited filters
|
|
71
|
+
[ctx.strategyName]: {
|
|
72
|
+
sourceDescriptions: true,
|
|
73
|
+
sourceDescriptionsDepth: ctx.currentDepth + 1,
|
|
74
|
+
sourceDescriptionsVisitedUrls: ctx.visitedUrls
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
} else {
|
|
80
|
+
// no existing parse result - fetch, parse, and dereference
|
|
81
|
+
sourceDescriptionDereferenced = await (0, _index.default)(retrievalURI, (0, _util.merge)(ctx.options, {
|
|
82
|
+
parse: {
|
|
83
|
+
mediaType: 'text/plain' // allow parser plugin detection
|
|
84
|
+
},
|
|
85
|
+
dereference: {
|
|
86
|
+
strategyOpts: {
|
|
87
|
+
// nested documents should dereference all their source descriptions
|
|
88
|
+
// (parent's name filter doesn't apply to nested documents)
|
|
89
|
+
// set at strategy-specific level to override any inherited filters
|
|
90
|
+
[ctx.strategyName]: {
|
|
91
|
+
sourceDescriptions: true,
|
|
92
|
+
sourceDescriptionsDepth: ctx.currentDepth + 1,
|
|
93
|
+
sourceDescriptionsVisitedUrls: ctx.visitedUrls
|
|
94
|
+
}
|
|
62
95
|
}
|
|
63
96
|
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
// merge dereferenced result into our parse result
|
|
67
101
|
for (const item of sourceDescriptionDereferenced) {
|
|
68
102
|
parseResult.push(item);
|
|
@@ -107,11 +141,48 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
107
141
|
|
|
108
142
|
/**
|
|
109
143
|
* Dereferences source descriptions from an Arazzo document.
|
|
144
|
+
*
|
|
145
|
+
* Each source description result is attached to its corresponding
|
|
146
|
+
* SourceDescriptionElement's meta as 'parseResult' for easy access,
|
|
147
|
+
* regardless of success or failure. On failure, the ParseResultElement
|
|
148
|
+
* contains annotations explaining what went wrong.
|
|
149
|
+
*
|
|
150
|
+
* @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
|
|
151
|
+
* @param parseResultRetrievalURI - URI from which the parseResult was retrieved
|
|
152
|
+
* @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
|
|
153
|
+
* in `dereference.strategyOpts` to filter which source descriptions to process.
|
|
154
|
+
* @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
|
|
155
|
+
* @returns Array of ParseResultElements. Returns one ParseResultElement per source description
|
|
156
|
+
* (each with class 'source-description' and metadata: name, type, retrievalURI).
|
|
157
|
+
* May return early with a single-element array containing a warning annotation when:
|
|
158
|
+
* - The API is not an Arazzo specification
|
|
159
|
+
* - The sourceDescriptions field is missing or not an array
|
|
160
|
+
* - Maximum dereference depth is exceeded (error annotation)
|
|
161
|
+
* Returns an empty array when no source description names match the filter.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* import { toValue } from '@speclynx/apidom-core';
|
|
166
|
+
*
|
|
167
|
+
* // Dereference all source descriptions
|
|
168
|
+
* await dereferenceSourceDescriptions(parseResult, uri, options);
|
|
169
|
+
*
|
|
170
|
+
* // Filter by name
|
|
171
|
+
* await dereferenceSourceDescriptions(parseResult, uri, mergeOptions(options, {
|
|
172
|
+
* dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
|
|
173
|
+
* }));
|
|
174
|
+
*
|
|
175
|
+
* // Access dereferenced document from source description element
|
|
176
|
+
* const sourceDesc = parseResult.api.sourceDescriptions.get(0);
|
|
177
|
+
* const dereferencedDoc = sourceDesc.meta.get('parseResult');
|
|
178
|
+
* const retrievalURI = toValue(dereferencedDoc.meta.get('retrievalURI'));
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
110
181
|
* @public
|
|
111
182
|
*/
|
|
112
|
-
async function dereferenceSourceDescriptions(parseResult,
|
|
183
|
+
async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
|
|
184
|
+
const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
|
|
113
185
|
const results = [];
|
|
114
|
-
const strategyName = 'arazzo-1';
|
|
115
186
|
|
|
116
187
|
// get API from dereferenced parse result
|
|
117
188
|
const {
|
|
@@ -136,34 +207,30 @@ async function dereferenceSourceDescriptions(parseResult, reference, options) {
|
|
|
136
207
|
// user config: strategy-specific options take precedence over global strategyOpts
|
|
137
208
|
const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
|
|
138
209
|
|
|
139
|
-
// recursion state comes from
|
|
140
|
-
const sharedOpts = options?.dereference?.strategyOpts?.[
|
|
210
|
+
// recursion state comes from strategy-specific options
|
|
211
|
+
const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
|
|
141
212
|
const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
|
|
142
213
|
const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
|
|
143
214
|
|
|
144
215
|
// add current file to visited URLs to prevent cycles
|
|
145
|
-
visitedUrls.add(
|
|
216
|
+
visitedUrls.add(baseURI);
|
|
146
217
|
if (currentDepth >= maxDepth) {
|
|
147
|
-
const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${
|
|
218
|
+
const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
|
|
148
219
|
annotation.classes.push('error');
|
|
149
220
|
const parseResult = new _apidomDatamodel.ParseResultElement([annotation]);
|
|
150
221
|
parseResult.classes.push('source-description');
|
|
151
222
|
return [parseResult];
|
|
152
223
|
}
|
|
153
224
|
const ctx = {
|
|
154
|
-
baseURI
|
|
225
|
+
baseURI,
|
|
155
226
|
options,
|
|
227
|
+
strategyName,
|
|
156
228
|
currentDepth,
|
|
157
229
|
visitedUrls
|
|
158
230
|
};
|
|
159
231
|
|
|
160
|
-
// determine which source descriptions to dereference
|
|
232
|
+
// determine which source descriptions to dereference (array filters by name)
|
|
161
233
|
const sourceDescriptionsOption = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
|
|
162
|
-
|
|
163
|
-
// handle false or other falsy values - no source descriptions should be dereferenced
|
|
164
|
-
if (!sourceDescriptionsOption) {
|
|
165
|
-
return results;
|
|
166
|
-
}
|
|
167
234
|
const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
|
|
168
235
|
if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
|
|
169
236
|
const name = (0, _apidomCore.toValue)(sd.name);
|
|
@@ -173,6 +240,8 @@ async function dereferenceSourceDescriptions(parseResult, reference, options) {
|
|
|
173
240
|
// process sequentially to ensure proper cycle detection with shared visitedUrls
|
|
174
241
|
for (const sourceDescription of sourceDescriptions) {
|
|
175
242
|
const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
|
|
243
|
+
// always attach result (even on failure - contains annotations)
|
|
244
|
+
sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
|
|
176
245
|
results.push(sourceDescriptionDereferenceResult);
|
|
177
246
|
}
|
|
178
247
|
return results;
|
package/src/dereference/strategies/arazzo-1/{source-description.mjs → source-descriptions.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement, cloneDeep } from '@speclynx/apidom-datamodel';
|
|
1
|
+
import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement, isParseResultElement, cloneDeep } from '@speclynx/apidom-datamodel';
|
|
2
2
|
import { isArazzoSpecification1Element, isSourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
|
|
3
3
|
import { isSwaggerElement } from '@speclynx/apidom-ns-openapi-2';
|
|
4
4
|
import { isOpenApi3_0Element } from '@speclynx/apidom-ns-openapi-3-0';
|
|
@@ -6,8 +6,7 @@ import { isOpenApi3_1Element } from '@speclynx/apidom-ns-openapi-3-1';
|
|
|
6
6
|
import { toValue } from '@speclynx/apidom-core';
|
|
7
7
|
import * as url from "../../../util/url.mjs";
|
|
8
8
|
import { merge as mergeOptions } from "../../../options/util.mjs";
|
|
9
|
-
import dereference from "../../index.mjs";
|
|
10
|
-
const ARAZZO_DEREFERENCE_RECURSION_KEY = 'arazzo-1';
|
|
9
|
+
import dereference, { dereferenceApiDOM } from "../../index.mjs";
|
|
11
10
|
/**
|
|
12
11
|
* Dereferences a single source description element.
|
|
13
12
|
* Returns ParseResultElement on success, or with annotation if skipped.
|
|
@@ -32,7 +31,10 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
32
31
|
parseResult.push(annotation);
|
|
33
32
|
return parseResult;
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
|
|
35
|
+
// normalize URI for consistent cycle detection and refSet cache key matching
|
|
36
|
+
const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
|
|
37
|
+
parseResult.setMetaProperty('retrievalURI', retrievalURI);
|
|
36
38
|
|
|
37
39
|
// skip if already visited (cycle detection)
|
|
38
40
|
if (ctx.visitedUrls.has(retrievalURI)) {
|
|
@@ -42,20 +44,54 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
42
44
|
return parseResult;
|
|
43
45
|
}
|
|
44
46
|
ctx.visitedUrls.add(retrievalURI);
|
|
47
|
+
|
|
48
|
+
// check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
|
|
49
|
+
const existingParseResult = sourceDescription.meta.get('parseResult');
|
|
45
50
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
let sourceDescriptionDereferenced;
|
|
52
|
+
if (isParseResultElement(existingParseResult)) {
|
|
53
|
+
// use existing parsed result - just dereference it (no re-fetch/re-parse)
|
|
54
|
+
sourceDescriptionDereferenced = await dereferenceApiDOM(existingParseResult, mergeOptions(ctx.options, {
|
|
55
|
+
parse: {
|
|
56
|
+
mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
|
|
57
|
+
},
|
|
58
|
+
resolve: {
|
|
59
|
+
baseURI: retrievalURI
|
|
60
|
+
},
|
|
61
|
+
dereference: {
|
|
62
|
+
strategyOpts: {
|
|
63
|
+
// nested documents should dereference all their source descriptions
|
|
64
|
+
// (parent's name filter doesn't apply to nested documents)
|
|
65
|
+
// set at strategy-specific level to override any inherited filters
|
|
66
|
+
[ctx.strategyName]: {
|
|
67
|
+
sourceDescriptions: true,
|
|
68
|
+
sourceDescriptionsDepth: ctx.currentDepth + 1,
|
|
69
|
+
sourceDescriptionsVisitedUrls: ctx.visitedUrls
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
} else {
|
|
75
|
+
// no existing parse result - fetch, parse, and dereference
|
|
76
|
+
sourceDescriptionDereferenced = await dereference(retrievalURI, mergeOptions(ctx.options, {
|
|
77
|
+
parse: {
|
|
78
|
+
mediaType: 'text/plain' // allow parser plugin detection
|
|
79
|
+
},
|
|
80
|
+
dereference: {
|
|
81
|
+
strategyOpts: {
|
|
82
|
+
// nested documents should dereference all their source descriptions
|
|
83
|
+
// (parent's name filter doesn't apply to nested documents)
|
|
84
|
+
// set at strategy-specific level to override any inherited filters
|
|
85
|
+
[ctx.strategyName]: {
|
|
86
|
+
sourceDescriptions: true,
|
|
87
|
+
sourceDescriptionsDepth: ctx.currentDepth + 1,
|
|
88
|
+
sourceDescriptionsVisitedUrls: ctx.visitedUrls
|
|
89
|
+
}
|
|
55
90
|
}
|
|
56
91
|
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
// merge dereferenced result into our parse result
|
|
60
96
|
for (const item of sourceDescriptionDereferenced) {
|
|
61
97
|
parseResult.push(item);
|
|
@@ -100,11 +136,48 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
|
|
|
100
136
|
|
|
101
137
|
/**
|
|
102
138
|
* Dereferences source descriptions from an Arazzo document.
|
|
139
|
+
*
|
|
140
|
+
* Each source description result is attached to its corresponding
|
|
141
|
+
* SourceDescriptionElement's meta as 'parseResult' for easy access,
|
|
142
|
+
* regardless of success or failure. On failure, the ParseResultElement
|
|
143
|
+
* contains annotations explaining what went wrong.
|
|
144
|
+
*
|
|
145
|
+
* @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
|
|
146
|
+
* @param parseResultRetrievalURI - URI from which the parseResult was retrieved
|
|
147
|
+
* @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
|
|
148
|
+
* in `dereference.strategyOpts` to filter which source descriptions to process.
|
|
149
|
+
* @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
|
|
150
|
+
* @returns Array of ParseResultElements. Returns one ParseResultElement per source description
|
|
151
|
+
* (each with class 'source-description' and metadata: name, type, retrievalURI).
|
|
152
|
+
* May return early with a single-element array containing a warning annotation when:
|
|
153
|
+
* - The API is not an Arazzo specification
|
|
154
|
+
* - The sourceDescriptions field is missing or not an array
|
|
155
|
+
* - Maximum dereference depth is exceeded (error annotation)
|
|
156
|
+
* Returns an empty array when no source description names match the filter.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* import { toValue } from '@speclynx/apidom-core';
|
|
161
|
+
*
|
|
162
|
+
* // Dereference all source descriptions
|
|
163
|
+
* await dereferenceSourceDescriptions(parseResult, uri, options);
|
|
164
|
+
*
|
|
165
|
+
* // Filter by name
|
|
166
|
+
* await dereferenceSourceDescriptions(parseResult, uri, mergeOptions(options, {
|
|
167
|
+
* dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
|
|
168
|
+
* }));
|
|
169
|
+
*
|
|
170
|
+
* // Access dereferenced document from source description element
|
|
171
|
+
* const sourceDesc = parseResult.api.sourceDescriptions.get(0);
|
|
172
|
+
* const dereferencedDoc = sourceDesc.meta.get('parseResult');
|
|
173
|
+
* const retrievalURI = toValue(dereferencedDoc.meta.get('retrievalURI'));
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
103
176
|
* @public
|
|
104
177
|
*/
|
|
105
|
-
export async function dereferenceSourceDescriptions(parseResult,
|
|
178
|
+
export async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
|
|
179
|
+
const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
|
|
106
180
|
const results = [];
|
|
107
|
-
const strategyName = 'arazzo-1';
|
|
108
181
|
|
|
109
182
|
// get API from dereferenced parse result
|
|
110
183
|
const {
|
|
@@ -129,34 +202,30 @@ export async function dereferenceSourceDescriptions(parseResult, reference, opti
|
|
|
129
202
|
// user config: strategy-specific options take precedence over global strategyOpts
|
|
130
203
|
const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
|
|
131
204
|
|
|
132
|
-
// recursion state comes from
|
|
133
|
-
const sharedOpts = options?.dereference?.strategyOpts?.[
|
|
205
|
+
// recursion state comes from strategy-specific options
|
|
206
|
+
const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
|
|
134
207
|
const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
|
|
135
208
|
const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
|
|
136
209
|
|
|
137
210
|
// add current file to visited URLs to prevent cycles
|
|
138
|
-
visitedUrls.add(
|
|
211
|
+
visitedUrls.add(baseURI);
|
|
139
212
|
if (currentDepth >= maxDepth) {
|
|
140
|
-
const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${
|
|
213
|
+
const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
|
|
141
214
|
annotation.classes.push('error');
|
|
142
215
|
const parseResult = new ParseResultElement([annotation]);
|
|
143
216
|
parseResult.classes.push('source-description');
|
|
144
217
|
return [parseResult];
|
|
145
218
|
}
|
|
146
219
|
const ctx = {
|
|
147
|
-
baseURI
|
|
220
|
+
baseURI,
|
|
148
221
|
options,
|
|
222
|
+
strategyName,
|
|
149
223
|
currentDepth,
|
|
150
224
|
visitedUrls
|
|
151
225
|
};
|
|
152
226
|
|
|
153
|
-
// determine which source descriptions to dereference
|
|
227
|
+
// determine which source descriptions to dereference (array filters by name)
|
|
154
228
|
const sourceDescriptionsOption = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
|
|
155
|
-
|
|
156
|
-
// handle false or other falsy values - no source descriptions should be dereferenced
|
|
157
|
-
if (!sourceDescriptionsOption) {
|
|
158
|
-
return results;
|
|
159
|
-
}
|
|
160
229
|
const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
|
|
161
230
|
if (!isSourceDescriptionElement(sd)) return false;
|
|
162
231
|
const name = toValue(sd.name);
|
|
@@ -166,6 +235,8 @@ export async function dereferenceSourceDescriptions(parseResult, reference, opti
|
|
|
166
235
|
// process sequentially to ensure proper cycle detection with shared visitedUrls
|
|
167
236
|
for (const sourceDescription of sourceDescriptions) {
|
|
168
237
|
const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
|
|
238
|
+
// always attach result (even on failure - contains annotations)
|
|
239
|
+
sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
|
|
169
240
|
results.push(sourceDescriptionDereferenceResult);
|
|
170
241
|
}
|
|
171
242
|
return results;
|
|
@@ -40,7 +40,10 @@ async function parseSourceDescription(sourceDescription, ctx) {
|
|
|
40
40
|
parseResult.push(annotation);
|
|
41
41
|
return parseResult;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
// normalize URI for consistent cycle detection and cache key matching
|
|
45
|
+
const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
|
|
46
|
+
parseResult.setMetaProperty('retrievalURI', retrievalURI);
|
|
44
47
|
|
|
45
48
|
// skip if already visited (cycle detection)
|
|
46
49
|
if (ctx.visitedUrls.has(retrievalURI)) {
|
|
@@ -56,6 +59,9 @@ async function parseSourceDescription(sourceDescription, ctx) {
|
|
|
56
59
|
mediaType: 'text/plain',
|
|
57
60
|
// allow parser plugin detection
|
|
58
61
|
parserOpts: {
|
|
62
|
+
// nested documents should parse all their source descriptions
|
|
63
|
+
// (parent's name filter doesn't apply to nested documents)
|
|
64
|
+
sourceDescriptions: true,
|
|
59
65
|
[ARAZZO_RECURSION_KEY]: {
|
|
60
66
|
sourceDescriptionsDepth: ctx.currentDepth + 1,
|
|
61
67
|
sourceDescriptionsVisitedUrls: ctx.visitedUrls
|
|
@@ -108,27 +114,42 @@ async function parseSourceDescription(sourceDescription, ctx) {
|
|
|
108
114
|
/**
|
|
109
115
|
* Parses source descriptions from an Arazzo document's ParseResult.
|
|
110
116
|
*
|
|
117
|
+
* Each source description result is attached to its corresponding
|
|
118
|
+
* SourceDescriptionElement's meta as 'parseResult' for easy access,
|
|
119
|
+
* regardless of success or failure. On failure, the ParseResultElement
|
|
120
|
+
* contains annotations explaining what went wrong.
|
|
121
|
+
*
|
|
111
122
|
* @param parseResult - ParseResult containing an Arazzo specification
|
|
112
123
|
* @param parseResultRetrievalURI - URI from which the parseResult was retrieved
|
|
113
|
-
* @param options - Full ReferenceOptions
|
|
124
|
+
* @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
|
|
125
|
+
* in `parse.parserOpts` to filter which source descriptions to process.
|
|
114
126
|
* @param parserName - Parser name for options lookup (defaults to 'arazzo-json-1')
|
|
115
|
-
* @returns Array of ParseResultElements.
|
|
116
|
-
*
|
|
127
|
+
* @returns Array of ParseResultElements. Returns one ParseResultElement per source description
|
|
128
|
+
* (each with class 'source-description' and metadata: name, type, retrievalURI).
|
|
117
129
|
* May return early with a single-element array containing a warning annotation when:
|
|
118
130
|
* - The API is not an Arazzo specification
|
|
119
131
|
* - The sourceDescriptions field is missing or not an array
|
|
120
132
|
* - Maximum parse depth is exceeded (error annotation)
|
|
121
|
-
* Returns an empty array when
|
|
133
|
+
* Returns an empty array when no source description names match the filter.
|
|
122
134
|
*
|
|
123
135
|
* @example
|
|
124
136
|
* ```typescript
|
|
137
|
+
* import { toValue } from '@speclynx/apidom-core';
|
|
125
138
|
* import { options, mergeOptions } from '@speclynx/apidom-reference';
|
|
126
139
|
* import { parseSourceDescriptions } from '@speclynx/apidom-reference/parse/parsers/arazzo-json-1';
|
|
127
140
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
141
|
+
* // Parse all source descriptions
|
|
142
|
+
* const results = await parseSourceDescriptions(parseResult, uri, options);
|
|
143
|
+
*
|
|
144
|
+
* // Filter by name
|
|
145
|
+
* const filtered = await parseSourceDescriptions(parseResult, uri, mergeOptions(options, {
|
|
146
|
+
* parse: { parserOpts: { sourceDescriptions: ['petStore'] } }
|
|
147
|
+
* }));
|
|
148
|
+
*
|
|
149
|
+
* // Access parsed document from source description element
|
|
150
|
+
* const sourceDesc = parseResult.api.sourceDescriptions.get(0);
|
|
151
|
+
* const parsedDoc = sourceDesc.meta.get('parseResult');
|
|
152
|
+
* const retrievalURI = toValue(parsedDoc.meta.get('retrievalURI'));
|
|
132
153
|
* ```
|
|
133
154
|
*
|
|
134
155
|
* @public
|
|
@@ -181,13 +202,8 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
|
|
|
181
202
|
visitedUrls
|
|
182
203
|
};
|
|
183
204
|
|
|
184
|
-
// determine which source descriptions to parse
|
|
205
|
+
// determine which source descriptions to parse (array filters by name)
|
|
185
206
|
const sourceDescriptionsOption = options?.parse?.parserOpts?.[parserName]?.sourceDescriptions ?? options?.parse?.parserOpts?.sourceDescriptions;
|
|
186
|
-
|
|
187
|
-
// handle false or other falsy values - no source descriptions should be parsed
|
|
188
|
-
if (!sourceDescriptionsOption) {
|
|
189
|
-
return results;
|
|
190
|
-
}
|
|
191
207
|
const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
|
|
192
208
|
if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
|
|
193
209
|
const name = (0, _apidomCore.toValue)(sd.name);
|
|
@@ -197,6 +213,8 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
|
|
|
197
213
|
// process sequentially to ensure proper cycle detection with shared visitedUrls
|
|
198
214
|
for (const sourceDescription of sourceDescriptions) {
|
|
199
215
|
const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
|
|
216
|
+
// always attach result (even on failure - contains annotations)
|
|
217
|
+
sourceDescription.meta.set('parseResult', sourceDescriptionParseResult);
|
|
200
218
|
results.push(sourceDescriptionParseResult);
|
|
201
219
|
}
|
|
202
220
|
return results;
|