@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,13 +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
- import parse from "../../index.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
7
+ import parse from "../../index.mjs";
8
+ import { arazzoDocumentURIs, resolveArazzo$selfField, validateSourceDescriptionAPI } from "../../../dereference/strategies/arazzo-1/util.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
11
9
  const ARAZZO_RECURSION_KEY = 'arazzo-1';
12
10
  /**
13
11
  * Parses a single source description element.
@@ -38,14 +36,25 @@ async function parseSourceDescription(sourceDescription, ctx) {
38
36
  const retrievalURI = url.sanitize(url.stripHash(url.resolve(ctx.baseURI, sourceDescriptionURI)));
39
37
  parseResult.setMetaProperty('retrievalURI', retrievalURI);
40
38
 
41
- // skip if already visited (cycle detection)
42
- 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)) {
43
41
  const annotation = new AnnotationElement(`Source description "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
44
42
  annotation.classes.push('warning');
45
43
  parseResult.push(annotation);
46
44
  return parseResult;
47
45
  }
48
- 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
+ }
49
58
  try {
50
59
  const sourceDescriptionParseResult = await parse(retrievalURI, mergeOptions(ctx.options, {
51
60
  parse: {
@@ -57,7 +66,8 @@ async function parseSourceDescription(sourceDescription, ctx) {
57
66
  sourceDescriptions: true,
58
67
  [ARAZZO_RECURSION_KEY]: {
59
68
  sourceDescriptionsDepth: ctx.currentDepth + 1,
60
- sourceDescriptionsVisitedUrls: ctx.visitedUrls
69
+ sourceDescriptionsAncestors: ctx.ancestors,
70
+ sourceDescriptionsParsed: ctx.parsed
61
71
  }
62
72
  }
63
73
  }
@@ -75,32 +85,14 @@ async function parseSourceDescription(sourceDescription, ctx) {
75
85
  return parseResult;
76
86
  }
77
87
 
78
- // only allow OpenAPI and Arazzo as source descriptions
88
+ // register parsed document for later references to it
79
89
  const {
80
90
  api: sourceDescriptionAPI
81
91
  } = parseResult;
82
- const isOpenApi = isSwaggerElement(sourceDescriptionAPI) || isOpenApi3_0Element(sourceDescriptionAPI) || isOpenApi3_1Element(sourceDescriptionAPI);
83
- const isArazzo = isArazzoSpecification1Element(sourceDescriptionAPI);
84
- if (!isOpenApi && !isArazzo) {
85
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" is not an OpenAPI or Arazzo document`);
86
- annotation.classes.push('warning');
87
- parseResult.push(annotation);
88
- return parseResult;
89
- }
90
-
91
- // validate declared type matches actual parsed type
92
- const declaredType = toValue(sourceDescription.type);
93
- if (typeof declaredType === 'string') {
94
- if (declaredType === 'openapi' && !isOpenApi) {
95
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "openapi" but parsed as Arazzo document`);
96
- annotation.classes.push('warning');
97
- parseResult.push(annotation);
98
- } else if (declaredType === 'arazzo' && !isArazzo) {
99
- const annotation = new AnnotationElement(`Source description "${retrievalURI}" declared as "arazzo" but parsed as OpenAPI document`);
100
- annotation.classes.push('warning');
101
- parseResult.push(annotation);
102
- }
92
+ for (const documentURI of arazzoDocumentURIs(retrievalURI, sourceDescriptionAPI)) {
93
+ ctx.parsed.set(documentURI, parseResult);
103
94
  }
95
+ validateSourceDescriptionAPI(parseResult, sourceDescription, sourceDescriptionAPI, retrievalURI);
104
96
  return parseResult;
105
97
  }
106
98
 
@@ -112,6 +104,13 @@ async function parseSourceDescription(sourceDescription, ctx) {
112
104
  * regardless of success or failure. On failure, the ParseResultElement
113
105
  * contains annotations explaining what went wrong.
114
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
+ *
115
114
  * @param parseResult - ParseResult containing an Arazzo specification
116
115
  * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
117
116
  * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
@@ -177,10 +176,9 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
177
176
  // recursion state comes from shared key (works across JSON/YAML)
178
177
  const sharedOpts = options?.parse?.parserOpts?.[ARAZZO_RECURSION_KEY] ?? {};
179
178
  const currentDepth = sharedOpts.sourceDescriptionsDepth ?? 0;
180
- const visitedUrls = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
181
-
182
- // add current file to visited URLs to prevent cycles
183
- visitedUrls.add(file.uri);
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)];
184
182
  if (currentDepth >= maxDepth) {
185
183
  const annotation = new AnnotationElement(`Maximum parse depth of ${maxDepth} has been exceeded by file "${file.uri}"`);
186
184
  annotation.classes.push('error');
@@ -189,10 +187,12 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
189
187
  return [parseResult];
190
188
  }
191
189
  const ctx = {
192
- baseURI: file.uri,
190
+ // base URI for resolving source description URLs honors Arazzo `$self` field
191
+ baseURI: resolveArazzo$selfField(file.uri, api),
193
192
  options,
194
193
  currentDepth,
195
- visitedUrls
194
+ ancestors,
195
+ parsed
196
196
  };
197
197
 
198
198
  // determine which source descriptions to parse (array filters by name)
@@ -203,11 +203,12 @@ export async function parseSourceDescriptions(parseResult, parseResultRetrievalU
203
203
  return typeof name === 'string' && sourceDescriptionsOption.includes(name);
204
204
  }) : api.sourceDescriptions;
205
205
 
206
- // process sequentially to ensure proper cycle detection with shared visitedUrls
206
+ // process sequentially to ensure proper cycle detection with shared state
207
207
  for (const sourceDescription of sourceDescriptions) {
208
208
  const sourceDescriptionParseResult = await parseSourceDescription(sourceDescription, ctx);
209
- // always attach result (even on failure - contains annotations)
210
- 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);
211
212
  results.push(sourceDescriptionParseResult);
212
213
  }
213
214
  return results;
@@ -17,6 +17,12 @@ export interface Arazzo1BundleVisitorOptions {
17
17
  */
