Skip to content

Commit d57c318

Browse files
authored
feat(reference): apply dereferencing architecture 2.0 to OpenAPI 3.1.0 (#3942)
Refs #3941
1 parent 3c72c9d commit d57c318

File tree

74 files changed

+1828
-872
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+1828
-872
lines changed

packages/apidom-ls/src/services/validation/validation-service.ts

+61-62
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import stampit from 'stampit';
21
import { CodeAction, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types';
32
import { TextDocument } from 'vscode-languageserver-textdocument';
4-
import { Element, findAtOffset, traverse, toValue, ObjectElement } from '@swagger-api/apidom-core';
3+
import {
4+
Element,
5+
ObjectElement,
6+
ParseResultElement,
7+
findAtOffset,
8+
traverse,
9+
toValue,
10+
} from '@swagger-api/apidom-core';
511
import { CodeActionKind, CodeActionParams } from 'vscode-languageserver-protocol';
612
import { evaluate, evaluateMulti } from '@swagger-api/apidom-json-path';
7-
import { dereferenceApiDOM, Reference, ReferenceSet } from '@swagger-api/apidom-reference';
13+
import { dereferenceApiDOM, Reference, ReferenceSet, options } from '@swagger-api/apidom-reference';
814

915
import {
1016
APIDOM_LINTER,
@@ -72,6 +78,45 @@ export class DefaultValidationService implements ValidationService {
7278
this.commentSeverity = undefined;
7379
}
7480

81+
private static createCachedParser(parser: any) {
82+
const cachedParser = Object.create(parser);
83+
84+
cachedParser.cache = new Map();
85+
cachedParser.parse = async function parse(
86+
file: {
87+
uri: string;
88+
mediaType: string;
89+
},
90+
...rest: unknown[]
91+
): Promise<ParseResultElement> {
92+
const cacheKey = `${file.uri}-${file.mediaType}`;
93+
94+
// cache hit
95+
if (this.cache.has(cacheKey)) {
96+
return this.cache.get(cacheKey);
97+
}
98+
99+
// preparing deferred and setting to cache
100+
let resolve: (value: ParseResultElement | PromiseLike<ParseResultElement>) => void;
101+
let reject: (reason?: any) => void;
102+
const deferred = new Promise<ParseResultElement>((res, rej) => {
103+
resolve = res;
104+
reject = rej;
105+
});
106+
this.cache.set(cacheKey, deferred);
107+
108+
// parsing and settling deferred
109+
parser
110+
.parse(file, ...rest)
111+
.then(resolve!)
112+
.catch(reject!);
113+
114+
return deferred;
115+
};
116+
117+
return cachedParser;
118+
}
119+
75120
public registerProvider(provider: ValidationProvider): void {
76121
this.validationProviders.push(provider);
77122
if (this.settings && provider.configure) {
@@ -181,64 +226,22 @@ export class DefaultValidationService implements ValidationService {
181226
nameSpace: ContentLanguage,
182227
validationContext?: ValidationContext,
183228
): Promise<Diagnostic[]> {
184-
const SharedReferenceSet = stampit(ReferenceSet, {
185-
statics: {
186-
refs: [],
187-
clean() {
188-
// @ts-ignore
189-
this.refs.forEach((ref) => {
190-
ref.refSet = null; // eslint-disable-line no-param-reassign
191-
});
192-
// @ts-ignore
193-
this.refs = [];
194-
},
195-
},
196-
init({ refs }, { stamp }) {
197-
this.rootRef = null;
198-
this.refs = stamp.refs;
199-
200-
// @ts-ignore
201-
refs.forEach((ref) => this.add(ref));
202-
},
203-
methods: {
204-
add(reference) {
205-
if (this.has(reference)) {
206-
// @ts-ignore
207-
const foundReference = this.find((ref) => ref.uri === reference.uri);
208-
const foundReferenceIndex = this.refs.indexOf(foundReference);
209-
210-
this.refs[foundReferenceIndex] = reference;
211-
} else {
212-
this.rootRef = this.rootRef === null ? reference : this.rootRef;
213-
this.refs.push(reference);
214-
}
215-
reference.refSet = this; // eslint-disable-line no-param-reassign
216-
217-
return this;
218-
},
219-
clean() {
220-
throw new Error('Use static SharedReferenceSet.clean() instead.');
221-
},
222-
},
223-
});
224-
225229
const diagnostics: Diagnostic[] = [];
226230
const pointersMap: Record<string, Pointer[]> = {};
231+
const derefPromises: Promise<Element | { error: Error; refEl: Element }>[] = [];
227232

228233
const baseURI = validationContext?.baseURI
229234
? validationContext?.baseURI
230235
: 'https://smartbear.com/';
231-
232-
const derefPromises: Promise<Element | { error: Error; refEl: Element }>[] = [];
233236
const apiReference = Reference({ uri: baseURI, value: result });
234-
let fragmentId = 0;
235-
for (const refEl of refElements) {
236-
fragmentId += 1;
237+
const cachedParsers = options.parse.parsers.map(DefaultValidationService.createCachedParser);
238+
239+
for (const [fragmentId, refEl] of refElements.entries()) {
237240
const referenceElementReference = Reference({
238241
uri: `${baseURI}#reference${fragmentId}`,
239242
value: refEl,
240243
});
241-
const sharedRefSet = SharedReferenceSet({ refs: [referenceElementReference, apiReference] });
244+
const refSet = ReferenceSet({ refs: [referenceElementReference, apiReference] });
242245

243246
try {
244247
const promise = dereferenceApiDOM(refEl, {
@@ -247,9 +250,10 @@ export class DefaultValidationService implements ValidationService {
247250
external: !toValue((refEl as ObjectElement).get('$ref')).startsWith('#'),
248251
},
249252
parse: {
253+
parsers: cachedParsers,
250254
mediaType: nameSpace.mediaType,
251255
},
252-
dereference: { refSet: sharedRefSet },
256+
dereference: { refSet },
253257
}).catch((e: Error) => {
254258
return { error: e, refEl };
255259
});
@@ -310,9 +314,6 @@ export class DefaultValidationService implements ValidationService {
310314
}
311315
} catch (ex) {
312316
console.error('error dereferencing', ex);
313-
} finally {
314-
// @ts-ignore
315-
SharedReferenceSet.clean();
316317
}
317318
return diagnostics;
318319
}
@@ -331,19 +332,16 @@ export class DefaultValidationService implements ValidationService {
331332
const baseURI = validationContext?.baseURI
332333
? validationContext?.baseURI
333334
: 'https://smartbear.com/';
334-
335335
const apiReference = Reference({ uri: baseURI, value: result });
336-
let fragmentId = 0;
337-
const refSet = ReferenceSet({ refs: [apiReference] });
338-
for (const refEl of refElements) {
339-
// @ts-ignore
340-
refSet.rootRef = null;
341-
fragmentId += 1;
336+
const cachedParsers = options.parse.parsers.map(DefaultValidationService.createCachedParser);
337+
338+
for (const [fragmentId, refEl] of refElements.entries()) {
342339
const referenceElementReference = Reference({
343340
uri: `${baseURI}#reference${fragmentId}`,
344341
value: refEl,
345342
});
346-
refSet.add(referenceElementReference);
343+
const refSet = ReferenceSet({ refs: [referenceElementReference, apiReference] });
344+
347345
try {
348346
// eslint-disable-next-line no-await-in-loop
349347
await dereferenceApiDOM(refEl, {
@@ -353,6 +351,7 @@ export class DefaultValidationService implements ValidationService {
353351
},
354352
parse: {
355353
mediaType: nameSpace.mediaType,
354+
parsers: cachedParsers,
356355
},
357356
dereference: { refSet },
358357
});

packages/apidom-ls/test/validate-references.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,11 @@ describe('reference validation', function () {
3737
const httpPort = 8123;
3838
let httpServer: ServerTerminable;
3939

40-
// eslint-disable-next-line mocha/no-hooks-for-single-case
4140
beforeEach(function () {
4241
const cwd = path.join(__dirname, 'fixtures', 'deref');
4342
httpServer = createHTTPServer({ port: httpPort, cwd });
4443
});
4544

46-
// eslint-disable-next-line mocha/no-hooks-for-single-case
4745
afterEach(async function () {
4846
languageService.terminate();
4947
await httpServer.terminate();
@@ -143,6 +141,7 @@ describe('reference validation', function () {
143141
}) as Diagnostic[],
144142
);
145143
});
144+
146145
specify(
147146
'should validate with apidom-reference including external refs with serial processing',
148147
async function () {
@@ -159,6 +158,7 @@ describe('reference validation', function () {
159158
const doc: TextDocument = TextDocument.create('foo://bar/invalid.json', 'json', 0, spec);
160159

161160
const valRes = await languageService.doValidation(doc, validationContext);
161+
162162
const exp = [
163163
{
164164
range: { start: { line: 23, character: 26 }, end: { line: 23, character: 56 } },
@@ -272,6 +272,7 @@ describe('reference validation', function () {
272272
);
273273
},
274274
);
275+
275276
specify('should validate with apidom-reference excluding external refs', async function () {
276277
this.timeout(10000);
277278
const validationContext: ValidationContext = {
@@ -316,6 +317,7 @@ describe('reference validation', function () {
316317
}) as Diagnostic[],
317318
);
318319
});
320+
319321
specify('should validate with legacy logic', async function () {
320322
this.timeout(10000);
321323
const validationContext: ValidationContext = {

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

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ const ApiDOMDereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(
8787
immutableRefSet.clean();
8888
}
8989

90+
mutableRefsSet.clean();
91+
9092
return dereferencedElement;
9193
},
9294
},

packages/apidom-reference/src/dereference/strategies/asyncapi-2/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ const AsyncApi2DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampi
100100
immutableRefSet.clean();
101101
}
102102

103+
mutableRefsSet.clean();
104+
103105
return dereferencedElement;
104106
},
105107
},

0 commit comments

Comments
 (0)