@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speclynx/apidom-reference",
3
- "version": "2.8.0",
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.8.0",
235
- "@speclynx/apidom-datamodel": "^2.8.0",
236
- "@speclynx/apidom-error": "^2.8.0",
237
- "@speclynx/apidom-json-pointer": "^2.8.0",
238
- "@speclynx/apidom-ns-arazzo-1": "^2.8.0",
239
- "@speclynx/apidom-ns-asyncapi-2": "^2.8.0",
240
- "@speclynx/apidom-ns-json-schema-2020-12": "^2.8.0",
241
- "@speclynx/apidom-ns-openapi-2": "^2.8.0",
242
- "@speclynx/apidom-ns-openapi-3-0": "^2.8.0",
243
- "@speclynx/apidom-ns-openapi-3-1": "^2.8.0",
244
- "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.8.0",
245
- "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.8.0",
246
- "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.8.0",
247
- "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.8.0",
248
- "@speclynx/apidom-parser-adapter-json": "^2.8.0",
249
- "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.8.0",
250
- "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.8.0",
251
- "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.8.0",
252
- "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.8.0",
253
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.8.0",
254
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.8.0",
255
- "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.8.0",
256
- "@speclynx/apidom-traverse": "^2.8.0",
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": "467e34a8f8a9d56622faf0794e4ef9c1fe86b849"
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 _sourceDescription = require("./source-description.cjs");
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, _sourceDescription.dereferenceSourceDescriptions)(dereferencedElement, reference, options);
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-description.mjs";
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;
@@ -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 = _interopRequireDefault(require("../../index.cjs"));
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
- const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
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
- 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
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, reference, options) {
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 shared key (works across JSON/YAML)
140
- const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
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(reference.uri);
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 "${reference.uri}"`);
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: reference.uri,
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;
@@ -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"; // shared key for recursion state (works across JSON/YAML documents)
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
- const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
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
- 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
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, reference, options) {
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 shared key (works across JSON/YAML)
133
- const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
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(reference.uri);
211
+ visitedUrls.add(baseURI);
139
212
  if (currentDepth >= maxDepth) {
140
- const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`);
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: reference.uri,
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
- const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
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 (caller responsibility to construct)
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. On success, returns one ParseResultElement per
116
- * source description (each with class 'source-description' and name/type metadata).
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 sourceDescriptions option is disabled or no names match.
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
- * const fullOptions = mergeOptions(options, {
129
- * parse: { parserOpts: { sourceDescriptions: true } }
130
- * });
131
- * const results = await parseSourceDescriptions(parseResult, uri, fullOptions);
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;