@speclynx/apidom-reference 5.1.1 → 5.2.2

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": "5.1.1",
3
+ "version": "5.2.2",
4
4
  "description": "Advanced algorithms for semantic ApiDOM manipulations like dereferencing or resolution.",
5
5
  "keywords": [
6
6
  "apidom",
@@ -299,35 +299,35 @@
299
299
  "license": "Apache-2.0",
300
300
  "dependencies": {
301
301
  "@babel/runtime-corejs3": "^8.0.0",
302
- "@speclynx/apidom-core": "5.1.1",
303
- "@speclynx/apidom-datamodel": "5.1.1",
304
- "@speclynx/apidom-error": "5.1.1",
305
- "@speclynx/apidom-json-pointer": "5.1.1",
306
- "@speclynx/apidom-ns-arazzo-1": "5.1.1",
307
- "@speclynx/apidom-ns-asyncapi-2": "5.1.1",
308
- "@speclynx/apidom-ns-json-schema-2020-12": "5.1.1",
309
- "@speclynx/apidom-ns-openapi-2": "5.1.1",
310
- "@speclynx/apidom-ns-openapi-3-0": "5.1.1",
311
- "@speclynx/apidom-ns-openapi-3-1": "5.1.1",
312
- "@speclynx/apidom-ns-overlay-1": "5.1.1",
313
- "@speclynx/apidom-parser-adapter-arazzo-json-1": "5.1.1",
314
- "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "5.1.1",
315
- "@speclynx/apidom-parser-adapter-asyncapi-json-2": "5.1.1",
316
- "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "5.1.1",
317
- "@speclynx/apidom-parser-adapter-json": "5.1.1",
318
- "@speclynx/apidom-parser-adapter-openapi-json-2": "5.1.1",
319
- "@speclynx/apidom-parser-adapter-openapi-json-3-0": "5.1.1",
320
- "@speclynx/apidom-parser-adapter-openapi-json-3-1": "5.1.1",
321
- "@speclynx/apidom-parser-adapter-openapi-yaml-2": "5.1.1",
322
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "5.1.1",
323
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "5.1.1",
324
- "@speclynx/apidom-parser-adapter-overlay-json-1": "5.1.1",
325
- "@speclynx/apidom-parser-adapter-overlay-yaml-1": "5.1.1",
326
- "@speclynx/apidom-parser-adapter-yaml-1-2": "5.1.1",
327
- "@speclynx/apidom-traverse": "5.1.1",
328
- "@swaggerexpert/arazzo-runtime-expression": "^3.1.0",
329
- "axios": "^1.19.0",
330
- "picomatch": "^4.0.5",
302
+ "@speclynx/apidom-core": "5.2.2",
303
+ "@speclynx/apidom-datamodel": "5.2.2",
304
+ "@speclynx/apidom-error": "5.2.2",
305
+ "@speclynx/apidom-json-pointer": "5.2.2",
306
+ "@speclynx/apidom-ns-arazzo-1": "5.2.2",
307
+ "@speclynx/apidom-ns-asyncapi-2": "5.2.2",
308
+ "@speclynx/apidom-ns-json-schema-2020-12": "5.2.2",
309
+ "@speclynx/apidom-ns-openapi-2": "5.2.2",
310
+ "@speclynx/apidom-ns-openapi-3-0": "5.2.2",
311
+ "@speclynx/apidom-ns-openapi-3-1": "5.2.2",
312
+ "@speclynx/apidom-ns-overlay-1": "5.2.2",
313
+ "@speclynx/apidom-parser-adapter-arazzo-json-1": "5.2.2",
314
+ "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "5.2.2",
315
+ "@speclynx/apidom-parser-adapter-asyncapi-json-2": "5.2.2",
316
+ "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "5.2.2",
317
+ "@speclynx/apidom-parser-adapter-json": "5.2.2",
318
+ "@speclynx/apidom-parser-adapter-openapi-json-2": "5.2.2",
319
+ "@speclynx/apidom-parser-adapter-openapi-json-3-0": "5.2.2",
320
+ "@speclynx/apidom-parser-adapter-openapi-json-3-1": "5.2.2",
321
+ "@speclynx/apidom-parser-adapter-openapi-yaml-2": "5.2.2",
322
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "5.2.2",
323
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "5.2.2",
324
+ "@speclynx/apidom-parser-adapter-overlay-json-1": "5.2.2",
325
+ "@speclynx/apidom-parser-adapter-overlay-yaml-1": "5.2.2",
326
+ "@speclynx/apidom-parser-adapter-yaml-1-2": "5.2.2",
327
+ "@speclynx/apidom-traverse": "5.2.2",
328
+ "@swaggerexpert/arazzo-runtime-expression": "^3.2.0",
329
+ "axios": "^1.20.0",
330
+ "picomatch": "^4.0.7",
331
331
  "process": "^0.11.10",
332
332
  "ramda": "~0.32.0",
333
333
  "ramda-adjunct": "^6.1.0"
@@ -346,5 +346,5 @@
346
346
  "README.md",
347
347
  "CHANGELOG.md"
348
348
  ],
349
- "gitHead": "b661f6f038953d118ad09eec0d7da7927db46f5c"
349
+ "gitHead": "2d0e035bf82913a9870ff7b26069e784e2a0e9a3"
350
350
  }
