@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.
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
- exports.refractToJSONSchemaElement = exports.maybeRefractToJSONSchemaElement = void 0;
7
+ exports.resolveArazzo$selfField = exports.refractToJSONSchemaElement = exports.maybeRefractToJSONSchemaElement = exports.identifiedBy$self = exports.arazzoDocumentURIs = void 0;
7
8
  Object.defineProperty(exports, "resolveSchema$idField", {
8
9
  enumerable: true,
9
10
  get: function () {
@@ -16,12 +17,96 @@ Object.defineProperty(exports, "resolveSchema$refField", {
16
17
  return _util.resolveSchema$refField;
17
18
  }
18
19
  });
20
+ exports.validateSourceDescriptionAPI = void 0;
19
21
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
22
+ var _apidomCore = require("@speclynx/apidom-core");
20
23
  var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
24
+ var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
25
+ var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
26
+ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
27
+ var url = _interopRequireWildcard(require("../../../util/url.cjs"));
21
28
  var _util = require("../openapi-3-1/util.cjs");
29
+ /**
30
+ * Computes the base URI of an Arazzo document per Arazzo 1.1.0 rules.
31
+ *
32
+ * When the Arazzo Object carries a `$self` field, it establishes the document's
33
+ * canonical identity and the base URI for resolving relative references. `$self`
34
+ * may itself be relative, in which case it's resolved against the retrieval URI.
35
+ * Any fragment is stripped, as `$self` MUST NOT contain one. When `$self` is
36
+ * absent (or `element` is not an Arazzo document), the retrieval URI is the base URI.
37
+ *
38
+ * @public
39
+ */
40
+ const resolveArazzo$selfField = (retrievalURI, element) => {
41
+ if (!(0, _apidomNsArazzo.isArazzoSpecification1Element)(element)) {
42
+ return retrievalURI;
43
+ }
44
+ const $self = (0, _apidomCore.toValue)(element.$self);
45
+ if (typeof $self !== 'string' || $self === '') {
46
+ return retrievalURI;
47
+ }
48
+ return url.resolve(retrievalURI, url.sanitize(url.stripHash($self)));
49
+ };
50
+
51
+ /**
52
+ * URIs an Arazzo document is known by: its retrieval URI and, when the
53
+ * document carries a `$self` field resolving elsewhere, its identity.
54
+ */
55
+ exports.resolveArazzo$selfField = resolveArazzo$selfField;
56
+ const arazzoDocumentURIs = (retrievalURI, element) => {
57
+ const identity = resolveArazzo$selfField(retrievalURI, element);
58
+ return identity === retrievalURI ? [retrievalURI] : [retrievalURI, identity];
59
+ };
60
+
61
+ /**
62
+ * Annotates a source description result when the document is not an OpenAPI
63
+ * or Arazzo document, or does not match the type the source description declares.
64
+ */
65
+ exports.arazzoDocumentURIs = arazzoDocumentURIs;
66
+ const validateSourceDescriptionAPI = (parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI) => {
67
+ // only allow OpenAPI and Arazzo as source descriptions
68
+ const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
69
+ const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
70
+ if (!isOpenApi && !isArazzo) {
71
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
72
+ annotation.classes.push('warning');
73
+ parseResult.push(annotation);
74
+ return;
75
+ }
76
+
77
+ // validate declared type matches actual type
78
+ const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
79
+ if (typeof declaredType === 'string') {
80
+ if (declaredType === 'openapi' && !isOpenApi) {
81
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but is an Arazzo document`);
82
+ annotation.classes.push('warning');
83
+ parseResult.push(annotation);
84
+ } else if (declaredType === 'arazzo' && !isArazzo) {
85
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but is an OpenAPI document`);
86
+ annotation.classes.push('warning');
87
+ parseResult.push(annotation);
88
+ }
89
+ }
90
+ };
91
+
92
+ /**
93
+ * Predicate matching a Reference by the identity (`$self`) of the Arazzo
94
+ * document it holds, rather than by its retrieval URI. Implements identity-based
95
+ * referencing from Arazzo 1.1.0: a URI matching a loaded document's `$self`
96
+ * refers to that document, regardless of where it was retrieved from.
97
+ *
98
+ * @public
99
+ */
100
+ exports.validateSourceDescriptionAPI = validateSourceDescriptionAPI;
101
+ const identifiedBy$self = uri => reference => {
102
+ const result = reference.value?.result;
103
+ return (0, _apidomNsArazzo.isArazzoSpecification1Element)(result) && resolveArazzo$selfField(reference.uri, result) === uri;
104
+ };
105
+
22
106
  /**
23
107
  * Cached version of JSONSchemaElement.refract.
24
108
  */
109
+ exports.identifiedBy$self = identifiedBy$self;
25
110
  const refractToJSONSchemaElement = element => {
26
111
  if (refractToJSONSchemaElement.cache.has(element)) {
27
112
  return refractToJSONSchemaElement.cache.get(element);
@@ -1,6 +1,85 @@
1
- import { isPrimitiveElement } from '@speclynx/apidom-datamodel';
2
- import { refractJSONSchema } from '@speclynx/apidom-ns-arazzo-1';
1
+ import { AnnotationElement, isPrimitiveElement } from '@speclynx/apidom-datamodel';
2
+ import { toValue } from '@speclynx/apidom-core';
3
+ import { refractJSONSchema, isArazzoSpecification1Element } from '@speclynx/apidom-ns-arazzo-1';
4
+ import { isSwaggerElement } from '@speclynx/apidom-ns-openapi-2';
5
+ import { isOpenApi3_0Element } from '@speclynx/apidom-ns-openapi-3-0';
6
+ import { isOpenApi3_1Element } from '@speclynx/apidom-ns-openapi-3-1';
7
+ import * as url from "../../../util/url.mjs";
3
8
  export { resolveSchema$refField, resolveSchema$idField } from "../openapi-3-1/util.mjs";
9
+ /**
10
+ * Computes the base URI of an Arazzo document per Arazzo 1.1.0 rules.
11
+ *
12
+ * When the Arazzo Object carries a `$self` field, it establishes the document's
13
+ * canonical identity and the base URI for resolving relative references. `$self`
14
+ * may itself be relative, in which case it's resolved against the retrieval URI.
15
+ * Any fragment is stripped, as `$self` MUST NOT contain one. When `$self` is
16
+ * absent (or `element` is not an Arazzo document), the retrieval URI is the base URI.
17
+ *
18
+ * @public
19
+ */
20
+ export const resolveArazzo$selfField = (retrievalURI, element) => {
21
+ if (!isArazzoSpecification1Element(element)) {
22
+ return retrievalURI;
23
+ }
24
+ const $self = toValue(element.$self);
25
+ if (typeof $self !== 'string' || $self === '') {
26
+ return retrievalURI;
27
+ }
28
+ return url.resolve(retrievalURI, url.sanitize(url.stripHash($self)));
29
+ };
30
+
31
+ /**
32
+ * URIs an Arazzo document is known by: its retrieval URI and, when the
33
+ * document carries a `$self` field resolving elsewhere, its identity.
34
+ */
35
+ export const arazzoDocumentURIs = (retrievalURI, element) => {
36
+ const identity = resolveArazzo$selfField(retrievalURI, element);
37
+ return identity === retrievalURI ? [retrievalURI] : [retrievalURI, identity];
38
+ };
39
+
40
+ /**
41
+ * Annotates a source description result when the document is not an OpenAPI
42
+ * or Arazzo document, or does not match the type the source description declares.
43
+ */
44
+ export const validateSourceDescriptionAPI = (parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI) => {
45
+ // only allow OpenAPI and Arazzo as source descriptions
46
+ const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
47
+ const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
48
+ if (!isOpenApi && !isArazzo) {
49
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
50
+ annotation.classes.push('warning');
51
+ parseResult.push(annotation);
52
+ return;
53
+ }
54
+
55
+ // validate declared type matches actual type
56
+ const declaredType = toValue(sourceDescription.type);
57
+ if (typeof declaredType === 'string') {
58
+ if (declaredType === 'openapi' && !isOpenApi) {
59
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but is an Arazzo document`);
60
+ annotation.classes.push('warning');
61
+ parseResult.push(annotation);
62
+ } else if (declaredType === 'arazzo' && !isArazzo) {
63
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but is an OpenAPI document`);
64
+ annotation.classes.push('warning');
65
+ parseResult.push(annotation);
66
+ }
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Predicate matching a Reference by the identity (`$self`) of the Arazzo
72
+ * document it holds, rather than by its retrieval URI. Implements identity-based
73
+ * referencing from Arazzo 1.1.0: a URI matching a loaded document's `$self`
74
+ * refers to that document, regardless of where it was retrieved from.
75
+ *
76
+ * @public
77
+ */
78
+ export const identifiedBy$self = uri => reference => {
79
+ const result = reference.value?.result;
80
+ return isArazzoSpecification1Element(result) && resolveArazzo$selfField(reference.uri, result) === uri;
81
+ };
82
+
4
83
  /**
5
84
  * Cached version of JSONSchemaElement.refract.
6
85
  */
@@ -37,6 +37,13 @@ var _EvaluationJsonSchemaUriError = _interopRequireDefault(require("../../../err
37
37
  class Arazzo1DereferenceVisitor {
38
38
  indirections;
39
39
  reference;
40
+
41
+ /**
42
+ * Base URI of the current document for resolving relative references.
43
+ * Honors the Arazzo `$self` field when present, otherwise falls back
44
+ * to the retrieval URI (`this.reference.uri`).
45
+ */
46
+ baseURI;
40
47
  options;
41
48
  visited;
42
49
 
@@ -55,6 +62,7 @@ class Arazzo1DereferenceVisitor {
55
62
  }) {
56
63
  this.indirections = indirections;
57
64
  this.reference = reference;
65
+ this.baseURI = (0, _util2.resolveArazzo$selfField)(reference.uri, reference.value?.result);
58
66
  this.options = options;
59
67
  this.ancestors = new _util3.AncestorLineage(...ancestors);
60
68
  this.visited = visited;
@@ -66,7 +74,7 @@ class Arazzo1DereferenceVisitor {
66
74
  return [ancestorsLineage, directAncestors];
67
75
  }
68
76
  toBaseURI(uri) {
69
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
77
+ return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
70
78
  }
71
79
  async toReference(uri) {
72
80
  // detect maximum depth of resolution
@@ -85,6 +93,12 @@ class Arazzo1DereferenceVisitor {
85
93
  if (refSet.has(baseURI)) {
86
94
  return refSet.find((0, _ramda.propEq)(baseURI, 'uri'));
87
95
  }
96
+
97
+ // identity-based referencing: URI matches `$self` of an already processed Arazzo document
98
+ const referenceBy$self = refSet.find((0, _util2.identifiedBy$self)(baseURI));
99
+ if (referenceBy$self !== undefined) {
100
+ return referenceBy$self;
101
+ }
88
102
  const parseResult = await (0, _index.default)(url.unsanitize(baseURI), {
89
103
  ...this.options,
90
104
  parse: {
@@ -288,19 +302,19 @@ class Arazzo1DereferenceVisitor {
288
302
  }
289
303
  const indirectionsSize = this.indirections.length;
290
304
  try {
291
- // compute baseURI using rules around $id and $ref keywords
305
+ // compute baseURI using rules around $self, $id and $ref keywords
292
306
  let reference = await this.toReference(url.unsanitize(this.reference.uri));
293
307
  let {
294
308
  uri: retrievalURI
295
309
  } = reference;
296
- const $refBaseURI = (0, _util.resolveSchema$refField)(retrievalURI, referencingElement);
310
+ const $refBaseURI = (0, _util.resolveSchema$refField)(this.baseURI, referencingElement);
297
311
  const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
298
312
  const file = new _File.default({
299
313
  uri: $refBaseURIStrippedHash
300
314
  });
301
315
  const isUnknownURI = (0, _ramda.none)(r => r.canRead(file), this.options.resolve.resolvers);
302
316
  const isURL = !isUnknownURI;
303
- let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
317
+ let isInternalReference = this.baseURI === $refBaseURI;
304
318
  let isExternalReference = !isInternalReference;
305
319
 
306
320
  // determining reference, proper evaluation and selection mechanism
@@ -327,7 +341,7 @@ class Arazzo1DereferenceVisitor {
327
341
  } else {
328
342
  // we're assuming here that we're dealing with JSON Pointer here
329
343
  retrievalURI = this.toBaseURI($refBaseURI);
330
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
344
+ isInternalReference = this.baseURI === retrievalURI;
331
345
  isExternalReference = !isInternalReference;
332
346
 
333
347
  // ignore resolving internal Schema Objects
@@ -354,7 +368,7 @@ class Arazzo1DereferenceVisitor {
354
368
  if (isURL && error instanceof _EvaluationJsonSchemaUriError.default) {
355
369
  if ((0, _$anchor.isAnchor)((0, _$anchor.uriToAnchor)($refBaseURI))) {
356
370
  // we're dealing with JSON Schema $anchor here
357
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
371
+ isInternalReference = this.baseURI === retrievalURI;
358
372
  isExternalReference = !isInternalReference;
359
373
 
360
374
  // ignore resolving internal Schema Objects
@@ -375,7 +389,7 @@ class Arazzo1DereferenceVisitor {
375
389
  } else {
376
390
  // we're assuming here that we're dealing with JSON Pointer here
377
391
  retrievalURI = this.toBaseURI($refBaseURI);
378
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
392
+ isInternalReference = this.baseURI === retrievalURI;
379
393
  isExternalReference = !isInternalReference;
380
394
 
381
395
  // ignore resolving internal Schema Objects
@@ -9,7 +9,7 @@ import { parse as parseRuntimeExpression } from '@swaggerexpert/arazzo-runtime-e
9
9
  import { isAnchor, uriToAnchor, evaluate as $anchorEvaluate } from "./selectors/$anchor.mjs";
10
10
  import { evaluate as uriEvaluate } from "./selectors/uri.mjs";
11
11
  import { resolveSchema$refField } from "../openapi-3-1/util.mjs";
12
- import { maybeRefractToJSONSchemaElement } from "./util.mjs";
12
+ import { maybeRefractToJSONSchemaElement, resolveArazzo$selfField, identifiedBy$self } from "./util.mjs";
13
13
  import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
14
14
  import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
15
15
  import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
@@ -28,6 +28,13 @@ import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUr
28
28
  class Arazzo1DereferenceVisitor {
29
29
  indirections;
30
30
  reference;
31
+
32
+ /**
33
+ * Base URI of the current document for resolving relative references.
34
+ * Honors the Arazzo `$self` field when present, otherwise falls back
35
+ * to the retrieval URI (`this.reference.uri`).
36
+ */
37
+ baseURI;
31
38
  options;
32
39
  visited;
33
40
 
@@ -46,6 +53,7 @@ class Arazzo1DereferenceVisitor {
46
53
  }) {
47
54
  this.indirections = indirections;
48
55
  this.reference = reference;
56
+ this.baseURI = resolveArazzo$selfField(reference.uri, reference.value?.result);
49
57
  this.options = options;
50
58
  this.ancestors = new AncestorLineage(...ancestors);
51
59
  this.visited = visited;
@@ -57,7 +65,7 @@ class Arazzo1DereferenceVisitor {
57
65
  return [ancestorsLineage, directAncestors];
58
66
  }
59
67
  toBaseURI(uri) {
60
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
68
+ return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
61
69
  }
62
70
  async toReference(uri) {
63
71
  // detect maximum depth of resolution
@@ -76,6 +84,12 @@ class Arazzo1DereferenceVisitor {
76
84
  if (refSet.has(baseURI)) {
77
85
  return refSet.find(propEq(baseURI, 'uri'));
78
86
  }
87
+
88
+ // identity-based referencing: URI matches `$self` of an already processed Arazzo document
89
+ const referenceBy$self = refSet.find(identifiedBy$self(baseURI));
90
+ if (referenceBy$self !== undefined) {
91
+ return referenceBy$self;
92
+ }
79
93
  const parseResult = await parse(url.unsanitize(baseURI), {
80
94
  ...this.options,
81
95
  parse: {
@@ -279,19 +293,19 @@ class Arazzo1DereferenceVisitor {
279
293
  }
280
294
  const indirectionsSize = this.indirections.length;
281
295
  try {
282
- // compute baseURI using rules around $id and $ref keywords
296
+ // compute baseURI using rules around $self, $id and $ref keywords
283
297
  let reference = await this.toReference(url.unsanitize(this.reference.uri));
284
298
  let {
285
299
  uri: retrievalURI
286
300
  } = reference;
287
- const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
301
+ const $refBaseURI = resolveSchema$refField(this.baseURI, referencingElement);
288
302
  const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
289
303
  const file = new File({
290
304
  uri: $refBaseURIStrippedHash
291
305
  });
292
306
  const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
293
307
  const isURL = !isUnknownURI;
294
- let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
308
+ let isInternalReference = this.baseURI === $refBaseURI;
295
309
  let isExternalReference = !isInternalReference;
296
310
 
297
311
  // determining reference, proper evaluation and selection mechanism
@@ -318,7 +332,7 @@ class Arazzo1DereferenceVisitor {
318
332
  } else {
319
333
  // we're assuming here that we're dealing with JSON Pointer here
320
334
  retrievalURI = this.toBaseURI($refBaseURI);
321
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
335
+ isInternalReference = this.baseURI === retrievalURI;
322
336
  isExternalReference = !isInternalReference;
323
337
 
324
338
  // ignore resolving internal Schema Objects
@@ -345,7 +359,7 @@ class Arazzo1DereferenceVisitor {
345
359
  if (isURL && error instanceof EvaluationJsonSchemaUriError) {
346
360
  if (isAnchor(uriToAnchor($refBaseURI))) {
347
361
  // we're dealing with JSON Schema $anchor here
348
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
362
+ isInternalReference = this.baseURI === retrievalURI;
349
363
  isExternalReference = !isInternalReference;
350
364
 
351
365
  // ignore resolving internal Schema Objects
@@ -366,7 +380,7 @@ class Arazzo1DereferenceVisitor {
366
380
  } else {
367
381
  // we're assuming here that we're dealing with JSON Pointer here
368
382
  retrievalURI = this.toBaseURI($refBaseURI);
369
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
383
+ isInternalReference = this.baseURI === retrievalURI;
370
384
  isExternalReference = !isInternalReference;
371
385
 
372
386
  // ignore resolving internal Schema Objects
@@ -8,14 +8,12 @@ Object.defineProperty(exports, "__esModule", {
8
8
  exports.parseSourceDescriptions = parseSourceDescriptions;
9
9
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
10
10
  var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
11
- var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
12
- var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
13
- var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
14
11
  var _apidomCore = require("@speclynx/apidom-core");
15
12
  var _File = _interopRequireDefault(require("../../../File.cjs"));
16
13
  var url = _interopRequireWildcard(require("../../../util/url.cjs"));
17
14
  var _util = require("../../../options/util.cjs");
18
15
  var _index = _interopRequireDefault(require("../../index.cjs"));
16
+ var _util2 = require("../../../dereference/strategies/arazzo-1/util.cjs");
19
17
  // shared key for recursion state (works across JSON/YAML parsers)
20
18
  const ARAZZO_RECURSION_KEY = 'arazzo-1';
21
19
  /**
@@ -47,14 +45,25 @@ async function parseSourceDescription(sourceDescription, ctx) {
47
45
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
48
46
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
49
47
 
50
- // skip if already visited (cycle detection)
51
- if (ctx.visitedUrls.has(retrievalURI)) {
48
+ // skip if the document is still being parsed higher up the chain (cycle detection)
49
+ if (ctx.ancestors.includes(retrievalURI)) {
52
50
  const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
53
51
  annotation.classes.push('warning');
54
52
  parseResult.push(annotation);
55
53
  return parseResult;
56
54
  }
57
- ctx.visitedUrls.add(retrievalURI);
55
+
56
+ // a document reached again through a different path is a shared dependency, not a cycle;
57
+ // point at the result where it was parsed instead of parsing it again
58
+ const sharedParseResult = ctx.parsed.get(retrievalURI);
59
+ if (sharedParseResult !== undefined) {
60
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been parsed. Reusing existing parse result`);
61
+ annotation.classes.push('info');
62
+ parseResult.push(annotation);
63
+ parseResult.meta.set('parseResult', sharedParseResult);
64
+ (0, _util2.validateSourceDescriptionAPI)(parseResult, sourceDescription, sharedParseResult.api, retrievalURI);
65
+ return parseResult;
66
+ }
58
67
  try {
59
68
  const sourceDescriptionParseResult = await (0, _index.default)(retrievalURI, (0, _util.merge)(ctx.options, {
60
69
  parse: {
@@ -66,7 +75,8 @@ async function parseSourceDescription(sourceDescription, ctx) {
66
75
  sourceDescriptions: true,
67
76
  [ARAZZO_RECURSION_KEY]: {
68
77
  sourceDescriptionsDepth: ctx.currentDepth + 1,
69
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
78
+ sourceDescriptionsAncestors: ctx.ancestors,
79
+ sourceDescriptionsParsed: ctx.parsed
70
80
  }
71
81
  }
72
82
  }
@@ -84,32 +94,14 @@ async function parseSourceDescription(sourceDescription, ctx) {
84
94
  return parseResult;
85
95
  }
86
96
 
87
- // only allow OpenAPI and Arazzo as source descriptions
97
+ // register parsed document for later references to it
88
98
  const {
89
99
  api: sourceDescriptionAPI
90
100
  } = parseResult;
91
- const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
92
- const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
93
- if (!isOpenApi && !isArazzo) {
94
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
95
- annotation.classes.push('warning');
96
- parseResult.push(annotation);
97
- return parseResult;
98
- }
99
-
100
- // validate declared type matches actual parsed type
101
- const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
102
- if (typeof declaredType === 'string') {
103
- if (declaredType === 'openapi' && !isOpenApi) {
104
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document`);
105
- annotation.classes.push('warning');
106
- parseResult.push(annotation);
107
- } else if (declaredType === 'arazzo' && !isArazzo) {
108
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document`);
109
- annotation.classes.push('warning');
110
- parseResult.push(annotation);
111
- }
101
+ for (const documentURI of (0, _util2.arazzoDocumentURIs)(retrievalURI, sourceDescriptionAPI)) {
102
+ ctx.parsed.set(documentURI, parseResult);
112
103
  }
104
+ (0, _util2.validateSourceDescriptionAPI)(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
113
105
  return parseResult;
114
106
  }
115
107
 
@@ -121,6 +113,13 @@ async function parseSourceDescription(sourceDescription, ctx) {
121
113
  * regardless of success or failure. On failure, the ParseResultElement
122
114
  * contains annotations explaining what went wrong.
123
115
  *
116
+ * A document referenced from more than one place is parsed only once.
117
+ * Every later source description referencing it gets its own result carrying
118
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
119
+ * the document was parsed; that result is also what gets attached to the
120
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
121
+ * forms a cycle and is skipped with a 'warning' annotation instead.
122
+ *
124
123
  * @param parseResult - ParseResult containing an Arazzo specification
125
124
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
126
125
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -186,10 +185,9 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
186
185
  // recursion state comes from shared key (works across JSON/YAML)
187
186
  const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
188
187
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
189
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
190
-
191
- // add current file to visited URLs to prevent cycles
192
- visitedUrls.add(file.uri);
188
+ const parsed = sharedOpts.sourceDescriptionsParsed ?? new Map();
189
+ // current file is an ancestor of everything parsed below
190
+ const ancestors = [...(sharedOpts.sourceDescriptionsAncestors ?? []), ...(0, _util2.arazzoDocumentURIs)(file.uri, api)];
193
191
  if (currentDepth >= maxDepth) {
194
192
  const annotation = new _apidomDatamodel.AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
195
193
  annotation.classes.push('error');
@@ -198,10 +196,12 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
198
196
  return [parseResult];
199
197
  }
200
198
  const ctx = {
201
- baseURI: file.uri,
199
+ // base URI for resolving source description URLs honors Arazzo `$self` field
200
+ baseURI: (0, _util2.resolveArazzo$selfField)(file.uri, api),
202
201
  options,
203
202
  currentDepth,
204
- visitedUrls
203
+ ancestors,
204
+ parsed
205
205
  };
206
206
 
207
207
  // determine which source descriptions to parse (array filters by name)
@@ -212,11 +212,12 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
212
212
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
213
213
  }) : api.sourceDescriptions;
214
214
 
215
- // process sequentially to ensure proper cycle detection with shared visitedUrls
215
+ // process sequentially to ensure proper cycle detection with shared state
216
216
  for (const sourceDescription of sourceDescriptions) {
217
217
  const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
218
- // always attach result (even on failure - contains annotations)
219
- sourceDescription.meta.set('parseResult', sourceDescriptionParseResult);
218
+ // always attach result (even on failure - contains annotations);
219
+ // a shared document attaches the result where it was parsed
220
+ sourceDescription.meta.set('parseResult', sourceDescriptionParseResult.meta.get('parseResult') ?? sourceDescriptionParseResult);
220
221
  results.push(sourceDescriptionParseResult);
221
222
  }
222
223
  return results;