@speclynx/apidom-reference 5.2.1 → 5.2.4

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.
@@ -8,9 +8,6 @@ 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"));
@@ -48,14 +45,25 @@ async function parseSourceDescription(sourceDescription, ctx) {
48
45
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
49
46
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
50
47
 
51
- // skip if already visited (cycle detection)
52
- 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)) {
53
50
  const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
54
51
  annotation.classes.push('warning');
55
52
  parseResult.push(annotation);
56
53
  return parseResult;
57
54
  }
58
- 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
+ }
59
67
  try {
60
68
  const sourceDescriptionParseResult = await (0, _index.default)(retrievalURI, (0, _util.merge)(ctx.options, {
61
69
  parse: {
@@ -67,7 +75,8 @@ async function parseSourceDescription(sourceDescription, ctx) {
67
75
  sourceDescriptions: true,
68
76
  [ARAZZO_RECURSION_KEY]: {
69
77
  sourceDescriptionsDepth: ctx.currentDepth + 1,
70
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
78
+ sourceDescriptionsAncestors: ctx.ancestors,
79
+ sourceDescriptionsParsed: ctx.parsed
71
80
  }
72
81
  }
73
82
  }
@@ -85,32 +94,14 @@ async function parseSourceDescription(sourceDescription, ctx) {
85
94
  return parseResult;
86
95
  }
87
96
 
88
- // only allow OpenAPI and Arazzo as source descriptions
97
+ // register parsed document for later references to it
89
98
  const {
90
99
  api: sourceDescriptionAPI
91
100
  } = parseResult;
92
- const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
93
- const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
94
- if (!isOpenApi && !isArazzo) {
95
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
96
- annotation.classes.push('warning');
97
- parseResult.push(annotation);
98
- return parseResult;
99
- }
100
-
101
- // validate declared type matches actual parsed type
102
- const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
103
- if (typeof declaredType === 'string') {
104
- if (declaredType === 'openapi' && !isOpenApi) {
105
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document`);
106
- annotation.classes.push('warning');
107
- parseResult.push(annotation);
108
- } else if (declaredType === 'arazzo' && !isArazzo) {
109
- const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document`);
110
- annotation.classes.push('warning');
111
- parseResult.push(annotation);
112
- }
101
+ for (const documentURI of (0, _util2.arazzoDocumentURIs)(retrievalURI, sourceDescriptionAPI)) {
102
+ ctx.parsed.set(documentURI, parseResult);
113
103
  }
104
+ (0, _util2.validateSourceDescriptionAPI)(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
114
105
  return parseResult;
115
106
  }
116
107
 
@@ -122,6 +113,13 @@ async function parseSourceDescription(sourceDescription, ctx) {
122
113
  * regardless of success or failure. On failure, the ParseResultElement
123
114
  * contains annotations explaining what went wrong.
124
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
+ *
125
123
  * @param parseResult - ParseResult containing an Arazzo specification
126
124
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
127
125
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -187,14 +185,9 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
187
185
  // recursion state comes from shared key (works across JSON/YAML)
188
186
  const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
189
187
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
190
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
191
-
192
- // base URI for resolving source description URLs honors Arazzo `$self` field
193
- const baseURI = (0, _util2.resolveArazzo$selfField)(file.uri, api);
194
-
195
- // add current file to visited URLs to prevent cycles (by retrieval URI and by identity)
196
- visitedUrls.add(file.uri);
197
- visitedUrls.add(baseURI);
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)];
198
191
  if (currentDepth >= maxDepth) {
199
192
  const annotation = new _apidomDatamodel.AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
200
193
  annotation.classes.push('error');
@@ -203,10 +196,12 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
203
196
  return [parseResult];
204
197
  }
205
198
  const ctx = {
206
- baseURI,
199
+ // base URI for resolving source description URLs honors Arazzo `$self` field
200
+ baseURI: (0, _util2.resolveArazzo$selfField)(file.uri, api),
207
201
  options,
208
202
  currentDepth,
209
- visitedUrls
203
+ ancestors,
204
+ parsed
210
205
  };
211
206
 
212
207
  // determine which source descriptions to parse (array filters by name)
@@ -217,11 +212,12 @@ async function parseSourceDescriptions(parseResult, parseResultRetrievalURI, opt
217
212
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
218
213
  }) : api.sourceDescriptions;
219
214
 
220
- // process sequentially to ensure proper cycle detection with shared visitedUrls
215
+ // process sequentially to ensure proper cycle detection with shared state
221
216
  for (const sourceDescription of sourceDescriptions) {
222
217
  const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
223
- // always attach result (even on failure - contains annotations)
224
- 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);
225
221
  results.push(sourceDescriptionParseResult);
226
222
  }
227
223
  return results;
@@ -1,14 +1,11 @@
1
1
  import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement } 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 File from "../../../File.mjs";