@@ -43,6 +43,13 @@ const cf = (0, _apidomCore.fixedFields)(_apidomNsArazzo.ComponentsElement, {
43
43
  */
44
44
  class Arazzo1BundleVisitor {
45
45
  reference;
46
+
47
+ /**
48
+ * Base URI of the current document for resolving relative references.
49
+ * Honors the Arazzo `$self` field when present, otherwise falls back
50
+ * to the retrieval URI (`this.reference.uri`).
51
+ */
52
+ baseURI;
46
53
  options;
47
54
 
48
55
  /**
@@ -81,12 +88,13 @@ class Arazzo1BundleVisitor {
81
88
  reservedNames = new Map()
82
89
  }) {
83
90
  this.reference = reference;
91
+ this.baseURI = (0, _util.resolveArazzo$selfField)(reference.uri, reference.value?.result);
84
92
  this.options = options;
85
93
  this.assignments = assignments;
86
94
  this.reservedNames = reservedNames;
87
95
  }
88
96
  toBaseURI(uri) {
89
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
97
+ return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
90
98
  }
91
99
  async toReference(uri) {
92
100
  // detect maximum depth of resolution
@@ -105,6 +113,12 @@ class Arazzo1BundleVisitor {
105
113
  if (refSet.has(baseURI)) {
106
114
  return refSet.find(ref => ref.uri === baseURI);
107
115
  }
116
+
117
+ // identity-based referencing: URI matches `$self` of an already processed Arazzo document
118
+ const referenceBy$self = refSet.find((0, _util.identifiedBy$self)(baseURI));
119
+ if (referenceBy$self !== undefined) {
120
+ return referenceBy$self;
121
+ }
108
122
  const parseResult = await (0, _index.default)(url.unsanitize(baseURI), {
109
123
  ...this.options,
110
124
  parse: {
@@ -279,19 +293,18 @@ class Arazzo1BundleVisitor {
279
293
  }
280
294
  const $ref = (0, _apidomCore.toValue)(referencingElement.$ref);
281
295
  try {
282
- // compute baseURI using rules around $id and $ref keywords. The current
283
- // document's URI is the retrieval URI — derive it directly (not via
284
- // toReference) so an internal $ref needs no resolution and never trips the
296
+ // compute baseURI using rules around $self, $id and $ref keywords. The
297
+ // current document's base URI is derived directly (not via toReference)
298
+ // so an internal $ref needs no resolution and never trips the
285
299
  // resolve.maxDepth guard.
286
- const retrievalURI = this.reference.uri;
287
- const $refBaseURI = (0, _util.resolveSchema$refField)(retrievalURI, referencingElement);
300
+ const $refBaseURI = (0, _util.resolveSchema$refField)(this.baseURI, referencingElement);
288
301
  const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
289
302
  const file = new _File.default({
290
303
  uri: $refBaseURIStrippedHash
291
304
  });
292
305
  const isUnknownURI = (0, _ramda.none)(r => r.canRead(file), this.options.resolve.resolvers);
293
306
  const isURL = !isUnknownURI;
294
- const isInternalReference = url.stripHash(this.reference.uri) === $refBaseURIStrippedHash;
307
+ const isInternalReference = this.baseURI === $refBaseURIStrippedHash;
295
308
  const isExternalReference = !isInternalReference;
296
309
 
297
310
  // internal Schema Object $refs resolve against the in-document $id graph;
@@ -345,7 +358,10 @@ class Arazzo1BundleVisitor {
345
358
  // is internal even when its URI form (e.g. a URN or absolute $id) is not a
346
359
  // bare fragment. Leave it untouched — it resolves against the in-document
347
360
  // $id graph, and embedding the current document into itself would be wrong.
348
- if (url.stripHash(schemaReference.uri) === url.stripHash(this.reference.uri)) {
361
+ // The same holds for a $ref resolving into the entry document (e.g. by its
362
+ // `$self` identity) — it's already part of the bundle.
363
+ const schemaReferenceURI = url.stripHash(schemaReference.uri);
364
+ if (schemaReferenceURI === url.stripHash(this.reference.uri) || schemaReferenceURI === url.stripHash(this.reference.refSet.rootRef.uri)) {
349
365
  return;
350
366
  }
351
367
 
@@ -15,7 +15,7 @@ import Reference from "../../../Reference.mjs";
15
15
  import File from "../../../File.mjs";
16
16
  import { isAnchor, uriToAnchor, evaluate as $anchorEvaluate } from "../../../dereference/strategies/arazzo-1/selectors/$anchor.mjs";
17
17
  import { evaluate as uriEvaluate } from "../../../dereference/strategies/arazzo-1/selectors/uri.mjs";
18
- import { resolveSchema$refField, resolveSchema$idField, maybeRefractToJSONSchemaElement } from "../../../dereference/strategies/arazzo-1/util.mjs";
18
+ import { resolveSchema$refField, resolveSchema$idField, maybeRefractToJSONSchemaElement, resolveArazzo$selfField, identifiedBy$self } from "../../../dereference/strategies/arazzo-1/util.mjs";
19
19
  import { toPascalCase, sanitizeComponentName, uniqueName as resolveUniqueName } from "../../util.mjs";
20
20
  /**
21
21
  * The Components Object field where embedded JSON Schema resources are hoisted.
@@ -35,6 +35,13 @@ const cf = fixedFields(ComponentsElement, {
35
35
  */
36
36
  class Arazzo1BundleVisitor {
37
37
  reference;
38
+
39
+ /**
40
+ * Base URI of the current document for resolving relative references.
41
+ * Honors the Arazzo `$self` field when present, otherwise falls back
42
+ * to the retrieval URI (`this.reference.uri`).
43
+ */
44
+ baseURI;
38
45
  options;
39
46
 
40
47
  /**
@@ -73,12 +80,13 @@ class Arazzo1BundleVisitor {
73
80
  reservedNames = new Map()
74
81
  }) {
75
82
  this.reference = reference;
83
+ this.baseURI = resolveArazzo$selfField(reference.uri, reference.value?.result);
76
84
  this.options = options;
77
85
  this.assignments = assignments;
78
86
  this.reservedNames = reservedNames;
79
87
  }
80
88
  toBaseURI(uri) {
81
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
89
+ return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
82
90
  }
83
91
  async toReference(uri) {
84
92
  // detect maximum depth of resolution
@@ -97,6 +105,12 @@ class Arazzo1BundleVisitor {
97
105
  if (refSet.has(baseURI)) {
98
106
  return refSet.find(ref => ref.uri === baseURI);
99
107
  }
108
+
109
+ // identity-based referencing: URI matches `$self` of an already processed Arazzo document
110
+ const referenceBy$self = refSet.find(identifiedBy$self(baseURI));
111
+ if (referenceBy$self !== undefined) {
112
+ return referenceBy$self;
113
+ }
100
114
  const parseResult = await parse(url.unsanitize(baseURI), {
101
115
  ...this.options,
102
116
  parse: {
@@ -271,19 +285,18 @@ class Arazzo1BundleVisitor {
271
285
  }
272
286
  const $ref = toValue(referencingElement.$ref);
273
287
  try {
274
- // compute baseURI using rules around $id and $ref keywords. The current
275
- // document's URI is the retrieval URI — derive it directly (not via
276
- // toReference) so an internal $ref needs no resolution and never trips the
288
+ // compute baseURI using rules around $self, $id and $ref keywords. The
289
+ // current document's base URI is derived directly (not via toReference)
290
+ // so an internal $ref needs no resolution and never trips the
277
291
  // resolve.maxDepth guard.
278
- const retrievalURI = this.reference.uri;
279
- const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
292
+ const $refBaseURI = resolveSchema$refField(this.baseURI, referencingElement);
280
293
  const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
281
294
  const file = new File({
282
295
  uri: $refBaseURIStrippedHash
283
296
  });
284
297
  const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
285
298
  const isURL = !isUnknownURI;
286
- const isInternalReference = url.stripHash(this.reference.uri) === $refBaseURIStrippedHash;
299
+ const isInternalReference = this.baseURI === $refBaseURIStrippedHash;
287
300
  const isExternalReference = !isInternalReference;
288
301
 
289
302
  // internal Schema Object $refs resolve against the in-document $id graph;
@@ -337,7 +350,10 @@ class Arazzo1BundleVisitor {
337
350
  // is internal even when its URI form (e.g. a URN or absolute $id) is not a
338
351
  // bare fragment. Leave it untouched — it resolves against the in-document
339
352
  // $id graph, and embedding the current document into itself would be wrong.
340
- if (url.stripHash(schemaReference.uri) === url.stripHash(this.reference.uri)) {
353
+ // The same holds for a $ref resolving into the entry document (e.g. by its
354
+ // `$self` identity) — it's already part of the bundle.
355
+ const schemaReferenceURI = url.stripHash(schemaReference.uri);
356
+ if (schemaReferenceURI === url.stripHash(this.reference.uri) || schemaReferenceURI === url.stripHash(this.reference.refSet.rootRef.uri)) {
341
357
  return;
342
358
  }
343
359
 
@@ -17,12 +17,24 @@ Object.defineProperty(exports, "dereferenceSourceDescriptions", {
17
17
  return _sourceDescriptions.dereferenceSourceDescriptions;
18
18
  }
19
19
  });
20
+ Object.defineProperty(exports, "identifiedBy$self", {
21
+ enumerable: true,
22
+ get: function () {
23
+ return _util.identifiedBy$self;
24
+ }
25
+ });
20
26
  Object.defineProperty(exports, "maybeRefractToJSONSchemaElement", {
21
27
  enumerable: true,
22
28
  get: function () {
23
29
  return _util.maybeRefractToJSONSchemaElement;
24
30
  }
25
31
  });
32
+ Object.defineProperty(exports, "resolveArazzo$selfField", {
33
+ enumerable: true,
34
+ get: function () {
35
+ return _util.resolveArazzo$selfField;
36
+ }
37
+ });
26
38
  Object.defineProperty(exports, "resolveSchema$idField", {
27
39
  enumerable: true,
28
40
  get: function () {
@@ -104,6 +104,6 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
104
104
  }
105
105
  }
106
106
  export { Arazzo1DereferenceVisitor };
107
- export { resolveSchema$refField, resolveSchema$idField, maybeRefractToJSONSchemaElement } from "./util.mjs";
107
+ export { resolveSchema$refField, resolveSchema$idField, resolveArazzo$selfField, identifiedBy$self, maybeRefractToJSONSchemaElement } from "./util.mjs";
108
108
  export { dereferenceSourceDescriptions } from "./source-descriptions.mjs";
109
109
  export default Arazzo1DereferenceStrategy;
@@ -7,13 +7,11 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.dereferenceSourceDescriptions = dereferenceSourceDescriptions;
8
8
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
9
9
  var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
10
- var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
11
- var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
12
- var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
13
10
  var _apidomCore = require("@speclynx/apidom-core");
14
11
  var url = _interopRequireWildcard(require("../../../util/url.cjs"));
15
12
  var _util = require("../../../options/util.cjs");
16
13
  var _index = _interopRequireWildcard(require("../../index.cjs"));
14
+ var _util2 = require("./util.cjs");
17
15
  /**
18
16
  * Dereferences a single source description element.
19
17
  * Returns ParseResultElement on success, or with annotation if skipped.
@@ -43,14 +41,25 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
43
41
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
44
42
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
45
43
 
46
- // skip if already visited (cycle detection)
47
- if (ctx.visitedUrls.has(retrievalURI)) {
44
+ // skip if the document is still being dereferenced higher up the chain (cycle detection)
45
+ if (ctx.ancestors.includes(retrievalURI)) {
48
46
  const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
49
47
  annotation.classes.push('warning');
50
48
  parseResult.push(annotation);
51
49
  return parseResult;
52
50
  }
53
- ctx.visitedUrls.add(retrievalURI);
51
+
52
+ // a document reached again through a different path is a shared dependency, not a cycle;
53
+ // point at the result where it was dereferenced instead of dereferencing it again
54
+ const sharedDereferenceResult = ctx.dereferenced.get(retrievalURI);
55
+ if (sharedDereferenceResult !== undefined) {
56
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been dereferenced. Reusing existing dereference result`);
57
+ annotation.classes.push('info');
58
+ parseResult.push(annotation);
59
+ parseResult.meta.set('parseResult', sharedDereferenceResult);
60
+ (0, _util2.validateSourceDescriptionAPI)(parseResult, sourceDescription, sharedDereferenceResult.api, retrievalURI);
61
+ return parseResult;
62
+ }
54
63
 
55
64
  // check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
56
65
  const existingParseResult = sourceDescription.meta.get('parseResult');
@@ -73,7 +82,8 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
73
82
  [ctx.strategyName]: {
74
83
  sourceDescriptions: true,
75
84
  sourceDescriptionsDepth: ctx.currentDepth + 1,
76
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
85
+ sourceDescriptionsAncestors: ctx.ancestors,
86
+ sourceDescriptionsDereferenced: ctx.dereferenced
77
87
  }
78
88
  }
79
89
  }
@@ -92,7 +102,8 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
92
102
  [ctx.strategyName]: {
93
103
  sourceDescriptions: true,
94
104
  sourceDescriptionsDepth: ctx.currentDepth + 1,
95
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
105
+ sourceDescriptionsAncestors: ctx.ancestors,
106
+ sourceDescriptionsDereferenced: ctx.dereferenced
96
107
  }
97
108
  }
98
109
  }
@@ -112,32 +123,14 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
112
123
  return parseResult;
113
124
  }
114
125
 
115
- // only allow OpenAPI and Arazzo as source descriptions
126
+ // register dereferenced document for later references to it
116
127
  const {
117
128
  api: sourceDescriptionAPI
118
129
  } = parseResult;
119
- const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
120
- const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
121
- if (!isOpenApi && !isArazzo) {
122
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
123
- annotation.classes.push('warning');
124
- parseResult.push(annotation);
125
- return parseResult;
126
- }
127
-
128
- // validate declared type matches actual dereferenced type
129
- const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
130
- if (typeof declaredType === 'string') {
131
- if (declaredType === 'openapi' && !isOpenApi) {
132
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but dereferenced as Arazzo document`);
133
- annotation.classes.push('warning');
134
- parseResult.push(annotation);
135
- } else if (declaredType === 'arazzo' && !isArazzo) {
136
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but dereferenced as OpenAPI document`);
137
- annotation.classes.push('warning');
138
- parseResult.push(annotation);
139
- }
130
+ for (const documentURI of (0, _util2.arazzoDocumentURIs)(retrievalURI, sourceDescriptionAPI)) {
131
+ ctx.dereferenced.set(documentURI, parseResult);
140
132
  }
133
+ (0, _util2.validateSourceDescriptionAPI)(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
141
134
  return parseResult;
142
135
  }
143
136
 
@@ -149,6 +142,13 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
149
142
  * regardless of success or failure. On failure, the ParseResultElement
150
143
  * contains annotations explaining what went wrong.
151
144
  *
145
+ * A document referenced from more than one place is dereferenced only once.
146
+ * Every later source description referencing it gets its own result carrying
147
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
148
+ * the document was dereferenced; that result is also what gets attached to the
149
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
150
+ * forms a cycle and is skipped with a 'warning' annotation instead.
151
+ *
152
152
  * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
153
153
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
154
154
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -183,7 +183,7 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
183
183
  * @public
184
184
  */
185
185
  async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
186
- const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
186
+ const retrievalURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
187
187
  const results = [];
188
188
 
189
189
  // get API from dereferenced parse result
@@ -212,23 +212,24 @@ async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalUR
212
212
  // recursion state comes from strategy-specific options
213
213
  const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
214
214
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
215
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
216
-
217
- // add current file to visited URLs to prevent cycles
218
- visitedUrls.add(baseURI);
215
+ const dereferenced = sharedOpts.sourceDescriptionsDereferenced ?? new Map();
216
+ // current file is an ancestor of everything dereferenced below
217
+ const ancestors = [...(sharedOpts.sourceDescriptionsAncestors ?? []), ...(0, _util2.arazzoDocumentURIs)(retrievalURI, api)];
219
218
  if (currentDepth >= maxDepth) {
220
- const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
219
+ const annotation = new _apidomDatamodel.AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${retrievalURI}"`);
221
220
  annotation.classes.push('error');
222
221
  const parseResult = new _apidomDatamodel.ParseResultElement([annotation]);
223
222
  parseResult.classes.push('source-description');
224
223
  return [parseResult];
225
224
  }
226
225
  const ctx = {
227
- baseURI,
226
+ // base URI for resolving source description URLs honors Arazzo `$self` field
227
+ baseURI: (0, _util2.resolveArazzo$selfField)(retrievalURI, api),
228
228
  options,
229
229
  strategyName,
230
230
  currentDepth,
231
- visitedUrls
231
+ ancestors,
232
+ dereferenced
232
233
  };
233
234
 
234
235
  // determine which source descriptions to dereference (array filters by name)
@@ -239,11 +240,12 @@ async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalUR
239
240
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
240
241
  }) : api.sourceDescriptions;
241
242
 
242
- // process sequentially to ensure proper cycle detection with shared visitedUrls
243
+ // process sequentially to ensure proper cycle detection with shared state
243
244
  for (const sourceDescription of sourceDescriptions) {
244
245
  const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
245
- // always attach result (even on failure - contains annotations)
246
- sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
246
+ // always attach result (even on failure - contains annotations);
247
+ // a shared document attaches the result where it was dereferenced
248
+ sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult.meta.get('parseResult') ?? sourceDescriptionDereferenceResult);
247
249
  results.push(sourceDescriptionDereferenceResult);
248
250
  }
249
251
  return results;
@@ -1,12 +1,10 @@
1
1
  import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement, isParseResultElement } from '@speclynx/apidom-datamodel';