18
18
  declare class Arazzo1BundleVisitor {
19
19
  protected readonly reference: Reference;
20
+ /**
21
+ * Base URI of the current document for resolving relative references.
22
+ * Honors the Arazzo `$self` field when present, otherwise falls back
23
+ * to the retrieval URI (`this.reference.uri`).
24
+ */
25
+ protected readonly baseURI: string;
20
26
  protected readonly options: ReferenceOptions;
21
27
  /**
22
28
  * The entry parse result. Derived from the shared refSet's root reference.
@@ -28,6 +28,6 @@ declare class Arazzo1DereferenceStrategy extends DereferenceStrategy {
28
28
  dereference(file: File, options: ReferenceOptions): Promise<Element>;
29
29
  }
30
30
  export { Arazzo1DereferenceVisitor };
31
- export { resolveSchema$refField, resolveSchema$idField, maybeRefractToJSONSchemaElement, } from './util.ts';
31
+ export { resolveSchema$refField, resolveSchema$idField, resolveArazzo$selfField, identifiedBy$self, maybeRefractToJSONSchemaElement, } from './util.ts';
32
32
  export { dereferenceSourceDescriptions } from './source-descriptions.ts';
33
33
  export default Arazzo1DereferenceStrategy;
@@ -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,5 +1,38 @@
1
- import { Element } from '@speclynx/apidom-datamodel';
1
+ import { Element, ParseResultElement } from '@speclynx/apidom-datamodel';
2
+ import { SourceDescriptionElement } from '@speclynx/apidom-ns-arazzo-1';
3
+ import type Reference from '../../../Reference.ts';
2
4
  export { resolveSchema$refField, resolveSchema$idField } from '../openapi-3-1/util.ts';
5
+ /**
6
+ * Computes the base URI of an Arazzo document per Arazzo 1.1.0 rules.
7
+ *
8
+ * When the Arazzo Object carries a `$self` field, it establishes the document's
9
+ * canonical identity and the base URI for resolving relative references. `$self`
10
+ * may itself be relative, in which case it's resolved against the retrieval URI.
11
+ * Any fragment is stripped, as `$self` MUST NOT contain one. When `$self` is
12
+ * absent (or `element` is not an Arazzo document), the retrieval URI is the base URI.
13
+ *
14
+ * @public
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;
27
+ /**
28
+ * Predicate matching a Reference by the identity (`$self`) of the Arazzo
29
+ * document it holds, rather than by its retrieval URI. Implements identity-based
30
+ * referencing from Arazzo 1.1.0: a URI matching a loaded document's `$self`
31
+ * refers to that document, regardless of where it was retrieved from.
32
+ *
33
+ * @public
34
+ */
35
+ export declare const identifiedBy$self: (uri: string) => (reference: Reference) => boolean;
3
36
  /**
4
37
  * Cached version of JSONSchemaElement.refract.
5
38
  */
@@ -19,6 +19,12 @@ export interface Arazzo1DereferenceVisitorOptions {
19
19
  declare class Arazzo1DereferenceVisitor {
20
20
  protected readonly indirections: Element[];
21
21
  protected readonly reference: Reference;
22
+ /**
23
+ * Base URI of the current document for resolving relative references.
24
+ * Honors the Arazzo `$self` field when present, otherwise falls back
25
+ * to the retrieval URI (`this.reference.uri`).
26
+ */
27
+ protected readonly baseURI: string;
22
28
  protected readonly options: ReferenceOptions;
23
29
  protected readonly visited: WeakSet<Element>;
24
30
  /**
@@ -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