From 2ab2840e9ada525909693539dde21242ed68e4f6 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Thu, 4 Jan 2024 10:22:54 +0100 Subject: [PATCH] feat(ls): add lint rule for OpenAPI Parameter defined in path template (#3571) Refs #3546 --- packages/apidom-ls/src/config/codes.ts | 1 + .../parameter/lint/in-path-template.ts | 17 +++ .../config/openapi/parameter/lint/index.ts | 2 + .../services/validation/linter-functions.ts | 38 +++++- ...eter-defined-within-path-template-2-0.yaml | 42 ++++++ ...eter-defined-within-path-template-3-0.yaml | 74 ++++++++++ ...eter-defined-within-path-template-3-1.yaml | 92 +++++++++++++ packages/apidom-ls/test/validate.ts | 126 ++++++++++++++++++ 8 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 packages/apidom-ls/src/config/openapi/parameter/lint/in-path-template.ts create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index d90be22d7..f5b1bc48c 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -734,6 +734,7 @@ enum ApilintCodes { OPENAPI2_PARAMETER_FIELD_UNIQUE_ITEMS_TYPE = 3101700, OPENAPI2_PARAMETER_FIELD_ENUM_TYPE = 3101800, OPENAPI2_PARAMETER_FIELD_MULTIPLE_OF_TYPE = 3101900, + OPENAPI2_PARAMETER_FIELD_IN_PATH_TEMPLATE = 3102000, OPENAPI2_ITEMS = 3110000, OPENAPI2_ITEMS_FIELD_TYPE_EQUALS = 3110100, diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/in-path-template.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/in-path-template.ts new file mode 100644 index 000000000..bcc6e1a75 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/in-path-template.ts @@ -0,0 +1,17 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI2, OpenAPI3 } from '../../target-specs'; + +const inPathTemplateLint: LinterMeta = { + code: ApilintCodes.OPENAPI2_PARAMETER_FIELD_IN_PATH_TEMPLATE, + source: 'apilint', + message: 'parameter is not defined within path template', + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintOpenAPIParameterInPathTemplate', + marker: 'value', + targetSpecs: [...OpenAPI2, ...OpenAPI3], +}; + +export default inPathTemplateLint; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts index b36f86963..8177ed434 100644 --- a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts @@ -40,6 +40,7 @@ import minLengthTypeLint from './min-length--type'; import uniqueItemsTypeLint from './unique-items--type'; import enumTypeLint from './enum--type'; import multipleOfTypeLint from './multiple-of--type'; +import inPathTemplateLint from './in-path-template'; const lints = [ nameTypeLint, @@ -84,6 +85,7 @@ const lints = [ allowedFields2_0Lint, allowedFields3_0Lint, allowedFields3_1Lint, + inPathTemplateLint, ]; export default lints; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index dcf80f4d1..17ef0fdc2 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -9,9 +9,10 @@ import { ArraySlice, ObjectElement, isArrayElement, + includesClasses, } from '@swagger-api/apidom-core'; import { CompletionItem } from 'vscode-languageserver-types'; -import { test, resolve } from 'openapi-path-templating'; +import { test, resolve, parse } from 'openapi-path-templating'; // eslint-disable-next-line import/no-cycle import { @@ -1068,4 +1069,39 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'apilintOpenAPIParameterInPathTemplate', + function: (element: Element) => { + if (element.element === 'parameter') { + const parameterLocation = toValue((element as ObjectElement).get('in')); + + if (parameterLocation !== 'path') return true; + + const isInPathItemElement = + isArrayElement(element.parent) && + includesClasses(['path-item-parameters'], element.parent); + + if (!isInPathItemElement) return true; + + const pathItemElement = element.parent.parent.parent; + const isPathItemPartOfPathTemplating = isStringElement(pathItemElement.meta.get('path')); + + if (!isPathItemPartOfPathTemplating) return true; + + const pathTemplate = toValue(pathItemElement.meta.get('path')); + const parameterName = toValue((element as ObjectElement).get('name')); + + const parseResult = parse(pathTemplate); + if (!parseResult.result.success) return true; + + const parts: [string, string][] = []; + parseResult.ast.translate(parts); + + return parts.some( + ([name, value]) => name === 'template-expression-param-name' && value === parameterName, + ); + } + return true; + }, + }, ]; diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml new file mode 100644 index 000000000..c3ec80bfb --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml @@ -0,0 +1,42 @@ +swagger: '2.0' +info: + title: Foo + version: 0.1.0 +parameters: + test_id: + name: test_id + in: path + required: true + type: string + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/{bar_id}: + delete: + summary: Delete bar id + operationId: deleteBar + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: foo_id + in: path + required: true + type: string + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + type: string + schema: + type: string + format: uuid + title: Foo Id diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml new file mode 100644 index 000000000..35837bb26 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml @@ -0,0 +1,74 @@ +openapi: 3.0.0 +info: + title: Foo + version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/{bar_id}: + delete: + summary: Delete bar id + operationId: deleteBar + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + /subscribe: + post: + description: subscribes a client + responses: + '201': + description: subscription successfully created + content: + application/json: + schema: {} + callbacks: + onData: + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + type: object + properties: + userData: + type: string + parameters: + - name: baz_id + in: path + required: true + schema: + type: string + format: uuid + title: Baz Id + responses: + '202': + description: "OK" + diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml new file mode 100644 index 000000000..02f49f282 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml @@ -0,0 +1,92 @@ +openapi: 3.1.0 +info: + title: Foo + version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +webhooks: + newWebhook: + post: + requestBody: + description: new webook + content: + application/json: + schema: {} + parameters: + - name: hook_id + in: path + required: true + schema: + type: string + format: uuid + title: Hook Id + responses: + "200": + description: OK +paths: + /foo/{bar_id}: + delete: + summary: Delete bar id + operationId: deleteBar + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + /subscribe: + post: + description: subscribes a client + responses: + '201': + description: subscription successfully created + content: + application/json: + schema: {} + callbacks: + onData: + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + type: object + properties: + userData: + type: string + parameters: + - name: baz_id + in: path + required: true + schema: + type: string + format: uuid + title: Baz Id + responses: + '202': + description: "OK" diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 7c19689d3..821fdb5b5 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3549,4 +3549,130 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas 2.0 / yaml - parameter object should be defined within path template', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-2-0.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-2-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 26, character: 8 }, end: { line: 33, character: 23 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3102000, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - parameter object should be defined within path template', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-3-0.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 26, character: 8 }, end: { line: 32, character: 23 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3102000, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.1 / yaml - parameter object should be defined within path template', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-3-1.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-3-1.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 45, character: 8 }, end: { line: 51, character: 23 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3102000, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });