@speclynx/apidom-reference 2.5.0 → 2.6.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +109 -6
  3. package/dist/apidom-reference.browser.js +287 -69
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +25 -25
  6. package/src/configuration/saturated.cjs +4 -2
  7. package/src/configuration/saturated.mjs +5 -3
  8. package/src/dereference/strategies/arazzo-1/index.cjs +1 -1
  9. package/src/dereference/strategies/arazzo-1/index.mjs +1 -1
  10. package/src/dereference/strategies/asyncapi-2/index.cjs +1 -1
  11. package/src/dereference/strategies/asyncapi-2/index.mjs +1 -1
  12. package/src/dereference/strategies/openapi-2/index.cjs +1 -1
  13. package/src/dereference/strategies/openapi-2/index.mjs +1 -1
  14. package/src/dereference/strategies/openapi-3-0/index.cjs +1 -1
  15. package/src/dereference/strategies/openapi-3-0/index.mjs +1 -1
  16. package/src/parse/parsers/arazzo-json-1/index.cjs +12 -2
  17. package/src/parse/parsers/arazzo-json-1/index.mjs +14 -4
  18. package/src/parse/parsers/arazzo-json-1/source-description.cjs +177 -0
  19. package/src/parse/parsers/arazzo-json-1/source-description.mjs +171 -0
  20. package/src/parse/parsers/arazzo-yaml-1/index.cjs +12 -2
  21. package/src/parse/parsers/arazzo-yaml-1/index.mjs +12 -2
  22. package/src/resolve/util.cjs +1 -1
  23. package/src/resolve/util.mjs +1 -1
  24. package/types/apidom-reference.d.ts +4 -4
  25. package/types/parse/parsers/Parser.d.ts +3 -2
  26. package/types/parse/parsers/arazzo-json-1/index.d.ts +5 -1
  27. package/types/parse/parsers/arazzo-json-1/source-description.d.ts +13 -0
  28. package/types/parse/parsers/arazzo-yaml-1/index.d.ts +5 -1
  29. package/types/resolve/resolvers/Resolver.d.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speclynx/apidom-reference",
3
- "version": "2.5.0",
3
+ "version": "2.6.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.5.0",
235
- "@speclynx/apidom-datamodel": "^2.5.0",
236
- "@speclynx/apidom-error": "^2.5.0",
237
- "@speclynx/apidom-json-pointer": "^2.5.0",
238
- "@speclynx/apidom-ns-arazzo-1": "^2.5.0",
239
- "@speclynx/apidom-ns-asyncapi-2": "^2.5.0",
240
- "@speclynx/apidom-ns-json-schema-2020-12": "^2.5.0",
241
- "@speclynx/apidom-ns-openapi-2": "^2.5.0",
242
- "@speclynx/apidom-ns-openapi-3-0": "^2.5.0",
243
- "@speclynx/apidom-ns-openapi-3-1": "^2.5.0",
244
- "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.5.0",
245
- "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.5.0",
246
- "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.5.0",
247
- "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.5.0",
248
- "@speclynx/apidom-parser-adapter-json": "^2.5.0",
249
- "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.5.0",
250
- "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.5.0",
251
- "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.5.0",
252
- "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.5.0",
253
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.5.0",
254
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.5.0",
255
- "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.5.0",
256
- "@speclynx/apidom-traverse": "^2.5.0",
234
+ "@speclynx/apidom-core": "^2.6.0",
235
+ "@speclynx/apidom-datamodel": "^2.6.0",
236
+ "@speclynx/apidom-error": "^2.6.0",
237
+ "@speclynx/apidom-json-pointer": "^2.6.0",
238
+ "@speclynx/apidom-ns-arazzo-1": "^2.6.0",
239
+ "@speclynx/apidom-ns-asyncapi-2": "^2.6.0",
240
+ "@speclynx/apidom-ns-json-schema-2020-12": "^2.6.0",
241
+ "@speclynx/apidom-ns-openapi-2": "^2.6.0",
242
+ "@speclynx/apidom-ns-openapi-3-0": "^2.6.0",
243
+ "@speclynx/apidom-ns-openapi-3-1": "^2.6.0",
244
+ "@speclynx/apidom-parser-adapter-arazzo-json-1": "^2.6.0",
245
+ "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "^2.6.0",
246
+ "@speclynx/apidom-parser-adapter-asyncapi-json-2": "^2.6.0",
247
+ "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "^2.6.0",
248
+ "@speclynx/apidom-parser-adapter-json": "^2.6.0",
249
+ "@speclynx/apidom-parser-adapter-openapi-json-2": "^2.6.0",
250
+ "@speclynx/apidom-parser-adapter-openapi-json-3-0": "^2.6.0",
251
+ "@speclynx/apidom-parser-adapter-openapi-json-3-1": "^2.6.0",
252
+ "@speclynx/apidom-parser-adapter-openapi-yaml-2": "^2.6.0",
253
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "^2.6.0",
254
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "^2.6.0",
255
+ "@speclynx/apidom-parser-adapter-yaml-1-2": "^2.6.0",
256
+ "@speclynx/apidom-traverse": "^2.6.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": "6473d0f9ea30c7fe89aae039e91a0859eaf3cd1c"
277
+ "gitHead": "d96eb344cc26a57935aa3a425ea2447d6e8e77b2"
278
278
  }
