From f40f3989180aba2bb87aa127c962918085aac429 Mon Sep 17 00:00:00 2001 From: frantuma Date: Mon, 31 Jul 2023 12:56:53 +0200 Subject: [PATCH] feat(ls): add linting rule for OAS server variable enum and default Refs #2787 Refs #2788 --- packages/apidom-ls/src/config/codes.ts | 2 + .../lint/default--in-enum-3-0.ts | 25 +++++ .../lint/default--in-enum-3-1.ts | 20 ++++ .../lint/enum--non-empty-3-0.ts | 2 +- .../openapi/server-variable/lint/index.ts | 4 + .../services/validation/linter-functions.ts | 21 +++- .../validation/oas/server-variables-3-0.yaml | 34 ++++++ .../validation/oas/server-variables.yaml | 31 ++++++ packages/apidom-ls/test/validate.ts | 104 ++++++++++++++++++ 9 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-0.ts create mode 100644 packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-1.ts create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/server-variables-3-0.yaml create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/server-variables.yaml diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 1056c89dc..d5b274092 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -700,6 +700,7 @@ enum ApilintCodes { OPENAPI3_O_SERVER_VARIABLE_FIELD_ENUM_NON_EMPTY, OPENAPI3_O_SERVER_VARIABLE_FIELD_DEFAULT_TYPE = 5080200, OPENAPI3_O_SERVER_VARIABLE_FIELD_DEFAULT_REQUIRED, + OPENAPI3_O_SERVER_VARIABLE_FIELD_DEFAULT_IN_ENUM, OPENAPI3_O_SERVER_VARIABLE_FIELD_DESCRIPTION_TYPE = 5080300, OPENAPI3_0_PATHS = 5090000, @@ -921,6 +922,7 @@ enum ApilintCodes { OPENAPI3_1_SERVER_VARIABLE = 7060000, OPENAPI3_1_SERVER_VARIABLE_FIELD_ENUM_TYPE = 7060100, + OPENAPI3_1_SERVER_VARIABLE_FIELD_DEFAULT_IN_ENUM, OPENAPI3_1_PARAMETER = 7070000, OPENAPI3_1_PARAMETER_FIELD_CONTENT_ALLOWED_FIELDS = 7070100, diff --git a/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-0.ts b/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-0.ts new file mode 100644 index 000000000..69ffbab2f --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-0.ts @@ -0,0 +1,25 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const defaultInEnum3_0Lint: LinterMeta = { + code: ApilintCodes.OPENAPI3_O_SERVER_VARIABLE_FIELD_DEFAULT_IN_ENUM, + source: 'apilint', + message: "'default' value should exist in the enum's values.", + severity: DiagnosticSeverity.Warning, + linterFunction: 'apilintIncludedInArray', + linterParams: ['parent.enum', false], + marker: 'value', + target: 'default', + data: {}, + targetSpecs: [ + { namespace: 'openapi', version: '3.0.0' }, + { namespace: 'openapi', version: '3.0.1' }, + { namespace: 'openapi', version: '3.0.2' }, + { namespace: 'openapi', version: '3.0.3' }, + ], +}; + +export default defaultInEnum3_0Lint; diff --git a/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-1.ts b/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-1.ts new file mode 100644 index 000000000..4c1244274 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/server-variable/lint/default--in-enum-3-1.ts @@ -0,0 +1,20 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const defaultInEnum3_1Lint: LinterMeta = { + code: ApilintCodes.OPENAPI3_1_SERVER_VARIABLE_FIELD_DEFAULT_IN_ENUM, + source: 'apilint', + message: "'default' value must exist in the enum's values.", + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintIncludedInArray', + linterParams: ['parent.enum', false], + marker: 'value', + target: 'default', + data: {}, + targetSpecs: [{ namespace: 'openapi', version: '3.1.0' }], +}; + +export default defaultInEnum3_1Lint; diff --git a/packages/apidom-ls/src/config/openapi/server-variable/lint/enum--non-empty-3-0.ts b/packages/apidom-ls/src/config/openapi/server-variable/lint/enum--non-empty-3-0.ts index 5f0537e6e..e65b5de3a 100644 --- a/packages/apidom-ls/src/config/openapi/server-variable/lint/enum--non-empty-3-0.ts +++ b/packages/apidom-ls/src/config/openapi/server-variable/lint/enum--non-empty-3-0.ts @@ -9,7 +9,7 @@ const enumNonEmpty3_0Lint: LinterMeta = { source: 'apilint', message: "'enum' array should not be empty.", severity: DiagnosticSeverity.Warning, - linterFunction: 'apilintArrayEmpty', + linterFunction: 'apilintArrayNotEmpty', linterParams: [], marker: 'key', target: 'enum', diff --git a/packages/apidom-ls/src/config/openapi/server-variable/lint/index.ts b/packages/apidom-ls/src/config/openapi/server-variable/lint/index.ts index 77fc4a714..0edd49ccd 100644 --- a/packages/apidom-ls/src/config/openapi/server-variable/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/server-variable/lint/index.ts @@ -3,6 +3,8 @@ import enumType3_0Lint from './enum--type-3-0'; import enumNonEmpty3_0Lint from './enum--non-empty-3-0'; import enumType3_1Lint from './enum--type-3-1'; import defaultTypeLint from './default--type'; +import defaultInEnum3_0Lint from './default--in-enum-3-0'; +import defaultInEnum3_1Lint from './default--in-enum-3-1'; import defaultRequiredLint from './default--required'; import descriptionTypeLint from './description--type'; @@ -10,6 +12,8 @@ const lints = [ enumType3_0Lint, enumNonEmpty3_0Lint, enumType3_1Lint, + defaultInEnum3_0Lint, + defaultInEnum3_1Lint, defaultTypeLint, defaultRequiredLint, descriptionTypeLint, diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 4dbfa1e79..488d03623 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -432,7 +432,7 @@ export const standardLinterfunctions: FunctionItem[] = [ }, }, { - functionName: 'apilintArrayEmpty', + functionName: 'apilintArrayNotEmpty', function: (element: Element): boolean => { if (element) { const elValue = element.toValue(); @@ -972,4 +972,23 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'apilintIncludedInArray', + function: (element: Element, path: string, arrayMustExist: boolean): boolean => { + if (element && (isString(element) || isNumber(element))) { + const api = root(element); + + const targetEl = processPath(element, path, api); + if (!targetEl) { + return !arrayMustExist; + } + const isIncluded = + (targetEl as ArrayElement).findElements((e) => e.toValue() === element.toValue(), { + recursive: false, + }).length > 0; + return isIncluded; + } + return true; + }, + }, ]; diff --git a/packages/apidom-ls/test/fixtures/validation/oas/server-variables-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/server-variables-3-0.yaml new file mode 100644 index 000000000..3e2903dc3 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/server-variables-3-0.yaml @@ -0,0 +1,34 @@ +openapi: 3.0.2 +info: + title: test + version: 1.0.0 +servers: + - url: https://{host}:{port}/{basePath}/{otherPath} + description: Live Bookstore dynamic servers + variables: + host: + default: test.book.com + enum: + - test.book.com + - staging.book.com + - sandbox.book.com + - production.book.com + port: + default: '443' + enum: [] + basePath: + default: v2.0 + enum: + - v1.0 + - v1.1 + - v1.2 + - v1.3 + otherPath: + default: foo +paths: + /a: + get: + operationId: aget + responses: + '200': + description: test diff --git a/packages/apidom-ls/test/fixtures/validation/oas/server-variables.yaml b/packages/apidom-ls/test/fixtures/validation/oas/server-variables.yaml new file mode 100644 index 000000000..9f799bbea --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/server-variables.yaml @@ -0,0 +1,31 @@ +openapi: 3.1.0 +info: + title: test + version: 1.0.0 +servers: + - url: https://{host}:{port}/{basePath}/{otherPath} + description: Live Bookstore dynamic servers + variables: + host: + default: test.book.com + enum: + - test.book.com + - staging.book.com + - sandbox.book.com + - production.book.com + port: + default: '443' + enum: [] + basePath: + default: v2.0 + enum: + - v1.0 + - v1.1 + - v1.2 + - v1.3 + otherPath: + default: foo +paths: + /a: + get: + operationId: aget diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 43f67a027..b3aadcadc 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3203,4 +3203,108 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas / yaml - test server variable default and enum', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'server-variables.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/server-variables.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 15, character: 6 }, end: { line: 15, character: 10 } }, + message: "'enum' must be a non-empty array of strings", + severity: 1, + code: 7060100, + source: 'apilint', + data: {}, + }, + { + range: { start: { line: 16, character: 17 }, end: { line: 16, character: 22 } }, + message: "'default' value must exist in the enum's values.", + severity: 1, + code: 7060101, + source: 'apilint', + data: {}, + }, + { + range: { start: { line: 19, character: 17 }, end: { line: 19, character: 21 } }, + message: "'default' value must exist in the enum's values.", + severity: 1, + code: 7060101, + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas / yaml - test server variable default and enum 3.0', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'server-variables-3-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/server-variables.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 15, character: 6 }, end: { line: 15, character: 10 } }, + message: "'enum' array should not be empty.", + severity: 2, + code: 5080101, + source: 'apilint', + data: {}, + }, + { + range: { start: { line: 16, character: 17 }, end: { line: 16, character: 22 } }, + message: "'default' value should exist in the enum's values.", + severity: 2, + code: 5080202, + source: 'apilint', + data: {}, + }, + { + range: { start: { line: 19, character: 17 }, end: { line: 19, character: 21 } }, + message: "'default' value should exist in the enum's values.", + severity: 2, + code: 5080202, + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });