@speclynx/apidom-reference 2.6.0 → 2.7.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +60 -33
  3. package/dist/apidom-reference.browser.js +79395 -79177
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +25 -25
  6. package/src/bundle/index.cjs +1 -1
  7. package/src/bundle/index.mjs +1 -1
  8. package/src/configuration/saturated.cjs +2 -4
  9. package/src/configuration/saturated.mjs +3 -5
  10. package/src/dereference/index.cjs +1 -1
  11. package/src/dereference/index.mjs +1 -1
  12. package/src/dereference/strategies/arazzo-1/index.cjs +10 -0
  13. package/src/dereference/strategies/arazzo-1/index.mjs +10 -0
  14. package/src/dereference/strategies/arazzo-1/source-description.cjs +179 -0
  15. package/src/dereference/strategies/arazzo-1/source-description.mjs +172 -0
  16. package/src/dereference/strategies/openapi-3-0/visitor.cjs +3 -3
  17. package/src/dereference/strategies/openapi-3-0/visitor.mjs +3 -3
  18. package/src/dereference/strategies/openapi-3-1/visitor.cjs +3 -3
  19. package/src/dereference/strategies/openapi-3-1/visitor.mjs +3 -3
  20. package/src/errors/InvalidJsonSchema$anchorError.cjs +1 -1
  21. package/src/errors/InvalidJsonSchema$anchorError.mjs +1 -1
  22. package/src/errors/UnmatchedParserError.cjs +11 -0
  23. package/src/errors/UnmatchedParserError.mjs +6 -0
  24. package/src/index.cjs +3 -1
  25. package/src/index.mjs +1 -0
  26. package/src/parse/index.cjs +3 -3
  27. package/src/parse/index.mjs +3 -3
  28. package/src/parse/parsers/arazzo-json-1/index.cjs +1 -4
  29. package/src/parse/parsers/arazzo-json-1/index.mjs +1 -4
  30. package/src/parse/parsers/arazzo-json-1/source-description.cjs +14 -19
  31. package/src/parse/parsers/arazzo-json-1/source-description.mjs +14 -20
  32. package/src/parse/parsers/arazzo-yaml-1/index.cjs +1 -4
  33. package/src/parse/parsers/arazzo-yaml-1/index.mjs +1 -4
  34. package/src/resolve/index.cjs +2 -2
  35. package/src/resolve/index.mjs +2 -2
  36. package/src/resolve/resolvers/file/index-browser.cjs +1 -1
  37. package/src/resolve/resolvers/file/index-browser.mjs +1 -1
  38. package/src/resolve/strategies/apidom/index.cjs +1 -1
  39. package/src/resolve/strategies/apidom/index.mjs +1 -1
  40. package/src/resolve/strategies/asyncapi-2/index.cjs +1 -1
  41. package/src/resolve/strategies/asyncapi-2/index.mjs +1 -1
  42. package/src/resolve/strategies/openapi-2/index.cjs +1 -1
  43. package/src/resolve/strategies/openapi-2/index.mjs +1 -1
  44. package/src/resolve/strategies/openapi-3-0/index.cjs +1 -1
  45. package/src/resolve/strategies/openapi-3-0/index.mjs +1 -1
  46. package/src/resolve/strategies/openapi-3-1/index.cjs +1 -1
  47. package/src/resolve/strategies/openapi-3-1/index.mjs +1 -1
  48. package/src/resolve/util.cjs +1 -1
  49. package/src/resolve/util.mjs +1 -1
  50. package/types/apidom-reference.d.ts +6 -0
  51. package/types/dereference/strategies/arazzo-1/source-description.d.ts +8 -0
  52. package/types/errors/UnmatchedParserError.d.ts +7 -0
  53. package/types/index.d.ts +1 -0
  54. package/types/parse/parsers/arazzo-json-1/index.d.ts +0 -3
  55. package/types/parse/parsers/arazzo-json-1/source-description.d.ts +1 -6
  56. package/types/parse/parsers/arazzo-yaml-1/index.d.ts +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speclynx/apidom-reference",