2
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
3
  import { toValue } from '@speclynx/apidom-core';
7
4
  import * as url from "../../../util/url.mjs";
8
5
  import { merge as mergeOptions } from "../../../options/util.mjs";
9
6
  import dereference, { dereferenceApiDOM } from "../../index.mjs";
7
+ import { arazzoDocumentURIs, resolveArazzo$selfField, validateSourceDescriptionAPI } from "./util.mjs";
10
8
  /**
11
9
  * Dereferences a single source description element.
12
10
  * Returns ParseResultElement on success, or with annotation if skipped.
@@ -36,14 +34,25 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
36
34
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
37
35
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
38
36
 
39
- // skip if already visited (cycle detection)
40
- if (ctx.visitedUrls.has(retrievalURI)) {
37
+ // skip if the document is still being dereferenced higher up the chain (cycle detection)
38
+ if (ctx.ancestors.includes(retrievalURI)) {
41
39
  const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
42
40
  annotation.classes.push('warning');
43
41
  parseResult.push(annotation);
44
42
  return parseResult;
45
43
  }
46
- ctx.visitedUrls.add(retrievalURI);
44
+
45
+ // a document reached again through a different path is a shared dependency, not a cycle;
46
+ // point at the result where it was dereferenced instead of dereferencing it again
47
+ const sharedDereferenceResult = ctx.dereferenced.get(retrievalURI);
48
+ if (sharedDereferenceResult !== undefined) {
49
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been dereferenced. Reusing existing dereference result`);
50
+ annotation.classes.push('info');
51
+ parseResult.push(annotation);
52
+ parseResult.meta.set('parseResult', sharedDereferenceResult);
53
+ validateSourceDescriptionAPI(parseResult, sourceDescription, sharedDereferenceResult.api, retrievalURI);
54
+ return parseResult;
55
+ }
47
56
 
48
57
  // check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
49
58
  const existingParseResult = sourceDescription.meta.get('parseResult');
@@ -66,7 +75,8 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
66
75
  [ctx.strategyName]: {
67
76
  sourceDescriptions: true,
68
77
  sourceDescriptionsDepth: ctx.currentDepth + 1,
69
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
78
+ sourceDescriptionsAncestors: ctx.ancestors,
79
+ sourceDescriptionsDereferenced: ctx.dereferenced
70
80
  }
71
81
  }
72
82
  }
@@ -85,7 +95,8 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
85
95
  [ctx.strategyName]: {
86
96
  sourceDescriptions: true,
87
97
  sourceDescriptionsDepth: ctx.currentDepth + 1,
88
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
98
+ sourceDescriptionsAncestors: ctx.ancestors,
99
+ sourceDescriptionsDereferenced: ctx.dereferenced
89
100
  }
90
101
  }
91
102
  }
@@ -105,32 +116,14 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
105
116
  return parseResult;
106
117
  }
107
118
 
108
- // only allow OpenAPI and Arazzo as source descriptions
119
+ // register dereferenced document for later references to it
109
120
  const {
110
121
  api: sourceDescriptionAPI
111
122
  } = parseResult;
112
- const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
113
- const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
114
- if (!isOpenApi && !isArazzo) {
115
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
116
- annotation.classes.push('warning');
117
- parseResult.push(annotation);
118
- return parseResult;
119
- }
120
-
121
- // validate declared type matches actual dereferenced type
122
- const declaredType = toValue(sourceDescription.type);
123
- if (typeof declaredType === 'string') {
124
- if (declaredType === 'openapi' && !isOpenApi) {
125
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but dereferenced as Arazzo document`);
126
- annotation.classes.push('warning');
127
- parseResult.push(annotation);
128
- } else if (declaredType === 'arazzo' && !isArazzo) {
129
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but dereferenced as OpenAPI document`);
130
- annotation.classes.push('warning');
131
- parseResult.push(annotation);
132
- }
123
+ for (const documentURI of arazzoDocumentURIs(retrievalURI, sourceDescriptionAPI)) {
124
+ ctx.dereferenced.set(documentURI, parseResult);
133
125
  }
126
+ validateSourceDescriptionAPI(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
134
127
  return parseResult;
135
128
  }
136
129
 
@@ -142,6 +135,13 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
142
135
  * regardless of success or failure. On failure, the ParseResultElement
143
136
  * contains annotations explaining what went wrong.
144
137
  *
138
+ * A document referenced from more than one place is dereferenced only once.
139
+ * Every later source description referencing it gets its own result carrying
140
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
141
+ * the document was dereferenced; that result is also what gets attached to the
142
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
143
+ * forms a cycle and is skipped with a 'warning' annotation instead.
144
+ *
145
145
  * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
146
146
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
147
147
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -176,7 +176,7 @@ async function dereferenceSourceDescription(sourceDescription, ctx) {
176
176
  * @public
177
177
  */