@@ -62,10 +62,12 @@ _index25.options.parse.parsers = [new _index7.default({
62
62
  sourceMap: false
63
63
  }), new _index13.default({
64
64
  allowEmpty: true,
65
- sourceMap: false
65
+ sourceMap: false,
66
+ parseFn: _index25.parse
66
67
  }), new _index14.default({
67
68
  allowEmpty: true,
68
- sourceMap: false
69
+ sourceMap: false,
70
+ parseFn: _index25.parse
69
71
  }), new _index15.default({
70
72
  allowEmpty: true,
71
73
  sourceMap: false
@@ -26,7 +26,7 @@ import OpenAPI3_1DereferenceStrategy from "../dereference/strategies/openapi-3-1
26
26
  import AsyncAPI2DereferenceStrategy from "../dereference/strategies/asyncapi-2/index.mjs";
27
27
  import Arazzo1DereferenceStrategy from "../dereference/strategies/arazzo-1/index.mjs";
28
28
  import OpenAPI3_1BundleStrategy from "../bundle/strategies/openapi-3-1/index.mjs";
29
- import { options } from "../index.mjs";
29
+ import { options, parse } from "../index.mjs";
30
30
  options.parse.parsers = [new OpenAPIJSON2Parser({
31
31
  allowEmpty: true,
32
32
  sourceMap: false
@@ -53,10 +53,12 @@ options.parse.parsers = [new OpenAPIJSON2Parser({
53
53
  sourceMap: false
54
54
  }), new ArazzoJSON1Parser({
55
55
  allowEmpty: true,
56
- sourceMap: false
56
+ sourceMap: false,
57
+ parseFn: parse
57
58
  }), new ArazzoYAML1Parser({
58
59
  allowEmpty: true,
59
- sourceMap: false
60
+ sourceMap: false,
61
+ parseFn: parse
60
62
  }), new APIDOMJSONParser({
61
63
  allowEmpty: true,
62
64
  sourceMap: false
@@ -37,7 +37,7 @@ class Arazzo1DereferenceStrategy extends _DereferenceStrategy.default {
37
37
  }
38
38
 
39
39
  // assert by inspecting ApiDOM
40
- return (0, _apidomNsArazzo.isArazzoSpecification1Element)(file.parseResult?.api);
40
+ return (0, _apidomNsArazzo.isArazzoSpecification1Element)(file.parseResult?.result);
41
41
  }
42
42
  async dereference(file, options) {
43
43
  const namespace = new _apidomDatamodel.Namespace().use(_apidomNsArazzo.default);
@@ -25,7 +25,7 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
25
25
  }
26
26
 
27
27
  // assert by inspecting ApiDOM
28
- return isArazzoSpecification1Element(file.parseResult?.api);
28
+ return isArazzoSpecification1Element(file.parseResult?.result);
29
29
  }
30
30
  async dereference(file, options) {
31
31
  const namespace = new Namespace().use(arazzo1Namespace);
@@ -33,7 +33,7 @@ class AsyncAPI2DereferenceStrategy extends _DereferenceStrategy.default {
33
33
  }
34
34
 
35
35
  // assert by inspecting ApiDOM
36
- return (0, _apidomNsAsyncapi.isAsyncApi2Element)(file.parseResult?.api);
36
+ return (0, _apidomNsAsyncapi.isAsyncApi2Element)(file.parseResult?.result);
37
37
  }
38
38
  async dereference(file, options) {
39
39
  const namespace = new _apidomDatamodel.Namespace().use(_apidomNsAsyncapi.default);
@@ -25,7 +25,7 @@ class AsyncAPI2DereferenceStrategy extends DereferenceStrategy {
25
25
  }
26
26
 
27
27
  // assert by inspecting ApiDOM
28
- return isAsyncApi2Element(file.parseResult?.api);
28
+ return isAsyncApi2Element(file.parseResult?.result);
29
29
  }
30
30
  async dereference(file, options) {
31
31
  const namespace = new Namespace().use(asyncApi2Namespace);
@@ -33,7 +33,7 @@ class OpenAPI2DereferenceStrategy extends _DereferenceStrategy.default {
33
33
  }
34
34
 
35
35
  // assert by inspecting ApiDOM
36
- return (0, _apidomNsOpenapi.isSwaggerElement)(file.parseResult?.api);
36
+ return (0, _apidomNsOpenapi.isSwaggerElement)(file.parseResult?.result);
37
37
  }
38
38
  async dereference(file, options) {
39
39
  const namespace = new _apidomDatamodel.Namespace().use(_apidomNsOpenapi.default);
@@ -25,7 +25,7 @@ class OpenAPI2DereferenceStrategy extends DereferenceStrategy {
25
25
  }
26
26
 
27
27
  // assert by inspecting ApiDOM
28
- return isSwaggerElement(file.parseResult?.api);
28
+ return isSwaggerElement(file.parseResult?.result);
29
29
  }
30
30
  async dereference(file, options) {
31
31
  const namespace = new Namespace().use(openApi2Namespace);
@@ -33,7 +33,7 @@ class OpenAPI3_0DereferenceStrategy extends _DereferenceStrategy.default {
33
33
  }
34
34
 
35
35
  // assert by inspecting ApiDOM
36
- return (0, _apidomNsOpenapi.isOpenApi3_0Element)(file.parseResult?.api);
36
+ return (0, _apidomNsOpenapi.isOpenApi3_0Element)(file.parseResult?.result);
37
37
  }
38
38
  async dereference(file, options) {
39
39
  const namespace = new _apidomDatamodel.Namespace().use(_apidomNsOpenapi.default);
@@ -25,7 +25,7 @@ class OpenAPI3_0DereferenceStrategy extends DereferenceStrategy {
25
25
  }
26
26
 
27
27
  // assert by inspecting ApiDOM
28
- return isOpenApi3_0Element(file.parseResult?.api);
28
+ return isOpenApi3_0Element(file.parseResult?.result);
29
29
  }
30
30
  async dereference(file, options) {
31
31
  const namespace = new Namespace().use(openApi3_0Namespace);
@@ -7,6 +7,7 @@ var _ramda = require("ramda");
7
7
  var _apidomParserAdapterArazzoJson = require("@speclynx/apidom-parser-adapter-arazzo-json-1");
8
8
  var _ParserError = _interopRequireDefault(require("../../../errors/ParserError.cjs"));
9
9
  var _Parser = _interopRequireDefault(require("../Parser.cjs"));
10
+ var _sourceDescription = require("./source-description.cjs");
10
11
  /**
11
12
  * @public
12
13
  */
@@ -16,8 +17,10 @@ var _Parser = _interopRequireDefault(require("../Parser.cjs"));
16
17
  */
17
18
  class ArazzoJSON1Parser extends _Parser.default {
18
19
  refractorOpts;
20
+ parseFn;
19
21
  constructor(options) {
20
22
  const {
23
+ parseFn,
21
24
  fileExtensions = [],
22
25
  mediaTypes = _apidomParserAdapterArazzoJson.mediaTypes,
23
26
  ...rest
@@ -28,6 +31,7 @@ class ArazzoJSON1Parser extends _Parser.default {
28
31
  fileExtensions,
29
32
  mediaTypes
30
33
  });
34
+ this.parseFn = parseFn;
31
35
  }
32
36
  async canParse(file) {
33
37
  const hasSupportedFileExtension = this.fileExtensions.length === 0 ? true : this.fileExtensions.includes(file.extension);
@@ -39,11 +43,17 @@ class ArazzoJSON1Parser extends _Parser.default {
39
43
  }
40
44
  return false;
41
45
  }
42
- async parse(file) {
46
+ async parse(file, options) {
43
47
  const source = file.toString();
44
48
  try {
45
49
  const parserOpts = (0, _ramda.pick)(['sourceMap', 'strict', 'refractorOpts'], this);
46
- return await (0, _apidomParserAdapterArazzoJson.parse)(source, parserOpts);
50
+ const parseResult = await (0, _apidomParserAdapterArazzoJson.parse)(source, parserOpts);
51
+ const shouldParseSourceDescriptions = options?.parse?.parserOpts?.[this.name]?.sourceDescriptions ?? options?.parse?.parserOpts?.sourceDescriptions;
52
+ if (shouldParseSourceDescriptions) {
53
+ const sourceDescriptions = await _sourceDescription.parseSourceDescriptions.call(this, parseResult.api, file, options);
54
+ parseResult.push(...sourceDescriptions);
55
+ }
56
+ return parseResult;
47
57
  } catch (error) {
48
58
  throw new _ParserError.default(`Error parsing "${file.uri}"`, {
49
59
  cause: error
@@ -1,7 +1,8 @@
1
1
  import { pick } from 'ramda';
2
- import { parse, mediaTypes as Arazzo1MediaTypes, detect } from '@speclynx/apidom-parser-adapter-arazzo-json-1';
2
+ import { parse, mediaTypes as ArazzoJSON1MediaTypes, detect } from '@speclynx/apidom-parser-adapter-arazzo-json-1';
3
3
  import ParserError from "../../../errors/ParserError.mjs";
4
4
  import Parser from "../Parser.mjs";
5
+ import { parseSourceDescriptions } from "./source-description.mjs";
5
6
  /**
6
7
  * @public
7
8
  */
@@ -10,10 +11,12 @@ import Parser from "../Parser.mjs";
10
11
  */
11
12
  class ArazzoJSON1Parser extends Parser {
12
13
  refractorOpts;
14
+ parseFn;
13
15
  constructor(options) {
14
16
  const {
17
+ parseFn,
15
18
  fileExtensions = [],
16
- mediaTypes = Arazzo1MediaTypes,
19
+ mediaTypes = ArazzoJSON1MediaTypes,
17
20
  ...rest
18
21
  } = options ?? {};
19
22
  super({
@@ -22,6 +25,7 @@ class ArazzoJSON1Parser extends Parser {
22
25
  fileExtensions,
23
26
  mediaTypes
24
27
  });
28
+ this.parseFn = parseFn;
25
29
  }
26
30
  async canParse(file) {
27
31
  const hasSupportedFileExtension = this.fileExtensions.length === 0 ? true : this.fileExtensions.includes(file.extension);
@@ -33,11 +37,17 @@ class ArazzoJSON1Parser extends Parser {
33
37
  }
34
38
  return false;
35
39
  }
36
- async parse(file) {
40
+ async parse(file, options) {
37
41
  const source = file.toString();
38
42
  try {
39
43
  const parserOpts = pick(['sourceMap', 'strict', 'refractorOpts'], this);
40
- return await parse(source, parserOpts);
44
+ const parseResult = await parse(source, parserOpts);
45
+ const shouldParseSourceDescriptions = options?.parse?.parserOpts?.[this.name]?.sourceDescriptions ?? options?.parse?.parserOpts?.sourceDescriptions;
46
+ if (shouldParseSourceDescriptions) {
47
+ const sourceDescriptions = await parseSourceDescriptions.call(this, parseResult.api, file, options);
48
+ parseResult.push(...sourceDescriptions);
49
+ }
50
+ return parseResult;
41
51
  } catch (error) {
42
52
  throw new ParserError(`Error parsing "${file.uri}"`, {
43
53
  cause: error
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
4
+ exports.__esModule = true;
5
+ exports.parseSourceDescriptions = parseSourceDescriptions;
6
+ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
7
+ var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
8
+ var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-2");
9
+ var _apidomNsOpenapi2 = require("@speclynx/apidom-ns-openapi-3-0");
10
+ var _apidomNsOpenapi3 = require("@speclynx/apidom-ns-openapi-3-1");
11
+ var _apidomCore = require("@speclynx/apidom-core");
12
+ var url = _interopRequireWildcard(require("../../../util/url.cjs"));
13
+ var _util = require("../../../options/util.cjs");
14
+ // shared key for recursion state (works across JSON/YAML parsers)
15
+ const ARAZZO_RECURSION_KEY = 'arazzo-1';
16
+ /**
17
+ * Parses a single source description element.
18
+ * Returns ParseResultElement on success, or undefined if skipped.
19
+ */
20
+ async function parseSourceDescription(sourceDescription, ctx) {
21
+ const parseResult = new _apidomDatamodel.ParseResultElement();
22
+ if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sourceDescription)) {
23
+ const annotation = new _apidomDatamodel.AnnotationElement('Element is not a valid SourceDescriptionElement. Skipping.');
24
+ annotation.classes.push('warning');
25
+ parseResult.push(annotation);
26
+ return parseResult;
27
+ }
28
+
29
+ // set class and metadata from source description element
30
+ parseResult.classes.push('source-description');
31
+ if ((0, _apidomDatamodel.isStringElement)(sourceDescription.name)) parseResult.setMetaProperty('name', (0, _apidomDatamodel.cloneDeep)(sourceDescription.name));
32
+ if ((0, _apidomDatamodel.isStringElement)(sourceDescription.type)) parseResult.setMetaProperty('type', (0, _apidomDatamodel.cloneDeep)(sourceDescription.type));
33
+ const sourceDescriptionURI = (0, _apidomCore.toValue)(sourceDescription.url);
34
+ if (typeof sourceDescriptionURI !== 'string') {
35
+ const annotation = new _apidomDatamodel.AnnotationElement('Source description URL is missing or not a string. Skipping.');
36
+ annotation.classes.push('warning');
37
+ parseResult.push(annotation);
38
+ return parseResult;
39
+ }
40
+ const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
41
+
42
+ // skip if already visited (cycle detection)
43
+ if (ctx.visitedUrls.has(retrievalURI)) {
44
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle.`);
45
+ annotation.classes.push('warning');
46
+ parseResult.push(annotation);
47
+ return parseResult;
48
+ }
49
+ ctx.visitedUrls.add(retrievalURI);
50
+ try {
51
+ const sdParseResult = await ctx.parseFn(retrievalURI, (0, _util.merge)(ctx.options, {
52
+ parse: {
53
+ mediaType: 'text/plain',
54
+ // allow parser plugin detection
55
+ parserOpts: {
56
+ [ARAZZO_RECURSION_KEY]: {
57
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
58
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
59
+ }
60
+ }
61
+ }
62
+ }));
63
+ // merge parsed result into our parse result
64
+ for (const item of sdParseResult) {
65
+ parseResult.push(item);
66
+ }
67
+ } catch (error) {
68
+ // create error annotation instead of failing entire parse
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ const annotation = new _apidomDatamodel.AnnotationElement(`Error parsing source description "${retrievalURI}": ${message}`);
71
+ annotation.classes.push('error');
72
+ parseResult.push(annotation);
73
+ return parseResult;
74
+ }
75
+
76
+ // only allow OpenAPI and Arazzo as source descriptions
77
+ const {
78
+ api: sourceDescriptionAPI
79
+ } = parseResult;
80
+ const isOpenApi = (0, _apidomNsOpenapi.isSwaggerElement)(sourceDescriptionAPI) || (0, _apidomNsOpenapi2.isOpenApi3_0Element)(sourceDescriptionAPI) || (0, _apidomNsOpenapi3.isOpenApi3_1Element)(sourceDescriptionAPI);
81
+ const isArazzo = (0, _apidomNsArazzo.isArazzoSpecification1Element)(sourceDescriptionAPI);
82
+ if (!isOpenApi && !isArazzo) {
83
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document. Skipping.`);
84
+ annotation.classes.push('warning');
85
+ parseResult.push(annotation);
86
+ return parseResult;
87
+ }
88
+
89
+ // validate declared type matches actual parsed type
90
+ const declaredType = (0, _apidomCore.toValue)(sourceDescription.type);
91
+ if (typeof declaredType === 'string') {
92
+ if (declaredType === 'openapi' && !isOpenApi) {
93
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document.`);
94
+ annotation.classes.push('warning');
95
+ parseResult.push(annotation);
96
+ } else if (declaredType === 'arazzo' && !isArazzo) {
97
+ const annotation = new _apidomDatamodel.AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document.`);
98
+ annotation.classes.push('warning');
99
+ parseResult.push(annotation);
100
+ }
101
+ }
102
+ return parseResult;
103
+ }
104
+
105
+ /**
106
+ * Shared function for parsing source descriptions.
107
+ * Call with `.call(this, ...)` where `this` has `name` and `parseFn` properties.
108
+ * @public
109
+ */
110
+ async function parseSourceDescriptions(api, file, options) {
111
+ const results = [];
112
+
113
+ /**
114
+ * Validate prerequisites for parsing source descriptions.
115
+ * Return warning annotations if validation fails.
116
+ */
117
+ if (!(0, _apidomNsArazzo.isArazzoSpecification1Element)(api)) {
118
+ const annotation = new _apidomDatamodel.AnnotationElement('Cannot parse source descriptions: API is not an Arazzo specification.');
119
+ annotation.classes.push('warning');
120
+ return [new _apidomDatamodel.ParseResultElement([annotation])];
121
+ }
122
+ if (!(0, _apidomDatamodel.isArrayElement)(api.sourceDescriptions)) {
123
+ const annotation = new _apidomDatamodel.AnnotationElement('Cannot parse source descriptions: sourceDescriptions field is missing or not an array.');
124
+ annotation.classes.push('warning');
125
+ return [new _apidomDatamodel.ParseResultElement([annotation])];
126
+ }
127
+ if (typeof this.parseFn !== 'function') {
128
+ const annotation = new _apidomDatamodel.AnnotationElement('Source descriptions found but parseFn is not configured. Skipping source description parsing.');
129
+ annotation.classes.push('error');
130
+ return [new _apidomDatamodel.ParseResultElement([annotation])];
131
+ }
132
+
133
+ // user config: parser-specific options take precedence over global parserOpts
134
+ const maxDepth = options?.parse?.parserOpts?.[this.name]?.sourceDescriptionsMaxDepth ?? options?.parse?.parserOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
135
+
136
+ // recursion state comes from shared key (works across JSON/YAML)
137
+ const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
138
+ const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
139
+ const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
140
+
141
+ // add current file to visited URLs to prevent cycles
142
+ visitedUrls.add(file.uri);
143
+ if (currentDepth >= maxDepth) {
144
+ const annotation = new _apidomDatamodel.AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
145
+ annotation.classes.push('error');
146
+ const parseResult = new _apidomDatamodel.ParseResultElement([annotation]);
147
+ parseResult.classes.push('source-description');
148
+ return [parseResult];
149
+ }
150
+ const ctx = {
151
+ parseFn: this.parseFn,
152
+ baseURI: file.uri,
153
+ options,
154
+ currentDepth,
155
+ visitedUrls
156
+ };
157
+
158
+ // determine which source descriptions to parse
159
+ const sourceDescriptionsOption = options?.parse?.parserOpts?.[this.name]?.sourceDescriptions ?? options?.parse?.parserOpts?.sourceDescriptions;
160
+
161
+ // handle false or other falsy values - no source descriptions should be parsed
162
+ if (!sourceDescriptionsOption) {
163
+ return results;
164
+ }
165
+ const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
166
+ if (!(0, _apidomNsArazzo.isSourceDescriptionElement)(sd)) return false;
167
+ const name = (0, _apidomCore.toValue)(sd.name);
168
+ return typeof name === 'string' && sourceDescriptionsOption.includes(name);
169
+ }) : api.sourceDescriptions;
170
+
171
+ // process sequentially to ensure proper cycle detection with shared visitedUrls
172
+ for (const sourceDescription of sourceDescriptions) {
173
+ const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
174
+ results.push(sourceDescriptionParseResult);
175
+ }
176
+ return results;
177
+ }
@@ -0,0 +1,171 @@
1
+ import { ParseResultElement, AnnotationElement, isArrayElement, isStringElement, cloneDeep } from '@speclynx/apidom-datamodel';
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
+ import { toValue } from '@speclynx/apidom-core';
7
+ import * as url from "../../../util/url.mjs";
8
+ import { merge as mergeOptions } from "../../../options/util.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
9
+ const ARAZZO_RECURSION_KEY = 'arazzo-1';
10
+ /**
11
+ * Parses a single source description element.
12
+ * Returns ParseResultElement on success, or undefined if skipped.
13
+ */
14
+ async function parseSourceDescription(sourceDescription, ctx) {
15
+ const parseResult = new ParseResultElement();
16
+ if (!isSourceDescriptionElement(sourceDescription)) {
17
+ const annotation = new AnnotationElement('Element is not a valid SourceDescriptionElement. Skipping.');
18
+ annotation.classes.push('warning');
19
+ parseResult.push(annotation);
20
+ return parseResult;
21
+ }
22
+
23
+ // set class and metadata from source description element
24
+ parseResult.classes.push('source-description');
25
+ if (isStringElement(sourceDescription.name)) parseResult.setMetaProperty('name', cloneDeep(sourceDescription.name));
26
+ if (isStringElement(sourceDescription.type)) parseResult.setMetaProperty('type', cloneDeep(sourceDescription.type));
27
+ const sourceDescriptionURI = toValue(sourceDescription.url);
28
+ if (typeof sourceDescriptionURI !== 'string') {
29
+ const annotation = new AnnotationElement('Source description URL is missing or not a string. Skipping.');
30
+ annotation.classes.push('warning');
31
+ parseResult.push(annotation);
32
+ return parseResult;
33
+ }
34
+ const retrievalURI = url.resolve(ctx.baseURI, sourceDescriptionURI);
35
+
36
+ // skip if already visited (cycle detection)
37
+ if (ctx.visitedUrls.has(retrievalURI)) {
38
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle.`);
39
+ annotation.classes.push('warning');
40
+ parseResult.push(annotation);
41
+ return parseResult;
42
+ }
43
+ ctx.visitedUrls.add(retrievalURI);
44
+ try {
45
+ const sdParseResult = await ctx.parseFn(retrievalURI, mergeOptions(ctx.options, {
46
+ parse: {
47
+ mediaType: 'text/plain',
48
+ // allow parser plugin detection
49
+ parserOpts: {
50
+ [ARAZZO_RECURSION_KEY]: {
51
+ sourceDescriptionsDepth: ctx.currentDepth + 1,
52
+ sourceDescriptionsVisitedUrls: ctx.visitedUrls
53
+ }
54
+ }
55
+ }
56
+ }));
57
+ // merge parsed result into our parse result
58
+ for (const item of sdParseResult) {
59
+ parseResult.push(item);
60
+ }
61
+ } catch (error) {
62
+ // create error annotation instead of failing entire parse
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ const annotation = new AnnotationElement(`Error parsing source description "${retrievalURI}": ${message}`);
65
+ annotation.classes.push('error');
66
+ parseResult.push(annotation);
67
+ return parseResult;
68
+ }
69
+
70
+ // only allow OpenAPI and Arazzo as source descriptions
71
+ const {
72
+ api: sourceDescriptionAPI
73
+ } = parseResult;
74
+ const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
75
+ const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
76
+ if (!isOpenApi && !isArazzo) {
77
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document. Skipping.`);
78
+ annotation.classes.push('warning');
79
+ parseResult.push(annotation);
80
+ return parseResult;
81
+ }
82
+
83
+ // validate declared type matches actual parsed type
84
+ const declaredType = toValue(sourceDescription.type);
85
+ if (typeof declaredType === 'string') {
86
+ if (declaredType === 'openapi' && !isOpenApi) {
87
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document.`);
88
+ annotation.classes.push('warning');
89
+ parseResult.push(annotation);
90
+ } else if (declaredType === 'arazzo' && !isArazzo) {
91
+ const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document.`);
92
+ annotation.classes.push('warning');
93
+ parseResult.push(annotation);
94
+ }
95
+ }
96
+ return parseResult;
97
+ }
98
+
99
+ /**
100
+ * Shared function for parsing source descriptions.
101
+ * Call with `.call(this, ...)` where `this` has `name` and `parseFn` properties.
102
+ * @public
103
+ */
104
+ export async function parseSourceDescriptions(api, file, options) {
105
+ const results = [];
106
+
107
+ /**
108
+ * Validate prerequisites for parsing source descriptions.
109
+ * Return warning annotations if validation fails.
110
+ */
111
+ if (!isArazzoSpecification1Element(api)) {
112
+ const annotation = new AnnotationElement('Cannot parse source descriptions: API is not an Arazzo specification.');
113
+ annotation.classes.push('warning');
114
+ return [new ParseResultElement([annotation])];
115
+ }
116
+ if (!isArrayElement(api.sourceDescriptions)) {
117
+ const annotation = new AnnotationElement('Cannot parse source descriptions: sourceDescriptions field is missing or not an array.');
118
+ annotation.classes.push('warning');
119
+ return [new ParseResultElement([annotation])];
120
+ }
121
+ if (typeof this.parseFn !== 'function') {
122
+ const annotation = new AnnotationElement('Source descriptions found but parseFn is not configured. Skipping source description parsing.');
123
+ annotation.classes.push('error');
124
+ return [new ParseResultElement([annotation])];
125
+ }
126
+
127
+ // user config: parser-specific options take precedence over global parserOpts
128
+ const maxDepth = options?.parse?.parserOpts?.[this.name]?.sourceDescriptionsMaxDepth ?? options?.parse?.parserOpts?.sourceDescriptionsMaxDepth ?? +Infinity;
129
+
130
+ // recursion state comes from shared key (works across JSON/YAML)
131
+ const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
132
+ const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
133
+ const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
134
+
135
+ // add current file to visited URLs to prevent cycles
136
+ visitedUrls.add(file.uri);
137
+ if (currentDepth >= maxDepth) {
138
+ const annotation = new AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
139
+ annotation.classes.push('error');
140
+ const parseResult = new ParseResultElement([annotation]);
141
+ parseResult.classes.push('source-description');
142
+ return [parseResult];
143
+ }
144
+ const ctx = {
145
+ parseFn: this.parseFn,
146
+ baseURI: file.uri,
147
+ options,
148
+ currentDepth,
149
+ visitedUrls
150
+ };
151
+
152
+ // determine which source descriptions to parse
153
+ const sourceDescriptionsOption = options?.parse?.parserOpts?.[this.name]?.sourceDescriptions ?? options?.parse?.parserOpts?.sourceDescriptions;
154
+
155
+ // handle false or other falsy values - no source descriptions should be parsed
156
+ if (!sourceDescriptionsOption) {
157
+ return results;
158
+ }
159
+ const sourceDescriptions = Array.isArray(sourceDescriptionsOption) ? api.sourceDescriptions.filter(sd => {
160
+ if (!isSourceDescriptionElement(sd)) return false;
161
+ const name = toValue(sd.name);
162
+ return typeof name === 'string' && sourceDescriptionsOption.includes(name);
163
+ }) : api.sourceDescriptions;
164
+
165
+ // process sequentially to ensure proper cycle detection with shared visitedUrls
166
+ for (const sourceDescription of sourceDescriptions) {
167
+ const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
168
+ results.push(sourceDescriptionParseResult);
169
+ }
170
+ return results;
171
+ }