Skip to content

Commit 3b1e343

Browse files
authored
feat(reference): expose low level API for dereferencing Arazzo source descriptions (#75)
1 parent 467e34a commit 3b1e343

File tree

3 files changed

+173
-8
lines changed

3 files changed

+173
-8
lines changed

packages/apidom-reference/src/dereference/strategies/arazzo-1/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import File from '../../../File.ts';
1010
import Reference from '../../../Reference.ts';
1111
import ReferenceSet from '../../../ReferenceSet.ts';
1212
import Arazzo1DereferenceVisitor from './visitor.ts';
13-
import { dereferenceSourceDescriptions } from './source-description.ts';
13+
import { dereferenceSourceDescriptions } from './source-descriptions.ts';
1414
import type { ReferenceOptions } from '../../../options/index.ts';
1515

1616
export type {
@@ -114,8 +114,9 @@ class Arazzo1DereferenceStrategy extends DereferenceStrategy {
114114
if (shouldDereferenceSourceDescriptions) {
115115
const sourceDescriptions = await dereferenceSourceDescriptions(
116116
dereferencedElement as ParseResultElement,
117-
reference,
117+
reference.uri,
118118
options,
119+
this.name,
119120
);
120121
(dereferencedElement as ParseResultElement).push(...sourceDescriptions);
121122
}
@@ -156,5 +157,6 @@ export {
156157
resolveSchema$idField,
157158
maybeRefractToJSONSchemaElement,
158159
} from './util.ts';
160+
export { dereferenceSourceDescriptions } from './source-descriptions.ts';
159161

160162
export default Arazzo1DereferenceStrategy;

packages/apidom-reference/src/dereference/strategies/arazzo-1/source-description.ts renamed to packages/apidom-reference/src/dereference/strategies/arazzo-1/source-descriptions.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { isOpenApi3_0Element } from '@speclynx/apidom-ns-openapi-3-0';
1515
import { isOpenApi3_1Element } from '@speclynx/apidom-ns-openapi-3-1';
1616
import { toValue } from '@speclynx/apidom-core';
1717

18-
import Reference from '../../../Reference.ts';
1918
import * as url from '../../../util/url.ts';
2019
import type { ReferenceOptions } from '../../../options/index.ts';
2120
import { merge as mergeOptions } from '../../../options/util.ts';
@@ -152,15 +151,29 @@ async function dereferenceSourceDescription(
152151

153152
/**
154153
* Dereferences source descriptions from an Arazzo document.
154+
*
155+
* @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
156+
* @param parseResultRetrievalURI - URI from which the parseResult was retrieved
157+
* @param options - Full ReferenceOptions (caller responsibility to construct)
158+
* @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
159+
* @returns Array of ParseResultElements. On success, returns one ParseResultElement per
160+
* source description (each with class 'source-description' and name/type metadata).
161+
* May return early with a single-element array containing a warning annotation when:
162+
* - The API is not an Arazzo specification
163+
* - The sourceDescriptions field is missing or not an array
164+
* - Maximum dereference depth is exceeded (error annotation)
165+
* Returns an empty array when sourceDescriptions option is disabled or no names match.
166+
*
155167
* @public
156168
*/
157169
export async function dereferenceSourceDescriptions(
158170
parseResult: ParseResultElement,
159-
reference: Reference,
171+
parseResultRetrievalURI: string,
160172
options: ReferenceOptions,
173+
strategyName: string = 'arazzo-1',
161174
): Promise<ParseResultElement[]> {
175+
const baseURI = url.sanitize(url.stripHash(parseResultRetrievalURI));
162176
const results: ParseResultElement[] = [];
163-
const strategyName = 'arazzo-1';
164177

165178
// get API from dereferenced parse result
166179
const { api } = parseResult;
@@ -196,11 +209,11 @@ export async function dereferenceSourceDescriptions(
196209
const visitedUrls: Set<string> = sharedOpts.sourceDescriptionsVisitedUrls ?? new Set();
197210

198211
// add current file to visited URLs to prevent cycles
199-
visitedUrls.add(reference.uri);
212+
visitedUrls.add(baseURI);
200213

201214
if (currentDepth >= maxDepth) {
202215
const annotation = new AnnotationElement(
203-
`Maximum dereference depth of ${maxDepth} has been exceeded by file "${reference.uri}"`,
216+
`Maximum dereference depth of ${maxDepth} has been exceeded by file "${baseURI}"`,
204217
);
205218
annotation.classes.push('error');
206219
const parseResult = new ParseResultElement([annotation]);
@@ -209,7 +222,7 @@ export async function dereferenceSourceDescriptions(
209222
}
210223

211224
const ctx: DereferenceSourceDescriptionContext = {
212-
baseURI: reference.uri,
225+
baseURI,
213226
options,
214227
currentDepth,
215228
visitedUrls,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { assert } from 'chai';
4+
import { ParseResultElement, isParseResultElement } from '@speclynx/apidom-datamodel';
5+
import { parse } from '@speclynx/apidom-parser-adapter-arazzo-json-1';
6+
import { fileURLToPath } from 'node:url';
7+
8+
import { dereferenceSourceDescriptions } from '../../../../../src/dereference/strategies/arazzo-1/index.ts';
9+
import { options, mergeOptions } from '../../../../../src/configuration/saturated.ts';
10+
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12+
13+
describe('dereference', function () {
14+
context('dereferenceSourceDescriptions', function () {
15+
context('given naked parser adapter usage', function () {
16+
specify('should dereference source descriptions from ParseResult', async function () {
17+
const uri = path.join(__dirname, 'fixtures', 'root.json');
18+
const data = fs.readFileSync(uri).toString();
19+
20+
// use naked parser adapter directly
21+
const parseResult = await parse(data);
22+
23+
// use exported dereferenceSourceDescriptions function
24+
const sourceDescriptions = await dereferenceSourceDescriptions(
25+
parseResult,
26+
uri,
27+
mergeOptions(options, {
28+
dereference: { strategyOpts: { sourceDescriptions: true } },
29+
}),
30+
);
31+
32+
assert.strictEqual(sourceDescriptions.length, 1);
33+
34+
const sdParseResult = sourceDescriptions[0]!;
35+
assert.isTrue(isParseResultElement(sdParseResult));
36+
assert.isTrue(sdParseResult.classes.includes('source-description'));
37+
assert.strictEqual(sdParseResult.meta.get('name')!.toValue(), 'petStore');
38+
assert.strictEqual(sdParseResult.meta.get('type')!.toValue(), 'openapi');
39+
});
40+
41+
specify('should filter source descriptions by name', async function () {
42+
const uri = path.join(__dirname, 'fixtures', 'root-multi.json');
43+
const data = fs.readFileSync(uri).toString();
44+
const parseResult = await parse(data);
45+
46+
const sourceDescriptions = await dereferenceSourceDescriptions(
47+
parseResult,
48+
uri,
49+
mergeOptions(options, {
50+
dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
51+
}),
52+
);
53+
54+
assert.strictEqual(sourceDescriptions.length, 1);
55+
56+
const sdParseResult = sourceDescriptions[0]! as ParseResultElement;
57+
assert.isTrue(isParseResultElement(sdParseResult));
58+
assert.strictEqual(sdParseResult.meta.get('name')!.toValue(), 'petStore');
59+
});
60+
61+
specify('should respect sourceDescriptionsMaxDepth option', async function () {
62+
const uri = path.join(__dirname, 'fixtures', 'root-recursive.json');
63+
const data = fs.readFileSync(uri).toString();
64+
const parseResult = await parse(data);
65+
66+
const sourceDescriptions = await dereferenceSourceDescriptions(
67+
parseResult,
68+
uri,
69+
mergeOptions(options, {
70+
dereference: {
71+
strategyOpts: { sourceDescriptions: true, sourceDescriptionsMaxDepth: 1 },
72+
},
73+
}),
74+
);
75+
76+
assert.strictEqual(sourceDescriptions.length, 1);
77+
78+
const nestedArazzo = sourceDescriptions[0]! as ParseResultElement;
79+
assert.isTrue(isParseResultElement(nestedArazzo));
80+
assert.isTrue(nestedArazzo.classes.includes('source-description'));
81+
// nested arazzo has its API + error annotation for max depth exceeded
82+
assert.strictEqual(nestedArazzo.length, 2);
83+
84+
const annotationResult = nestedArazzo.get(1)! as ParseResultElement;
85+
assert.isTrue(isParseResultElement(annotationResult));
86+
const annotation = annotationResult.get(0);
87+
assert.strictEqual(annotation?.element, 'annotation');
88+
assert.isTrue(annotation?.classes.includes('error'));
89+
assert.include(annotation?.toValue(), 'Maximum dereference depth of 1 has been exceeded');
90+
});
91+
92+
specify('should return empty array when sourceDescriptions is false', async function () {
93+
const uri = path.join(__dirname, 'fixtures', 'root.json');
94+
const data = fs.readFileSync(uri).toString();
95+
const parseResult = await parse(data);
96+
97+
const sourceDescriptions = await dereferenceSourceDescriptions(
98+
parseResult,
99+
uri,
100+
mergeOptions(options, {
101+
dereference: { strategyOpts: { sourceDescriptions: false } },
102+
}),
103+
);
104+
105+
assert.strictEqual(sourceDescriptions.length, 0);
106+
});
107+
108+
specify('should allow overriding strategyName parameter', async function () {
109+
const uri = path.join(__dirname, 'fixtures', 'root.json');
110+
const data = fs.readFileSync(uri).toString();
111+
const parseResult = await parse(data);
112+
113+
// use custom strategy name for options lookup
114+
const sourceDescriptions = await dereferenceSourceDescriptions(
115+
parseResult,
116+
uri,
117+
mergeOptions(options, {
118+
dereference: { strategyOpts: { 'custom-strategy': { sourceDescriptions: true } } },
119+
}),
120+
'custom-strategy',
121+
);
122+
123+
assert.strictEqual(sourceDescriptions.length, 1);
124+
assert.isTrue(isParseResultElement(sourceDescriptions[0]));
125+
});
126+
127+
specify('should default to arazzo-1 strategyName for options lookup', async function () {
128+
const uri = path.join(__dirname, 'fixtures', 'root.json');
129+
const data = fs.readFileSync(uri).toString();
130+
const parseResult = await parse(data);
131+
132+
// only set strategy-specific option, no global sourceDescriptions
133+
const sourceDescriptions = await dereferenceSourceDescriptions(
134+
parseResult,
135+
uri,
136+
mergeOptions(options, {
137+
dereference: { strategyOpts: { 'arazzo-1': { sourceDescriptions: true } } },
138+
}),
139+
);
140+
141+
assert.strictEqual(sourceDescriptions.length, 1);
142+
143+
const sdParseResult = sourceDescriptions[0]!;
144+
assert.isTrue(isParseResultElement(sdParseResult));
145+
assert.isTrue(sdParseResult.classes.includes('source-description'));
146+
assert.strictEqual(sdParseResult.meta.get('name')!.toValue(), 'petStore');
147+
});
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)