Skip to content

Commit 82bacff

Browse files
committed
feat(apidom-ls): add rules for channel parameter and empty spec
1 parent 9d2762e commit 82bacff

File tree

16 files changed

+261
-185
lines changed

16 files changed

+261
-185
lines changed

packages/apidom-ls/src/config/asyncapi/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import serverMeta from './server/meta';
1313
import securityRequirementMeta from './securityrequirement/meta';
1414
import serverVariableMeta from './server-variable/meta';
1515
import channelsMeta from './channels/meta';
16+
import parameterMeta from './parameter/meta';
1617

1718
export default {
1819
'*': {
@@ -27,6 +28,7 @@ export default {
2728
channels: channelsMeta,
2829
asyncApi2: asyncapi2Meta,
2930
asyncApiVersion: asyncapiVersionMeta,
31+
parameter: parameterMeta,
3032
// 'json-schema-type': jsonSchemaTypeMeta,
3133
schema: jsonSchemaMeta,
3234
securityScheme: securitySchemeMeta,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import channelParameterExistLint from './parameter-key-exist';
2+
3+
const parameterLints = [channelParameterExistLint];
4+
5+
export default parameterLints;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import ApilintCodes from '../../../codes';
2+
import { LinterMeta } from '../../../../apidom-language-types';
3+
4+
const channelParameterExistLint: LinterMeta = {
5+
code: ApilintCodes.CHANNEL_PARAMETERS_EXIST,
6+
source: 'apilint',
7+
message: 'parameter key must be defined in channel name',
8+
severity: 1,
9+
linterFunction: 'apilintChannelParameterExist',
10+
marker: 'key',
11+
data: {},
12+
};
13+
14+
export default channelParameterExistLint;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import parameterLints from './lint/lints';
2+
import { FormatMeta } from '../../../apidom-language-types';
3+
4+
const parameterMeta: FormatMeta = {
5+
lint: parameterLints,
6+
};
7+
8+
export default parameterMeta;

packages/apidom-ls/src/config/codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const ApilintCodes = {
143143
OPERATION_TRAITS: (code += 1),
144144
OPERATION_MESSAGE: (code += 1),
145145
OPERATION_ID_UNIQUE: (code += 1),
146+
CHANNEL_PARAMETERS_EXIST: (code += 1),
146147
};
147148

148149
export default ApilintCodes;

packages/apidom-ls/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ export { Asyncapi21JsonSchemaValidationProvider } from './services/validation/pr
1818
export { Asyncapi22JsonSchemaValidationProvider } from './services/validation/providers/asyncapi-22-json-schema-validation-provider';
1919
export { OpenAPi31JsonSchemaValidationProvider } from './services/validation/providers/openapi-31-json-schema-validation-provider';
2020

21-
export { isJsonDoc, isAsyncDoc, getText } from './parser-factory';
22-
export { perfStart, perfEnd } from './utils/utils';
21+
export {
22+
perfStart,
23+
perfEnd,
24+
isAsyncDoc,
25+
isJsonDoc,
26+
getText,
27+
isSpecVersionSet,
28+
} from './utils/utils';
2329

2430
export type {
2531
LanguageService,

packages/apidom-ls/src/parser-factory.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import * as openapi3_1Adapter_Yaml from '@swagger-api/apidom-parser-adapter-open
1010
import * as asyncapi2Adapter_Yaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2';
1111
// @ts-ignore
1212
import { refractorPluginReplaceEmptyElement } from '@swagger-api/apidom-ns-asyncapi-2';
13+
import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOas } from '@swagger-api/apidom-ns-openapi-3-1';
1314
import { TextDocument } from 'vscode-languageserver-textdocument';
1415
import { ParseResultElement } from '@swagger-api/apidom-core';
1516

16-
import { setMetadataMap } from './utils/utils';
17+
import { isAsyncDoc, isJsonDoc, isSpecVersionSet, setMetadataMap } from './utils/utils';
1718
import { MetadataMaps } from './apidom-language-types';
1819

1920
export interface ParserOptions {
@@ -22,36 +23,6 @@ export interface ParserOptions {
2223
parser?: unknown;
2324
}
2425

25-
export function getText(document: TextDocument | string, trim = false): string {
26-
let text = '';
27-
if (typeof document === 'string') {
28-
text = document;
29-
} else {
30-
text = document.getText();
31-
}
32-
if (trim) text = text.trim();
33-
return text;
34-
}
35-
export function isAsyncDoc(document: TextDocument | string): boolean {
36-
return getText(document).indexOf('asyncapi') > -1;
37-
}
38-
39-
export interface RegexMap {
40-
[key: string]: RegExp;
41-
}
42-
43-
export function isJsonDoc(document: TextDocument | string): boolean {
44-
const text = getText(document, true);
45-
const JSON_START = /^\[|^\{(?!\{)/;
46-
const JSON_ENDS: RegexMap = {
47-
'[': /]$/,
48-
'{': /}$/,
49-
};
50-
51-
const jsonStart: RegExpMatchArray | null = text.match(JSON_START);
52-
return jsonStart != null && JSON_ENDS[jsonStart[0]].test(text);
53-
}
54-
5526
export function getParser(document: TextDocument | string): ApiDOMParser {
5627
const async = isAsyncDoc(document);
5728
const json = isJsonDoc(document);
@@ -67,7 +38,7 @@ export function getParser(document: TextDocument | string): ApiDOMParser {
6738
if (!async && !json) {
6839
return ApiDOMParser().use(openapi3_1Adapter_Yaml);
6940
}
70-
return ApiDOMParser().use(openapi3_1Adapter);
41+
return ApiDOMParser().use(asyncapi2Adapter);
7142
}
7243

7344
export async function parse(
@@ -77,6 +48,7 @@ export async function parse(
7748
// TODO improve detection mechanism
7849
const text: string = typeof textDocument === 'string' ? textDocument : textDocument.getText();
7950
const async = isAsyncDoc(textDocument);
51+
const versionSet = isSpecVersionSet(textDocument);
8052
const json = isJsonDoc(textDocument);
8153
let result;
8254
if (async && json) {
@@ -86,17 +58,28 @@ export async function parse(
8658
sourceMap: true,
8759
refractorOpts: { plugins: [refractorPluginReplaceEmptyElement()] },
8860
});
61+
} else if (!versionSet) {
62+
result = await asyncapi2Adapter_Yaml.parse(text, {
63+
sourceMap: true,
64+
refractorOpts: { plugins: [refractorPluginReplaceEmptyElement()] },
65+
});
8966
} else if (!async && json) {
9067
result = await openapi3_1Adapter.parse(text, { sourceMap: true });
9168
} else if (!async && !json) {
92-
result = await openapi3_1Adapter_Yaml.parse(text, { sourceMap: true });
69+
result = await openapi3_1Adapter_Yaml.parse(text, {
70+
sourceMap: true,
71+
refractorOpts: { plugins: [refractorPluginReplaceEmptyElementOas()] },
72+
});
9373
} else {
9474
// fallback
95-
result = await openapi3_1Adapter.parse(text, { sourceMap: true });
75+
result = await asyncapi2Adapter_Yaml.parse(text, {
76+
sourceMap: true,
77+
refractorOpts: { plugins: [refractorPluginReplaceEmptyElement()] },
78+
});
9679
}
9780
const { api } = result;
9881
if (api === undefined) return result;
99-
const docNs: string = isAsyncDoc(text) ? 'asyncapi' : 'openapi';
82+
const docNs: string = isAsyncDoc(text) || !versionSet ? 'asyncapi' : 'openapi';
10083
// TODO (francesco@tumanischvili@smartbear.com) use the type related metadata at root level defining the tokenTypes and modifiers
10184
setMetadataMap(api, docNs, metadataMaps); // TODO (francesco@tumanischvili@smartbear.com) move to parser/adapter, extending the one standard
10285
api.freeze(); // !! freeze and add parent !!

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

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ import {
5353
perfEnd,
5454
debug,
5555
trace,
56+
isAsyncDoc,
57+
isJsonDoc,
58+
isSpecVersionSet,
5659
} from '../../utils/utils';
57-
import { isAsyncDoc, isJsonDoc } from '../../parser-factory';
5860
import { standardLinterfunctions } from '../validation/linter-functions';
5961

6062
export interface CompletionsCollector {
@@ -225,6 +227,11 @@ export class DefaultCompletionService implements CompletionService {
225227
): Promise<CompletionList> {
226228
perfStart(PerfLabels.START);
227229

230+
const completionList: CompletionList = {
231+
items: [],
232+
isIncomplete: false,
233+
};
234+
228235
const position =
229236
'position' in completionParamsOrPosition
230237
? completionParamsOrPosition.position
@@ -254,6 +261,41 @@ export class DefaultCompletionService implements CompletionService {
254261

255262
const offset = textDocument.offsetAt(position);
256263

264+
// if no spec version has been set, provide completion for it anyway
265+
// TODO handle also JSON, must identify offset
266+
// TODO move to adapter
267+
if (
268+
!isSpecVersionSet(textDocument) &&
269+
!isJson &&
270+
textDocument.positionAt(offset).character === 0
271+
) {
272+
const isEmpty = isEmptyLine(textDocument, offset);
273+
trace('doCompletion - no version', { isEmpty });
274+
const asyncItem = CompletionItem.create('asyncapi');
275+
asyncItem.insertText = `asyncapi: '2.2.0$1'${isEmpty ? '' : '\n'}`;
276+
asyncItem.documentation = {
277+
kind: 'markdown',
278+
value:
279+
'The version string signifies the version of the AsyncAPI Specification that the document complies to. The format for this string _must_ be `major`.`minor`.`patch`. The `patch` _may_ be suffixed by a hyphen and extra alphanumeric characters.\n\n ---- \n\nA `major`.`minor` shall be used to designate the AsyncAPI Specification version, and will be considered compatible with the AsyncAPI Specification specified by that `major`.`minor` version. The patch version will not be considered by tooling, making no distinction between `1.0.0` and `1.0.1`.\n\n ---- \n\nIn subsequent versions of the AsyncAPI Specification, care will be given such that increments of the `minor` version should not interfere with operations of tooling developed to a lower minor version. Thus a hypothetical `1.1.0` specification should be usable with tooling designed for `1.0.0`.',
280+
};
281+
asyncItem.kind = CompletionItemKind.Keyword;
282+
asyncItem.insertTextFormat = 2;
283+
asyncItem.insertTextMode = 2;
284+
completionList.items.push(asyncItem);
285+
const oasItem = CompletionItem.create('openapi');
286+
oasItem.insertText = `openapi: '3.1.0$1'${isEmpty ? '' : '\n'}`;
287+
oasItem.documentation = {
288+
kind: 'markdown',
289+
value:
290+
'**REQUIRED**. This string MUST be the [version number](#versions) of the OpenAPI Specification that the OpenAPI document uses. The `openapi` field SHOULD be used by tooling to interpret the OpenAPI document. This is *not* related to the API [`info.version`](#infoVersion) string.',
291+
};
292+
oasItem.kind = CompletionItemKind.Keyword;
293+
oasItem.insertTextFormat = 2;
294+
oasItem.insertTextMode = 2;
295+
completionList.items.push(oasItem);
296+
trace('doCompletion - no version', `completionList: ${JSON.stringify(completionList)}`);
297+
}
298+
257299
/*
258300
process errored YAML input badly handled by YAML parser (see https://github.com/swagger-api/apidom/issues/194)
259301
similarly to what done in swagger-editor: check if we are in a partial "prefix" scenario, in this case add a `:`
@@ -270,6 +312,7 @@ export class DefaultCompletionService implements CompletionService {
270312
let textModified = false;
271313
if (!isJson) {
272314
if (isPartialKey(textDocument, offset)) {
315+
debug('doCompletion - isPartialKey', { offset });
273316
processedText = `${textDocument.getText().slice(0, offset - 1)}:${textDocument
274317
.getText()
275318
.slice(offset)}`;
@@ -283,12 +326,14 @@ export class DefaultCompletionService implements CompletionService {
283326
PerfLabels.PARSE_FIRST,
284327
);
285328
perfEnd(PerfLabels.PARSE_FIRST);
286-
if (!result) return CompletionList.create();
329+
if (!result) return completionList;
287330

288331
perfStart(PerfLabels.CORRECT_PARTIAL);
332+
debug('doCompletion - correctPartialKeys');
289333
processedText = correctPartialKeys(result, textDocument, isJson);
290334
perfEnd(PerfLabels.CORRECT_PARTIAL);
291335
if (processedText) {
336+
debug('doCompletion - parsing processedText');
292337
perfStart(PerfLabels.PARSE_SECOND);
293338
result = await this.settings!.documentCache?.get(
294339
textDocument,
@@ -298,17 +343,13 @@ export class DefaultCompletionService implements CompletionService {
298343
perfEnd(PerfLabels.PARSE_SECOND);
299344
textModified = true;
300345
}
301-
if (!result) return CompletionList.create();
346+
if (!result) return completionList;
302347

303348
const { api } = result;
304349
// if we cannot parse nothing to do
305-
if (api === undefined) return CompletionList.create();
350+
if (api === undefined) return completionList;
306351
const docNs: string = isAsyncDoc(text) ? 'asyncapi' : 'openapi';
307352
const specVersion = getSpecVersion(api);
308-
const completionList: CompletionList = {
309-
items: [],
310-
isIncomplete: false,
311-
};
312353

313354
let targetOffset = textModified ? offset - 1 : offset;
314355
let emptyLine = false;
@@ -538,6 +579,7 @@ export class DefaultCompletionService implements CompletionService {
538579
caretContext === CaretContext.OBJECT_VALUE_INNER ||
539580
caretContext === CaretContext.OBJECT_VALUE_START)
540581
) {
582+
debug('doCompletion - adding property');
541583
for (const p of completionNode) {
542584
if (!node.parent || node.parent !== p || emptyLine) {
543585
proposed[p.key.toValue()] = CompletionItem.create('__');
@@ -566,6 +608,7 @@ export class DefaultCompletionService implements CompletionService {
566608
} else {
567609
overwriteRange = undefined;
568610
}
611+
trace('doCompletion - calling getMetadataPropertyCompletions');
569612
const apidomCompletions = this.getMetadataPropertyCompletions(
570613
api,
571614
completionNode,
@@ -575,6 +618,7 @@ export class DefaultCompletionService implements CompletionService {
575618
quotes,
576619
);
577620
for (const item of apidomCompletions) {
621+
trace('doCompletion - apidomCompletions item', item);
578622
/*
579623
see https://github.com/microsoft/monaco-editor/issues/1889#issuecomment-642809145
580624
contrary to docs, range must start with the request offset. Workaround is providing
@@ -773,6 +817,7 @@ export class DefaultCompletionService implements CompletionService {
773817
specVersion: string,
774818
quotes: string | undefined,
775819
): CompletionItem[] {
820+
debug('getMetadataPropertyCompletions', node.element, yaml, docNs, specVersion, quotes);
776821
const apidomCompletions: ApidomCompletionItem[] = [];
777822
let set: string[] = [];
778823
if (node.classes) {
@@ -785,6 +830,7 @@ export class DefaultCompletionService implements CompletionService {
785830
}
786831
set.unshift(node.element);
787832
set.forEach((s) => {
833+
debug('getMetadataPropertyCompletions - class', s);
788834
const classCompletions: ApidomCompletionItem[] = doc.meta
789835
.get('metadataMap')
790836
?.get(s)
@@ -793,6 +839,7 @@ export class DefaultCompletionService implements CompletionService {
793839
if (classCompletions) {
794840
apidomCompletions.push(...classCompletions.filter((ci) => !ci.target));
795841
}
842+
debug('getMetadataPropertyCompletions - class apidomCompletions', apidomCompletions);
796843
// check also parent for completions with `target` property
797844
// get parent
798845
if (node.parent && isMember(node.parent)) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { isString } from 'ramda-adjunct';
44
import { ArraySlice, Element, filter, ObjectElement, toValue } from '@swagger-api/apidom-core';
55

66
import { DerefContext, Format, LanguageSettings } from '../../apidom-language-types';
7-
import { getParser, isJsonDoc } from '../../parser-factory';
7+
import { getParser } from '../../parser-factory';
8+
import { isJsonDoc } from '../../utils/utils';
89

910
export interface DerefService {
1011
doDeref(textDocument: TextDocument, derefContext: DerefContext): Promise<string>;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import {
1111
isArray,
1212
getSpecVersion,
1313
correctPartialKeys,
14+
isAsyncDoc,
15+
isJsonDoc,
1416
} from '../../utils/utils';
15-
import { isAsyncDoc, isJsonDoc } from '../../parser-factory';
1617

1718
export interface HoverService {
1819
computeHover(textDocument: TextDocument, position: Position): Promise<Hover | undefined>;

0 commit comments

Comments
 (0)