8
5
  import * as url from "../../../util/url.mjs";
9
6
  import { merge as mergeOptions } from "../../../options/util.mjs";
10
7
  import parse from "../../index.mjs";
11
- import { resolveArazzo$selfField } from "../../../dereference/strategies/arazzo-1/util.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
8
+ import { arazzoDocumentURIs, resolveArazzo$selfField, validateSourceDescriptionAPI } from "../../../dereference/strategies/arazzo-1/util.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
12
9
  const ARAZZO_RECURSION_KEY = 'arazzo-1';
13
10
  /**
14
11
  * Parses a single source description element.
@@ -39,14 +36,25 @@ async function parseSourceDescription(sourceDescription, ctx) {
39
36
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
40
37
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
41
38
 
42
- // skip if already visited (cycle detection)
43
- if (ctx.visitedUrls.has(retrievalURI)) {
39
+ // skip if the document is still being parsed higher up the chain (cycle detection)
40
+ if (ctx.ancestors.includes(retrievalURI)) {
44
41
  const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
45
42
  annotation.classes.push('warning');
46
43
  parseResult.push(annotation);
47
44
  return parseResult;
48
45
  }
49
- ctx.visitedUrls.add(retrievalURI);
46
+
47
+ // a document reached again through a different path is a shared dependency, not a cycle;
48
+ // point at the result where it was parsed instead of parsing it again
49
+ const sharedParseResult = ctx.parsed.get(retrievalURI);
50
+ if (sharedParseResult !== undefined) {
51
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been parsed. Reusing existing parse result`);
52
+ annotation.classes.push('info');
53
+ parseResult.push(annotation);
54
+ parseResult.meta.set('parseResult', sharedParseResult);
55
+ validateSourceDescriptionAPI(parseResult, sourceDescription, sharedParseResult.api, retrievalURI);
56
+ return parseResult;
57
+ }
50
58
  try {
51
59
  const sourceDescriptionParseResult = await parse(retrievalURI, mergeOptions(ctx.options, {
52
60
  parse: {
@@ -58,7 +66,8 @@ async function parseSourceDescription(sourceDescription, ctx) {
58
66
  sourceDescriptions: true,
59
67
  [ARAZZO_RECURSION_KEY]: {
60
68
  sourceDescriptionsDepth: ctx.currentDepth + 1,
61
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
69
+ sourceDescriptionsAncestors: ctx.ancestors,
70
+ sourceDescriptionsParsed: ctx.parsed
62
71
  }
63
72
  }
64
73
  }
@@ -76,32 +85,14 @@ async function parseSourceDescription(sourceDescription, ctx) {
76
85
  return parseResult;
77
86
  }
78
87
 
79
- // only allow OpenAPI and Arazzo as source descriptions
88
+ // register parsed document for later references to it
80
89
  const {
81
90
  api: sourceDescriptionAPI
82
91
  } = parseResult;
83
- const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
84
- const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
85
- if (!isOpenApi && !isArazzo) {
86
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
87
- annotation.classes.push('warning');
88
- parseResult.push(annotation);
89
- return parseResult;
90
- }
91
-
92
- // validate declared type matches actual parsed type
93
- const declaredType = toValue(sourceDescription.type);
94
- if (typeof declaredType === 'string') {
95
- if (declaredType === 'openapi' && !isOpenApi) {
96
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document`);
97
- annotation.classes.push('warning');
98
- parseResult.push(annotation);
99
- } else if (declaredType === 'arazzo' && !isArazzo) {
100
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document`);
101
- annotation.classes.push('warning');
102
- parseResult.push(annotation);
103
- }
92
+ for (const documentURI of arazzoDocumentURIs(retrievalURI, sourceDescriptionAPI)) {
93
+ ctx.parsed.set(documentURI, parseResult);
104
94
  }
95
+ validateSourceDescriptionAPI(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
105
96
  return parseResult;
106
97
  }
107
98
 
@@ -113,6 +104,13 @@ async function parseSourceDescription(sourceDescription, ctx) {
113
104
  * regardless of success or failure. On failure, the ParseResultElement
114
105
  * contains annotations explaining what went wrong.
115
106
  *
107
+ * A document referenced from more than one place is parsed only once.
108
+ * Every later source description referencing it gets its own result carrying
109
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
110
+ * the document was parsed; that result is also what gets attached to the
111
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
112
+ * forms a cycle and is skipped with a 'warning' annotation instead.
113
+ *
116
114
  * @param parseResult - ParseResult containing an Arazzo specification
117
115
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
118
116
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -178,14 +176,9 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
178
176
  // recursion state comes from shared key (works across JSON/YAML)
179
177
  const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
180
178
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
181
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
182
-
183
- // base URI for resolving source description URLs honors Arazzo `$self` field
184
- const baseURI = resolveArazzo$selfField(file.uri, api);
185
-
186
- // add current file to visited URLs to prevent cycles (by retrieval URI and by identity)
187
- visitedUrls.add(file.uri);
188
- visitedUrls.add(baseURI);
179
+ const parsed = sharedOpts.sourceDescriptionsParsed ?? new Map();
180
+ // current file is an ancestor of everything parsed below
181
+ const ancestors = [...(sharedOpts.sourceDescriptionsAncestors ?? []), ...arazzoDocumentURIs(file.uri, api)];
189
182
  if (currentDepth >= maxDepth) {
190
183
  const annotation = new AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
191
184
  annotation.classes.push('error');
@@ -194,10 +187,12 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
194
187
  return [parseResult];
195
188
  }
196
189
  const ctx = {
197
- baseURI,
190
+ // base URI for resolving source description URLs honors Arazzo `$self` field
191
+ baseURI: resolveArazzo$selfField(file.uri, api),
198
192
  options,
199
193
  currentDepth,
200
- visitedUrls
194
+ ancestors,
195
+ parsed
201
196
  };
202
197
 
203
198
  // determine which source descriptions to parse (array filters by name)
@@ -208,11 +203,12 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
208
203
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
209
204
  }) : api.sourceDescriptions;
210
205
 
211
- // process sequentially to ensure proper cycle detection with shared visitedUrls
206
+ // process sequentially to ensure proper cycle detection with shared state
212
207
  for (const sourceDescription of sourceDescriptions) {
213
208
  const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
214
- // always attach result (even on failure - contains annotations)
215
- sourceDescription.meta.set('parseResult', sourceDescriptionParseResult);
209
+ // always attach result (even on failure - contains annotations);
210
+ // a shared document attaches the result where it was parsed
211
+ sourceDescription.meta.set('parseResult', sourceDescriptionParseResult.meta.get('parseResult') ?? sourceDescriptionParseResult);
216
212
  results.push(sourceDescriptionParseResult);
217
213
  }
218
214
  return results;
@@ -8,6 +8,13 @@ import type { ReferenceOptions } from '../../../options/index.ts';
8
8
  * regardless of success or failure. On failure, the ParseResultElement
9
9
  * contains annotations explaining what went wrong.
10
10
  *
11
+ * A document referenced from more than one place is dereferenced only once.
12
+ * Every later source description referencing it gets its own result carrying
13
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
14
+ * the document was dereferenced; that result is also what gets attached to the
15
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
16
+ * forms a cycle and is skipped with a 'warning' annotation instead.
17
+ *
11
18
  * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
12
19
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
13
20
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -1,4 +1,5 @@
1
- import { Element } from '@speclynx/apidom-datamodel';
1
+ import { Element, ParseResultElement } from '@speclynx/apidom-datamodel';
2
+ import { SourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
2
3
  import type Reference from '../../../Reference.ts';
3
4
  export { resolveSchema$refField, resolveSchema$idField } from '../openapi-3-1/util.ts';
4
5
  /**
@@ -13,6 +14,16 @@ export { resolveSchema$refField, resolveSchema$idField } from '../openapi-3-1/ut
13
14
  * @public
14
15
  */
