@speclynx/apidom-reference 2.8.0 → 2.9.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.9.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.9.0",
235
+ "@speclynx/apidom-datamodel": "^2.9.0",
236
+ "@speclynx/apidom-error": "^2.9.0",
237
+ "@speclynx/apidom-json-pointer": "^2.9.0",
238
+ "@speclynx/apidom-ns-arazzo-1": "^2.9.0",
239
+ "@speclynx/apidom-ns-asyncapi-2": "^2.9.0",
240
+ "@speclynx/apidom-ns-json-schema-2020-12": "^2.9.0",
241
+ "@speclynx/apidom-ns-openapi-2": "^2.9.0",
242
+ "@speclynx/apidom-ns-openapi-3-0": "^2.9.0",
243
+ "@speclynx/apidom-ns-openapi-3-1": "^2.9.0",
244
+ "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.9.0",
245
+ "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.9.0",
246
+ "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.9.0",
247
+ "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.9.0",
248
+ "@speclynx/apidom-parser-adapter-json": "^2.9.0",
249
+ "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.9.0",
250
+ "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.9.0",
251
+ "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.9.0",
252
+ "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.9.0",
253
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.9.0",
254
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.9.0",
255
+ "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.9.0",
256
+ "@speclynx/apidom-traverse": "^2.9.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": "171956df4417a88238134c1854a5ff823aa144d8"
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,9 @@ 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)));
43
42
 
44
43
  // skip if already visited (cycle detection)
45
44
  if (ctx.visitedUrls.has(retrievalURI)) {
@@ -49,20 +48,54 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
49
48
  return parseResult;
50
49
  }
51
50
  ctx.visitedUrls.add(retrievalURI);
51
+
52
+ // check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
53
+ const existingParseResult = sourceDescription.meta.get('parseResult');
52
54
  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
55
+ let sourceDescriptionDereferenced;
56
+ if ((0, _apidomDatamodel.isParseResultElement)(existingParseResult)) {
57
+ // use existing parsed result - just dereference it (no re-fetch/re-parse)
58
+ sourceDescriptionDereferenced = await (0, _index.dereferenceApiDOM)(existingParseResult, (0, _util.merge)(ctx.options, {
59
+ parse: {
60
+ mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
61
+ },
62
+ resolve: {
63
+ baseURI: retrievalURI
64
+ },
65
+ dereference: {
66
+ strategyOpts: {
67
+ // nested documents should dereference all their source descriptions
68
+ // (parent's name filter doesn't apply to nested documents)
69
+ // set at strategy-specific level to override any inherited filters
70
+ [ctx.strategyName]: {
71
+ sourceDescriptions: true,
72
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
73
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
74
+ }
75
+ }
76
+ }
77
+ }));
78
+ } else {
79
+ // no existing parse result - fetch, parse, and dereference
80
+ sourceDescriptionDereferenced = await (0, _index.default)(retrievalURI, (0, _util.merge)(ctx.options, {
81
+ parse: {
82
+ mediaType: 'text/plain' // allow parser plugin detection
83
+ },
84
+ dereference: {
85
+ strategyOpts: {
86
+ // nested documents should dereference all their source descriptions
87
+ // (parent's name filter doesn't apply to nested documents)
88
+ // set at strategy-specific level to override any inherited filters
89
+ [ctx.strategyName]: {
90
+ sourceDescriptions: true,
91
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
92
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
93
+ }
62
94
  }
63
95
  }
64
- }
65
- }));
96
+ }));
97
+ }
98
+
66
99
  // merge dereferenced result into our parse result
