Skip to content

Commit

Permalink
feat(reference): apply dereferencing architecture 2.0 to OpenAPI 3.1.0 (
Browse files Browse the repository at this point in the history
#3942)

Refs #3941
  • Loading branch information
char0n authored Mar 20, 2024
1 parent 3c72c9d commit d57c318
Show file tree
Hide file tree
Showing 74 changed files with 1,828 additions and 872 deletions.
123 changes: 61 additions & 62 deletions packages/apidom-ls/src/services/validation/validation-service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import stampit from 'stampit';
import { CodeAction, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Element, findAtOffset, traverse, toValue, ObjectElement } from '@swagger-api/apidom-core';
import {
Element,
ObjectElement,
ParseResultElement,
findAtOffset,
traverse,
toValue,
} from '@swagger-api/apidom-core';
import { CodeActionKind, CodeActionParams } from 'vscode-languageserver-protocol';
import { evaluate, evaluateMulti } from '@swagger-api/apidom-json-path';
import { dereferenceApiDOM, Reference, ReferenceSet } from '@swagger-api/apidom-reference';
import { dereferenceApiDOM, Reference, ReferenceSet, options } from '@swagger-api/apidom-reference';

import {
APIDOM_LINTER,
Expand Down Expand Up @@ -72,6 +78,45 @@ export class DefaultValidationService implements ValidationService {
this.commentSeverity = undefined;
}

private static createCachedParser(parser: any) {
const cachedParser = Object.create(parser);

cachedParser.cache = new Map();
cachedParser.parse = async function parse(
file: {
uri: string;
mediaType: string;
},
...rest: unknown[]
): Promise<ParseResultElement> {
const cacheKey = `${file.uri}-${file.mediaType}`;

// cache hit
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}

// preparing deferred and setting to cache
let resolve: (value: ParseResultElement | PromiseLike<ParseResultElement>) => void;
let reject: (reason?: any) => void;
const deferred = new Promise<ParseResultElement>((res, rej) => {
resolve = res;
reject = rej;
});
this.cache.set(cacheKey, deferred);

// parsing and settling deferred
parser
.parse(file, ...rest)
.then(resolve!)
.catch(reject!);

return deferred;
};

return cachedParser;
}

public registerProvider(provider: ValidationProvider): void {
this.validationProviders.push(provider);
if (this.settings && provider.configure) {
Expand Down Expand Up @@ -181,64 +226,22 @@ export class DefaultValidationService implements ValidationService {
nameSpace: ContentLanguage,
validationContext?: ValidationContext,
): Promise<Diagnostic[]> {
const SharedReferenceSet = stampit(ReferenceSet, {
statics: {
refs: [],
clean() {
// @ts-ignore
this.refs.forEach((ref) => {
ref.refSet = null; // eslint-disable-line no-param-reassign
});
// @ts-ignore
this.refs = [];
},
},
init({ refs }, { stamp }) {
this.rootRef = null;
this.refs = stamp.refs;

// @ts-ignore
refs.forEach((ref) => this.add(ref));
},
methods: {
add(reference) {
if (this.has(reference)) {
// @ts-ignore
const foundReference = this.find((ref) => ref.uri === reference.uri);
const foundReferenceIndex = this.refs.indexOf(foundReference);

this.refs[foundReferenceIndex] = reference;
} else {
this.rootRef = this.rootRef === null ? reference : this.rootRef;
this.refs.push(reference);
}
reference.refSet = this; // eslint-disable-line no-param-reassign

return this;
},
clean() {
throw new Error('Use static SharedReferenceSet.clean() instead.');
},
},
});

const diagnostics: Diagnostic[] = [];
const pointersMap: Record<string, Pointer[]> = {};
const derefPromises: Promise<Element | { error: Error; refEl: Element }>[] = [];

const baseURI = validationContext?.baseURI
? validationContext?.baseURI
: 'https://smartbear.com/';

const derefPromises: Promise<Element | { error: Error; refEl: Element }>[] = [];
const apiReference = Reference({ uri: baseURI, value: result });
let fragmentId = 0;
for (const refEl of refElements) {
fragmentId += 1;
const cachedParsers = options.parse.parsers.map(DefaultValidationService.createCachedParser);

for (const [fragmentId, refEl] of refElements.entries()) {
const referenceElementReference = Reference({
uri: `${baseURI}#reference${fragmentId}`,
value: refEl,
});
const sharedRefSet = SharedReferenceSet({ refs: [referenceElementReference, apiReference] });
const refSet = ReferenceSet({ refs: [referenceElementReference, apiReference] });

try {
const promise = dereferenceApiDOM(refEl, {
Expand All @@ -247,9 +250,10 @@ export class DefaultValidationService implements ValidationService {
external: !toValue((refEl as ObjectElement).get('$ref')).startsWith('#'),
},
parse: {
parsers: cachedParsers,
mediaType: nameSpace.mediaType,
},
dereference: { refSet: sharedRefSet },
dereference: { refSet },
}).catch((e: Error) => {
return { error: e, refEl };
});
Expand Down Expand Up @@ -310,9 +314,6 @@ export class DefaultValidationService implements ValidationService {
}
} catch (ex) {
console.error('error dereferencing', ex);
} finally {
// @ts-ignore
SharedReferenceSet.clean();
}
return diagnostics;
}
Expand All @@ -331,19 +332,16 @@ export class DefaultValidationService implements ValidationService {
const baseURI = validationContext?.baseURI
? validationContext?.baseURI
: 'https://smartbear.com/';

const apiReference = Reference({ uri: baseURI, value: result });
let fragmentId = 0;
const refSet = ReferenceSet({ refs: [apiReference] });
for (const refEl of refElements) {
// @ts-ignore
refSet.rootRef = null;
fragmentId += 1;
const cachedParsers = options.parse.parsers.map(DefaultValidationService.createCachedParser);

for (const [fragmentId, refEl] of refElements.entries()) {
const referenceElementReference = Reference({
uri: `${baseURI}#reference${fragmentId}`,
value: refEl,
});
refSet.add(referenceElementReference);
const refSet = ReferenceSet({ refs: [referenceElementReference, apiReference] });

try {
// eslint-disable-next-line no-await-in-loop
await dereferenceApiDOM(refEl, {
Expand All @@ -353,6 +351,7 @@ export class DefaultValidationService implements ValidationService {
},
parse: {
mediaType: nameSpace.mediaType,
parsers: cachedParsers,
},
dereference: { refSet },
});
Expand Down
6 changes: 4 additions & 2 deletions packages/apidom-ls/test/validate-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ describe('reference validation', function () {
const httpPort = 8123;
let httpServer: ServerTerminable;

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

// eslint-disable-next-line mocha/no-hooks-for-single-case
afterEach(async function () {
languageService.terminate();
await httpServer.terminate();
Expand Down Expand Up @@ -143,6 +141,7 @@ describe('reference validation', function () {
}) as Diagnostic[],
);
});

specify(
'should validate with apidom-reference including external refs with serial processing',
async function () {
Expand All @@ -159,6 +158,7 @@ describe('reference validation', function () {
const doc: TextDocument = TextDocument.create('foo://bar/invalid.json', 'json', 0, spec);

const valRes = await languageService.doValidation(doc, validationContext);

const exp = [
{
range: { start: { line: 23, character: 26 }, end: { line: 23, character: 56 } },
Expand Down Expand Up @@ -272,6 +272,7 @@ describe('reference validation', function () {
);
},
);

specify('should validate with apidom-reference excluding external refs', async function () {
this.timeout(10000);
const validationContext: ValidationContext = {
Expand Down Expand Up @@ -316,6 +317,7 @@ describe('reference validation', function () {
}) as Diagnostic[],
);
});

specify('should validate with legacy logic', async function () {
this.timeout(10000);
const validationContext: ValidationContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ const ApiDOMDereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(
immutableRefSet.clean();
}

mutableRefsSet.clean();

return dereferencedElement;
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ const AsyncApi2DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampi
immutableRefSet.clean();
}

mutableRefsSet.clean();

return dereferencedElement;
},
},
Expand Down
Loading

0 comments on commit d57c318

Please sign in to comment.