3
- "version": "2.6.0",
3
+ "version": "2.7.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.6.0",
235
- "@speclynx/apidom-datamodel": "^2.6.0",
236
- "@speclynx/apidom-error": "^2.6.0",
237
- "@speclynx/apidom-json-pointer": "^2.6.0",
238
- "@speclynx/apidom-ns-arazzo-1": "^2.6.0",
239
- "@speclynx/apidom-ns-asyncapi-2": "^2.6.0",
240
- "@speclynx/apidom-ns-json-schema-2020-12": "^2.6.0",
241
- "@speclynx/apidom-ns-openapi-2": "^2.6.0",
242
- "@speclynx/apidom-ns-openapi-3-0": "^2.6.0",
243
- "@speclynx/apidom-ns-openapi-3-1": "^2.6.0",
244
- "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.6.0",
245
- "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.6.0",
246
- "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.6.0",
247
- "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.6.0",
248
- "@speclynx/apidom-parser-adapter-json": "^2.6.0",
249
- "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.6.0",
250
- "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.6.0",
251
- "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.6.0",
252
- "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.6.0",
253
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.6.0",
254
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.6.0",
255
- "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.6.0",
256
- "@speclynx/apidom-traverse": "^2.6.0",
234
+ "@speclynx/apidom-core": "^2.7.0",
235
+ "@speclynx/apidom-datamodel": "^2.7.0",
236
+ "@speclynx/apidom-error": "^2.7.0",
237
+ "@speclynx/apidom-json-pointer": "^2.7.0",
238
+ "@speclynx/apidom-ns-arazzo-1": "^2.7.0",
239
+ "@speclynx/apidom-ns-asyncapi-2": "^2.7.0",
240
+ "@speclynx/apidom-ns-json-schema-2020-12": "^2.7.0",
241
+ "@speclynx/apidom-ns-openapi-2": "^2.7.0",
242
+ "@speclynx/apidom-ns-openapi-3-0": "^2.7.0",
243
+ "@speclynx/apidom-ns-openapi-3-1": "^2.7.0",
244
+ "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.7.0",
245
+ "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.7.0",
246
+ "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.7.0",
247
+ "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.7.0",
248
+ "@speclynx/apidom-parser-adapter-json": "^2.7.0",
249
+ "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.7.0",
250
+ "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.7.0",
251
+ "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.7.0",
252
+ "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.7.0",
253
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.7.0",
254
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.7.0",
255
+ "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.7.0",
256
+ "@speclynx/apidom-traverse": "^2.7.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": "d96eb344cc26a57935aa3a425ea2447d6e8e77b2"
277
+ "gitHead": "82ac9430aa882d0a5c63a17f7da8f5c0dcbca737"
278
278
  }