67
100
  for (const item of sourceDescriptionDereferenced) {
68
101
  parseResult.push(item);
@@ -107,11 +140,45 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
107
140
 
108
141
  /**
109
142
  * Dereferences source descriptions from an Arazzo document.
143
+ *
144
+ * Each source description result is attached to its corresponding
145
+ * SourceDescriptionElement's meta as 'parseResult' for easy access,
146
+ * regardless of success or failure. On failure, the ParseResultElement
147
+ * contains annotations explaining what went wrong.
148
+ *
149
+ * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
150
+ * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
151
+ * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
152
+ * in `dereference.strategyOpts` to filter which source descriptions to process.
153
+ * @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
154
+ * @returns Array of ParseResultElements. Returns one ParseResultElement per source description
155
+ * (each with class 'source-description' and name/type metadata).
156
+ * May return early with a single-element array containing a warning annotation when:
157
+ * - The API is not an Arazzo specification
158
+ * - The sourceDescriptions field is missing or not an array
159
+ * - Maximum dereference depth is exceeded (error annotation)
160
+ * Returns an empty array when no source description names match the filter.
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * // Dereference all source descriptions
165
+ * await dereferenceSourceDescriptions(parseResult, uri, options);
166
+ *
167
+ * // Filter by name
168
+ * await dereferenceSourceDescriptions(parseResult, uri, mergeOptions(options, {
169
+ * dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
170
+ * }));
171
+ *
172
+ * // Access dereferenced document from source description element
173
+ * const sourceDesc = parseResult.api.sourceDescriptions.get(0);
174
+ * const dereferencedDoc = sourceDesc.meta.get('parseResult');
175
+ * ```
176
+ *
110
177
  * @public
111
178
  */
112
- async function dereferenceSourceDescriptions(parseResult, reference, options) {
179
+ async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
180
+ const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
113
181
  const results = [];
114
- const strategyName = 'arazzo-1';
115
182
 
116
183
  // get API from dereferenced parse result
117
184
  const {
@@ -136,34 +203,30 @@ async function dereferenceSourceDescriptions(parseResult, reference, options) {
136
203
  // user config: strategy-specific options take precedence over global strategyOpts
137
204
  const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
138
205
 
139
- // recursion state comes from shared key (works across JSON/YAML)
140
- const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
206
+ // recursion state comes from strategy-specific options
207
+ const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
141
208
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
142
209
  const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
143
210
 
144
211
  // add current file to visited URLs to prevent cycles
145
- visitedUrls.add(reference.uri);
212
+ visitedUrls.add(baseURI);
146
213
  if (currentDepth >= maxDepth) {
147
- const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`);
214
+ const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
148
215
  annotation.classes.push('error');
149
216
  const parseResult = new _apidomDatamodel.ParseResultElement([annotation]);
150
217
  parseResult.classes.push('source-description');
151
218
  return [parseResult];
152
219
  }
153
220
  const ctx = {
154
- baseURI: reference.uri,
221
+ baseURI,
155
222
  options,
223
+ strategyName,
156
224
  currentDepth,
157
225
  visitedUrls
158
226
  };
159
227
 
160
- // determine which source descriptions to dereference
228
+ // determine which source descriptions to dereference (array filters by name)
161
229
  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
230
  const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
168
231
  if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
169
232
  const name = (0, _apidomCore.toValue)(sd.name);
@@ -173,6 +236,8 @@ async function dereferenceSourceDescriptions(parseResult, reference, options) {
173
236
  // process sequentially to ensure proper cycle detection with shared visitedUrls
174
237
  for (const sourceDescription of sourceDescriptions) {
175
238
  const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
239
+ // always attach result (even on failure - contains annotations)
240
+ sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
176
241
  results.push(sourceDescriptionDereferenceResult);
177
242
  }
178
243
  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,9 @@ 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)));
36
37
 
37
38
  // skip if already visited (cycle detection)
38
39
  if (ctx.visitedUrls.has(retrievalURI)) {
@@ -42,20 +43,54 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
42
43
  return parseResult;
43
44
  }
44
45
  ctx.visitedUrls.add(retrievalURI);
46
+
47
+ // check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
48
+ const existingParseResult = sourceDescription.meta.get('parseResult');
45
49
  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
50
+ let sourceDescriptionDereferenced;
51
+ if (isParseResultElement(existingParseResult)) {
52
+ // use existing parsed result - just dereference it (no re-fetch/re-parse)
53
+ sourceDescriptionDereferenced = await dereferenceApiDOM(existingParseResult, mergeOptions(ctx.options, {
54
+ parse: {
55
+ mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
56
+ },
57
+ resolve: {
58
+ baseURI: retrievalURI
59
+ },
60
+ dereference: {
61
+ strategyOpts: {
62
+ // nested documents should dereference all their source descriptions
63
+ // (parent's name filter doesn't apply to nested documents)
64
+ // set at strategy-specific level to override any inherited filters
65
+ [ctx.strategyName]: {
66
+ sourceDescriptions: true,
67
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
68
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
69
+ }
70
+ }
71
+ }
72
+ }));
73
+ } else {
74
+ // no existing parse result - fetch, parse, and dereference
75
+ sourceDescriptionDereferenced = await dereference(retrievalURI, mergeOptions(ctx.options, {
76
+ parse: {
77
+ mediaType: 'text/plain' // allow parser plugin detection
78
+ },
79
+ dereference: {
80
+ strategyOpts: {
81
+ // nested documents should dereference all their source descriptions
82
+ // (parent's name filter doesn't apply to nested documents)
83
+ // set at strategy-specific level to override any inherited filters
84
+ [ctx.strategyName]: {
85
+ sourceDescriptions: true,
86
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
87
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
88
+ }
55
89
  }
56
90
  }
57
- }
58
- }));
91
+ }));
92
+ }
93
+
59
94
  // merge dereferenced result into our parse result
60
95
  for (const item of sourceDescriptionDereferenced) {
61
96
  parseResult.push(item);
@@ -100,11 +135,45 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
100
135
 
101
136
  /**
102
137
  * Dereferences source descriptions from an Arazzo document.
138
+ *
139
+ * Each source description result is attached to its corresponding
140
+ * SourceDescriptionElement's meta as 'parseResult' for easy access,
141
+ * regardless of success or failure. On failure, the ParseResultElement
142
+ * contains annotations explaining what went wrong.
143
+ *
144
+ * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
145
+ * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
146
+ * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
147
+ * in `dereference.strategyOpts` to filter which source descriptions to process.
148
+ * @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
149
+ * @returns Array of ParseResultElements. Returns one ParseResultElement per source description
150
+ * (each with class 'source-description' and name/type metadata).
151
+ * May return early with a single-element array containing a warning annotation when:
152
+ * - The API is not an Arazzo specification
153
+ * - The sourceDescriptions field is missing or not an array
154
+ * - Maximum dereference depth is exceeded (error annotation)
155
+ * Returns an empty array when no source description names match the filter.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * // Dereference all source descriptions
160
+ * await dereferenceSourceDescriptions(parseResult, uri, options);
161
+ *
162
+ * // Filter by name
163
+ * await dereferenceSourceDescriptions(parseResult, uri, mergeOptions(options, {
164
+ * dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
165
+ * }));
166
+ *
167
+ * // Access dereferenced document from source description element
168
+ * const sourceDesc = parseResult.api.sourceDescriptions.get(0);
169
+ * const dereferencedDoc = sourceDesc.meta.get('parseResult');
170
+ * ```
171
+ *
103
172
  * @public
104
173
  */
105
- export async function dereferenceSourceDescriptions(parseResult, reference, options) {
174
+ export async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
175
+ const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
106
176
  const results = [];
107
- const strategyName = 'arazzo-1';
108
177
 
109
178
  // get API from dereferenced parse result
110
179
  const {
@@ -129,34 +198,30 @@ export async function dereferenceSourceDescriptions(parseResult, reference, opti
129
198
  // user config: strategy-specific options take precedence over global strategyOpts
130
199
  const maxDepth = options?.dereference?.strategyOpts?.[strategyName]?.sourceDescriptionsMaxDepth ?? options?.dereference?.strategyOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
131
200
 
132
- // recursion state comes from shared key (works across JSON/YAML)
133
- const sharedOpts = options?.dereference?.strategyOpts?.[ARAZZO_DEREFERENCE_RECURSION_KEY] ?? {};
201
+ // recursion state comes from strategy-specific options
202
+ const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
134
203
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
135
204
  const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
136
205
 
137
206
  // add current file to visited URLs to prevent cycles
138
- visitedUrls.add(reference.uri);
207
+ visitedUrls.add(baseURI);
139
208
  if (currentDepth >= maxDepth) {
140
- const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`);
209
+ const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
141
210
  annotation.classes.push('error');
142
211
  const parseResult = new ParseResultElement([annotation]);
143
212
  parseResult.classes.push('source-description');
144
213
  return [parseResult];
145
214
  }
146
215
  const ctx = {
147
- baseURI: reference.uri,
216
+ baseURI,
148
217
  options,
218
+ strategyName,
149
219
  currentDepth,
150
220
  visitedUrls
151
221
  };
152
222
 
153
- // determine which source descriptions to dereference
223
+ // determine which source descriptions to dereference (array filters by name)
154
224
  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
225
  const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
161
226
  if (!isSourceDescriptionElement(sd)) return false;
162
227
  const name = toValue(sd.name);
@@ -166,6 +231,8 @@ export async function dereferenceSourceDescriptions(parseResult, reference, opti
166
231
  // process sequentially to ensure proper cycle detection with shared visitedUrls
167
232
  for (const sourceDescription of sourceDescriptions) {
168
233
  const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
234
+ // always attach result (even on failure - contains annotations)
235
+ sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
169
236
  results.push(sourceDescriptionDereferenceResult);
170
237
  }
171
238
  return results;
@@ -40,7 +40,9 @@ 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)));
44
46
 
45
47
  // skip if already visited (cycle detection)
46
48
  if (ctx.visitedUrls.has(retrievalURI)) {
@@ -56,6 +58,9 @@ async function parseSourceDescription(sourceDescription, ctx) {
56
58
  mediaType: 'text/plain',
57
59
  // allow parser plugin detection
58
60
  parserOpts: {
61
+ // nested documents should parse all their source descriptions
62
+ // (parent's name filter doesn't apply to nested documents)
63
+ sourceDescriptions: true,
59
64
  [ARAZZO_RECURSION_KEY]: {
60
65
  sourceDescriptionsDepth: ctx.currentDepth + 1,
61
66
  sourceDescriptionsVisitedUrls: ctx.visitedUrls
@@ -108,27 +113,40 @@ async function parseSourceDescription(sourceDescription, ctx) {
108
113
  /**
109
114
  * Parses source descriptions from an Arazzo document's ParseResult.
110
115
  *
116
+ * Each source description result is attached to its corresponding
117
+ * SourceDescriptionElement's meta as 'parseResult' for easy access,
118
+ * regardless of success or failure. On failure, the ParseResultElement
119
+ * contains annotations explaining what went wrong.
120
+ *
111
121
  * @param parseResult - ParseResult containing an Arazzo specification
112
122
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
113
- * @param options - Full ReferenceOptions (caller responsibility to construct)
123
+ * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
124
+ * in `parse.parserOpts` to filter which source descriptions to process.
114
125
  * @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).
126
+ * @returns Array of ParseResultElements. Returns one ParseResultElement per source description
127
+ * (each with class 'source-description' and name/type metadata).
117
128
  * May return early with a single-element array containing a warning annotation when:
118
129
  * - The API is not an Arazzo specification
119
130
  * - The sourceDescriptions field is missing or not an array
120
131
  * - Maximum parse depth is exceeded (error annotation)
121
- * Returns an empty array when sourceDescriptions option is disabled or no names match.
132
+ * Returns an empty array when no source description names match the filter.
122
133
  *
123
134
  * @example
124
135
  * ```typescript
125
136
  * import { options, mergeOptions } from '@speclynx/apidom-reference';
126
137
  * import { parseSourceDescriptions } from '@speclynx/apidom-reference/parse/parsers/arazzo-json-1';
127
138
  *
128
- * const fullOptions = mergeOptions(options, {
129
- * parse: { parserOpts: { sourceDescriptions: true } }
130
- * });
131
- * const results = await parseSourceDescriptions(parseResult, uri, fullOptions);
139
+ * // Parse all source descriptions
140
+ * const results = await parseSourceDescriptions(parseResult, uri, options);
141
+ *
142
+ * // Filter by name
143
+ * const filtered = await parseSourceDescriptions(parseResult, uri, mergeOptions(options, {
144
+ * parse: { parserOpts: { sourceDescriptions: ['petStore'] } }
145
+ * }));
146
+ *
147
+ * // Access parsed document from source description element
148
+ * const sourceDesc = parseResult.api.sourceDescriptions.get(0);
149
+ * const parsedDoc = sourceDesc.meta.get('parseResult');
132
150
  * ```
133
151
  *
134
152
  * @public
@@ -181,13 +199,8 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
181
199
  visitedUrls
182
200
  };
183
201
 
184
- // determine which source descriptions to parse
202
+ // determine which source descriptions to parse (array filters by name)
185
203
  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
204
  const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
192
205
  if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
193
206
  const name = (0, _apidomCore.toValue)(sd.name);
@@ -197,6 +210,8 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
197
210
  // process sequentially to ensure proper cycle detection with shared visitedUrls
198
211
  for (const sourceDescription of sourceDescriptions) {
199
212
  const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
213
+ // always attach result (even on failure - contains annotations)
214
+ sourceDescription.meta.set('parseResult', sourceDescriptionParseResult);
200
215
  results.push(sourceDescriptionParseResult);
201
216
  }
202
217
  return results;