178
178
  export async function dereferenceSourceDescriptions(parseResult, parseResultRetrievalURI, options, strategyName = 'arazzo-1') {
179
- const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
179
+ const retrievalURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
180
180
  const results = [];
181
181
 
182
182
  // get API from dereferenced parse result
@@ -205,23 +205,24 @@ export async function dereferenceSourceDescriptions(parseResult, parseResultRetr
205
205
  // recursion state comes from strategy-specific options
206
206
  const sharedOpts = options?.dereference?.strategyOpts?.[strategyName] ?? {};
207
207
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
208
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
209
-
210
- // add current file to visited URLs to prevent cycles
211
- visitedUrls.add(baseURI);
208
+ const dereferenced = sharedOpts.sourceDescriptionsDereferenced ?? new Map();
209
+ // current file is an ancestor of everything dereferenced below
210
+ const ancestors = [...(sharedOpts.sourceDescriptionsAncestors ?? []), ...arazzoDocumentURIs(retrievalURI, api)];
212
211
  if (currentDepth >= maxDepth) {
213
- const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`);
212
+ const annotation = new AnnotationElement(`Maximum dereference depth of ${maxDepth} has been exceeded by file "${retrievalURI}"`);
214
213
  annotation.classes.push('error');
215
214
  const parseResult = new ParseResultElement([annotation]);
216
215
  parseResult.classes.push('source-description');
217
216
  return [parseResult];
218
217
  }
219
218
  const ctx = {
220
- baseURI,
219
+ // base URI for resolving source description URLs honors Arazzo `$self` field
220
+ baseURI: resolveArazzo$selfField(retrievalURI, api),
221
221
  options,
222
222
  strategyName,
223
223
  currentDepth,
224
- visitedUrls
224
+ ancestors,
225
+ dereferenced
225
226
  };
226
227
 
227
228
  // determine which source descriptions to dereference (array filters by name)
@@ -232,11 +233,12 @@ export async function dereferenceSourceDescriptions(parseResult, parseResultRetr
232
233
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
233
234
  }) : api.sourceDescriptions;
234
235
 
235
- // process sequentially to ensure proper cycle detection with shared visitedUrls
236
+ // process sequentially to ensure proper cycle detection with shared state
236
237
  for (const sourceDescription of sourceDescriptions) {
237
238
  const sourceDescriptionDereferenceResult = await dereferenceSourceDescription(sourceDescription, ctx);
238
- // always attach result (even on failure - contains annotations)
239
- sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult);
239
+ // always attach result (even on failure - contains annotations);
240
+ // a shared document attaches the result where it was dereferenced
241
+ sourceDescription.meta.set('parseResult', sourceDescriptionDereferenceResult.meta.get('parseResult') ?? sourceDescriptionDereferenceResult);
240
242
  results.push(sourceDescriptionDereferenceResult);
241
243
  }
242
244
  return results;