15
16
  export declare const resolveArazzo$selfField: (retrievalURI: string, element: unknown) => string;
17
+ /**
18
+ * URIs an Arazzo document is known by: its retrieval URI and, when the
19
+ * document carries a `$self` field resolving elsewhere, its identity.
20
+ */
21
+ export declare const arazzoDocumentURIs: (retrievalURI: string, element: unknown) => string[];
22
+ /**
23
+ * Annotates a source description result when the document is not an OpenAPI
24
+ * or Arazzo document, or does not match the type the source description declares.
25
+ */
26
+ export declare const validateSourceDescriptionAPI: (parseResult: ParseResultElement, sourceDescription: SourceDescriptionElement, sourceDescriptionAPI: Element | undefined, retrievalURI: string) => void;
16
27
  /**
17
28
  * Predicate matching a Reference by the identity (`$self`) of the Arazzo
18
29
  * document it holds, rather than by its retrieval URI. Implements identity-based
@@ -8,6 +8,13 @@ import type { ReferenceOptions } from '../../../options/index.ts';
8
8
  * regardless of success or failure. On failure, the ParseResultElement
9
9
  * contains annotations explaining what went wrong.
10
10
  *
11
+ * A document referenced from more than one place is parsed only once.
12
+ * Every later source description referencing it gets its own result carrying
13
+ * an 'info' annotation and a 'parseResult' meta pointing at the result where
14
+ * the document was parsed; that result is also what gets attached to the
15
+ * SourceDescriptionElement's meta. A document referencing one of its ancestors
16
+ * forms a cycle and is skipped with a 'warning' annotation instead.
17
+ *
11
18
  * @param parseResult - ParseResult containing an Arazzo specification
12
19
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
13
20
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names