@@ -45,7 +45,7 @@ const bundle = async (uri, options) => {
45
45
 
46
46
  // we couldn't find any bundle strategy for this File
47
47
  if ((0, _ramda.isEmpty)(bundleStrategies)) {
48
- throw new _UnmatchedBundleStrategyError.default(file.uri);
48
+ throw new _UnmatchedBundleStrategyError.default(`Could not find a bundle strategy that can bundle the file "${file.uri}"`);
49
49
  }
50
50
  try {
51
51
  const {
@@ -39,7 +39,7 @@ const bundle = async (uri, options) => {
39
39
 
40
40
  // we couldn't find any bundle strategy for this File
41
41
  if (isEmpty(bundleStrategies)) {
42
- throw new UnmatchedBundleStrategyError(file.uri);
42
+ throw new UnmatchedBundleStrategyError(`Could not find a bundle strategy that can bundle the file "${file.uri}"`);
43
43
  }
44
44
  try {
45
45
  const {
@@ -62,12 +62,10 @@ _index25.options.parse.parsers = [new _index7.default({
62
62
  sourceMap: false
63
63
  }), new _index13.default({
64
64
  allowEmpty: true,
65
- sourceMap: false,
66
- parseFn: _index25.parse
65
+ sourceMap: false
67
66
  }), new _index14.default({
68
67
  allowEmpty: true,
69
- sourceMap: false,
70
- parseFn: _index25.parse
68
+ sourceMap: false
71
69
  }), new _index15.default({
72
70
  allowEmpty: true,
73
71
  sourceMap: false
@@ -26,7 +26,7 @@ import OpenAPI3_1DereferenceStrategy from "../dereference/strategies/openapi-3-1
26
26
  import AsyncAPI2DereferenceStrategy from "../dereference/strategies/asyncapi-2/index.mjs";
27
27
  import Arazzo1DereferenceStrategy from "../dereference/strategies/arazzo-1/index.mjs";
28
28
  import OpenAPI3_1BundleStrategy from "../bundle/strategies/openapi-3-1/index.mjs";
29
- import { options, parse } from "../index.mjs";
29
+ import { options } from "../index.mjs";
30
30
  options.parse.parsers = [new OpenAPIJSON2Parser({
31
31
  allowEmpty: true,
32
32
  sourceMap: false
@@ -53,12 +53,10 @@ options.parse.parsers = [new OpenAPIJSON2Parser({
53
53
  sourceMap: false
54
54
  }), new ArazzoJSON1Parser({
55
55
  allowEmpty: true,
56
- sourceMap: false,
57
- parseFn: parse
56
+ sourceMap: false
58
57
  }), new ArazzoYAML1Parser({
59
58
  allowEmpty: true,
60
- sourceMap: false,
61
- parseFn: parse
59
+ sourceMap: false
62
60
  }), new APIDOMJSONParser({
63
61
  allowEmpty: true,
64
62
  sourceMap: false
@@ -37,7 +37,7 @@ const dereferenceApiDOM = async (element, options) => {
37
37
 
38
38
  // we couldn't find any dereference strategy for this File
39
39
  if ((0, _ramda.isEmpty)(dereferenceStrategies)) {
40
- throw new _UnmatchedDereferenceStrategyError.default(file.uri);
40
+ throw new _UnmatchedDereferenceStrategyError.default(`Could not find a dereference strategy that can dereference the file "${file.uri}"`);
41
41
  }
42
42
  try {
43
43
  const {
@@ -31,7 +31,7 @@ export const dereferenceApiDOM = async (element, options) => {
31
31
 
32
32
  // we couldn't find any dereference strategy for this File
33
33
  if (isEmpty(dereferenceStrategies)) {
34
- throw new UnmatchedDereferenceStrategyError(file.uri);
34
+ throw new UnmatchedDereferenceStrategyError(`Could not find a dereference strategy that can dereference the file "${file.uri}"`);
35
35
  }
36
36
  try {
37
37
  const {
@@ -12,6 +12,7 @@ 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 _sourceDescription = require("./source-description.cjs");
15
16
  var _util = require("./util.cjs");
16
17
  exports.resolveSchema$refField = _util.resolveSchema$refField;
17
18
  exports.resolveSchema$idField = _util.resolveSchema$idField;
@@ -77,6 +78,15 @@ class Arazzo1DereferenceStrategy extends _DereferenceStrategy.default {
77
78
  mutable: true
78
79
  });
79
80
 
81
+ /**
82
+ * Dereference source descriptions if option is enabled.
83
+ */
84
+ const shouldDereferenceSourceDescriptions = options?.dereference?.strategyOpts?.[this.name]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
85
+ if (shouldDereferenceSourceDescriptions) {
86
+ const sourceDescriptions = await (0, _sourceDescription.dereferenceSourceDescriptions)(dereferencedElement, reference, options);
87
+ dereferencedElement.push(...sourceDescriptions);
88
+ }
89
+
80
90
  /**
81
91
  * If immutable option is set, replay refs from the refSet.
82
92
  */
@@ -5,6 +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-description.mjs";
8
9
  /**
9
10
  * @public
10
11
  */
@@ -65,6 +66,15 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
65
66
  mutable: true
66
67
  });
67
68
 
69
+ /**
70
+ * Dereference source descriptions if option is enabled.
71
+ */
72
+ const shouldDereferenceSourceDescriptions = options?.dereference?.strategyOpts?.[this.name]?.sourceDescriptions ?? options?.dereference?.strategyOpts?.sourceDescriptions;
73
+ if (shouldDereferenceSourceDescriptions) {
74
+ const sourceDescriptions = await dereferenceSourceDescriptions(dereferencedElement, reference, options);
75
+ dereferencedElement.push(...sourceDescriptions);
76
+ }
77
+
68
78
  /**
69
79
  * If immutable option is set, replay refs from the refSet.
70
80
  */
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
+ var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
5
+ exports.__esModule = true;
6
+ exports.dereferenceSourceDescriptions = dereferenceSourceDescriptions;
7
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
8
+ var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
9
+ var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
10
+ var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
11
+ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
12
+ var _apidomCore = require("@speclynx/apidom-core");
13
+ var url = _interopRequireWildcard(require("../../../util/url.cjs"));
14
+ var _util = require("../../../options/util.cjs");
15
+ var _index = _interopRequireDefault(require("../../index.cjs"));
16
+ // shared key for recursion state (works across JSON/YAML documents)
17
+ const ARAZZO_DEREFERENCE_RECURSION_KEY = 'arazzo-1';
18
+ /**
19
+ * Dereferences a single source description element.
20
+ * Returns ParseResultElement on success, or with annotation if skipped.
21
+ */
22
+ async function dereferenceSourceDescription(sourceDescription, ctx) {
23
+ const parseResult = new _apidomDatamodel.ParseResultElement();
24
+ if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sourceDescription)) {
25
+ const annotation = new _apidomDatamodel.AnnotationElement('Element is not a valid SourceDescriptionElement. Skipping');
26
+ annotation.classes.push('warning');
27
+ parseResult.push(annotation);
28
+ return parseResult;
29
+ }
30
+
31
+ // set class and metadata from source description element
32
+ parseResult.classes.push('source-description');
33
+ if ((0, _apidomDatamodel.isStringElement)(sourceDescription.name)) parseResult.setMetaProperty('name', (0, _apidomDatamodel.cloneDeep)(sourceDescription.name));
34
+ if ((0, _apidomDatamodel.isStringElement)(sourceDescription.type)) parseResult.setMetaProperty('type', (0, _apidomDatamodel.cloneDeep)(sourceDescription.type));
35
+ const sourceDescriptionURI = (0, _apidomCore.toValue)(sourceDescription.url);
36
+ if (typeof sourceDescriptionURI !== 'string') {
37
+ const annotation = new _apidomDatamodel.AnnotationElement('Source description URL is missing or not a string. Skipping');
38
+ annotation.classes.push('warning');
39
+ parseResult.push(annotation);
40
+ return parseResult;
41
+ }
42
+ const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
43
+
44
+ // skip if already visited (cycle detection)
45
+ if (ctx.visitedUrls.has(retrievalURI)) {
46
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
47
+ annotation.classes.push('warning');
48
+ parseResult.push(annotation);
49
+ return parseResult;
50
+ }
51
+ ctx.visitedUrls.add(retrievalURI);
52
+ try {
53
+ const sourceDescriptionDereferenced = await (0, _index.default)(retrievalURI, (0, _util.merge)(ctx.options, {
54
+ parse: {
55
+ mediaType: 'text/plain' // allow parser plugin detection
56
+ },
57
+ dereference: {
58
+ strategyOpts: {
59
+ [ARAZZO_DEREFERENCE_RECURSION_KEY]: {
60
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
61
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
62
+ }
63
+ }
64
+ }
65
+ }));
66
+ // merge dereferenced result into our parse result
67
+ for (const item of sourceDescriptionDereferenced) {
68
+ parseResult.push(item);
69
+ }
70
+ } catch (error) {
71
+ // create error annotation instead of failing entire dereference
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ const annotation = new _apidomDatamodel.AnnotationElement(`Error dereferencing source description "${retrievalURI}": ${message}`);
74
+ annotation.classes.push('error');
75
+ parseResult.push(annotation);
76
+ return parseResult;
77
+ }
78
+
79
+ // only allow OpenAPI and Arazzo as source descriptions
80
+ const {
81
+ api: sourceDescriptionAPI
82
+ } = parseResult;
83
+ const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
84
+ const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
85
+ if (!isOpenApi && !isArazzo) {
86
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
87
+ annotation.classes.push('warning');
88
+ parseResult.push(annotation);
89
+ return parseResult;
90
+ }
91
+
92
+ // validate declared type matches actual dereferenced type
93
+ const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
94
+ if (typeof declaredType === 'string') {
95
+ if (declaredType === 'openapi' && !isOpenApi) {
96
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but dereferenced as Arazzo document`);
97
+ annotation.classes.push('warning');
98
+ parseResult.push(annotation);
99
+ } else if (declaredType === 'arazzo' && !isArazzo) {
100
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but dereferenced as OpenAPI document`);
101
+ annotation.classes.push('warning');
102
+ parseResult.push(annotation);
103
+ }
104
+ }
105
+ return parseResult;
106
+ }
107
+
108
+ /**
109
+ * Dereferences source descriptions from an Arazzo document.
110
+ * @public
111
+ */
112
+ async function dereferenceSourceDescriptions(parseResult, reference, options) {
113
+ const results = [];
114
+ const strategyName = 'arazzo-1';
115
+
116
+ // get API from dereferenced parse result
117
+ const {
118
+ api
119
+ } = parseResult;
120
+
121
+ /**
122
+ * Validate prerequisites for dereferencing source descriptions.
123
+ * Return warning annotations if validation fails.
124
+ */
125
+ if (!(0, _apidomNsArazzo.isArazzoSpecification1Element)(api)) {
126
+ const annotation = new _apidomDatamodel.AnnotationElement('Cannot dereference source descriptions: API is not an Arazzo specification');
127
+ annotation.classes.push('warning');
128
+ return [new _apidomDatamodel.ParseResultElement([annotation])];
129
+ }
130
+ if (!(0, _apidomDatamodel.isArrayElement)(api.sourceDescriptions)) {
131
+ const annotation = new _apidomDatamodel.AnnotationElement('Cannot dereference source descriptions: sourceDescriptions field is missing or not an array');
132
+ annotation.classes.push('warning');
133
+ return [new _apidomDatamodel.ParseResultElement([annotation])];
134
+ }
135
+
136
+ // user config: strategy-specific options take precedence over global strategyOpts
137
+ const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
138
+
139
+ // recursion state comes from shared key (works across JSON/YAML)
140
+ const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
141
+ const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
142
+ const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
143
+
144
+ // add current file to visited URLs to prevent cycles
145
+ visitedUrls.add(reference.uri);
146
+ if (currentDepth >= maxDepth) {
147
+ const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`);
148
+ annotation.classes.push('error');
149
+ const parseResult = new _apidomDatamodel.ParseResultElement([annotation]);
150
+ parseResult.classes.push('source-description');
151
+ return [parseResult];
152
+ }
153
+ const ctx = {
154
+ baseURI: reference.uri,
155
+ options,
156
+ currentDepth,
157
+ visitedUrls
158
+ };
159
+
160
+ // determine which source descriptions to dereference
161
+ 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
+ const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
168
+ if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
169
+ const name = (0, _apidomCore.toValue)(sd.name);
170
+ return typeof name === 'string' && sourceDescriptionsOption.includes(name);
171
+ }) : api.sourceDescriptions;
172
+
173
+ // process sequentially to ensure proper cycle detection with shared visitedUrls
174
+ for (const sourceDescription of sourceDescriptions) {
175
+ const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
176
+ results.push(sourceDescriptionDereferenceResult);
177
+ }
178
+ return results;
179
+ }
@@ -0,0 +1,172 @@
1
+ import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement, cloneDeep } from '@speclynx/apidom-datamodel';
2
+ import { isArazzoSpecification1Element, isSourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
3
+ import { isSwaggerElement } from '@speclynx/apidom-ns-openapi-2';
4
+ import { isOpenApi3_0Element } from '@speclynx/apidom-ns-openapi-3-0';
5
+ import { isOpenApi3_1Element } from '@speclynx/apidom-ns-openapi-3-1';
6
+ import { toValue } from '@speclynx/apidom-core';
7
+ import * as url from "../../../util/url.mjs";
8
+ import { merge as mergeOptions } from "../../../options/util.mjs";
9
+ import dereference from "../../index.mjs"; // shared key for recursion state (works across JSON/YAML documents)
10
+ const ARAZZO_DEREFERENCE_RECURSION_KEY = 'arazzo-1';
11
+ /**
12
+ * Dereferences a single source description element.
13
+ * Returns ParseResultElement on success, or with annotation if skipped.
14
+ */
15
+ async function dereferenceSourceDescription(sourceDescription, ctx) {
16
+ const parseResult = new ParseResultElement();
17
+ if (!isSourceDescriptionElement(sourceDescription)) {
18
+ const annotation = new AnnotationElement('Element is not a valid SourceDescriptionElement. Skipping');
19
+ annotation.classes.push('warning');
20
+ parseResult.push(annotation);
21
+ return parseResult;
22
+ }
23
+
24
+ // set class and metadata from source description element
25
+ parseResult.classes.push('source-description');
26
+ if (isStringElement(sourceDescription.name)) parseResult.setMetaProperty('name', cloneDeep(sourceDescription.name));
27
+ if (isStringElement(sourceDescription.type)) parseResult.setMetaProperty('type', cloneDeep(sourceDescription.type));
28
+ const sourceDescriptionURI = toValue(sourceDescription.url);
29
+ if (typeof sourceDescriptionURI !== 'string') {
30
+ const annotation = new AnnotationElement('Source description URL is missing or not a string. Skipping');
31
+ annotation.classes.push('warning');
32
+ parseResult.push(annotation);
33
+ return parseResult;
34
+ }
35
+ const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
36
+
37
+ // skip if already visited (cycle detection)
38
+ if (ctx.visitedUrls.has(retrievalURI)) {
39
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
40
+ annotation.classes.push('warning');
41
+ parseResult.push(annotation);
42
+ return parseResult;
43
+ }
44
+ ctx.visitedUrls.add(retrievalURI);
45
+ try {
46
+ const sourceDescriptionDereferenced = await dereference(retrievalURI, mergeOptions(ctx.options, {
47
+ parse: {
48
+ mediaType: 'text/plain' // allow parser plugin detection
49
+ },
50
+ dereference: {
51
+ strategyOpts: {
52
+ [ARAZZO_DEREFERENCE_RECURSION_KEY]: {
53
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
54
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
55
+ }
56
+ }
57
+ }
58
+ }));
59
+ // merge dereferenced result into our parse result
60
+ for (const item of sourceDescriptionDereferenced) {
61
+ parseResult.push(item);
62
+ }
63
+ } catch (error) {
64
+ // create error annotation instead of failing entire dereference
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ const annotation = new AnnotationElement(`Error dereferencing source description "${retrievalURI}": ${message}`);
67
+ annotation.classes.push('error');
68
+ parseResult.push(annotation);
69
+ return parseResult;
70
+ }
71
+
72
+ // only allow OpenAPI and Arazzo as source descriptions
73
+ const {
74
+ api: sourceDescriptionAPI
75
+ } = parseResult;
76
+ const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
77
+ const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
78
+ if (!isOpenApi && !isArazzo) {
79
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
80
+ annotation.classes.push('warning');
81
+ parseResult.push(annotation);
82
+ return parseResult;
83
+ }
84
+
85
+ // validate declared type matches actual dereferenced type
86
+ const declaredType = toValue(sourceDescription.type);
87
+ if (typeof declaredType === 'string') {
88
+ if (declaredType === 'openapi' && !isOpenApi) {
89
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but dereferenced as Arazzo document`);
90
+ annotation.classes.push('warning');
91
+ parseResult.push(annotation);
92
+ } else if (declaredType === 'arazzo' && !isArazzo) {
93
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but dereferenced as OpenAPI document`);
94
+ annotation.classes.push('warning');
95
+ parseResult.push(annotation);
96
+ }
97
+ }
98
+ return parseResult;
99
+ }
100
+
101
+ /**
102
+ * Dereferences source descriptions from an Arazzo document.
103
+ * @public
104
+ */
105
+ export async function dereferenceSourceDescriptions(parseResult, reference, options) {
106
+ const results = [];
107
+ const strategyName = 'arazzo-1';
108
+
109
+ // get API from dereferenced parse result
110
+ const {
111
+ api
112
+ } = parseResult;
113
+
114
+ /**
115
+ * Validate prerequisites for dereferencing source descriptions.
116
+ * Return warning annotations if validation fails.
117
+ */
118
+ if (!isArazzoSpecification1Element(api)) {
119
+ const annotation = new AnnotationElement('Cannot dereference source descriptions: API is not an Arazzo specification');
120
+ annotation.classes.push('warning');
121
+ return [new ParseResultElement([annotation])];
122
+ }
123
+ if (!isArrayElement(api.sourceDescriptions)) {
124
+ const annotation = new AnnotationElement('Cannot dereference source descriptions: sourceDescriptions field is missing or not an array');
125
+ annotation.classes.push('warning');
126
+ return [new ParseResultElement([annotation])];
127
+ }
128
+
129
+ // user config: strategy-specific options take precedence over global strategyOpts
130
+ const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
131
+
132
+ // recursion state comes from shared key (works across JSON/YAML)
133
+ const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
134
+ const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
135
+ const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
136
+
137
+ // add current file to visited URLs to prevent cycles
138
+ visitedUrls.add(reference.uri);
139
+ if (currentDepth >= maxDepth) {
140
+ const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`);
141
+ annotation.classes.push('error');
142
+ const parseResult = new ParseResultElement([annotation]);
143
+ parseResult.classes.push('source-description');
144
+ return [parseResult];
145
+ }
146
+ const ctx = {
147
+ baseURI: reference.uri,
148
+ options,
149
+ currentDepth,
150
+ visitedUrls
151
+ };
152
+
153
+ // determine which source descriptions to dereference
154
+ 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
+ const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
161
+ if (!isSourceDescriptionElement(sd)) return false;
162
+ const name = toValue(sd.name);
163
+ return typeof name === 'string' && sourceDescriptionsOption.includes(name);
164
+ }) : api.sourceDescriptions;
165
+
166
+ // process sequentially to ensure proper cycle detection with shared visitedUrls
167
+ for (const sourceDescription of sourceDescriptions) {
168
+ const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
169
+ results.push(sourceDescriptionDereferenceResult);
170
+ }
171
+ return results;
172
+ }
@@ -389,7 +389,7 @@ class OpenAPI3_0DereferenceVisitor {
389
389
 
390
390
  // operationRef and operationId fields are mutually exclusive
391
391
  if ((0, _apidomDatamodel.isStringElement)(linkElement.operationRef) && (0, _apidomDatamodel.isStringElement)(linkElement.operationId)) {
392
- throw new _apidomError.ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive.');
392
+ throw new _apidomError.ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive');
393
393
  }
394
394
  let operationElement;
395
395
  if ((0, _apidomDatamodel.isStringElement)(linkElement.operationRef)) {
@@ -440,7 +440,7 @@ class OpenAPI3_0DereferenceVisitor {
440
440
  operationElement = (0, _apidomTraverse.find)(reference.value.result, e => (0, _apidomNsOpenapi.isOperationElement)(e) && (0, _apidomDatamodel.isElement)(e.operationId) && e.operationId.equals(operationId));
441
441
  // OperationElement not found by its operationId
442
442
  if ((0, _ramdaAdjunct.isUndefined)(operationElement)) {
443
- throw new _apidomError.ApiDOMError(`OperationElement(operationId=${operationId}) not found.`);
443
+ throw new _apidomError.ApiDOMError(`OperationElement(operationId=${operationId}) not found`);
444
444
  }
445
445
  const linkElementCopy = (0, _apidomDatamodel.cloneShallow)(linkElement);
446
446
  linkElementCopy.operationId?.meta.set('operation', operationElement);
@@ -462,7 +462,7 @@ class OpenAPI3_0DereferenceVisitor {
462
462
 
463
463
  // value and externalValue fields are mutually exclusive
464
464
  if (exampleElement.hasKey('value') && (0, _apidomDatamodel.isStringElement)(exampleElement.externalValue)) {
465
- throw new _apidomError.ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive.');
465
+ throw new _apidomError.ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive');
466
466
  }
467
467
  const retrievalURI = this.toBaseURI((0, _apidomCore.toValue)(exampleElement.externalValue));
468
468
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
@@ -383,7 +383,7 @@ class OpenAPI3_0DereferenceVisitor {
383
383
 
384
384
  // operationRef and operationId fields are mutually exclusive
385
385
  if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
386
- throw new ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive.');
386
+ throw new ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive');
387
387
  }
388
388
  let operationElement;
389
389
  if (isStringElement(linkElement.operationRef)) {
@@ -434,7 +434,7 @@ class OpenAPI3_0DereferenceVisitor {
434
434
  operationElement = find(reference.value.result, e => isOperationElement(e) && isElement(e.operationId) && e.operationId.equals(operationId));
435
435
  // OperationElement not found by its operationId
436
436
  if (isUndefined(operationElement)) {
437
- throw new ApiDOMError(`OperationElement(operationId=${operationId}) not found.`);
437
+ throw new ApiDOMError(`OperationElement(operationId=${operationId}) not found`);
438
438
  }
439
439
  const linkElementCopy = cloneShallow(linkElement);
440
440
  linkElementCopy.operationId?.meta.set('operation', operationElement);
@@ -456,7 +456,7 @@ class OpenAPI3_0DereferenceVisitor {
456
456
 
457
457
  // value and externalValue fields are mutually exclusive
458
458
  if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
459
- throw new ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive.');
459
+ throw new ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive');
460
460
  }
461
461
  const retrievalURI = this.toBaseURI(toValue(exampleElement.externalValue));
462
462
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
@@ -411,7 +411,7 @@ class OpenAPI3_1DereferenceVisitor {
411
411
 
412
412
  // operationRef and operationId fields are mutually exclusive
413
413
  if ((0, _apidomDatamodel.isStringElement)(linkElement.operationRef) && (0, _apidomDatamodel.isStringElement)(linkElement.operationId)) {
414
- throw new _apidomError.ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive.');
414
+ throw new _apidomError.ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive');
415
415
  }
416
416
  let operationElement;
417
417
  if ((0, _apidomDatamodel.isStringElement)(linkElement.operationRef)) {
@@ -462,7 +462,7 @@ class OpenAPI3_1DereferenceVisitor {
462
462
  operationElement = (0, _apidomTraverse.find)(reference.value.result, e => (0, _apidomNsOpenapi.isOperationElement)(e) && (0, _apidomDatamodel.isElement)(e.operationId) && e.operationId.equals(operationId));
463
463
  // OperationElement not found by its operationId
464
464
  if ((0, _ramdaAdjunct.isUndefined)(operationElement)) {
465
- throw new _apidomError.ApiDOMError(`OperationElement(operationId=${operationId}) not found.`);
465
+ throw new _apidomError.ApiDOMError(`OperationElement(operationId=${operationId}) not found`);
466
466
  }
467
467
  const linkElementCopy = (0, _apidomDatamodel.cloneShallow)(linkElement);
468
468
  linkElementCopy.operationId?.meta.set('operation', operationElement);
@@ -483,7 +483,7 @@ class OpenAPI3_1DereferenceVisitor {
483
483
 
484
484
  // value and externalValue fields are mutually exclusive
485
485
  if (exampleElement.hasKey('value') && (0, _apidomDatamodel.isStringElement)(exampleElement.externalValue)) {
486
- throw new _apidomError.ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive.');
486
+ throw new _apidomError.ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive');
487
487
  }
488
488
  const retrievalURI = this.toBaseURI((0, _apidomCore.toValue)(exampleElement.externalValue));
489
489
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;